cable management
cable management was a reverse challenge from DiceCTF 2022 that was released midway through the CTF, with 7 solves.
Since I got first blood in around an hour (whereas other teams started flagging it only 12 hours after release), I thought it would be interesting to share how I approached the problem.
Description: Help me manage my cables! Note: Flag may take a while to verify.
First glance
We are given a 5 MB 64-bit ELF chall
that asks for user input.
It doesn't directly answer back if we send it a small string: instead, it waits for enough characters. This way, we can already retrieve the flag length, which is 29.
We can also notice if we send 29 bytes at once it takes a long time to process, but if we send it in two parts it takes less time to process the second part. This may suggest the flag is checked progressively (for example one byte at a time), and not all at once.
Finally, one can notice sending "dice{aaaaaaaaaaaaaaaaaaaaaaaa" takes roughly 2 seconds more than sending "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa". At this point a timing attack may work out (although possibly very slow), but at the time of the CTF I didn't think of it and directly started reversing.
Reversing (but not too much)
Let's load the binary in IDA. The main function is quite straightforward:
The function I renamed read_one_bit
is passed as an argument to another function, f1
, which has to return 1 in order to output the success message.
read_one_bit
is self-explanatory: it reads one bit of the user input. More precisely, it reads a byte from stdin
with getc
every 8 calls, stores its bits in .data
variables. Therefore, each call, it returns a new bit from the user input.
The main logic of the binary lies inside the f1
function (0x1540). IDA refuses to decompile it because the stack frame is huge (0x5269a0 bytes).
The first thing this function does is calling memcpy
to copy a certain buffer (0x4058, here renamed map
) from .data
to the stack frame. Its length (rdx
argument) is 0x526990 = 5400976 bytes, so pretty huge: actually, it takes up almost 99% of the binary's size. We can then observe the function seems to loop on this map and perform different actions depending on the read value.
Skimming through it shows map
mostly comprises bytes such as 0x00 and 0xCD. At this moment, I went to check out other functions inside the binary, and stumbled upon the function at 0x1390.
Since it performs integer division by 2324, it makes sense to view the variables I renamed x
and y
as coordinates. Furthermore, we can try dumping and visualizing the map as a 2324-bytes-wide image:
Looks nice... what could it mean? Let's zoom in on a pattern.
The map mostly consists of 232 of these patterns horizontally glued next to each other. Here is the same pattern in hexadecimal, where I removed null bytes for clarity.
We notice two things:
Some patterns have an 0xEC byte on the fifth line, some don't and have a 0xCD instead. This may be important data in flag verification.
All the patterns have an 0x11 byte at the top center.
Let's come back on the f1
function to see how this 0x11 byte is handled.
It calls the function pointed by r13
, which is the function pointer argument passed by main (read_one_bit
). Therefore, for each of these patterns in the map, one bit of the user input is read. This confirms that the flag is indeed 232 / 8 = 29 bytes long.
At this point, what I did is extract the data formed by the 0xEC and 0xCD bytes in each pattern.
I tried some wild guessing to find out how it could relate to the flag, but could not find anything meaningful. Instead, I went for a different approach.
Leaking the flag
Before going any further into reversing, I wanted to experiment with how the map evolved in memory. Since the flag is read bit by bit, perhaps some local modifications are performed for each bit, which can in turn leak information.
I wrote a very simple GDB script to dump the state of the map after the program finished its execution for a given input.
I generated a dump for "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" and compared it with the original map.
We can see the lines where changes happened. Most importantly, it seems that some bits of data (0xEA/0xCD) now appear on line 45+:
I decided to extract these bits:
Then, I also generated a dump for "dice{aaaaaaaaaaaaaaaaaaaaaaaa" and extracted the relevant bits. The result is extremely promising:
Suddenly a bunch of zeroes! Assuming they're here because the flag does start with dice{
, it is now easy to write a script that bruteforces the flag bit by bit to make this suffix of zeroes grow.
After around 40 minutes, the flag is succesfully leaked! And we still have no clue what the challenge is about.
Once the CTF ended, the author mentioned the source of their inspiration for this challenge: Wireworld.
To conclude this write-up, I would say that in reverse challenges, you should always look for obvious side channels (time, number of instructions, code or memory coverage...) before actually going too deep into the reversing process.
Last updated