Echoes (Reversing) – H4ck3d CTF [EN]
This binary is a reversing challenge from H4ck3d conference CTF. You can download it here. To solve it I had to unpack it and then used a crackme style approach.
I started by running the binary and exploring its behavior.
The program responds in different ways when the input length changes.
Here I show some examples.
In some cases error messages like Error 0x432872
are displayed.
Then, I tried to disassembly the binary using IDA but the resulting code didn’t seem to match the observed behavior. This led me to think that the code might be obfuscated or packed. To check this, I used Detect It Easy and found that the binary had sections named TTX0 and TTX1. This usually means that the binary was packed using UPX. Additionally, Detect It Easy has an heuristic scan that suggested that the binary was packed using UPX. Another alternative is to use PEiD.
UPX packer is frequently used because its free. It can be used to unpack binaries too but I tried to use this feature without success. This generally means that the binary has been modified to avoid unpacking or, in some cases, that a different version of UPX was used to pack it. So I tried to unpack it manually using x32dbg. To do this, I started by letting it run until user code:
A UPX packed binary starts by pushing the registers to the stack to preserve them using the pushad
instruction and then it executes the unpacking routines.
With this in mind, I tried the strategy of placing a hardware access breakpoint at the address of the last pushed register to stop the execution when the program retrieves the values using popad
after the unpacking has finished.
First, I looked in the hexdump for the memory address pointed by ESP
.
Selected the 4 bytes pointed by ESP
and set the breakpoint by right clicking and choosing “Breakpoint”, “Hardware, Access”, “Dword”.
Then, I continued the execution until the breakpoint was triggered, just after the popad
instruction.
When the program has unpacked the code and restored the popped values, it jumps to the original entry point (OEP) that corresponds to the start of the unpacked code in memory.
This is known as tail jump.
So I executed the binary until the OEP and used Scylla to dump the unpacked code from memory. To do this, you have to choose Scylla from the “plugins” menu and press dump:
This dump won’t work out of the box because it has the import table broken (IAT). To fix it, you have to press “IAT Autosearch” to find the table, “Generate Imports” to list the entries and “Fix Dump” to fix the table in the dumped executable.
After doing this, I had an unpacked working binary so I disassembled it using IDA and searched for the error string that I had observed while testing the program.
When I found the string, I used the cross-references feature (the shortcut is x key) to see where it was being displayed.
I found a function that was using the string but was not well reconstructed by IDA so I looked for the prologue and defined it manually (Right click and “Create function”).
Then, I switched to the graphical view pressing space and this allowed me to see that the function executes a basic block and then performs a series of decisions that define which of the next blocks will be executed. These are the ones that IDA displays side by side at the same height and one of them references the error string that I saw before.
Zooming in a little I saw that which block will be executed depends exclusively on the value of ESI
register.
Taking this into account together with the fact that the program behaves differently depending on the length of the string entered made me suppose that ESI
stored the length of the string.
So I looked for another block that displayed a message (Segmentation fault) and checked which conditions were necessary to get there (yellow blocks).
The first comparison dictates that the length of the string must be greater or equal than 56 (cmp esi, 38h
and jge
), the second one is that the length should be less or equal than 72 (cmp esi, 48h
and jg
), and the last one is that the length must be equal to 72 (only jz
since it uses the previous cmp
).
To test this I built a 72 char string and when I entered it the program reached the basic block that prints Segmentation fault.
It also adds something that resembles the flag but it isn’t.
This confirmed that ESI
holds the length of the string and which basic block executes depends on this.
Now, to get the flag, I searched for other block that didn’t have a hardcoded error message and checked the string length required to get there (green blocks).
In this case, the length must be greater than 72 and, the length minus 73 (add esi, 0xFFFFFFB7
), 6 (sub esi, 6
) and 2 (sub esi, 2
) must be cero (jz
).
Adding 0xFFFFFFB7
is in fact a subtraction because integers use two’s complement representation.
In the next image there is an example of how to make the conversion.
As a result of all this conditions, to trigger the execution of this block, the length of the string must be 81.
So finally, I entered a string with 81 chars to get the flag.