Exploring Katanaverse Implementation
In this detailed explanation, I will walk you through the process of constructing Katanaverse, the binary utilized as a bi0sCTF 2024 Challenge. This challenge falls within the domain of reverse engineering and requires participants to employ puzzle-solving skills. Katanaverse presents a unique blend of custom architecture and concepts from Quantum Computing.
In Katanaverse 0.0, participants tackled the Weighted Maxcut Problem, aiming to split connected dots on a graph into two groups to maximize the weight of lines between them. This demanded strategic thinking and experimentation. In Katanaverse 1.0, participants employed a straightforward but exhaustive approach, trying every possible solution until finding the best one.
Also you can find the source codes and binaries of both the challenges on my Github repo .
Overview of the Challenge
The entire challenge is divided into multiple parts
- VM which includes the bit manipulation and the creation of classified array
- Generation of constants
- AES key generation via the known part of classified 2D array
- srand seed generation using the input trigger and the known part of the classfied 2D array
- Constraint check for classified array using rand values
- Generate 12 points for each group of classified array
- Convert alpha beta notation of points into xyz coordinates
- Apply Quantum Approximate Optimization Algorithm on those 12 points
- The result of the algorithm is formatted and aes encrypted
- The encrpted value is then compared against the encrypted bytes of the correct flag
#1 VM
Control flow of the VM
- Prompt for Flag
- Takes the input
- Transforms the flag into its string encoded in base 64.
- Register S0 stores the updated flag’s length.
- Binary representations of flag bytes are created and placed on the stack.
- As a result, each time a flag byte is converted, the byte is removed from memory.
- Memory is discovered to be empty following the conversion.
- A bit modification that is kept in memory is carried out using stored binary from the stack.
- VM exits
- It replicates its memory into a “classified” 2D array
- It stores the altered input length from register S0 in a new variable for use in future applications.
The classified array is a 2D array of size (inp_len-1)*6 X 8
8 corresponds to each bit of the byte
For more details on the Chronos VM, you can refer to my blog post on Chronos VM .
Bit manipulation

#2 Constant Generation
Since the flag format is included in the creation of classified array, we know the initial few bytes of classified array
Those known bytes are used as aes_key
AES Key Generation
1 | uint8_t aes_key[16]; |
Similarly, seed for srand is also determined using these known classified bytes
1 | int seed_gen = 0; |
An input trigger is also used in the seed generation
#3 Constraints
rand() function is used to generate 5 unique numbers within the range of 0 to 5
Hence for every 6 bytes, a constraint between the first and last bit for that byte is given
For example, if the numbers generated are 0,1,2,3,4 then first 5 bytes of that group of 6 is taken and constraint between the first and last bit is given as 0 or 1
0 means the bits are not same, they are either 0 and 1 or 1 and 0
1 means the bits are same, they are either 0 and 0 or 1 and 1
These constraints are provided to make bruting less tedious
conditions array has those constraints
In katanaverse 0.0, the modified input length is 25
So the classified array (inp_len-1) X 6 is 144
Each 6 bytes correspond to the pair of bytes of our modified input
hence if we take first 6 bytes of classified array and given the constraints for 5 bytes given, there are only 8 possibilities
1 | int conditions[120] = {1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1}; |
#4 Generation of 12 points
Each time 6 bytes of classified array is taken and 6 X 8 bits are divided into 12 groups of 4 bits(nibble) each
These 4 bits (nibble) at a time determine which operation will be performed
There are 16 operations named as Blaze, Horizon, Nova, Quantum, Echo,…

These are the combination of coordinate shift that will be performed on the given qubit along it’s x,y and z axis
This will change the position of the qubit
These operations can be visualised using the Bloch Sphere
Initial qubit position is given by two variables alpha and beta as 1 + 0i , 0 + 0i
When an operation is performed on the qubit, it will undergo change in the alpha and beta values of the qubit
Hence, taking inital qubit state as given above we perform 12 such operations on the qubit and store the state of the qubit after every operation
Hence we have 12 combinations of alpha and beta at the end for each 6 bytes of the classified array
These alpha and beta are converted to their respective x,y,z coordinate form
The function choice is used to decide which operation will be performed based on the 4 bits
1 | for(int i=0;i<(inp_len-1)*6;i+=6) |
The functions Blaze, Horizon, Nova, Quantum, Echo … represent the operations and technically to perform these operation is called to “pass the qubit through the custom gates”
Within each function, there is an encrypted shellcode, which on decrypting is the qiskit code to implement that specific gate (operation/coordinate shift in 3D cuz it is a sphere)
It will look something like this
1 | OPENQASM 2.0; |
Here the operation performed is,
- 45 degree along x axis
- 60 degree along y axis
- 45 degree along z axis
Likewise each function has different operations
This is where the given binary will cease to execute normally and throw a segmantation fault because you need a proper platform to execute this qiskit code
It can be done in Quantum Lab provided by IBM
Although, conversion from alpha beta to xyz coordinates and vice versa can be done here
Hence at the end of this step we have 12 points for each group of 6 bytes of the classified array
We have 24 sets of 12 points
1 | Note: For the encrypting the shellcode, the constants subtract_key and xor_key are determined using the known part of the classified array, the routine is a simple subtract and xor of each byte in the order |
#5 QAOA
Quantum Approximate Optimization Algorithm is a way to divide the given set of points in a way that they are equally divided
QAOA is used to solve max cut problem here
The weights are the distances between the points
We make 41 such pairs and calculate distances between these pairs and assign them as their weights
So now we have a network of 12 points and 41 connections connecting them
Now QAOA is applied on this network and based on their weights each point will be assigned to one of the group, that is Group A or Group B
Group A say is represented by 0, so Group B is represented by 1
so the output of the QAOA applied on one network will be 12 bits
For example: 001111001010
Hence we can say that 1ast and second point is assigned to Group A, then next 4 points are assigned to Group B and it goes on
Thus, all the points are divided in 2 groups
In the binary, the function that is supposed to perform qaoa is broken, there are only encrypted bytes which give out a hint about using qaoa
This is the decrypted string part under qaoa for katanaverse 0.0
1 | Oh no! Not only is this function flawed, it is nonexistent. Could you please solve this? |
The encryption scheme used here is same as that used for the shellcodes above
Sample implementation of QAOA in qiskit is given as:
1 | import networkx as nx |
Sample output:
1 | energy: -91.93458817618827 |
Here the elist array is the array containing the 41 pairs of points and their associated weights
While calculating weights in our binary, we multiple each weight (distance between 2 points) with a constant value 10 and then round it off
solution: [1 0 1 1 0 1 0 1 0 1 1 0] is the result, 12 bits
Similarly, for Katanaverse 1.0, the brute force method is utilised to divide the points.
#6 Formatting the result
The result is then divided into 2 parts of 6 bits each
Each 6 bits is appended by a 01 (or addition of 64 in decimal to the given bits) at the start and made into a proper 8 bits, thus giving out 2 bytes for each solution
For Katanaverse 0.0, QAOA is performed 24 times and thus there are 24*2 bytes at the end
1 | qaoa(elist, &res_bits); |
fin_val holds the final 48 bytes
#7 AES Encryption
These 48 bytes are then aes encrypted using the aes_key generated earlier and compared against the encrypted bytes obtained on performing the entire process with the correct flag as input
1 | uint16_t inputSize = sizeof(fin_val)/sizeof(fin_val[0]); // Calculate input size |
Also, note that the source code of the AES encryption can be found here .
You can also find the link to the writeup of the challenge here - Katanaverse-1-0-bi0sCTF-2024
Ciao !!
- Title: Exploring Katanaverse Implementation
- Author: Sejal Koshta
- Created at : 2024-02-02 12:45:19
- Updated at : 2024-08-12 11:52:24
- Link: https://k1n0r4.github.io/2024/02/02/Exploring-Katanaverse-Implementation/
- License: This work is licensed under CC BY-NC-SA 4.0.