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.

test input test input largo test input mas largo

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.

secciones upx

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:

ejecutar hasta codigo usuario

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.

pushad

First, I looked in the hexdump for the memory address pointed by ESP.

esp en dump

Selected the 4 bytes pointed by ESP and set the breakpoint by right clicking and choosing “Breakpoint”, “Hardware, Access”, “Dword”.

breakpoint esp

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.

popad

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:

scylla

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.

busqueda string error

When I found the string, I used the cross-references feature (the shortcut is x key) to see where it was being displayed.

referencia al string

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”).

definir funcion

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.

decisiones

Zooming in a little I saw that which block will be executed depends exclusively on the value of ESI register.

dependen de esi

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).

otro mensaje

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.

consigo otro mensaje

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).

target

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.

consigo target