Introduction
If you do only one thing to improve your CTF performance, learn Python scripting. Nearly every CTF category -- crypto, forensics, pwn, web, reverse engineering -- eventually requires you to automate something: decode a stream of bytes, talk to a server, try 256 keys in a loop, or reassemble a scrambled file. Doing those things by hand is slow and error-prone. A ten-line Python script runs in milliseconds and never makes a typo.
Python is the lingua franca of CTF solving for good reason: its standard library covers binary I/O, encoding, sockets, and cryptographic primitives right out of the box, and the third-party pwntools library adds purpose-built helpers for talking to challenge servers. You do not need to be a software engineer to use it -- clear, imperative scripts are enough.
This guide walks through the most-used patterns in order: reading and writing binary files, encoding and decoding data, opening raw sockets, upgrading to pwntools, automating brute force loops, and a quick-reference card of one-liners you can copy directly into your terminal.
sudo apt install python3 python3-pip on Debian/Ubuntu. All code here is Python 3 only -- do not use Python 2.Reading and writing binary files
Many CTF files are not plain text. Images, compiled binaries, encrypted blobs, and network captures are all raw bytes. Python distinguishes between text mode ('r' / 'w') and binary mode ('rb' / 'wb'). Always open non-text files in binary mode or you will get encoding errors and corrupted data.
Reading a binary file
# Read the whole file into a bytes objectwith open('challenge.bin', 'rb') as f:data = f.read()# Print the first 32 bytes as hex, two digits eachprint(' '.join(f'{b:02x}' for b in data[:32]))# Check the file's magic bytes (first 4 bytes)print(data[:4]) # e.g. b'\x89PNG' for a PNG file
Unpacking structured binary data with struct
The struct module converts raw bytes into Python integers and floats using format strings. The prefix < means little-endian (x86 byte order), > means big-endian (network byte order).
import structwith open('challenge.bin', 'rb') as f:data = f.read()# Unpack the first 4 bytes as a little-endian unsigned 32-bit integer(value,) = struct.unpack('<I', data[:4])print(f'First dword: {value} (0x{value:08x})')# Unpack a header with multiple fieldsmagic, version, length = struct.unpack('<4sHI', data[:10])print(magic, version, length)
Writing bytes back to a file
# Patch one byte and write the result outpatched = bytearray(data) # mutable copypatched[0] = 0x89 # change first bytewith open('patched.bin', 'wb') as f:f.write(patched)
bytearray when you need to mutate bytes in place. bytes objects are immutable -- you cannot assign to individual positions.Encoding and decoding
Encoding and decoding is one of the most common tasks in CTF crypto and forensics challenges. Python's base64 module, hex literals, and XOR operations are all you need for the majority of cases.
Base64
import base64# Decode a base64 stringencoded = 'cGljb0NURntweXRob25fcm9ja3N9'decoded = base64.b64decode(encoded)print(decoded) # b'picoCTF{...}'print(decoded.decode()) # picoCTF{...}# Encode bytes to base64raw = b'hello CTF'print(base64.b64encode(raw).decode()) # aGVsbG8gQ1RG# URL-safe variant (replaces + and / with - and _)print(base64.urlsafe_b64encode(raw).decode())
Hex encoding
# bytes -> hex stringdata = b'\x70\x69\x63\x6f'print(data.hex()) # 7069636fprint(data.hex(' ')) # 70 69 63 6f (space-separated)# hex string -> bytesraw = bytes.fromhex('7069636f')print(raw) # b'pico'
XOR decryption
XOR is the single most common "encryption" in entry-level CTF challenges. A single-byte XOR key applied to every byte of ciphertext is trivially brute-forced -- only 256 possible keys.
ciphertext = bytes.fromhex('1b0f0d07171c1e5a1c0a5a071d1e5a0d1e04')key = 0x7f # known or guessed key# Decrypt by XORing every byte with the keyplaintext = bytes(b ^ key for b in ciphertext)print(plaintext.decode(errors='replace'))
ROT / Caesar cipher in Python
Python's str.maketrans and str.translate make ROT-N trivial. The challenge picoCTF 2023 / rotation is a classic ROT cipher -- here is the script that solves the general case:
def rot(text: str, n: int) -> str:uppers = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'lowers = 'abcdefghijklmnopqrstuvwxyz'table = str.maketrans(uppers + lowers,uppers[n:] + uppers[:n] +lowers[n:] + lowers[:n])return text.translate(table)ciphertext = 'Vhfrpbzr gb gur ebgngvba punyyratr!'# Try all 26 rotationsfor n in range(26):candidate = rot(ciphertext, n)if 'picoCTF' in candidate or 'flag' in candidate.lower():print(f'ROT-{n}: {candidate}')
Challenges that use encoding
Sockets: talking to challenge servers
Many picoCTF challenges give you a host and port -- nc challenge.picoctf.org 12345 -- and expect you to send crafted input and parse the response. Python's built-in socket module lets you do this programmatically, which is essential when the server sends a math problem you have to answer in under one second, or when you need to automate hundreds of requests.
Basic socket template
import socketHOST = 'challenge.picoctf.org'PORT = 12345with socket.create_connection((HOST, PORT)) as s:# Receive initial banner / promptbanner = s.recv(4096)print(banner.decode())# Send a response (must end with newline)s.sendall(b'hello\n')# Receive the next messageresponse = s.recv(4096)print(response.decode())
Receiving until a delimiter
Servers rarely send exactly one packet. Use a helper to accumulate data until you see a known delimiter like a prompt character or newline.
def recv_until(s: socket.socket, marker: bytes) -> bytes:buf = b''while marker not in buf:chunk = s.recv(1024)if not chunk:breakbuf += chunkreturn bufwith socket.create_connection((HOST, PORT)) as s:data = recv_until(s, b': ') # wait for a ': ' promptprint(data.decode())s.sendall(b'42\n')result = recv_until(s, b'\n')print(result.decode())
Challenges that use sockets
pwntools basics
Raw sockets work, but pwntools is dramatically nicer. It is a purpose-built CTF library that wraps sockets, processes, and binary packing into a clean API. Once you start using it you will rarely go back to raw sockets.
Install
pip install pwntools# or inside a virtual environment:python3 -m venv ctf-env && source ctf-env/bin/activatepip install pwntools
Connecting to a remote service
from pwn import *# Connect to a TCP servicer = remote('challenge.picoctf.org', 12345)# Receive until a specific string appearsr.recvuntil(b'Enter your answer: ')# Send a line (automatically appends \n)r.sendline(b'42')# Receive one lineflag_line = r.recvline()print(flag_line)r.close()
Binary packing and unpacking
For binary exploitation (pwn) challenges, you need to convert between integers and their little-endian byte representation. pwntools makes this one call.
from pwn import *# Pack a 32-bit integer (little-endian by default)payload = p32(0xdeadbeef) # b'\xef\xbe\xad\xde'payload = p64(0xdeadbeef) # 64-bit version# Unpack bytes back to an integerval = u32(b'\xef\xbe\xad\xde') # 3735928559 == 0xdeadbeefval = u64(b'\xef\xbe\xad\xde' + b'\x00' * 4)# Set target architecture context (affects p32/p64 defaults)context.arch = 'amd64'context.log_level = 'debug' # print all send/recv traffic
Full exploit template
from pwn import *r = remote('challenge.picoctf.org', 12345)# Read the challenge promptprompt = r.recvuntil(b'> ')log.info(prompt.decode())# Build and send payloadpayload = b'A' * 64 + p64(0x401337)r.sendline(payload)# Drop into an interactive shell if the server spawns oner.interactive()
recvuntil, recvline, and interactive() remove dozens of lines of buffer-management boilerplate. The debug logging (context.log_level = 'debug') prints every byte sent and received, which makes debugging far easier.Automating repeated operations
The moment a challenge requires you to try more than a handful of values, write a loop. A classic example is single-byte XOR: the key is one of 256 possible byte values, and you can check all of them in milliseconds by testing whether the decrypted output looks like English text.
Brute-forcing a single-byte XOR key
ciphertext = bytes.fromhex('1b580b1e0f590b1c0f59131c0459151e1d590b1e0f')def score(text: bytes) -> int:"""Count printable ASCII characters as a quality heuristic."""return sum(0x20 <= b < 0x7f for b in text)best_key, best_plain, best_score = 0, b'', 0for key in range(256): # try all 256 possible keyscandidate = bytes(b ^ key for b in ciphertext)s = score(candidate)if s > best_score:best_score = sbest_key = keybest_plain = candidateprint(f'Key: 0x{best_key:02x}')print(f'Plaintext: {best_plain}')
Automating a server interaction loop
Some challenges present 100 math problems and require all correct answers within a time limit. Here is the pattern:
from pwn import *r = remote('challenge.picoctf.org', 12345)for _ in range(100):line = r.recvline().decode().strip()# e.g. line = 'What is 347 + 891?'nums = [int(x) for x in line.split() if x.lstrip('-').isdigit()]answer = nums[0] + nums[1]r.sendline(str(answer).encode())print(r.recvall().decode())
Quick reference
Copy-paste one-liners for the operations you reach for most often in a CTF session.
base64.b64decode(s)EasyDecode a base64 string to bytes
bytes.fromhex('deadbeef')EasyConvert a hex string to raw bytes
data.hex()EasyConvert bytes to a hex string
bytes(b ^ key for b in data)EasyXOR-decrypt every byte with a fixed key
struct.unpack('<I', data[:4])MediumUnpack 4 bytes as little-endian uint32
p32(0xdeadbeef)MediumPack integer to 4-byte little-endian (pwntools)
r.recvuntil(b': ')MediumReceive data until a delimiter (pwntools)
r.sendline(b'payload')EasySend bytes + newline to a remote (pwntools)
for key in range(256):EasyBrute-force all single-byte XOR keys
Recommended workflow for a new Python CTF challenge
- Read the problem statement and identify the primitive: encoding, XOR, socket service, binary exploitation, or file manipulation.
- For encoding challenges, paste the ciphertext into a script and try
base64.b64decode,bytes.fromhex, and ROT brute-force first. - For socket services, start with a pwntools
remote(), print every line you receive, then build the response logic. - For binary files, open in
'rb'mode, print the first 16 bytes as hex to identify the format, then usestructto parse the header. - Enable
context.log_level = 'debug'in pwntools whenever you are confused about what the server is sending.