PwnTools

A Python library that helps in creating scripts for binary exploitation, doing many things automagically

PwnTools is a Python library and includes some command-line tools as well. You can install it to your current Python version with the following command:

python3 -m pip install pwntools

The full documentation is available on docs.pwntools.com:

Builtin binaries

When you install PwnTools, it comes with a few small but useful binaries for binary exploitation. Here are some and how to use them.

checksec - Check security protections

This tool checks a few security-related settings on a binary, which will help you visualize what attacks might work, and which ones won't. You can run it like checksec ./binary and see an output like the following:

In this output, green is safe, and red is unsafe. Note that if these are all green, it does not mean the binary has no vulnerabilities. It just means that you will have to use some other tricks during exploitation. Let's go over what these mean:

RELRO (RELocation Read-Only)

RELRO decides whether or not a few sections in the binary are read-only, preventing relocation tables from being overwritten in some cases. There are 3 possible values for this setting:

Stack canary

A stack canary is a reference to canaries in a coal mine. When a canary got sick, the miners would know it is unsafe here and stop mining. For a binary, it is the same idea: Right before the return address on the stack, a random value is placed. Then before actually returning at the ret instruction, it checks if this random value is still the same. If it was overwritten by a buffer overflow the value would change, and then the program would panic and exit before anything malicious can happen.

NX (Non-eXecutable stack)

Sometimes attackers will place shellcode on the stack, within their input. This can then be jumped to if they can control the Instruction Pointer, to execute arbitrary instructions, such as a shell.

With NX enabled, the stack is made non-executable to not allow any code to be run that came from the stack. This way, instructions are stored in the code section, and data is stored on the stack/heap.

In this case, an attacker would have to use existing instructions to execute what they want as they cannot add their own, often requiring some sort of Return-Oriented Programming (ROP).

PIE (Position Independent Executable)

With PIE enabled, the code of the binary will be loaded at a random memory location. This means you cannot use hardcoded addresses for functions or other instructions, as you won't know beforehand at what address they will be.

However, everything is offset together, meaning the relative addresses stay the same. So if you can leak any code address you can find all the relative addresses from that. If for some reason you can make a relative jump, this also won't stop you.

The address PwnTools shows for this when it is disabled means the base address the binary will always start from. This address is often 0x400000 by default, but it may be changed in the binary itself.

cyclic - Create cyclic patterns

$ cyclic 200
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab

And when you have triggered the vulnerability, you can take the value you find as the instruction pointer and reverse look it up again to find the exact offset in one go:

$ cyclic -l 0x6161616f
56

Useful syntax

The pwn Python library has many useful features, some more known than others. Here is a collection of a few pieces of syntax that can drastically simplify your exploit scripts.

Learn it once and you can never go back.

Importing

from pwn import *

Now all the functions and variables of pwntools are imported to the current context.

# Run a local binary
p = process("./binary")
# Connect to a remote host using TCP (same as `$ nc 10.10.10.10 1337`)
p = remote("10.10.10.10", 1337)
# Load an ELF binary
libc = ELF("./libc.so.6")
libc = ELF("./libc.so.6", checksec=False)  # Disable the default `checksec` on import

# Load a binary, and then create a process from it
context.binary = elf = ELF("./binary")
p = process()
p = process(aslr=False)  # You can also disable ASLR here

Interaction

# Open an interactive shell like `nc` would (useful at the end / for debugging)
p.interactive()

When working with interaction in PwnTools, you should almost exclusively use bytestings. These guarantee that your payload or text won't be misinterpreted by UTF-8 conversions, and can be easily created using the b"" syntax.

Receiving data

p.recvline()  # Receive until \n
p.recvuntil(b"here it comes: ")  # Receive until custom text
p.recv(42)  # Receive a specific amount of bytes
p.clean()  # Receive all for 0.05 seconds

# In pwntools >= 4.10.0 you can use regex with capture groups to extract text
p.recvregex(rb"0x([0-9a-f]+) and more", capture=True).group(1)

Sending data

p.sendline(b"Hello, world!")  # Send a line of input
p.send(b"text")  # Send raw bytes (no newline)

p.sendlineafter(b"> ", b"command")  # Send line after some data has been received

Creating payloads

Packing

# Pack an integer into bytes (little-endian)
p64(0xdeadbeef)  # b'\xef\xbe\xad\xde\x00\x00\x00\x00'
p32(0xdeadbeef)  # b'\xde\xad\xbe\xef'

# Set endianness to big
context.endian = "big"
p64(0xdeadbeef)  # b'\x00\x00\x00\x00\xde\xad\xbe\xef'

flat()

# Simply concatenate values, and automatically pack them
flat([b"some string", 1337])  # b'some string9\x05\x00\x00\x00\x00\x00\x00'

# Easily put values at specific offsets
flat({
    12: 0xdeadbeef
})  # b'aaaabaaacaaa\xef\xbe\xad\xde'
context.bits = 64  # Based on context variable
flat({
    12: 0xdeadbeef
})  # b'aaaabaaacaaa\xef\xbe\xad\xde\x00\x00\x00\x00'

# You can even put a list of values you want to place there
flat({
    4: [b"some string", 1337],
    32: 0xdeadbeef
})  # b'aaaasome string9\x05\x00\x00\x00\x00\x00\x00agaaahaaa\xef\xbe\xad\xde\x00\x00\x00\x00'

Last updated