Keygenme
Challenge
Can you get the flag? Reverse engineer this binary.
Solution
Reverse the program using Ghidra. We find the main function:
If FUN_00101209
returns 0
our key is invalid. So, we want to reverse that function:
So, param_1
is the user provided key. If the length of that key is not 0x24=36
then the function immediately returns 0
. Thus, the flag will be exactly 36 characters.
The flag/key starts with picoCTF{br1ng_y0ur_0wn_k3y_
because of the below code snippet:
Converting each of those numbers to ascii and then reversing them produces picoCTF{
, br1ng_y0
, ur_0wn_k
, and 3y_
respectively. We need to reverse the strings because of little endianness.
Now, we could try statically reversing the key checking function to get the flag, which I did try, but after many hours it became apparent that a dynamic analysis approach would be much simpler.
(Note that using GEF to debug this program is easier than GDB. Nevertheless, this writeup uses GDB.)
We can run the binary in gdb and set a breakpoint at strlen
, since this function is called close to the location that the user input is checked character by character against the acStack56
variable. So, run gdb keygenme
and then break strlen
. Now, run the program with r
and then enter c
17 times to get to the point where we can enter a license key. We enter picoCTF{br1ng_y0ur_0wn_k3y_AAAAAAAA}
. We use AAAAAAAA
as the unknown portion since A=0x41
, which is easy to identify in a hexadecimal memory dump.
Once the dummy key is entered we can keep continuing until the dummy key is in a register. We run layout reg
and layout next
to see the registers and assembly at the same time. Now, run x/32c $rax
to see the first 32 decoded characters starting at the address $rax
points to. This will show the start of the flag character by character. If we start running si
to step into the function, we see calls to MD5, so we go to the next breakpoint with c
. Continuing again once more and running x/32c $rax
shows that our input is in the rax register.
Now that we have reached the relevant code, we run s
to step over the string length check. Then, we step in (si
) repeatedly. When doing this we notice that the loop is taking each character of our input and moving it to the rdx
register and then moving each character of the valid input to the rax
register for comparison. Eventually, after spamming si
long enough we get to a point where rax=0x31
but rdx=0x41
. Thus, we have reached the first A
in our dummy flag. Our key at rdx
is incorrect so we change the value of rdx
to the expected value by running set $rdx=$rax
and note down the correct value. Then, we run si
and continue setting $rdx=$rax
and noting down the correct value until we have the entire flag. Running set $rdx=$rax
is necessary in order for the program to continue checking since if it notices one wrong character the function stops. (Note that it might be possible to set a breakpoint on the cmp
instruction and skip out on spamming the si
command.) We can stop getting values once we have 8, since that is the number of unknown characters.
The values I gathered using this method are as follows: 0x31 0x39 0x38 0x33 0x36 0x63 0x64 0x38
. We convert these to ascii using CyberChef to get 19836cd8
. So, the flag is what we know before plus 19836cd8
plus }
.
Flag
picoCTF{br1ng_y0ur_0wn_k3y_19836cd8}
Last updated