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,713 @@
1
+ # CTF Forensics - Signals and Hardware
2
+
3
+ ## Table of Contents
4
+ - [VGA Signal Decoding](#vga-signal-decoding)
5
+ - [HDMI TMDS Decoding](#hdmi-tmds-decoding)
6
+ - [DisplayPort 8b/10b + LFSR Decoding](#displayport-8b10b--lfsr-decoding)
7
+ - [Voyager Golden Record Audio (0xFun 2026)](#voyager-golden-record-audio-0xfun-2026)
8
+ - [Side-Channel Power Analysis (EHAX 2026)](#side-channel-power-analysis-ehax-2026)
9
+ - [Saleae Logic 2 UART Decode (EHAX 2026)](#saleae-logic-2-uart-decode-ehax-2026)
10
+ - [Flipper Zero .sub File (0xFun 2026)](#flipper-zero-sub-file-0xfun-2026)
11
+ - [Keyboard Acoustic Side-Channel (ApoorvCTF 2026)](#keyboard-acoustic-side-channel-apoorvctf-2026)
12
+ - [CD Audio Disc Image Steganography (BSidesSF 2026)](#cd-audio-disc-image-steganography-bsidessf-2026)
13
+ - [Caps-Lock LED Morse Code Extraction from Video (STEM CTF 2018)](#caps-lock-led-morse-code-extraction-from-video-stem-ctf-2018)
14
+ - [Linux input_event Keylogger Dump Parsing (Pwn2Win 2016)](#linux-input_event-keylogger-dump-parsing-pwn2win-2016)
15
+ - [I2C Bus Protocol Decoding (EKOPARTY CTF 2016)](#i2c-bus-protocol-decoding-ekoparty-ctf-2016)
16
+ - [IBM-29 Punched Card OCR (EKOPARTY CTF 2016)](#ibm-29-punched-card-ocr-ekoparty-ctf-2016)
17
+ - [Serial UART Data Decoding from WAV Audio (EasyCTF 2017)](#serial-uart-data-decoding-from-wav-audio-easyctf-2017)
18
+ - [USB MIDI Launchpad Traffic Reconstruction (Sthack 2017)](#usb-midi-launchpad-traffic-reconstruction-sthack-2017)
19
+ - [Tektronix Logic-Analyzer CSV Clock-Edge Extraction (35C3 2018)](#tektronix-logic-analyzer-csv-clock-edge-extraction-35c3-2018)
20
+
21
+ ---
22
+
23
+ ## VGA Signal Decoding
24
+
25
+ **Frame structure:** 800x525 total (640x480 active + blanking). Each sample = 5 bytes: R, G, B, HSync, VSync. Color is 6-bit (0-63).
26
+
27
+ ```python
28
+ import numpy as np
29
+ from PIL import Image
30
+
31
+ data = open('vga.bin', 'rb').read()
32
+
33
+ TOTAL_W, TOTAL_H = 800, 525
34
+ ACTIVE_W, ACTIVE_H = 640, 480
35
+ BYTES_PER_SAMPLE = 5 # R, G, B, hsync, vsync
36
+
37
+ # Parse raw samples
38
+ samples = np.frombuffer(data, dtype=np.uint8).reshape(-1, BYTES_PER_SAMPLE)
39
+ frame = samples.reshape(TOTAL_H, TOTAL_W, BYTES_PER_SAMPLE)
40
+
41
+ # Extract active region, scale 6-bit to 8-bit
42
+ active = frame[:ACTIVE_H, :ACTIVE_W, :3] # RGB only
43
+ img_arr = (active.astype(np.uint16) * 4).clip(0, 255).astype(np.uint8)
44
+ Image.fromarray(img_arr).save('vga_output.png')
45
+ ```
46
+
47
+ **Key lesson:** Total frame > visible area — always crop blanking. If colors look dark, check if 6-bit (multiply by 4).
48
+
49
+ ---
50
+
51
+ ## HDMI TMDS Decoding
52
+
53
+ **Structure:** 3 channels (R, G, B), each encoded as 10-bit TMDS (Transition-Minimized Differential Signaling) symbols. Bit 9 = inversion flag, bit 8 = XOR/XNOR mode. Decode is deterministic from MSBs down.
54
+
55
+ ```python
56
+ def tmds_decode(symbol_10bit):
57
+ """Decode a 10-bit TMDS symbol to 8-bit pixel value."""
58
+ bits = [(symbol_10bit >> i) & 1 for i in range(10)]
59
+ # bits[9] = inversion flag, bits[8] = XOR/XNOR mode
60
+
61
+ # Step 1: undo optional inversion (bit 9)
62
+ if bits[9]:
63
+ d = [1 - bits[i] for i in range(8)]
64
+ else:
65
+ d = [bits[i] for i in range(8)]
66
+
67
+ # Step 2: undo XOR/XNOR chain (bit 8 selects mode)
68
+ q = [d[0]]
69
+ if bits[8]:
70
+ for i in range(1, 8):
71
+ q.append(d[i] ^ q[i-1]) # XOR mode
72
+ else:
73
+ for i in range(1, 8):
74
+ q.append(d[i] ^ q[i-1] ^ 1) # XNOR mode
75
+
76
+ return sum(q[i] << i for i in range(8))
77
+
78
+ # Parse: read 10-bit symbols from binary, group into 3 channels
79
+ # Frame is 800x525 total, crop to 640x480 active
80
+ ```
81
+
82
+ **Identification:** Binary data with 10-bit aligned structure. Challenge mentions HDMI, DVI, or TMDS.
83
+
84
+ ---
85
+
86
+ ## DisplayPort 8b/10b + LFSR Decoding
87
+
88
+ **Structure:** 10-bit 8b/10b symbols decoded to 8-bit data, then LFSR-descrambled. Organized in 64-column Transport Units (60 data columns + 4 overhead).
89
+
90
+ ```python
91
+ # Standard 8b/10b decode table (partial — full table has 256 entries)
92
+ # Use a prebuilt table: map 10-bit symbol -> 8-bit data
93
+ # Key: running disparity tracks DC balance
94
+
95
+ # LFSR descrambler (x^16 + x^5 + x^4 + x^3 + 1)
96
+ def lfsr_descramble(data):
97
+ """DisplayPort LFSR descrambler. Resets on control symbols (BS/BE)."""
98
+ lfsr = 0xFFFF # Initial state
99
+ result = []
100
+ for byte in data:
101
+ out = byte
102
+ for bit_idx in range(8):
103
+ feedback = (lfsr >> 15) & 1
104
+ out ^= (feedback << bit_idx)
105
+ new_bit = ((lfsr >> 15) ^ (lfsr >> 4) ^ (lfsr >> 3) ^ (lfsr >> 2)) & 1
106
+ lfsr = ((lfsr << 1) | new_bit) & 0xFFFF
107
+ result.append(out & 0xFF)
108
+ return bytes(result)
109
+
110
+ # Transport Unit layout: 64 columns per TU
111
+ # Columns 0-59: pixel data (RGB)
112
+ # Columns 60-63: overhead (sync, stuffing)
113
+ # LFSR resets on control bytes (BS=0x1C, BE=0xFB)
114
+ ```
115
+
116
+ **Key lesson:** LFSR scrambler resets on control bytes — identify these to synchronize descrambling. Without reset points, output is garbled.
117
+
118
+ ---
119
+
120
+ ## Voyager Golden Record Audio (0xFun 2026)
121
+
122
+ **Pattern (11 Lines of Contact):** Analog image encoded as audio. Sync pulses (sharp negative spikes) delimit scan lines. Amplitude between pulses = pixel brightness.
123
+
124
+ ```python
125
+ import numpy as np
126
+ from scipy.io import wavfile
127
+ from PIL import Image
128
+
129
+ rate, audio = wavfile.read('golden_record.wav')
130
+ audio = audio.astype(np.float32)
131
+
132
+ # Find sync pulses (sharp negative spikes below threshold)
133
+ threshold = np.min(audio) * 0.7
134
+ sync_indices = np.where(audio < threshold)[0]
135
+
136
+ # Group consecutive sync samples into pulse starts
137
+ pulses = [sync_indices[0]]
138
+ for i in range(1, len(sync_indices)):
139
+ if sync_indices[i] - sync_indices[i-1] > 100:
140
+ pulses.append(sync_indices[i])
141
+
142
+ # Extract scan lines between pulses, resample to fixed width
143
+ WIDTH = 512
144
+ lines = []
145
+ for i in range(len(pulses) - 1):
146
+ line = audio[pulses[i]:pulses[i+1]]
147
+ resampled = np.interp(np.linspace(0, len(line)-1, WIDTH), np.arange(len(line)), line)
148
+ lines.append(resampled)
149
+
150
+ # Normalize and save as image
151
+ img_arr = np.array(lines)
152
+ img_arr = ((img_arr - img_arr.min()) / (img_arr.max() - img_arr.min()) * 255).astype(np.uint8)
153
+ Image.fromarray(img_arr).save('voyager_image.png')
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Side-Channel Power Analysis (EHAX 2026)
159
+
160
+ **Pattern (Power Leak):** Power consumption traces recorded during cryptographic operations. Correct key guesses cause measurably different power consumption at specific sample points.
161
+
162
+ **Data format:** Typically a multi-dimensional array: `[positions × guesses × traces × samples]`. E.g., 6 digit positions × 10 guesses (0-9) × 20 traces × 50 samples.
163
+
164
+ **Attack (Differential Power Analysis):**
165
+ ```python
166
+ import numpy as np
167
+ import hashlib
168
+
169
+ # Load power traces: shape = (positions, guesses, traces, samples)
170
+ data = np.load('power_traces.npy') # or parse from CSV/JSON
171
+ n_positions, n_guesses, n_traces, n_samples = data.shape
172
+
173
+ # For each position, find the guess with maximum power at the leak point
174
+ key_digits = []
175
+ for pos in range(n_positions):
176
+ # Average across traces for each guess
177
+ avg_power = data[pos].mean(axis=1) # shape: (guesses, samples)
178
+
179
+ # Find the sample point with maximum power variance across guesses
180
+ # This is the "leak point" where the correct guess stands out
181
+ variance_per_sample = avg_power.var(axis=0)
182
+ leak_sample = np.argmax(variance_per_sample)
183
+
184
+ # The guess with maximum power at the leak point is correct
185
+ best_guess = np.argmax(avg_power[:, leak_sample])
186
+ key_digits.append(best_guess)
187
+
188
+ key = ''.join(str(d) for d in key_digits)
189
+ print(f"Recovered key: {key}")
190
+
191
+ # Flag may be SHA256 of the key
192
+ flag = hashlib.sha256(key.encode()).hexdigest()
193
+ ```
194
+
195
+ **Identification:** Challenge mentions "power", "side-channel", "leakage", "traces", or "measurements". Data is a multi-dimensional numeric array with axes for positions/guesses/traces/samples.
196
+
197
+ **Key insight:** The "leak point" is the sample index where correct vs incorrect guesses show the largest power difference. Average across traces first to reduce noise, then find the sample with maximum variance across guesses.
198
+
199
+ ---
200
+
201
+ ## Saleae Logic 2 UART Decode (EHAX 2026)
202
+
203
+ **Pattern (Baby Serial):** Saleae Logic 2 `.sal` file (ZIP archive) containing digital channel captures. Data encoded as UART serial.
204
+
205
+ **File structure:** `.sal` is a ZIP containing `digital-0.bin` through `digital-7.bin` + `meta.json`. Only channel 0 typically has data.
206
+
207
+ **Binary format (digital-*.bin):**
208
+ ```text
209
+ <SALEAE> magic (8 bytes)
210
+ version: u32 = 2
211
+ type: u32 = 100 (digital)
212
+ initial_state: u32 (0 or 1)
213
+ ... header fields ...
214
+ Delta-encoded transitions (variable-length integers)
215
+ ```
216
+
217
+ **Delta encoding:** Each value represents the number of samples between state transitions. The signal alternates between HIGH and LOW at each delta.
218
+
219
+ **UART decode from deltas:**
220
+ ```python
221
+ import numpy as np
222
+
223
+ # Parse deltas from binary (after header)
224
+ # Reconstruct signal timeline
225
+ times = np.cumsum(deltas)
226
+ states = []
227
+ state = initial_state
228
+ for d in deltas:
229
+ states.append(state)
230
+ state ^= 1 # toggle on each transition
231
+
232
+ # UART decode: detect start bit (HIGH→LOW), sample 8 data bits at bit centers
233
+ # Baud rate detection: most common delta ≈ samples_per_bit
234
+ # At 1MHz sample rate: 115200 baud ≈ 8.7 samples/bit
235
+
236
+ def uart_decode(transitions, sample_rate=1_000_000, baud=115200):
237
+ bit_period = sample_rate / baud
238
+ bytes_out = []
239
+ i = 0
240
+ while i < len(transitions):
241
+ # Find start bit (falling edge)
242
+ if transitions[i] == 0: # LOW = start bit
243
+ byte_val = 0
244
+ for bit in range(8):
245
+ sample_time = (1.5 + bit) * bit_period # center of each bit
246
+ # Sample signal at this offset from start bit
247
+ bit_val = get_signal_at(sample_time)
248
+ byte_val |= (bit_val << bit) # LSB first
249
+ bytes_out.append(byte_val)
250
+ i += 1
251
+ return bytes(bytes_out)
252
+ ```
253
+
254
+ **Common pitfalls:**
255
+ - **Inverted polarity:** UART idle is HIGH (mark). If initial_state=1, the encoding may be inverted — try both
256
+ - **Baud rate guessing:** Check common rates: 9600, 19200, 38400, 57600, 115200, 230400
257
+ - **Output format:** Decoded bytes may be base64-encoded (containing a PNG image or text)
258
+ - **Saleae internal format ≠ export format:** The `.sal` internal binary uses a different encoding than CSV/binary export. Parse the raw delta transitions directly
259
+
260
+ **Quick approach:** Install Saleae Logic 2, open the `.sal` file, add UART analyzer with auto-baud detection, export decoded data.
261
+
262
+ ---
263
+
264
+ ## Flipper Zero .sub File (0xFun 2026)
265
+
266
+ RAW_Data binary -> filter noise bytes (0x80-0xFF) -> expand batch variable references -> XOR with hint text.
267
+
268
+ **Key insight:** Flipper Zero `.sub` files contain raw RF signal data. The RAW_Data field encodes binary as pulse timings. Filter out noise bytes (0x80-0xFF), expand any batch variable references, and XOR with hint text from the challenge to recover the flag.
269
+
270
+ ---
271
+
272
+ ## Keyboard Acoustic Side-Channel (ApoorvCTF 2026)
273
+
274
+ **Pattern (Author on the Run):** Recover typed text from audio recordings of keystrokes. Reference audio provides labeled samples (known keys), flag audio contains unknown keystrokes to classify.
275
+
276
+ **Step 1 — Detect keystrokes via energy peaks:**
277
+ ```python
278
+ import numpy as np
279
+ from scipy.signal import find_peaks
280
+ from scipy.io import wavfile
281
+
282
+ sr, audio = wavfile.read('flag.wav')
283
+ if audio.ndim > 1:
284
+ audio = audio.mean(axis=1)
285
+
286
+ # Sliding window energy envelope (10ms window)
287
+ win = int(0.01 * sr)
288
+ energy = np.array([np.sum(audio[i:i+win]**2) for i in range(0, len(audio) - win, win)])
289
+
290
+ # Find peaks with minimum 175ms separation
291
+ min_dist = int(0.175 * sr / win)
292
+ peaks, _ = find_peaks(energy, height=0.03 * energy.max(), distance=min_dist)
293
+ ```
294
+
295
+ **Step 2 — Extract MFCC features per keystroke:**
296
+ ```python
297
+ import librosa
298
+
299
+ def extract_features(audio, sr, peak_sample, window_ms=10):
300
+ win = int(window_ms / 1000 * sr)
301
+ start = max(0, peak_sample - win // 2)
302
+ segment = audio[start:start + win]
303
+ mfccs = librosa.feature.mfcc(y=segment.astype(float), sr=sr, n_mfcc=20)
304
+ return np.concatenate([mfccs.mean(axis=1), mfccs.std(axis=1)]) # 40-dim
305
+ ```
306
+
307
+ **Step 3 — Classify with KNN against labeled reference:**
308
+ ```python
309
+ from sklearn.neighbors import KNeighborsClassifier
310
+
311
+ # Build reference from labeled audio (26 keys × 50 presses each)
312
+ X_ref, y_ref = [], []
313
+ for key_idx, key in enumerate('abcdefghijklmnopqrstuvwxyz'):
314
+ for peak in reference_peaks[key_idx * 50:(key_idx + 1) * 50]:
315
+ X_ref.append(extract_features(ref_audio, sr, peak))
316
+ y_ref.append(key)
317
+
318
+ knn = KNeighborsClassifier(n_neighbors=5)
319
+ knn.fit(X_ref, y_ref)
320
+
321
+ # Classify flag keystrokes
322
+ flag = ''.join(knn.predict([extract_features(flag_audio, sr, p) for p in flag_peaks]))
323
+ ```
324
+
325
+ **Key insight:** Window size is critical — 10ms captures the initial impact transient which is most distinctive per key. Larger windows (20-30ms) include key release noise that reduces classification accuracy. Use all individual reference samples rather than averaging, as KNN handles variance better with more data points.
326
+
327
+ **Detection:** Two audio files provided (reference + target), or challenge mentions "typing", "keyboard", "acoustic".
328
+
329
+ ---
330
+
331
+ ## CD Audio Disc Image Steganography (BSidesSF 2026)
332
+
333
+ **Pattern (cdimage):** Visual images encoded as pit/land patterns on a CD surface. A `.cdda` file (raw CD Digital Audio) contains only two byte values (e.g., `0x0d` and `0xa8`) representing reflective lands and non-reflective pits. When rendered as a spiral on a disc image, the binary pattern forms readable text or images — similar to LightScribe but using the data layer.
334
+
335
+ **Key components:**
336
+ 1. **CIRC de-interleaving** — CD audio data is Cross-Interleaved for error correction. The encoding tool (e.g., [arduinocelentano/cdimage](https://github.com/arduinocelentano/cdimage)) pre-interleaves data to compensate. To decode, reverse the CIRC interleaving before rendering.
337
+ 2. **Spiral geometry** — bytes per track increases linearly: `tr(n) = tr0 + n * dtr`, physical radius `r(n) = r0 + n * dr`. Default params: `tr0=22951.52`, `dtr=1.387`, `r0=24.5mm`.
338
+ 3. **Polar-to-Cartesian rendering** — accumulate byte values into a polar grid `(radius_pixel, angle_bin)`, then convert to a circular disc image.
339
+
340
+ **De-interleaving (CIRC reverse):**
341
+
342
+ ```python
343
+ import numpy as np
344
+
345
+ def deinterleave_cdda(data):
346
+ """Reverse CIRC pre-interleaving from cdimage tool."""
347
+ D = 4
348
+ delays = [
349
+ -24*(3), -24*(1*D+2)+1, 8-24*(2*D+3), 8-24*(3*D+2)+1,
350
+ 16-24*(4*D+3), 16-24*(5*D+2)+1, 2-24*(6*D+3), 2-24*(7*D+2)+1,
351
+ 10-24*(8*D+3), 10-24*(9*D+2)+1, 18-24*(10*D+3), 18-24*(11*D+2)+1,
352
+ 4-24*(16*D+1), 4-24*(17*D)+1, 12-24*(18*D+1), 12-24*(19*D)+1,
353
+ 20-24*(20*D+1), 20-24*(21*D)+1, 6-24*(22*D+1), 6-24*(23*D)+1,
354
+ 14-24*(24*D+1), 14-24*(25*D)+1, 22-24*(26*D+1), 22-24*(27*D)+1
355
+ ]
356
+ # Build per-output-index offset: output[g*24+i] came from input[g*24+i + offset[i]]
357
+ offsets = [0] * 24
358
+ for pinf in range(24):
359
+ i = delays[pinf] % 24
360
+ if i < 0:
361
+ i += 24
362
+ dg = (i - delays[pinf]) // 24
363
+ offsets[i] = -(111 - dg) * 24 + (pinf - i)
364
+
365
+ total = len(data)
366
+ result = np.zeros(total, dtype=np.uint8)
367
+ for i in range(24):
368
+ out_pos = np.arange(i, total, 24, dtype=np.int64)
369
+ in_pos = out_pos + offsets[i]
370
+ valid = (in_pos >= 0) & (in_pos < total)
371
+ result[in_pos[valid]] = data[out_pos[valid]]
372
+ return result
373
+ ```
374
+
375
+ **Rendering de-interleaved data to disc image:**
376
+
377
+ ```python
378
+ from PIL import Image
379
+
380
+ def render_cdda_disc(data, img_size=1024, tr0=22951.52052, dtr=1.3865961805,
381
+ r0=24.5, rcd=57.5, scale=0.115, n_angle_bins=8192,
382
+ bright_byte=0x0d):
383
+ """Render de-interleaved CDDA data as a circular disc image."""
384
+ center = img_size // 2
385
+ dr = dtr * r0 / tr0
386
+ polar_sum = np.zeros((img_size, n_angle_bins), dtype=np.float64)
387
+ polar_count = np.zeros((img_size, n_angle_bins), dtype=np.float64)
388
+
389
+ tr, r, pos, c_float = tr0, r0, 0, 0.0
390
+ total = len(data)
391
+ while c_float < (800 * 1024 * 1024 - tr) and pos < total:
392
+ itr = int(tr)
393
+ r_px = int(r / scale)
394
+ if 0 <= r_px < img_size:
395
+ end = min(pos + itr, total)
396
+ chunk = data[pos:end]
397
+ n_tb = len(chunk)
398
+ if n_tb > 0:
399
+ angles = (np.arange(n_tb, dtype=np.int64) * n_angle_bins // n_tb) % n_angle_bins
400
+ is_bright = (chunk == bright_byte).astype(np.float64)
401
+ np.add.at(polar_sum[r_px], angles, is_bright)
402
+ np.add.at(polar_count[r_px], angles, 1.0)
403
+ c_float += tr
404
+ ic = pos + itr
405
+ while int(c_float) > ic:
406
+ ic += 1
407
+ pos = ic
408
+ tr += dtr
409
+ r += dr
410
+
411
+ density = np.where(polar_count > 0, polar_sum / polar_count, 0)
412
+ ys, xs = np.mgrid[0:img_size, 0:img_size]
413
+ dx, dy = (xs - center).astype(float), (ys - center).astype(float)
414
+ r_arr = np.sqrt(dx * dx + dy * dy).astype(int)
415
+ theta = np.arctan2(-dy, dx)
416
+ theta[theta < 0] += 2 * np.pi
417
+ a_idx = (theta / (2 * np.pi) * n_angle_bins).astype(int) % n_angle_bins
418
+ output = density[np.clip(r_arr, 0, img_size - 1), a_idx]
419
+ output[(r_arr < int(r0 / scale)) | (r_arr > int(rcd / scale))] = 0
420
+ return Image.fromarray((output * 255).astype(np.uint8))
421
+
422
+ # Full pipeline
423
+ data = np.fromfile('flag.cdda', dtype=np.uint8)
424
+ deinterleaved = deinterleave_cdda(data)
425
+ img = render_cdda_disc(deinterleaved)
426
+ img.save('disc_output.png')
427
+ ```
428
+
429
+ **Key insight:** Without CIRC de-interleaving, the radial structure (bright/dark rings) is visible but angular detail (text) is completely scrambled. The interleaving spreads each byte across ~108 groups (~2592 bytes), which at typical track lengths (~30K-50K bytes/revolution) shifts angular positions by up to 30 degrees — enough to destroy any readable pattern. The calibration image confirms correct decoding by showing known text.
430
+
431
+ **Calibration workflow:** The challenge provides `calibrate_img.cdda` with a known output (`calibrate_img.png` showing "Calibrate: 0123456789abc..."). Use this pair to verify geometry parameters (tr0, dtr, r0, scale) before decoding the flag file.
432
+
433
+ **Detection:** Challenge mentions "album", "CD rip", "CDDA", or provides large (~800MB) files with only 2 unique byte values. The `file` command reports "ISO-8859 text with CR line terminators" because `0x0d` (CR) is one of the two values.
434
+
435
+ ---
436
+
437
+ ## Caps-Lock LED Morse Code Extraction from Video (STEM CTF 2018)
438
+
439
+ **Pattern:** Extract Morse code from a security camera video by tracking the caps-lock LED pixel on a keyboard using OpenCV frame-by-frame analysis.
440
+
441
+ ```python
442
+ import cv2
443
+
444
+ vidcap = cv2.VideoCapture('SecurityCamera.mp4')
445
+ morse = []
446
+ while vidcap.isOpened():
447
+ ret, frame = vidcap.read()
448
+ if not ret: break
449
+ r, g, b = frame[58, 686] # caps-lock LED pixel coordinate
450
+ is_on = r > 200 and g > 200 and b > 200
451
+ morse.append(is_on)
452
+
453
+ # Convert on/off durations to dots, dashes, and spaces
454
+ # Short on = dot, long on = dash, medium off = letter space, long off = word space
455
+ durations = []
456
+ current = morse[0]
457
+ count = 0
458
+ for state in morse:
459
+ if state == current:
460
+ count += 1
461
+ else:
462
+ durations.append((current, count))
463
+ current = state
464
+ count = 1
465
+ durations.append((current, count))
466
+
467
+ # Calibrate thresholds from observed durations
468
+ # Typical: dot=2-4 frames, dash=6-10 frames, letter gap=4-6 frames, word gap=10+ frames
469
+ MORSE_MAP = {
470
+ '.-': 'A', '-...': 'B', '-.-.': 'C', '-..': 'D', '.': 'E',
471
+ '..-.': 'F', '--.': 'G', '....': 'H', '..': 'I', '.---': 'J',
472
+ '-.-': 'K', '.-..': 'L', '--': 'M', '-.': 'N', '---': 'O',
473
+ '.--.': 'P', '--.-': 'Q', '.-.': 'R', '...': 'S', '-': 'T',
474
+ '..-': 'U', '...-': 'V', '.--': 'W', '-..-': 'X', '-.--': 'Y',
475
+ '--..': 'Z', '.----': '1', '..---': '2', '...--': '3',
476
+ '....-': '4', '.....': '5', '-....': '6', '--...': '7',
477
+ '---..': '8', '----.': '9', '-----': '0',
478
+ }
479
+ ```
480
+
481
+ **Key insight:** Keyboard LEDs (caps lock, num lock, scroll lock) can be programmatically controlled and are visible in security camera footage. Track a specific pixel coordinate across video frames; on/off durations encode Morse code (short=dot, long=dash).
482
+
483
+ **Detection:** Video of a keyboard where an LED blinks irregularly. Challenge mentions "security camera", "keyboard", "blinking", or "Morse".
484
+
485
+ ---
486
+
487
+ ## Linux input_event Keylogger Dump Parsing (Pwn2Win 2016)
488
+
489
+ Raw binary dump with 24-byte repeating structure matching Linux's `struct input_event` (`struct timeval` + `__u16 type` + `__u16 code` + `__s32 value`). Filter for `type == EV_KEY (1)` and `value == 1` (key press), map keycodes via Linux kernel's `input-event-codes.h`.
490
+
491
+ ```python
492
+ import struct
493
+ with open('dump.bin', 'rb') as f:
494
+ while data := f.read(24):
495
+ tv_sec, tv_usec, type_, code, value = struct.unpack('<QQHHi', data)
496
+ if type_ == 1 and value == 1: # EV_KEY, key press
497
+ print(f"Key code: {code}") # Map via input-event-codes.h
498
+ ```
499
+
500
+ **Key insight:** `/dev/input/event*` captures have a fixed 24-byte `struct input_event` format. Filter EV_KEY type with value=1 for key presses. Map codes using Linux kernel header `input-event-codes.h`.
501
+
502
+ **Detection:** Binary file size divisible by 24. Challenge mentions keylogger, keyboard, or input device.
503
+
504
+ ---
505
+
506
+ ## I2C Bus Protocol Decoding (EKOPARTY CTF 2016)
507
+
508
+ Logic analyzer captures of I2C (Inter-Integrated Circuit) bus communications. Decode SDA (data) and SCL (clock) signals to extract transmitted bytes.
509
+
510
+ ```python
511
+ def decode_i2c(sda_signal, scl_signal):
512
+ """Decode I2C protocol from logic analyzer capture
513
+ Channel 0 = SDA (data), Channel 1 = SCL (clock)
514
+
515
+ I2C framing:
516
+ - START: SDA falls while SCL is high
517
+ - STOP: SDA rises while SCL is high
518
+ - Data: SDA sampled on SCL rising edge
519
+ - ACK: 9th bit (low = ACK, high = NACK)
520
+ """
521
+ bytes_out = []
522
+ current_byte = 0
523
+ bit_count = 0
524
+ in_frame = False
525
+
526
+ for i in range(len(scl_signal) - 1):
527
+ # Detect START condition
528
+ if sda_signal[i] == 1 and sda_signal[i+1] == 0 and scl_signal[i] == 1:
529
+ in_frame = True
530
+ bit_count = 0
531
+ current_byte = 0
532
+ continue
533
+
534
+ # Detect STOP condition
535
+ if sda_signal[i] == 0 and sda_signal[i+1] == 1 and scl_signal[i] == 1:
536
+ in_frame = False
537
+ continue
538
+
539
+ # Sample data on SCL rising edge
540
+ if in_frame and scl_signal[i] == 0 and scl_signal[i+1] == 1:
541
+ if bit_count < 8:
542
+ current_byte = (current_byte << 1) | sda_signal[i+1]
543
+ bit_count += 1
544
+ elif bit_count == 8:
545
+ bytes_out.append(current_byte)
546
+ bit_count = 0
547
+ current_byte = 0
548
+
549
+ return bytes_out
550
+
551
+ # Tools: Saleae Logic 2, sigrok/PulseView, OLS (Open Logic Sniffer)
552
+ # Import: File > Open Logic Sniffer capture
553
+ # Decode: Analyzers > I2C > Set SDA/SCL channels
554
+ ```
555
+
556
+ **Key insight:** I2C uses only 2 wires (SDA + SCL). START/STOP conditions occur when SDA changes while SCL is high. Data bits are sampled on SCL rising edges. Every 9th bit is an ACK. Use logic analyzer software (Saleae, sigrok) for automated decoding.
557
+
558
+ ---
559
+
560
+ ## IBM-29 Punched Card OCR (EKOPARTY CTF 2016)
561
+
562
+ Decode IBM-29 keypunch card images by detecting hole positions in a standard 80-column x 12-row grid.
563
+
564
+ ```python
565
+ from PIL import Image
566
+
567
+ # IBM-29 character encoding: column punch pattern -> character
568
+ IBM_029_MAP = {
569
+ (12,): 'A', (12,1): 'A', (12,2): 'B', (12,3): 'C', # etc.
570
+ (11,): '-', (11,1): 'J', (11,2): 'K', # etc.
571
+ (0,): '0', (1,): '1', (2,): '2', # zone 0 + digit
572
+ # Full mapping: http://www.columbia.edu/cu/computinghistory/029.html
573
+ }
574
+
575
+ def decode_punched_card(image_path, cols=80, rows=12,
576
+ x_spacing=7, y_spacing=20, x_offset=10, y_offset=10):
577
+ """Detect punches in card image and decode to text"""
578
+ img = Image.open(image_path).convert('L')
579
+ text = ""
580
+
581
+ for col in range(cols):
582
+ punches = []
583
+ for row in range(rows):
584
+ x = x_offset + col * x_spacing
585
+ y = y_offset + row * y_spacing
586
+ pixel = img.getpixel((x, y))
587
+ if pixel > 200: # white = punched hole
588
+ punches.append(row)
589
+
590
+ if punches:
591
+ key = tuple(punches)
592
+ text += IBM_029_MAP.get(key, '?')
593
+ else:
594
+ text += ' '
595
+
596
+ return text
597
+
598
+ # Process multiple card images
599
+ for i in range(14):
600
+ card_text = decode_punched_card(f'card_{i:02d}.png')
601
+ print(f"Card {i}: {card_text}")
602
+ ```
603
+
604
+ **Key insight:** IBM punched cards use a 12-row x 80-column grid. Each character is encoded by 1-3 holes in a column. The grid spacing varies by card reader/scanner resolution -- calibrate by measuring the distance between known reference holes. White/light pixels indicate punched holes.
605
+
606
+ ---
607
+
608
+ ## Serial UART Data Decoding from WAV Audio (EasyCTF 2017)
609
+
610
+ Audio files can contain serial (UART) data encoded as square wave signals. Decode by sampling amplitude levels and parsing bit timing.
611
+
612
+ ```python
613
+ import struct
614
+
615
+ with open('signal.wav', 'rb') as f:
616
+ f.read(44) # skip WAV header
617
+ samples = []
618
+ while True:
619
+ data = f.read(2)
620
+ if not data: break
621
+ samples.append(struct.unpack('<h', data)[0])
622
+
623
+ # Parameters: 9600 baud, 1 start bit, 8 data bits, no parity, 2 stop bits
624
+ SAMPLES_PER_BIT = len(samples) // expected_bits # ~40 for 9600 baud @ 384kHz
625
+ THRESHOLD = 0 # above = 1, below = 0
626
+
627
+ # Convert samples to bits
628
+ bits = [1 if s > THRESHOLD else 0 for s in samples]
629
+
630
+ # Find frames: start bit (0) + 8 data bits + stop bits (1,1)
631
+ output = []
632
+ i = 0
633
+ while i < len(bits) - 11:
634
+ if bits[i] == 0: # start bit
635
+ byte_bits = bits[i+1:i+9] # LSB first
636
+ byte_val = sum(b << j for j, b in enumerate(byte_bits))
637
+ output.append(byte_val)
638
+ i += 11 # skip start + 8 data + 2 stop
639
+ else:
640
+ i += 1
641
+
642
+ print(bytes(output))
643
+ ```
644
+
645
+ **Key insight:** UART serial data in audio appears as a square wave with well-defined bit timing. Key parameters to determine: baud rate (samples per bit), frame format (start/stop bits, parity), and bit endianness (UART is LSB-first). The start bit (low) provides synchronization for each byte frame.
646
+
647
+ **Detection:** WAV file with a clean square wave pattern visible in Audacity. Two distinct amplitude levels with regular timing. Challenge mentions "serial", "UART", "baud", or "RS-232".
648
+
649
+ ---
650
+
651
+ ## USB MIDI Launchpad Traffic Reconstruction (Sthack 2017)
652
+
653
+ USB traffic from MIDI controller devices (e.g., Novation Launchpad) encodes button presses as MIDI Note On/Off messages that can be reconstructed into visual patterns.
654
+
655
+ ```python
656
+ from scapy.all import rdpcap
657
+
658
+ pkts = rdpcap('capture.pcapng')
659
+ # Filter USB bulk transfer packets for MIDI data
660
+ # Launchpad MIDI: 0x90 = Note On, 0x80 = Note Off
661
+ # Format: [status, key, velocity]
662
+ # Key encodes (row, col): key = row*16 + col
663
+
664
+ characters = []
665
+ current_grid = [[0]*8 for _ in range(8)]
666
+
667
+ for pkt in pkts:
668
+ data = bytes(pkt)
669
+ # Find MIDI messages in USB payload
670
+ if len(data) >= 4:
671
+ status = data[-3]
672
+ key = data[-2]
673
+ velocity = data[-1]
674
+
675
+ if status == 0x90 and velocity > 0: # Note On
676
+ row, col = key // 16, key % 16
677
+ if 0 <= row < 8 and 0 <= col < 8:
678
+ current_grid[row][col] = 1
679
+ elif status == 0x80 or (status == 0x90 and velocity == 0): # Note Off
680
+ # All-off sequence = character separator
681
+ if all(current_grid[r][c] == 0 for r in range(8) for c in range(8)):
682
+ characters.append(current_grid)
683
+ current_grid = [[0]*8 for _ in range(8)]
684
+ ```
685
+
686
+ **Key insight:** MIDI devices use standardized message formats. Novation Launchpad maps its 8x8 grid to MIDI notes where `key = row*16 + col`. Note On (0x90) with velocity > 0 = button lit, Note Off (0x80) = button off. Sequences of all-off messages separate characters displayed on the grid.
687
+
688
+ **Detection:** USB PCAP with bulk transfer packets containing 3-byte or 4-byte payloads. USB device descriptor shows MIDI class (Audio class, subclass MIDI Streaming). Challenge mentions "MIDI", "Launchpad", "music controller", or "grid".
689
+
690
+ ---
691
+
692
+ ## Tektronix Logic-Analyzer CSV Clock-Edge Extraction (35C3 2018)
693
+
694
+ **Pattern:** Tektronix logic analyzers export multi-channel captures as ~10-MB CSV files with one column per signal (CLK, R, G, B, ...). Parse the file with Python, detect rising edges on the CLK column, and sample the data columns at each edge to reconstruct the transmitted image/stream.
695
+
696
+ ```python
697
+ import csv
698
+ with open('capture.csv') as f:
699
+ reader = csv.reader(f)
700
+ prev_clk = 0
701
+ bits = []
702
+ for row in reader:
703
+ try: clk = int(row[1])
704
+ except ValueError: continue
705
+ if prev_clk == 0 and clk == 1: # rising edge
706
+ bits.append((int(row[2]), int(row[3]), int(row[4])))
707
+ prev_clk = clk
708
+ # Reshape bits into image and render with PIL
709
+ ```
710
+
711
+ **Key insight:** Logic-analyzer CSV is always edge-sampled. Identify the clock column by its 50%-duty cycle, then sample the data columns synchronously at every rising edge. Works for any synchronous bus (RGB, SPI, I²C clock line).
712
+
713
+ **References:** 35C3 CTF 2018 — box of blink, writeup 12907