Description
Horses are locked down.
Setup
Connect to the challenge server with netcat.
Download the binary and any accompanying source for local analysis.
nc <challenge_host> <PORT_FROM_INSTANCE>wget <challenge_url>/lockdown-horses # binary for local analysisSolution
Walk me through it- Step 1Analyze the custom allocator in GhidraLoad the binary into Ghidra and identify any custom memory management routines - functions named alloc, free_chunk, or similar, that operate on a custom heap region rather than calling libc malloc/free. Map out the internal chunk structure.bash
checksec --file=lockdown-horsesbashstrings lockdown-horses | grep -i 'alloc\|free\|chunk\|horse'bashgdb -q ./lockdown-horsesLearn more
Custom allocators are commonly used in CTF heap challenges to avoid the security checks present in modern glibc (tcache key checking, double-free detection, etc.). The custom allocator maintains its own freelist of memory chunks, often in a static buffer. Its chunk metadata (size, in-use bit, next/prev freelist pointers) is typically simpler than glibc's and may lack integrity checks entirely.
In Ghidra, look for a global array used as a backing store and functions that partition it into chunks. The freelist is usually a global linked list head pointer. Understanding the exact layout of a free chunk's header is essential - you need to know the offset of the forward pointer to corrupt it.
- Step 2Find the overflow or UAF in the allocatorInteract with the program to trigger all its menu options. Look for operations that allow writing past a chunk boundary (heap overflow) or that let you use a pointer after it has been freed (UAF). Use GDB to watch memory as you trigger these operations.
Learn more
Heap overflows in custom allocators are especially powerful because there are no inline metadata integrity checks (unlike glibc's chunk size field verification). An overflow from chunk A into chunk B's header lets you corrupt B's size or freelist pointers, which the allocator will trust blindly on the next free or alloc.
Use-After-Free in a custom allocator means the freelist pointer in the freed chunk is now at the same address as a live application object. If the application lets you read from a freed slot, you can leak the freelist pointer (which reveals heap addresses). If it lets you write, you can overwrite the freelist pointer to redirect future allocations.
In GDB, use
watch *addrto set a hardware watchpoint on suspicious memory locations - execution will pause whenever that address is written to, revealing the exact code path responsible. - Step 3Corrupt the freelist for arbitrary writeWith a corrupted freelist pointer, allocate chunks until the allocator returns your target address. Write a shell command or shellcode there, or overwrite a function pointer. Use pwntools to automate the exploit remotely.python
python3 exploit.pybash# Template:pythonfrom pwn import *bashp = remote('<host>', <PORT_FROM_INSTANCE>)bash# Step 1: trigger UAF/overflowbash# Step 2: corrupt freelist pointer -> target_addrbash# Step 3: alloc again to get chunk at target_addrbash# Step 4: write payloadbashp.interactive()Learn more
The exploitation pattern mirrors glibc tcache poisoning but applied to the custom freelist. After overwriting a chunk's
nextpointer with an arbitrary addressT, the next two allocations of the same size return: first the corrupted chunk itself, then a chunk at addressT. Allocating the second chunk gives a write primitive atT.Useful targets for the write primitive: a function pointer stored in a global (such as a handler table or a vtable), a GOT entry (if not full RELRO), or the saved return address of a stack frame. Writing the address of
systemto a function pointer that is later called with user-controlled data completes the exploit.
Flag
picoCTF{...}
Custom heap allocator with no integrity checks - find the overflow or UAF, corrupt the freelist forward pointer to redirect a future allocation to a target address, then overwrite a function pointer with system.