ctf-agent 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +131 -0
- package/Dockerfile +192 -0
- package/README.md +522 -0
- package/agents/ctf-analyzer.md +58 -0
- package/agents/ctf-controller.md +151 -0
- package/agents/ctf-speedrun.md +74 -0
- package/bin/ctf-agent.js +119 -0
- package/docker-compose.yml +13 -0
- package/mcp_config.json +19 -0
- package/package.json +44 -0
- package/references/ctf-triage-ladder.md +68 -0
- package/references/exploit-databases.md +575 -0
- package/references/llm-safety-and-policy-compliance.md +173 -0
- package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
- package/references/security-events-and-intelligence.md +566 -0
- package/references/version-matrix.md +59 -0
- package/rules/ctf-execution-rules.md +44 -0
- package/rules/ctf-safety-framing-rules.md +140 -0
- package/rules/flag-validation-rules.md +25 -0
- package/scripts/__init__.py +1 -0
- package/scripts/ctf-tools.lock +205 -0
- package/scripts/ctf_init.py +1026 -0
- package/scripts/ctf_update.py +583 -0
- package/scripts/ctfd_client.py +81 -0
- package/scripts/cve_lookup.py +453 -0
- package/scripts/extract_flags.py +155 -0
- package/scripts/install_as_agent.py +278 -0
- package/scripts/install_ctf_tools.sh +603 -0
- package/scripts/parallel_triage.py +395 -0
- package/scripts/prompt_policy_sanitizer.py +283 -0
- package/scripts/scope_guard.py +444 -0
- package/scripts/skill_validator.py +504 -0
- package/scripts/workspace_cleaner.py +141 -0
- package/skills/ctf-ai-ml/SKILL.md +120 -0
- package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
- package/skills/ctf-ai-ml/llm-attacks.md +487 -0
- package/skills/ctf-ai-ml/model-attacks.md +422 -0
- package/skills/ctf-crypto/SKILL.md +320 -0
- package/skills/ctf-crypto/advanced-math.md +798 -0
- package/skills/ctf-crypto/classic-ciphers.md +651 -0
- package/skills/ctf-crypto/ecc-attacks.md +347 -0
- package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
- package/skills/ctf-crypto/exotic-crypto.md +528 -0
- package/skills/ctf-crypto/historical.md +113 -0
- package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
- package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
- package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
- package/skills/ctf-crypto/modern-ciphers.md +649 -0
- package/skills/ctf-crypto/prng-attacks.md +257 -0
- package/skills/ctf-crypto/prng.md +664 -0
- package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
- package/skills/ctf-crypto/rsa-attacks.md +487 -0
- package/skills/ctf-crypto/stream-ciphers.md +390 -0
- package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
- package/skills/ctf-forensics/3d-printing.md +121 -0
- package/skills/ctf-forensics/SKILL.md +379 -0
- package/skills/ctf-forensics/disk-advanced.md +497 -0
- package/skills/ctf-forensics/disk-and-memory.md +491 -0
- package/skills/ctf-forensics/disk-recovery.md +699 -0
- package/skills/ctf-forensics/linux-forensics.md +511 -0
- package/skills/ctf-forensics/network-advanced.md +583 -0
- package/skills/ctf-forensics/network.md +645 -0
- package/skills/ctf-forensics/peripheral-capture.md +287 -0
- package/skills/ctf-forensics/signals-and-hardware.md +713 -0
- package/skills/ctf-forensics/steganography.md +694 -0
- package/skills/ctf-forensics/stego-advanced-2.md +475 -0
- package/skills/ctf-forensics/stego-advanced.md +481 -0
- package/skills/ctf-forensics/stego-image.md +691 -0
- package/skills/ctf-forensics/windows.md +625 -0
- package/skills/ctf-malware/SKILL.md +181 -0
- package/skills/ctf-malware/c2-and-protocols.md +274 -0
- package/skills/ctf-malware/pe-and-dotnet.md +108 -0
- package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
- package/skills/ctf-misc/SKILL.md +498 -0
- package/skills/ctf-misc/bashjails.md +323 -0
- package/skills/ctf-misc/ctfd-navigation.md +465 -0
- package/skills/ctf-misc/dns.md +255 -0
- package/skills/ctf-misc/encodings-advanced.md +504 -0
- package/skills/ctf-misc/encodings.md +431 -0
- package/skills/ctf-misc/games-and-vms-2.md +254 -0
- package/skills/ctf-misc/games-and-vms-3.md +690 -0
- package/skills/ctf-misc/games-and-vms-4.md +229 -0
- package/skills/ctf-misc/games-and-vms.md +529 -0
- package/skills/ctf-misc/linux-privesc.md +333 -0
- package/skills/ctf-misc/pyjails.md +671 -0
- package/skills/ctf-misc/rf-sdr.md +91 -0
- package/skills/ctf-osint/SKILL.md +198 -0
- package/skills/ctf-osint/geolocation-and-media.md +464 -0
- package/skills/ctf-osint/social-media.md +312 -0
- package/skills/ctf-osint/web-and-dns.md +341 -0
- package/skills/ctf-pwn/SKILL.md +214 -0
- package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
- package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
- package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
- package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
- package/skills/ctf-pwn/advanced-exploits.md +773 -0
- package/skills/ctf-pwn/advanced.md +326 -0
- package/skills/ctf-pwn/field-notes.md +245 -0
- package/skills/ctf-pwn/format-string.md +694 -0
- package/skills/ctf-pwn/heap-fsop.md +285 -0
- package/skills/ctf-pwn/heap-techniques-2.md +333 -0
- package/skills/ctf-pwn/heap-techniques.md +513 -0
- package/skills/ctf-pwn/kernel-bypass.md +421 -0
- package/skills/ctf-pwn/kernel-techniques.md +366 -0
- package/skills/ctf-pwn/kernel.md +636 -0
- package/skills/ctf-pwn/overflow-basics.md +611 -0
- package/skills/ctf-pwn/rop-advanced.md +725 -0
- package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
- package/skills/ctf-pwn/sandbox-escape.md +313 -0
- package/skills/ctf-reverse/SKILL.md +163 -0
- package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
- package/skills/ctf-reverse/anti-analysis.md +693 -0
- package/skills/ctf-reverse/field-notes.md +376 -0
- package/skills/ctf-reverse/languages-compiled.md +666 -0
- package/skills/ctf-reverse/languages-platforms.md +592 -0
- package/skills/ctf-reverse/languages.md +553 -0
- package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
- package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
- package/skills/ctf-reverse/patterns-ctf.md +670 -0
- package/skills/ctf-reverse/patterns-runtime.md +274 -0
- package/skills/ctf-reverse/patterns.md +572 -0
- package/skills/ctf-reverse/platforms-hardware.md +387 -0
- package/skills/ctf-reverse/platforms.md +664 -0
- package/skills/ctf-reverse/tools-advanced-2.md +421 -0
- package/skills/ctf-reverse/tools-advanced.md +407 -0
- package/skills/ctf-reverse/tools-dynamic.md +679 -0
- package/skills/ctf-reverse/tools-emulation.md +319 -0
- package/skills/ctf-reverse/tools.md +573 -0
- package/skills/ctf-web/SKILL.md +153 -0
- package/skills/ctf-web/auth-and-access-2.md +82 -0
- package/skills/ctf-web/auth-and-access.md +783 -0
- package/skills/ctf-web/auth-infra.md +321 -0
- package/skills/ctf-web/auth-jwt.md +186 -0
- package/skills/ctf-web/client-side-advanced.md +739 -0
- package/skills/ctf-web/client-side.md +529 -0
- package/skills/ctf-web/cves.md +373 -0
- package/skills/ctf-web/field-notes.md +482 -0
- package/skills/ctf-web/node-and-prototype.md +200 -0
- package/skills/ctf-web/server-side-2.md +337 -0
- package/skills/ctf-web/server-side-advanced-2.md +559 -0
- package/skills/ctf-web/server-side-advanced-3.md +125 -0
- package/skills/ctf-web/server-side-advanced-4.md +480 -0
- package/skills/ctf-web/server-side-advanced.md +378 -0
- package/skills/ctf-web/server-side-deser.md +443 -0
- package/skills/ctf-web/server-side-exec-2.md +799 -0
- package/skills/ctf-web/server-side-exec.md +457 -0
- package/skills/ctf-web/server-side.md +629 -0
- package/skills/ctf-web/sql-injection.md +790 -0
- package/skills/ctf-web/web3.md +374 -0
- package/skills/ctf-writeup/SKILL.md +90 -0
- package/skills/solve-challenge/SKILL.md +269 -0
- package/skills.json +16 -0
|
@@ -0,0 +1,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
|