Headache#

Headache is a medium difficulty reverse engineering challenge on Hack The Box available at LINK. It is a Linux x86-64 ELF file.

headache: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, stripped

I’ll be working with Ghidra to reverse this file. Right off the start, we can see the binary is not very forthcoming to us.

Screenshot of file opened in Ghidra

Let’s run and and see what it does instead:

Output of launched file File displays “Initialising” string for a while then prompts as for key. It is not something I can find in provided binary, which immediately makes me think some packer has been utilized here. As I fail to find any obvious clues as to which one I decided to try Detect It Easy. This however also failed.

Output of Detect-It-Easy

Time to use more crude methods then. Let’s open gdb and dump the unpacked .text section directly from memory. I’m using GEF plugin which allows me to use

vmmap

command to get memory mappings of debugged process. Otherwise, if you are not running GEF, the same information can be obtained using from

sudo cat /proc/<pid>/maps

And pid of process can be found by running:

pidof <process_path> 

Anyway, this is our process memory mapping. Remember that in this case we cannot just stop at _start symbol and libc and read the data as our code needs to get unpacked here. The simplest method to stop at right time is to use ctrl+c when program displays “Initializing…” or ask for input. This would stop us somewhere in libc code and allow us to get the information that we need. Simple yet effective.

Output of vmmap gdb command

Our .text section (marked by r-x) is located at 0x0000555555555000 until 0x0000555555557000 in memory, a total of two pages or 0x2000 bytes. Let’s dump it:

dump binary memory dump.bin 0x0000555555555000 0x0000555555555000+0x2000

Now we can take our memory dump and examine it in Ghidra again. As this is just a bunch of bytes, Ghidra is unable to guess architecture. We need to select the one that matches the original file - in this case 64 bit x86 gcc with little endian.

Ghidra disassembly of dumped code Success! Now we see a good amount of functions and code does actually make sense. But where do we even begin? Let’s use the prints and reads that are used by binary to orient ourselves in code. In gdb use

break printf
break scanf
break puts

and rerun the binary. Once we are stopped on breakpoint use

bt

To see call stack of current function (which is somewhere in libc) and find address in our code.

On the side note, since Ghidra will map our code starting at 0x0 the addresses will differ between it and gdb. Use Window->Memory Map to relocate the code in Ghidra:

Ghidra memory map window

Press the Set Image Base button (house icon, marked with red on above screenshot) and set the base to the same value it has in GDB - in my case 0x0000555555555000. Now we have same values in both windows making our job so much easier.

With that done, back to GDB. Our first breakpoints stops in printf as it attempts to print “Initialising”.

GEF stops at printf call

As we see on screen shot, our backtrace shows this function will return into #1 at address 0x00005555555553fb. Which is within code that we are examining. Since return address always points to next instruction, let’s look at disassembly as to where printf has actually been called. Let’s repeat the same process for rest of printf/puts/scanf calls in binary. Looks like all those calls happen within function at 0x5555555553c1, let’s call it important_1. I have also changed names of some other functions to make decompiler output more readable. Here is this function:

Ghidra decompilation of important_1 func

I have also marked two points of interest in binary. Arrow #1 marks input length check, so that we know program expects 0x14 length string and arrow #2 show us where it compares it to some other values. This is trivial enough, let’s recover the reference string from memory then. Comparison in #2 happens at 0x555555555508 so let’s stop there. We can generate the right length of key with command

python3 -c 'print ("A"*0x14)'

so that it passes #1 check and actually gets to #2.

GEF stops at comparison at 0x555555555508

Looks like it is comparing our input ‘A’ with ‘H’, which is first character of flag in HTB. Let’s do a little trick to speed up the process, in gdb use commands

break *0x0000555555555508
commands
set $al=$dl
end

This will automatically set value in al register (one character from out input) to that of dl (flag char) to pass the check. Now all we need to do is to manually read the values of dl register and compile the flag. We can now submit it on the webpage and celebrate victory.

But what.. it does not work? Have we been bamboozled? Is there more to this binary? We have limited ourselves to only one function so let’s look further. Fairly quickly we can find there is a suspicious check at a function that calls our important_1 function, call it important_check:

Ghidra decompilation of important_check func

At #3 we can see there is a condition, it local_c is 0 then different code branch is taken than the one we had examined so far. But what causes the change? At #2 we can see that local_c is set to 0x65, but this is actually incorrect. Closer inspection of assembly around syscall at #1 shows us this:

Ghidra disassembly of syscall

Value of RAX is first set to 0x1 then XOR with RDX of value 0x64, which gives us 0x65. To examine this syscall we can refer to Syscalls x86_64. Value in RAX indicates which syscall is used and RDI, RSI, RDX, R10, R9 are then used for arguments. RAX=0x64 or 101 in decimal indicates that it calls sys_ptrace which coincidentally is used to detect debuggers or similar programs. RAX register is also used to store return value of syscall, as we can see it if then moved into local_c right after syscall. This means that our if statement at #3 depends on whether program detects a debugger is present! Let’s test our theory by manually altering branching. Let’s stop at comparison 0x555555555317 and alter the result.

GDB breaks at comparison

As we can see it is indeed not 0. Let’s manually set zero it by running command

set {int}($rbp-0x4) = 0x0

After we step through the program we can see that it indeed goes different way now, but if we launch it it still outputs the same strings and again asks us for key. If we examine the 3 functions called in this branch we can easily see that it happens in 0x555555555faf, which carries much of the same functionality as important_1. Let’s call it important_2 then. This function however is much more complicated overall. I have omitted some first instructions that are irrelevant for us, they print strings etc, but here is important part:

Ghidra decompiler, first part of important_2

At #1 we seem a similar scanf and a check for 0x14, which means that flag length is the same. At #2 and #3 we see some suspicious conditions. Param_1 corresponds to output of sys_ptrace commands, so perhaps this is also some kind of anti-debugging check. As we can see on previous screenshot, output value of syscall was -1, and (-1 + 2) *100 does indeed equal to 100. Since we changed the value in memory to 0 it should not bother us. Latter part of this function is much more interesting. There are some awful operations done on chars, but at #1 we can see a similar comparison.

Ghidra decompiler, second part of important_2

Put a break on and examine the registers. We can also use a previous trick to automatically bypass the sys_ptrace check for us. As this breakpoints already exists as breakpoint 7 for us I can use this instead:

commands 7
set {int}($rbp-0x4) = 0x0
end

As we hit our desired breakpoint, we can see that it is…wrong? Somehow? The instruction does not much disassembly in Ghidra.

GEF at supposed cmp instruction at 0x555555556646

If we look at vmmap again we can see that our code section is now writeable! We have been bamboozled again! I will spare you hours of pulling hairs, random SIGSEGVs in important_2 function etc. Source of our problems is here:

Ghidra decompiler source of annoyance for us

This function suspiciously takes as input start and end addresses of important_2 in memory. This function does some stuff, but do we actually need to understand it? I’m a huge believer that during reversing one needs to ignore as much as possible, so maybe we can just work around it? Since important_2 only breaks after we added a breakpoint into it, let’s remove it and test it again - now it works as intended! Same when we manually step through it. This means we have two possible solutions - patch out our annoying function in memory or use some basic breakpoint scripting to get past it. Since I never really used much of the latter, let’s try it. We can achieve our goal with just few commands. Firstly. Put break point at 0x55555555534b (which calls important_2, breakpoint number 10 for me) and our desired breakpoint at cmp at 0x0000555555556646 if you have not done so already (number 8 in my gdb). Now do:

commands 10
en 8
end

and change commands in breakpoint at sys_ptrace that we have set up previously to be:

commands 7
set {int}($rbp-0x4) = 0x0
dis 8
end

and finally

commands 8
set $al=$dl
end

Remember to disable any other breakpoints that you might have put in important_2 function! With this basic scripting we are finally at the cmp instruction! I have also put there set $al=$dl command that I have explained previously. With that we can try to manually read the flag from memory again. Now we finally got the right flag! This was truly a headache…

Back to Top