[Sekai CTF 2023] Conquest of Camelot writeup
Conquest of Camelot
Unlock the secrets and unearth Camelot’s grail hidden within.
Author: sahuang
Basic analysis
We are given a 64-bit ELF binary, and according to the challenge page, it’s written in OCaml
. Throwing the binary to IDA and searching for the prompted string, we can locate the main logic of the program at camlDune__exe__Camelot__entry
.
I’ve never worked with (or heard about) Ocaml before, so the binary threw me off a little bit. It uses a strange calling convention, and IDA can’t generate a good pseudocode. Luckily, the binary is unstripped and that saved me a bit of time in my analysis.
The program first init a constant RNG seed, then uses it to generate some sort of vector. However, this generation is unaffected by our input so I decided to skip it.
By debugging, we see that after receiving the input using camlStdlib__read_line_392
, at 0x40A24
, the program moves the length of our input into the rbx register. It then checks the input’s length using the equation: 2 * len + 1 == 0x49
. We can easily conclude that our input must be 36 bytes long.
To find out which function handles the input, I set a hardware breakpoint on the first 8 bytes of our input. The program breaks at address 0x4068D
, inside the function camlDune__exe__Camelot__fun_769
|
|
The function is pretty simple, it just convert each char of our input into a float, then save it to an address somewhere. But where is it called? Debugging pass the ret
instruction and we see that it is called by an indirect jump to rdi
at 0x4DDB1
, inside camlStdlib__Array__init_103
.
|
|
camlStdlib__Array__init_103
is called by the main function, and we can see that the address of camlDune__exe__Camelot__fun_769
is moved to a buffer where rbx points to.
|
|
From the function name, we can guess that the program is trying to build an array by applying camlDune__exe__Camelot__fun_769
on each char of our input. It is similar to the following list comprehension syntax in Python
|
|
The result of this operation is simply:
|
|
Encryption scheme
I kept setting hardware breakpoints at the float array and saw that it is processed inside camlDune__exe__Camelot__op1_120
. Ocaml adds a lot of bound checking for array access, so the function looks pretty intimidating, but it turns out the logic is pretty simple:
|
|
These operations are repeated multiple times and can be rewritten as:
|
|
This looks a lot like matrix multiplication. After some more debugging, we can confirm that our input is treated as a 1x36 matrix(or is it 36x1? I forget my linear algebra), and operand1
is a row inside a constant matrix.
The result of the multiplication is further processed inside camlDune__exe__Camelot__op2_187
. Here are some relevant instructions:
|
|
Same as the last function, these instructions are also in a loop, and this time, it is an add operation between matrixes.
So overall, the whole thing is actually just an AX+B
.
Jumping out of camlDune__exe__Camelot__op2_187
, we’re going to find ourselves in camlDune__exe__Camelot__search_for_grail_390
. We can see that our input is encrypted using the matrix operation AX+B
multiple times.
In the end, the result should be a 29x1 matrix and it is checked against another hardcoded matrix at 0xA7CF8
.
Dumping the matrixes
To find the flag, we must first find a way to get the content of A
and B
matrixes of AX+B
, and to do that, we need to look at how Ocaml stores a matrix in memory.
Setting a breakpoint inside camlDune__exe__Camelot__op1_120
and jumping to the address of the constant matrix shows us that each row is stored as a float array, and the whole matrix is simply a series of pointers to every row.
To get the size of a matrix, I relied on the bound-checking routines. Every time a cell of a matrix is accessed, Ocaml seems to perform the following check:
|
|
By single-stepping to the cmp
instructions, we can easily get the size of the needed matrix. After we get all that, a simple IdaPython script is enough to dump our matrix.
|
|
That is 1 matrix dumped, but the operation AX+B
is performed multiple times. So how many times exactly?
Looking back at camlDune__exe__Camelot__search_for_grail_390
, we see that the loop is run until rbx == 1
, the value of rbx used in the check is taken from the stack (at [rsp+18h+var_10]
).
|
|
At [rsp+18h+var_10]
, there is a link list as follows. Each element in the list contains a pointer to the 2 constant matrixes for encryption and a pointer to the next element. The number 1
signifies the end of the link list.
As seen in the picture above, there’re only 3 elements in the link list, which means that the encrypt routine only repeats 3 times. At this point, I decided not to extend my script any further and just ran it 6 times to get all the required matrixes.
Getting the flag
Since I’m not really good with matrix, this step took me a lot of time to finish. I tried using many different tools to solve AX+B
for matrix but they unfortunately didn’t work. In the end, good old z3 did the trick.
|
|
And the flag is: SEKAI{n3ur4l_N3T_313c7R0n_C0mbO_uwu}