Note that this is a very uncomprehensive guide and merely serves as the greatest-common-denominator use case when I’m solving pwn challenges. As such, there is much that pwntools has to offer that I will not cover either for brevity or because I am not aware of them.

That being said, I’ve broken this post down into four sections, the zeroth of which is a “preamble” that I use in all my scripts to set all the variables and initializes everything needed for the coming script. Sections one through three have some overlap but serve as slightly different setups for buffer overflow, format string, and shellcode vulnerabilities.

0. Preamble

This is mostly similar to pwninit’s auto-generated script with some slight modifications for the args in conn().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from pwn import *

exe = ELF("BINARY_NAME")

context.binary = exe


def conn():
	if args.REMOTE:
		r = remote("addr",1337)
	else:
		r = process([exe.path])
		if args.GDB:
			gdb.attach(r,gdbscript='''
			c
			''')
	return r

This allows us to reference our local copy of the binary via the variable ’exe’. This is a pwntools ELF object.

1
exe = ELF("BINARY_NAME")

Fills pwntools’ “context” feature with all the info it can find from the exe. Most importantly, it sets the endianness and architecture, which later functions will require to build our payloads appropriately.

1
context.binary = exe

This wraps up our connection in a tidy area based on what we’re doing. It will produce an object that we interact with in our main function dependent on args passed in execution of the script. The default spawns the local process for interaction, but if REMOTE is passed (e.g., python3 ./solve.py REMOTE) then it will switch over to the remote server during interaction. Local interactions may also optionally supply a GDB arg to attach GDB to the local process. (If you do this, you will likely have to set context.terminal which varies depending on your terminal setup. For tmux people, you can use context.terminal = ['tmux','splitw','-h'])

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def conn():
	if args.REMOTE:
		r = remote("addr",1337)
	else:
		r = process([exe.path])
		if args.GDB:
			gdb.attach(r,gdbscript='''
			c
			''')
	return r

1. Buffer Overflow / ret2win

We will assume that the preamble is in place. This script performs a very simple buffer overflow ret2win, where the return address we want to return to is stored in targetAddr.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
r = conn()


offset = 0
targetAddr = 0x0
payload = flat({
    offset: targetAddr
})
r.sendlineafter(b'DELIM_HERE', payload)

r.interactive()

flat() forms a payload of any length with a sequence of letters (the same as cyclic) and will do in-place replacement of these characters at an offset with some data in key:value format (offset:data). In this script, it will generate an offset number of characters, then place the data that targetAddr has at the end. It is important to note that flat() will automatically swap the endianness of data based on the context we set earlier.

1
2
3
payload = flat({
    offset: targetAddr
})

For example, if our offset is 8 and our target addr is 0x1337CAFE, then it will produce a (little-endian) payload: aaaabaaa\xfe\xca\x37\x13 You can also do multiple positions and data:

1
2
3
4
payload = flat({
    offset: targetAddr,
    offset2: 0x99999999
})

Note that if offset+len(targetAddr) < offset2, offset2 will overwrite the data in targetAddr. Make sure there is sufficient space to store addresses. If they are adjacent you may use a tuple and it will automatically make space for you.

1
2
3
flat({
    offset: [targetAddr, adjacentData, 0x0, 0x99999999]
})

2. Format String

Stuff here

3. Shellcode

Stuff here