Reversing ELFs on TryHackMe: Crackme8
Collecting Information
As always, we will start by collecting as much information as we can about our file and looking for low-hanging fruit (hardcoded strings, function names, etc).
From our strings
output, we do see the strings returned during the initial run (e.g. granted/denied) and also some interesting functions like giveFlag
but there’s no obvious flag.
GDB
To load this file into gdb
, we can simply use the command gdb ./crackme8
. I’m using pwndbg, which is a more user-friendly and feature-rich way of interfacing with GDB. My outputs may differ from yours, but the commands should all have parity.
Dissassembly
Once the file is loaded into the debugger, we can use two commands to gather some additional information:
info variables
- This will show us any hardcoded variable names. If we were to find any, we could then use
print <varname>
- which doesn’t appear to be the case for this file
- This will show us any hardcoded variable names. If we were to find any, we could then use
info functions
- This would be equivalent to above, just applicable to function names:
Unfortunately, we didn’t get anything particularly useful out of either of these commands, so we’ll move on to looking at the main
function which is (luckily) defined clearly. So let’s drilldown into the function and see what’s happening.
main()
To disassemble any function, we just need to run disass <func_name>
. However, before we do that, I like to change the disassembly representation to the Intel format. To do that, we can set disassembly-flavor intel
. This is a completely optional step, but I prefer seeing the information represented this way.
Dump of assembler code for function main:
0x0804849b <+0>: lea ecx,[esp+0x4]
0x0804849f <+4>: and esp,0xfffffff0
0x080484a2 <+7>: push DWORD PTR [ecx-0x4]
0x080484a5 <+10>: push ebp
0x080484a6 <+11>: mov ebp,esp
0x080484a8 <+13>: push ecx
0x080484a9 <+14>: sub esp,0x4
0x080484ac <+17>: mov eax,ecx
0x080484ae <+19>: cmp DWORD PTR [eax],0x2
0x080484b1 <+22>: je 0x80484d0 <main+53>
0x080484b3 <+24>: mov eax,DWORD PTR [eax+0x4]
0x080484b6 <+27>: mov eax,DWORD PTR [eax]
0x080484b8 <+29>: sub esp,0x8
0x080484bb <+32>: push eax
0x080484bc <+33>: push 0x8048660
0x080484c1 <+38>: call 0x8048340 <printf@plt>
0x080484c6 <+43>: add esp,0x10
0x080484c9 <+46>: mov eax,0x1
0x080484ce <+51>: jmp 0x804851c <main+129>
0x080484d0 <+53>: mov eax,DWORD PTR [eax+0x4]
0x080484d3 <+56>: add eax,0x4
0x080484d6 <+59>: mov eax,DWORD PTR [eax]
0x080484d8 <+61>: sub esp,0xc
0x080484db <+64>: push eax
0x080484dc <+65>: call 0x8048380 <atoi@plt>
0x080484e1 <+70>: add esp,0x10
0x080484e4 <+73>: cmp eax,0xcafef00d
0x080484e9 <+78>: je 0x8048502 <main+103>
In this block, there are two really important pieces:
0x080484ae <+19>: cmp DWORD PTR [eax],0x2
0x080484e4 <+73>: cmp eax,0xcafef00d
So in the first case, we’re comparing the value of the EAX register by reference to the value 0x2
(essentially, if (*eax == 2)
), which is equivalent to the decimal value 2. The second case, we’re comparing EAX to 0xcafef00d
, which is equivalent to the decimal value 3405705229. We need both of these checks to succeed in order for giveFlag
to … give us the flag. Here is a visualization of the flow of main()
:
┌────────┐
│crackme8│
└─┬────┬─┘
│ │
│ │
┌──────────┐ │ │ ┌───────────┐
│ Argument ├─────┘ └──────────┤No Argument│
└─────┬────┘ └─────┬─────┘
│ │
│ │
┌──────┴─────┐ │
│ 0x080484ae │ ;cmp DWORD PTR [eax],0x2 │
└──┬────┬────┘ │
│ │ │
┌─────┐ │ │ ┌───────┐ │
│ 2 ├──────────────┘ └─┤ else ├───┐ │
└──┬──┘ └───────┘ │ │
│ │ │
│ ;cmp eax,0xcafef00d │ │
│ ┌────────────┐ │ │
└─────┤ 0x080484e4 │ │ │
└───┬────┬───┘ │ │
│ │ │ │
┌────────────┐ │ │ ┌───────┐ │ │
│ 0xcafef00d ├──┘ └─┤ else ├─────────┐ │ │
└──┬─────────┘ └───────┘ │ │ │
│ │ │ │
│ │ │ │
┌───┴─────┐ │ │ │
│giveFlag ├─────────────────────────────┐ │ │ │
└─────────┘ │ │ │ │
│ │ │ │
┌─┴─┴─┴┐ │
│ Exit ├────────────────┘
└──────┘
Counter-Intuitive inputs
So here’s where the challenge becomes slightly counter-intuitive… we’re told that the usage is Usage: ./crackme8 password
but that is a misleading statement. What we actually need to do, is pass in 2
in order to pass through the first flow gate. In order to do this from within GDB we can just use run 2
. However, before doing that, we’ll want to set a breakpoint at 0x080484e4 (the second gate) via b *0x080484e4
because we will need to modify the contents of the register (if you’re wondering why we didn’t also just do that for the first gate at 0x080484ae, it’s frankly because I want to bake in a small GDB tutorial into this write-up and needed a place to explain how to actually run the program from inside the debugger… tangent off).
Second Gate (0x080484e4)
With our breakpoint set, and the program executed via run 2
, it’s time to now change the value of EAX in order to pass the second gate (0x080484e4 <+73>: cmp eax,0xcafef00d
). To accomplish this, we need to remember that the decimal value of 0xcafef00d
is 3405705229 and then just use the command: set $eax=3405705229
. Now, when we execute the next instruction, we should jump properly - having passed the comparison instruction:
(gdb) set $eax=3405705229 // setting EAX = 0xcafef00d
(gdb) ni // repeat this by just hitting enter a few times
0x080484e9 in main () // je 0x8048502 <main+103>
0x08048502 in main ()
0x08048505 in main ()
0x0804850a in main ()
Access granted. // BINGO!
0x0804850f in main ()
0x08048512 in main ()
flag{XXX} // Collect your loot!
0x08048517 in main ()