Getting Started

To get your feet wet with pwntools, let’s first go through a few examples.

When writing exploits, pwntools generally follows the “kitchen sink” approach.

>>> from pwn import *

This imports a lot of functionality into the global namespace. You can now assemble, disassemble, pack, unpack, and many other things with a single function.

A full list of everything that is imported is available on from pwn import *.


A series of tutorials for Pwntools exists online, at

Making Connections

You need to talk to the challenge binary in order to pwn it, right? pwntools makes this stupid simple with its pwnlib.tubes module.

This exposes a standard interface to talk to processes, sockets, serial ports, and all manner of things, along with some nifty helpers for common tasks. For example, remote connections via pwnlib.tubes.remote.

>>> conn = remote('',21)
>>> conn.recvline() 
b'220 ...'
>>> conn.send(b'USER anonymous\r\n')
>>> conn.recvuntil(b' ', drop=True)
>>> conn.recvline()
b'Please specify the password.\r\n'
>>> conn.close()

It’s also easy to spin up a listener

>>> l = listen()
>>> r = remote('localhost', l.lport)
>>> c = l.wait_for_connection()
>>> r.send(b'hello')
>>> c.recv()

Interacting with processes is easy thanks to pwnlib.tubes.process.

>>> sh = process('/bin/sh')
>>> sh.sendline(b'sleep 3; echo hello world;')
>>> sh.recvline(timeout=1)
>>> sh.recvline(timeout=5)
b'hello world\n'
>>> sh.close()

Not only can you interact with processes programmatically, but you can actually interact with processes.

>>> sh.interactive() 
$ whoami

There’s even an SSH module for when you’ve got to SSH into a box to perform a local/setuid exploit with pwnlib.tubes.ssh. You can quickly spawn processes and grab the output, or spawn a process and interact with it like a process tube.

>>> shell = ssh('bandit0', '', password='bandit0', port=2220)
>>> shell['whoami']
>>> shell.download_file('/etc/motd')
>>> sh ='sh')
>>> sh.sendline(b'sleep 3; echo hello world;') 
>>> sh.recvline(timeout=1)
>>> sh.recvline(timeout=5)
b'hello world\n'
>>> shell.close()

Packing Integers

A common task for exploit-writing is converting between integers as Python sees them, and their representation as a sequence of bytes. Usually folks resort to the built-in struct module.

pwntools makes this easier with pwnlib.util.packing. No more remembering unpacking codes, and littering your code with helper routines.

>>> import struct
>>> p32(0xdeadbeef) == struct.pack('I', 0xdeadbeef)
>>> leet = unhex('37130000')
>>> u32(b'abcd') == struct.unpack('I', b'abcd')[0]

The packing/unpacking operations are defined for many common bit-widths.

>>> u8(b'A') == 0x41

Setting the Target Architecture and OS

The target architecture can generally be specified as an argument to the routine that requires it.

>>> asm('nop')
>>> asm('nop', arch='arm')
b'\x00\xf0 \xe3'

However, it can also be set once in the global context. The operating system, word size, and endianness can also be set here.

>>> context.arch      = 'i386'
>>> context.os        = 'linux'
>>> context.endian    = 'little'
>>> context.word_size = 32

Additionally, you can use a shorthand to set all of the values at once.

>>> asm('nop')
>>> context(arch='arm', os='linux', endian='big', word_size=32)
>>> asm('nop')
b'\xe3 \xf0\x00'

Setting Logging Verbosity

You can control the verbosity of the standard pwntools logging via context.

For example, setting

>>> context.log_level = 'debug'

Will cause all of the data sent and received by a tube to be printed to the screen.

Assembly and Disassembly

Never again will you need to run some already-assembled pile of shellcode from the internet! The pwnlib.asm module is full of awesome.

>>> enhex(asm('mov eax, 0'))

But if you do, it’s easy to suss out!

>>> print(disasm(unhex('6a0258cd80ebf9')))
   0:   6a 02                   push   0x2
   2:   58                      pop    eax
   3:   cd 80                   int    0x80
   5:   eb f9                   jmp    0x0

However, you shouldn’t even need to write your own shellcode most of the time! pwntools comes with the pwnlib.shellcraft module, which is loaded with useful time-saving shellcodes.

Let’s say that we want to setreuid(getuid(), getuid()) followed by dup`ing file descriptor 4 to `stdin, stdout, and stderr, and then pop a shell!

>>> enhex(asm(shellcraft.setreuid() + shellcraft.dupsh(4))) 

Misc Tools

Never write another hexdump, thanks to pwnlib.util.fiddling.

Find offsets in your buffer that cause a crash, thanks to pwnlib.cyclic.

>>> cyclic(20)
>>> # Assume EIP = 0x62616166 (b'faab' which is pack(0x62616166))  at crash time
>>> cyclic_find(b'faab')

ELF Manipulation

Stop hard-coding things! Look them up at runtime with pwnlib.elf.

>>> e = ELF('/bin/cat')
>>> print(hex(e.address)) 
>>> print(hex(e.symbols['write'])) 
>>> print(hex(['write'])) 
>>> print(hex(e.plt['write'])) 

You can even patch and save the files.

>>> e = ELF('/bin/cat')
>>>, 4)
>>> e.asm(e.address, 'ret')
>>> disasm(open('/tmp/quiet-cat','rb').read(1))
'   0:   c3                      ret'