Jingle Bell ROP (pwn)
A classic ROP challenge
Last updated
A classic ROP challenge
Last updated
First we do a checksec
on the binary to check its protections:
Since the binary has no canary we do not need a canary leak to write a ROP chain. Furthermore, since it has no PIE (Position Independent Executable), the program is loaded at a constant offset every time we run it.
The program is rather small, and the Ghidra pseudocode is as follows (I have renamed some variables for simplicity):
The main vulnerability here is that fgets
allows us to enter 0x80
(which is 128 in decimal) bytes into local_48
when it should only contain 64 characters. So we have a 64-byte overflow into the buffer, which is more than enough to write a ROP chain.
However, the exploitation is not so straightforward since there is no win
function in the binary. Which means that we have to ret2libc in order to get a shell. We have to write 64 bytes to fill up the local_64
buffer, followed by another 8 bytes to fill saved RBP, then we can inject our ROP chain.
In order for us to do a ret2libc, we first need to figure out where libc is located. This is because the address of libc is randomised with each run of the program. One way to get a libc leak is by viewing resolved GOT (Global Offset Table) entries. These are places in the binary wherein the libc addresses of libc functions are stored. The GOT entries are only resolved once the function has been called. I decided to leak the libc address of the puts
function. We can find the location of the GOT entry in the binary by debugging it in pwndbg, then using the got
command.
For example in the snippet below, the GOT entry is located at address 0x404000
in the binary and is resolved to the libc address 0x7ffff7dfee50
(which is our libc leak).
So how do we write the ROP chain to get the libc leak? Well, we can make use of code which the program already provides! Look at the following snippet at the end of the main
function:
So if we jump to 0x401264
with RDI set to 0x404000
, we can effectively do puts(GOT entry of puts)
which would give us our libc leak. So our ROP chain would be something like
Note that there is a p64(0)
at the end because of the additional pop rbp
instruction in main
.
But what do we do after that?
In order to exploit the vulnerability after getting our libc leak, we can append to the ROP chain and force it to call the vuln
function again. This allows us to have a second chance at exploiting the buffer overflow. In other words our first ROP chain would look something like this:
However, using this ROP chain would cause our program to crash. The reason is because vuln
starts with an endbr64
instruction, which would crash if our rsp
value is not 16-bit aligned. As such, we need to add a RET instruction before VULN in our ROP chain
So our finalised first ROP chain would be
After we run this ROP chain, we get the libc address of puts
. We can use this to calculate the address of libc since the offset between libc's address and the address of puts
in that same libc file is always the same. From one run of the program, my libc
was at 0x7f5dd6f88000
while my puts
was at 0x7f5dd700fbd0
. So I can just calculate the libc address as such:
Once we've gotten the leak and back to the vuln
function for the second time, we can finally call system('/bin/sh')
using the system
function in libc and the '/bin/sh'
function in libc. pwntools makes forming such an ROP chain trivial:
In summary, we:
Made a first ROP chain to do puts(puts_got)
to get a libc leak and re-enter vuln
Made a second ROP chain to call system('/bin/sh')
My full exploit is given below: