Note: If you are looking for a simple Web-based CTF then my advise would be to not read this as it’ll affect your eyes as there is a lot of assembly dump down there which you won’t be able to handle.
Hi, I go by the alias Haxor_s007 and today’s write-up/Blog is about an interesting CTF challenge I did involving some intermediate level of reverse engineering and binary analysis.
This write up may not be beginner friendly but you’ll understand it if you do a bit of research and hold onto it 😉
Suggested Reading Material:
So let’s just start and dive right into it. The file can be downloaded from:
Determining File Type:
Executable and Linkable Format(ELF) is a binary file format used on UNIX / Linux operating systems for executable files and linkable code modules.
Linux’s file command will identify the file format by checking the target file for patterns of bytes that uniquely identify known file formats.
Confirmed. We’re dealing with a 32bit ELF file.
Let’s run the ELF and see what is outwardly observable as it executes.
It’s very important to understand that the output we see is not the whole story. An application could print a welcoming greeting on a screen while stealing all your data & formatting your hard drive in the background. So exercise caution when running any untrusted executables.
I am running the challenge executable in a Kali Linux virtual machine configured to be isolated from the VM host.
The executable doesn’t wait for any user input to the program does not appear to support any direct runtime interaction from the console.
At this point, we could go deeper with dynamic analysis by using tools to watch if the executable reads/writes files, makes network connections or spawns processes. But we’re going to take a different tack and have a look at what is in the file itself.
Identifying Strings Embedded in the Executable
The strings utility scans the bytes in a file for sequences of at least 4 bytes in the ASCII printable character range that are followed by a non-printable character.
Run against our challenge binary we get the following result
So, is that it then? Flag uncovered?
We don’t know the actual flag length. How much of the output above is actually the flag string? We could start with ‘FLAG’ and try to submit increasingly longer strings. Not very elegant.
Another thing to consider is how the strings utility actually works. If its criteria is simply a sequence of 4 or more bytes with values in the ASCII printable range followed by a non-printable character it very well might be giving us false positives by incorrectly interpreting bytes from the executable as ASCII characters.
Let’s dig deeper.
I’ll be using Binary Ninja(Can be downloaded from https://binary.ninja/) to disassemble the challenge executable.
When we open the executable in Binary Ninja it automatically analyzes the file. It identifies variables,functions, flow control, etc… Thanks to the analysis we can jump right to the main() function and start reverse engineering it.
Let’s break it down into smaller, manageable chunks.
Prior to marker 1, we have standard function prologue instructions, 16-bit stack alignment, and allocation of 48 (0x30) bytes on the stack.
At marker 1, we have a stack variable at esp+02C being initialized to 0. We know it’s a stack variable because it’s a positive offset from esp, the head of the stack. Since the stack grows downwards (via subtracting from esp) a positive offset from the address pointed to by esp references memory within the current function’s stack frame.
At marker 2 we put 0x18 into memory at the head of the stack (pointed to by esp). The next instruction is a call to malloc() which should be familiar to C programmers. malloc() takes a single argument: the size in bytes of the memory you’re requesting to allocate. The most commonly used x86 calling conventions to pass arguments to functions by pushing them on the stack just prior to the function call so it’s a safe bet that the value 0x18 is the size of the buffer requested from malloc().
Marker 3 is the return value from malloc(), the address of our allocated memory, is stored in a local stack variable at esp+0x2C. x86 calling conventions specify that the return value of a called function is passed back via eax which is the register operand of the instruction a marker 3. Note that this is the same stack variable that we saw initialized to zero at marker 1.
The block of instructions indicated by marker 4 is loading the arguments for the memset() function onto the stack. Calling conventions that use the stack dictate the arguments will be put on the stack in right to left order. This lays out the stack such that the called function can then pop the arguments of the stack in the order they are listed in its a function declaration. The declaration for memset() lists 3 arguments: void pointer to the memory to fill, the byte value to fill with and the size of the buffer / how many bytes to fill.
Looking again at marker 4 we can identify each argument on the stack.
- 0x18 at esp+0x8 – Size of the buffer. Same as passed to malloc() earlier.
- 0x0 at esp+0x4 – Byte value to fill with. We are zero-filling the memory.
- The value in eax at esp – eax contains the value at esp+0x2C. This is the address of our allocated memory.
Summing up what we’ve just analyzed in the previous excerpt: the code is allocating a buffer of length 0x18 with malloc(), a pointer to the buffer is saved on the stack at esp+0x2C, then the buffer is zero-filled by the memset() function.
Below is the same chunk of disassembly annotated based on what we’ve figured out so far.
This next excerpt follows immediately after the one above.
I’d like to highlight two groups of instructions.
The first group is the first four instructions of this excerpt, 080484aa to 080484bb. In the first of these instructions, we’re loading the address of our previously allocated buffer into eax. Remember that the address of our buffer is stored in the local variable on the stack at esp+0x2C. After we’ve done this, the three subsequent instructions are moving hex values into directly into our allocated memory.
- 0x47414c46 is loaded into our buffer at the very start of the memory (eax)
- 0x3930342d is loaded into our buffer starting at the fifth byte (eax+0x4, index is zero based))
- 0x32 is loaded into our buffer starting at the ninth byte (eax+0x8)
The second group of instructions (080484e9 to 080484f6) follows a similar pattern. Again we see large hex values loaded into our allocated memory.
- 0x75393438 is loaded into the region of the buffer pointed to by eax
- 0x6a326f69 is loaded into the region of the buffer pointed to by eax+0x4
- 0x66 is loaded into the region of the buffer pointed to by eax+0x8
What are these values? Four out of the six are 4 byte values. They could be large integers.
Or 32bit addresses to other regions of memory?
It turns out the hex values being put into the buffer are ASCII character codes in hex format. Examining the executable in Binary Ninja’s raw hex dump view at the address of these instructions reveals the ASCII characters represented by the hex values used in this instruction.
Note that in the annotated excerpt above the literal hex values in those instructions are now shown as text string segments (‘FLAG’, ‘-409’, ‘2’, etc…). The algorithms used by a disassembler are not perfect. They try to determine what every byte is used for(data, part of an opcode, etc…) but in some cases it is ambiguous. In our case, it saw these literal hex values and had no way to disambiguate between a string of ASCII characters, a 4 byte data type, an address, etc… So in the disassembled code they simply appeared as hex. Within Binary Ninja, you can select a range of bytes and tell the program to display them as a different type. This is what we’ve done here. We selected the literal hex values and told Binary Ninja to interpret them as a character constant. Doing so revealed that these instructions are building our flag string in our allocated buffer.
Why create a string out of hex ASCII codes like this? If the entire flag string was hard-coded as a character constant in the program it would be clearly identifiable in the executable’s .data segment. The strings utility would have revealed the entire flag right away. As it is, the flag string is broken up into segments interrupted by op-code bytes (some with value in the ASCII printable range). This throws off the result of the strings command and gives us the wrong flag value in the output. Compare the values in the strings output above and the string segments we see being loaded into our buffer in the disassembly. They do not match exactly.
Between the instructions loading the hex ASCII codes into our buffer, there is a block of instructions around a ‘repne scasb’ instruction (080484c1 to 080484e3). This chunk of code is starting at the beginning of our buffer and scanning forward until it finds a byte that matches the value in register al. Since eax is 0x0 and al is the low byte of eax this operation is scanning for the first occurrence of a null byte (0x00). Given that we zero-filled our buffer with memset() before starting to copy values into it this block of code serves to find the first byte where we have not yet put an ASCII character value. In other words, the index into our buffer indicating where we should resume loading ASCII characters of our flag string. We see more segments of our flag string being moved into our buffer in the subsequent instructions at 080484e9, 080484ef and 080484f6.
I assume the program does this (rather than fully building up the flag string in a series of sequential mov instructions) simply to add a twist to the analysis of the program and to obfuscate the results of the strings command as mentioned above.
At the very bottom of this code excerpt, we see a call to puts(). The argument passed to the function is the “Loading…”. This is part of the output we observed when we executed the program.
As we see in the final excerpt below the “Loading…” output is just window-dressing. The program isn’t loading anything in the sense of reading data from a file or network connection. It goes on to simply repeat the pattern we’ve already observed of moving ASCII bytes into the buffer and doing the ‘repne scasb’ scan.
At 08048544 we call puts() again to output the string “Where is the flag?”.
At this point, we can answer that question. It’s stored in memory at the address saved in esp+0x2C.
Capturing the Flag
So how do we get the full flag?
Having analyzed the disassembly we can just manually concatenate all the hex ASCII values we’ve identified as being moved into our buffer
- ‘FLAG’ at 080484ae
- ‘-409’ at 080484b4
- ‘2’ at 080484bb
- ‘849u’ at 080484e9
- ‘io2j’ at 080484ef
- ‘f’ at 080484f6
- ‘klsj’ at 08048530
- ‘4kl’ at 08048536
Combined they form: FLAG-4092849uio2jfklsj4kl
This method works in our case since the values are hard-coded into that application. But let’s imagine that these values were actually calculated at runtime using some complex algorithm. Maybe based on the specific system clock at the time the executable is run so they’re different every time. In a scenario like that our method fails.
How else can we proceed?
If our flag value is calculated at runtime we could run the program and yank the value out of the process memory once we know it’s been fully generated.
This is where a debugger & dynamic analysis comes in handy.
In our example, we know by the time we reach the instruction at 080484fc our flag string has been completely built. If we set a breakpoint on that instruction we can examine the flag to get its full value. Luckily we also know where to look. As mentioned, the buffer that stores our flag is at the memory address stored in variable esp+0x2C. With this information, we can pull the value of our completed flag out of process memory.
There are a few ways you could approach this. I’ve chosen to use Radare2’s r2pipe Python API and coded a custom script(Automate.py) a debugging session that will dump the flag. The script will launch a debugging session of the executable, set a breakpoint on an instruction after the point we know the flag is complete and then extract the string from memory.
When running the script generates the output below.
Realistically, we could’ve gone straight to this approach once we figured out the memory address of our buffer but fully analyzing the disassembly was good practice.
Challenge: This is just one approach to solve this, try solving this using the Hardware breakpoints and submit your results and queries at