Description
The name is the hint: the flag gets cached on the stack and never cleaned up. There is no format string and no ret2libc here. A buffer overflow lets you redirect execution so that win() loads the flag into a stack buffer (but never prints it), then UnderConstruction() prints uninitialized stack slots that still hold those leftover flag bytes.
Setup
Download the binary and check what you are dealing with first: file vuln reports a 32-bit i386, statically linked, non-PIE executable, so every function address is fixed and comes straight from the symbol table.
Read it in Ghidra (or objdump). Locate vuln(), win(), and UnderConstruction() so you know where each one lives before starting the analysis.
wget https://artifacts.picoctf.net/c/121/vulnchmod +x vulnfile vuln # ELF 32-bit LSB executable, Intel i386, statically linked, not strippednm vuln | grep -E ' (vuln|win|UnderConstruction)$'nc saturn.picoctf.net <PORT_FROM_INSTANCE>Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1See how the flag is left on the stack
ObservationThe challenge is called stack-cache, and Ghidra shows win() reading the flag into a local buffer that nothing ever prints. So the flag is cached on the stack rather than output, and exposing it needs a second function whose uninitialized locals sit in the same place.win() calls fopen("flag.txt", "r") and fgets(buf, 0x40, fp) into a local buffer at ebp-0x40, then returns. C does not zero a stack frame on return, so the flag bytes remain in that region of the stack until something overwrites them. win() deliberately never prints the buffer, so reaching it is not enough on its own.bash# In Ghidra: win() -> fopen("flag.txt") + fgets into a local buffer, no printf of it.bash# UnderConstruction() -> three printf calls, ten %p in total, on locals it never sets.What didn't work first
Tried: Look for a format string vulnerability because UnderConstruction() calls printf with %p specifiers.
The format string is hardcoded and never under your control, and a format string exploit needs user input reaching printf as its first argument. The bug here is a buffer overflow paired with uninitialized stack disclosure, so writing %p or %x into the input does nothing.
Tried: Assume reaching win() alone is enough to get the flag printed.
win() reads the flag into a local buffer with fgets and never calls printf or puts on it. Running it loads the bytes onto the stack silently and prints nothing. Chain a second function whose own uninitialized locals expose those same bytes.
Learn more
Why leftover stack memory leaks the flag. Local variables live in the function's stack frame, which is just a slice of the stack reused frame after frame. When
win()returns, its frame is logically gone but the bytes are untouched. A later function whose frame overlaps that same region, and which prints its uninitialized locals, will print whateverwin()left behind. This is classic uninitialized-memory disclosure, the "clean up your memory" lesson the flag spells out.Step 2Overflow to chain win() then UnderConstruction()
ObservationThe input function writes into a fixed-size buffer with no bounds check, which is a classic overflow. win() loads the flag without printing it and UnderConstruction() prints uninitialized stack slots, so chain both return addresses: seed the stack first, then print it.vuln() calls gets() on a buffer at ebp-0xa, so the saved return address sits 10 + 4 = 14 bytes in (ten bytes of buffer plus the four-byte saved EBP). A cyclic pattern confirms the same 14. Then build a payload that returns into win() and, as win()'s own return target, the address of UnderConstruction(). win() runs first and fills the stack with the flag; UnderConstruction() runs next and prints the now-flag-bearing uninitialized slots as hex.pythonpython3 - <<'PY' from pwn import * e = ELF("./vuln") # 32-bit, no PIE: symbol addresses are the real ones io = remote("saturn.picoctf.net", <PORT_FROM_INSTANCE>) OFF = 14 # 10-byte buffer at ebp-0xa + 4-byte saved EBP payload = b"A" * OFF payload += p32(e.sym["win"]) # run win(): loads flag onto the stack payload += p32(e.sym["UnderConstruction"]) # then print uninitialized stack (=flag) io.sendline(payload) print(io.recvall(timeout=3).decode(errors="ignore")) PYIf the printed hex does not contain the flag, re-check the 14-byte offset and that you packed the addresses with
p32rather thanp64. The two frames must share the stack regionwin()wrote to, which is what returning straight from one into the other guarantees: any extra padding or an intervening call shiftsUnderConstruction()'s frame away from it.What didn't work first
Tried: Put only win()'s address in the payload and check the output, expecting the flag to appear.
win() never prints the flag buffer, so it produces nothing at all and the terminal hangs or closes empty-handed. Put a second return address in the payload, pointing at UnderConstruction, so the leftover stack bytes get printed as hex.
Tried: Use cyclic() offset guessing from a crash in GDB and apply that same offset against the remote service without verifying whether the binary is PIE.
The binary is 32-bit and built without PIE, so function addresses are fixed and come straight from the ELF symbols. The usual mistake is packing them with p64 instead of p32, or adding another 4 for the saved EBP when the cyclic pattern already accounted for it, which lands the write past the return address and corrupts the frame instead of redirecting it.
Learn more
Why two chained returns. You cannot print the flag from
win()(it does not), andUnderConstruction()prints garbage on its own (its locals are never set). The exploit is to run them back to back so the second function inherits the first's leftover bytes. Stacking return addresses after the overflow is the simplest way to call two functions in sequence without a full ROP chain.Step 3Reassemble the hex into the flag
ObservationUnderConstruction() prints ten 4-byte words as hex integers rather than as ASCII, and it walks the frame from the high addresses down, so the words come out in reverse. Repack each with p32 and join them back to front to recover the original string.Collect the ten hex values UnderConstruction() prints ("User information" gives six, "Names of user" three, "Age of user" one), convert each back to 4 little-endian bytes with p32, and join them in reverse of the printed order: the last value printed, 0x6f636970, is "pico". The picoCTF{...} flag falls out of the reassembled buffer.pythonpython3 - <<'PY' import re from pwn import p32 leaked = io.recvall(timeout=3).decode(errors="ignore") vals = [int(x, 16) for x in re.findall(r"0x[0-9a-fA-F]+", leaked)] blob = b"".join(p32(v & 0xffffffff) for v in reversed(vals)) print(blob) # picoCTF{...} reads straight out PYExpected output
picoCTF{Cle4N_uP_M3m0rY_...}What didn't work first
Tried: Treat each printed hex value as big-endian and decode with bytes.fromhex() directly, without packing with p32.
The values are 32-bit integers stored little-endian in the stack buffer. Read them as big-endian hex and every 4-byte word comes out reversed, giving scrambled characters rather than ASCII. p32 repacks each integer into 4 little-endian bytes, restoring the layout win() left behind.
Tried: Grep the raw output for 'picoCTF' before reassembling, assuming the flag appears as a readable string in the hex dump.
UnderConstruction() prints each 4-byte chunk as one integer, not as individual characters, so the word picoCTF is split across two of those fields (0x6f636970 and 0x7b465443) and a substring search on the raw output finds nothing. Reassembling with p32 joins the words back into a contiguous string.
Learn more
Putting the bytes back together. Each leaked value is 4 bytes of the flag stored little-endian. Re-packing every value with
p32and joining them in reverse of the printed order reconstructs the original byte order ofwin()'s buffer, where the flag string lives. See Pwntools for CTF for the packing helpers.
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{Cle4N_uP_M3m0rY_...}
No format string and no ret2libc. The binary is 32-bit and non-PIE, so overflow the 14 bytes to the saved return address and stack p32(win) then p32(UnderConstruction): win() reads the flag into a stack buffer but never prints it, and UnderConstruction() prints the uninitialized stack slots that still hold those bytes. Reassemble the ten leaked words with p32, back to front, to read the flag.