@ecopoesis/homebridge-dmx 0.1.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/CLAUDE.md +176 -0
- package/README.md +146 -0
- package/config.schema.json +143 -0
- package/dist/cli/stick.d.ts +3 -0
- package/dist/cli/stick.d.ts.map +1 -0
- package/dist/cli/stick.js +176 -0
- package/dist/cli/stick.js.map +1 -0
- package/dist/color/cct.d.ts +3 -0
- package/dist/color/cct.d.ts.map +1 -0
- package/dist/color/cct.js +33 -0
- package/dist/color/cct.js.map +1 -0
- package/dist/color/dimmer.d.ts +3 -0
- package/dist/color/dimmer.d.ts.map +1 -0
- package/dist/color/dimmer.js +16 -0
- package/dist/color/dimmer.js.map +1 -0
- package/dist/color/hsvcct.d.ts +3 -0
- package/dist/color/hsvcct.d.ts.map +1 -0
- package/dist/color/hsvcct.js +52 -0
- package/dist/color/hsvcct.js.map +1 -0
- package/dist/color/index.d.ts +7 -0
- package/dist/color/index.d.ts.map +1 -0
- package/dist/color/index.js +28 -0
- package/dist/color/index.js.map +1 -0
- package/dist/color/parsers.d.ts +20 -0
- package/dist/color/parsers.d.ts.map +1 -0
- package/dist/color/parsers.js +232 -0
- package/dist/color/parsers.js.map +1 -0
- package/dist/color/rgb.d.ts +3 -0
- package/dist/color/rgb.d.ts.map +1 -0
- package/dist/color/rgb.js +32 -0
- package/dist/color/rgb.js.map +1 -0
- package/dist/color/rgbaw.d.ts +3 -0
- package/dist/color/rgbaw.d.ts.map +1 -0
- package/dist/color/rgbaw.js +48 -0
- package/dist/color/rgbaw.js.map +1 -0
- package/dist/color/rgbw.d.ts +3 -0
- package/dist/color/rgbw.d.ts.map +1 -0
- package/dist/color/rgbw.js +37 -0
- package/dist/color/rgbw.js.map +1 -0
- package/dist/color/rgbww.d.ts +3 -0
- package/dist/color/rgbww.d.ts.map +1 -0
- package/dist/color/rgbww.js +54 -0
- package/dist/color/rgbww.js.map +1 -0
- package/dist/color/types.d.ts +64 -0
- package/dist/color/types.d.ts.map +1 -0
- package/dist/color/types.js +91 -0
- package/dist/color/types.js.map +1 -0
- package/dist/config.d.ts +60 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +191 -0
- package/dist/config.js.map +1 -0
- package/dist/controller.d.ts +32 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +128 -0
- package/dist/controller.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +17 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +96 -0
- package/dist/platform.js.map +1 -0
- package/dist/platformAccessory.d.ts +18 -0
- package/dist/platformAccessory.d.ts.map +1 -0
- package/dist/platformAccessory.js +81 -0
- package/dist/platformAccessory.js.map +1 -0
- package/dist/settings.d.ts +14 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +15 -0
- package/dist/settings.js.map +1 -0
- package/dist/stick/frame.d.ts +16 -0
- package/dist/stick/frame.d.ts.map +1 -0
- package/dist/stick/frame.js +51 -0
- package/dist/stick/frame.js.map +1 -0
- package/dist/stick/kdf.d.ts +23 -0
- package/dist/stick/kdf.d.ts.map +1 -0
- package/dist/stick/kdf.js +90 -0
- package/dist/stick/kdf.js.map +1 -0
- package/dist/stick/protocol.d.ts +28 -0
- package/dist/stick/protocol.d.ts.map +1 -0
- package/dist/stick/protocol.js +78 -0
- package/dist/stick/protocol.js.map +1 -0
- package/dist/stick/session.d.ts +50 -0
- package/dist/stick/session.d.ts.map +1 -0
- package/dist/stick/session.js +301 -0
- package/dist/stick/session.js.map +1 -0
- package/examples/dmx.yaml +65 -0
- package/package.json +51 -0
- package/src/cli/stick.ts +198 -0
- package/src/color/cct.ts +40 -0
- package/src/color/dimmer.ts +24 -0
- package/src/color/hsvcct.ts +59 -0
- package/src/color/index.ts +34 -0
- package/src/color/parsers.ts +232 -0
- package/src/color/rgb.ts +33 -0
- package/src/color/rgbaw.ts +46 -0
- package/src/color/rgbw.ts +36 -0
- package/src/color/rgbww.ts +56 -0
- package/src/color/types.ts +150 -0
- package/src/config.ts +248 -0
- package/src/controller.ts +136 -0
- package/src/index.ts +9 -0
- package/src/platform.ts +118 -0
- package/src/platformAccessory.ts +102 -0
- package/src/settings.ts +18 -0
- package/src/stick/frame.ts +70 -0
- package/src/stick/kdf.ts +99 -0
- package/src/stick/protocol.ts +90 -0
- package/src/stick/session.ts +348 -0
- package/stick-de3.json +6 -0
- package/tools/aes-hook.c +1404 -0
- package/tools/analyze-pcap.mjs +143 -0
- package/tools/capture-aes-key.lldb +25 -0
- package/tools/capture-and-verify.sh +46 -0
- package/tools/capture-handshake.sh +59 -0
- package/tools/capture-send.sh +39 -0
- package/tools/crack-kdf.mjs +121 -0
- package/tools/crack-key.mjs +170 -0
- package/tools/decode-frame.mjs +110 -0
- package/tools/decrypt-011c.mjs +123 -0
- package/tools/decrypt-dmx.mjs +130 -0
- package/tools/decrypt-ssl2.ts +154 -0
- package/tools/derive-dmx-key.mjs +189 -0
- package/tools/diff-011c.mjs +127 -0
- package/tools/dump-pcap-summary.mjs +54 -0
- package/tools/dump-tcp-stream.mjs +123 -0
- package/tools/extract-011c.mjs +117 -0
- package/tools/extract-frame.mjs +78 -0
- package/tools/find-011c-key.mjs +140 -0
- package/tools/find-iv.mjs +153 -0
- package/tools/ghidra/README.md +136 -0
- package/tools/ghidra/__pycache__/dump-011c-callers.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-011c-handler.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-aes-candidates.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-anchors.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-auth2.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-authenticate.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-block-cipher.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-cipher.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-ctor-chain.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-curve.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-encompassing.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-handshake.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-kdf-chain.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-kdf-context.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-profile.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-real-encrypt.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-secret.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-setsoftware.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-stick3-vtable.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-swxref.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-vt-key-getter.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-cipher.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-ctor.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-md5.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-secret-users.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-stick3-crypto.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-stick3-device.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-stick3a-ctor.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-stick3anet-ctor.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-vtables.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/string-walk.cpython-313.pyc +0 -0
- package/tools/ghidra/dump-011c-callers.py +140 -0
- package/tools/ghidra/dump-011c-handler.py +158 -0
- package/tools/ghidra/dump-aes-candidates.py +99 -0
- package/tools/ghidra/dump-anchors.py +192 -0
- package/tools/ghidra/dump-auth2.py +50 -0
- package/tools/ghidra/dump-authenticate.py +71 -0
- package/tools/ghidra/dump-block-cipher.py +88 -0
- package/tools/ghidra/dump-cipher.py +106 -0
- package/tools/ghidra/dump-ctor-chain.py +107 -0
- package/tools/ghidra/dump-curve.py +112 -0
- package/tools/ghidra/dump-encompassing.py +146 -0
- package/tools/ghidra/dump-handshake.py +76 -0
- package/tools/ghidra/dump-kdf-chain.py +105 -0
- package/tools/ghidra/dump-kdf-context.py +72 -0
- package/tools/ghidra/dump-profile.py +59 -0
- package/tools/ghidra/dump-real-encrypt.py +90 -0
- package/tools/ghidra/dump-secret.py +84 -0
- package/tools/ghidra/dump-setsoftware.py +63 -0
- package/tools/ghidra/dump-stick3-vtable.py +182 -0
- package/tools/ghidra/dump-swxref.py +27 -0
- package/tools/ghidra/dump-vt-key-getter.py +170 -0
- package/tools/ghidra/find-cipher.py +210 -0
- package/tools/ghidra/find-ctor.py +109 -0
- package/tools/ghidra/find-md5.py +97 -0
- package/tools/ghidra/find-secret-users.py +98 -0
- package/tools/ghidra/find-stick3-crypto.py +190 -0
- package/tools/ghidra/find-stick3-device.py +111 -0
- package/tools/ghidra/find-stick3a-ctor.py +87 -0
- package/tools/ghidra/find-stick3anet-ctor.py +76 -0
- package/tools/ghidra/find-vtables.py +165 -0
- package/tools/ghidra/out-011c-callers-run.log +51 -0
- package/tools/ghidra/out-011c-run.log +50 -0
- package/tools/ghidra/out-vt-key-run.log +55 -0
- package/tools/ghidra/string-walk.py +180 -0
- package/tools/hmac-key.sh +51 -0
- package/tools/hwm-entitlements.plist +18 -0
- package/tools/inspect-plaintext.mjs +87 -0
- package/tools/lldb-capture-aes.py +137 -0
- package/tools/lldb_hmac_key.py +93 -0
- package/tools/lldb_pathA_scan.py +341 -0
- package/tools/lldb_pathC_keytrace.py +300 -0
- package/tools/lldb_pathD_kdf_watch.py +281 -0
- package/tools/pathA-launch-hwm.sh +86 -0
- package/tools/pathA-scan.sh +52 -0
- package/tools/pathC-keytrace.sh +49 -0
- package/tools/pathC-run.sh +71 -0
- package/tools/pathD-kdf-watch.sh +51 -0
- package/tools/quick-trigger.mjs +86 -0
- package/tools/run-hooked-hwmanager.sh +51 -0
- package/tools/scan-aes-schedule.mjs +135 -0
- package/tools/send_dmx.mjs +480 -0
- package/tools/stick-status.mjs +57 -0
- package/tools/test-aes-detector.mjs +100 -0
- package/tools/test-tcp-write.ts +274 -0
- package/tools/test-udp-dmx.ts +324 -0
- package/tools/test-x25519.mjs +87 -0
- package/tools/try-decrypt.ts +144 -0
- package/tools/try-hardcoded-key.mjs +86 -0
- package/tools/try-hardcoded-key.ts +103 -0
- package/tools/try-key.mjs +103 -0
- package/tools/verify-011c-key.mjs +121 -0
- package/tools/verify-kdf.mjs +142 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Deep analysis of a Stick-DE3 pcap: TCP connections + per-connection opcode
|
|
2
|
+
// timeline, and UDP DMX-frame header statistics (fieldA / seq progression,
|
|
3
|
+
// the +0x12 port field, timing). Built to diff a real Hardware Manager
|
|
4
|
+
// session against our send_dmx run.
|
|
5
|
+
//
|
|
6
|
+
// node tools/analyze-pcap.mjs <file.pcap>
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
|
|
10
|
+
const [, , pcapPath] = process.argv;
|
|
11
|
+
if (!pcapPath) { console.error('usage: node tools/analyze-pcap.mjs <file.pcap>'); process.exit(1); }
|
|
12
|
+
|
|
13
|
+
const buf = fs.readFileSync(pcapPath);
|
|
14
|
+
const magic = buf.readUInt32LE(0);
|
|
15
|
+
const le = magic === 0xa1b2c3d4;
|
|
16
|
+
if (!le && magic !== 0xd4c3b2a1) { console.error('bad pcap'); process.exit(2); }
|
|
17
|
+
const linkType = le ? buf.readUInt32LE(20) : buf.readUInt32BE(20);
|
|
18
|
+
const ethSkip = linkType === 1 ? 14 : 4;
|
|
19
|
+
|
|
20
|
+
const pkts = [];
|
|
21
|
+
let off = 24;
|
|
22
|
+
while (off + 16 <= buf.length) {
|
|
23
|
+
const tsSec = le ? buf.readUInt32LE(off) : buf.readUInt32BE(off);
|
|
24
|
+
const tsUsec = le ? buf.readUInt32LE(off + 4) : buf.readUInt32BE(off + 4);
|
|
25
|
+
const capLen = le ? buf.readUInt32LE(off + 8) : buf.readUInt32BE(off + 8);
|
|
26
|
+
off += 16;
|
|
27
|
+
if (off + capLen > buf.length) break;
|
|
28
|
+
const pkt = buf.subarray(off, off + capLen);
|
|
29
|
+
off += capLen;
|
|
30
|
+
if (pkt.length < ethSkip + 20) continue;
|
|
31
|
+
const ip = pkt.subarray(ethSkip);
|
|
32
|
+
if (((ip[0] >> 4) & 0xf) !== 4) continue;
|
|
33
|
+
const ihl = (ip[0] & 0x0f) * 4;
|
|
34
|
+
const proto = ip[9];
|
|
35
|
+
const src = `${ip[12]}.${ip[13]}.${ip[14]}.${ip[15]}`;
|
|
36
|
+
const dst = `${ip[16]}.${ip[17]}.${ip[18]}.${ip[19]}`;
|
|
37
|
+
const l4 = ip.subarray(ihl);
|
|
38
|
+
const t = tsSec + tsUsec / 1e6;
|
|
39
|
+
if (proto === 6) {
|
|
40
|
+
const sp = l4.readUInt16BE(0), dp = l4.readUInt16BE(2);
|
|
41
|
+
const flags = l4[13];
|
|
42
|
+
const dataOff = (l4[12] >> 4) * 4;
|
|
43
|
+
pkts.push({ t, proto: 'TCP', src, dst, sp, dp, flags, payload: Buffer.from(l4.subarray(dataOff)) });
|
|
44
|
+
} else if (proto === 17) {
|
|
45
|
+
const sp = l4.readUInt16BE(0), dp = l4.readUInt16BE(2);
|
|
46
|
+
const ulen = l4.readUInt16BE(4) - 8;
|
|
47
|
+
pkts.push({ t, proto: 'UDP', src, dst, sp, dp, payload: Buffer.from(l4.subarray(8, 8 + ulen)) });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const t0 = pkts.length ? pkts[0].t : 0;
|
|
52
|
+
const rel = (t) => (t - t0).toFixed(3).padStart(9);
|
|
53
|
+
|
|
54
|
+
// ── TCP connections ─────────────────────────────────────────────────────────
|
|
55
|
+
console.log(`\n=== ${pcapPath} ===`);
|
|
56
|
+
console.log(`${pkts.length} IP packets, span ${(pkts[pkts.length-1].t - t0).toFixed(1)}s\n`);
|
|
57
|
+
|
|
58
|
+
const conns = new Map();
|
|
59
|
+
for (const p of pkts) {
|
|
60
|
+
if (p.proto !== 'TCP') continue;
|
|
61
|
+
const key = [p.src + ':' + p.sp, p.dst + ':' + p.dp].sort().join(' <-> ');
|
|
62
|
+
if (!conns.has(key)) conns.set(key, []);
|
|
63
|
+
conns.get(key).push(p);
|
|
64
|
+
}
|
|
65
|
+
console.log(`TCP connections: ${conns.size}`);
|
|
66
|
+
for (const [key, ps] of conns) {
|
|
67
|
+
const dur = ps[ps.length - 1].t - ps[0].t;
|
|
68
|
+
const dataP = ps.filter((p) => p.payload.length > 0);
|
|
69
|
+
console.log(`\n ${key} start=${rel(ps[0].t)}s dur=${dur.toFixed(3)}s pkts=${ps.length} (${dataP.length} w/ payload)`);
|
|
70
|
+
for (const p of dataP) {
|
|
71
|
+
const dir = p.sp === 2431 ? 'S->C' : 'C->S';
|
|
72
|
+
let op = '';
|
|
73
|
+
const pl = p.payload;
|
|
74
|
+
if (pl.length >= 10 && (pl.subarray(0,8).toString()==='Stick_3A' || pl.subarray(0,8).toString()==='LSAG_ALL')) {
|
|
75
|
+
op = `op=0x${pl.readUInt16LE(8).toString(16).padStart(4,'0')}`;
|
|
76
|
+
}
|
|
77
|
+
console.log(` ${rel(p.t)} ${dir} ${String(pl.length).padStart(4)}B ${op.padEnd(11)} ${pl.subarray(0,28).toString('hex')}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── UDP DMX frames ──────────────────────────────────────────────────────────
|
|
82
|
+
const dmx = pkts.filter((p) => p.proto === 'UDP' && p.payload.length === 576);
|
|
83
|
+
console.log(`\n\nUDP 576-byte DMX frames: ${dmx.length}`);
|
|
84
|
+
if (dmx.length) {
|
|
85
|
+
const f = dmx[0], h = f.payload;
|
|
86
|
+
console.log(` src ${f.src}:${f.sp} -> dst ${f.dst}:${f.dp}`);
|
|
87
|
+
console.log(` first frame header (32B): ${h.subarray(0,32).toString('hex')}`);
|
|
88
|
+
const fieldA0 = h.readUInt32LE(0x0a);
|
|
89
|
+
console.log(` +0x08 opcode = 0x${h.readUInt16LE(8).toString(16)}`);
|
|
90
|
+
console.log(` +0x0a fieldA = ${fieldA0} (0x${fieldA0.toString(16)}) [8B: ${h.subarray(0x0a,0x12).toString('hex')}]`);
|
|
91
|
+
console.log(` +0x12 port = ${h.readUInt16LE(0x12)}`);
|
|
92
|
+
console.log(` +0x14 chanCount = ${h.readUInt16LE(0x14)}`);
|
|
93
|
+
console.log(` +0x16 const = ${h[0x16]}`);
|
|
94
|
+
console.log(` +0x17 seq = ${h[0x17]}`);
|
|
95
|
+
console.log(` +0x18 nonce = ${h.subarray(0x18,0x20).toString('hex')}`);
|
|
96
|
+
|
|
97
|
+
// progressions
|
|
98
|
+
let dT = [], dA = [], dS = [], ports = new Set(), chans = new Set();
|
|
99
|
+
for (let i = 1; i < dmx.length; i++) {
|
|
100
|
+
dT.push(dmx[i].t - dmx[i-1].t);
|
|
101
|
+
let a = dmx[i].payload.readUInt32LE(0x0a) - dmx[i-1].payload.readUInt32LE(0x0a);
|
|
102
|
+
let s = dmx[i].payload[0x17] - dmx[i-1].payload[0x17];
|
|
103
|
+
if (s < 0) s += 256;
|
|
104
|
+
dA.push(a); dS.push(s);
|
|
105
|
+
}
|
|
106
|
+
for (const p of dmx) { ports.add(p.payload.readUInt16LE(0x12)); chans.add(p.payload.readUInt16LE(0x14)); }
|
|
107
|
+
const uniq = (arr) => [...new Set(arr)].sort((a,b)=>a-b);
|
|
108
|
+
const avg = (arr) => arr.reduce((s,x)=>s+x,0)/arr.length;
|
|
109
|
+
console.log(`\n fieldA delta/frame : ${uniq(dA).join(',')} (start ${fieldA0}, end ${dmx[dmx.length-1].payload.readUInt32LE(0x0a)})`);
|
|
110
|
+
console.log(` seq delta/frame : ${uniq(dS).join(',')}`);
|
|
111
|
+
console.log(` +0x12 port values : ${[...ports].join(',')}`);
|
|
112
|
+
console.log(` +0x14 chanCount : ${[...chans].join(',')}`);
|
|
113
|
+
console.log(` inter-frame gap : avg ${(avg(dT)*1000).toFixed(1)}ms (${(1/avg(dT)).toFixed(1)} Hz)`);
|
|
114
|
+
console.log(` first DMX frame at : ${rel(dmx[0].t)}s last at ${rel(dmx[dmx.length-1].t)}s`);
|
|
115
|
+
|
|
116
|
+
// ── unified-counter contiguity check ──────────────────────────────────────
|
|
117
|
+
// HWM draws fieldA (UDP) and the TCP token from ONE monotonic counter.
|
|
118
|
+
// Build a merged time-ordered list of (TCP token) + (UDP fieldA) and verify
|
|
119
|
+
// the UDP frames continue the TCP token sequence without going backwards.
|
|
120
|
+
const tcpTok = [];
|
|
121
|
+
for (const p of pkts) {
|
|
122
|
+
if (p.proto !== 'TCP' || p.payload.length < 18) continue;
|
|
123
|
+
const m = p.payload.subarray(0, 8).toString();
|
|
124
|
+
if (m !== 'Stick_3A' && m !== 'LSAG_ALL') continue;
|
|
125
|
+
if (p.sp === 2431) continue; // client->Stick only
|
|
126
|
+
const op = p.payload.readUInt16LE(8);
|
|
127
|
+
tcpTok.push({ t: p.t, kind: 'TCP', op, val: p.payload.readUInt32LE(10) });
|
|
128
|
+
}
|
|
129
|
+
const merged = [...tcpTok, ...dmx.map((p) => ({ t: p.t, kind: 'UDP', val: p.payload.readUInt32LE(0x0a) }))]
|
|
130
|
+
.sort((a, b) => a.t - b.t);
|
|
131
|
+
// last TCP token before the first UDP frame
|
|
132
|
+
const firstUdpT = dmx[0].t;
|
|
133
|
+
const tcpBefore = tcpTok.filter((x) => x.t < firstUdpT);
|
|
134
|
+
const lastTok = tcpBefore.length ? tcpBefore[tcpBefore.length - 1].val : null;
|
|
135
|
+
console.log(`\n unified counter:`);
|
|
136
|
+
console.log(` last TCP token before 1st UDP frame : ${lastTok}`);
|
|
137
|
+
console.log(` first UDP fieldA : ${dmx[0].payload.readUInt32LE(0x0a)}`);
|
|
138
|
+
let backwards = 0, prev = -1;
|
|
139
|
+
for (const e of merged) { if (e.val < prev) backwards++; prev = Math.max(prev, e.val); }
|
|
140
|
+
const contiguous = lastTok != null && dmx[0].payload.readUInt32LE(0x0a) === lastTok + 1;
|
|
141
|
+
console.log(` UDP fieldA == lastTCPtoken + 1 ? : ${contiguous ? 'YES' : 'NO'}`);
|
|
142
|
+
console.log(` backwards steps in merged sequence : ${backwards} ${backwards ? '<-- Stick will drop these' : '(monotonic OK)'}`);
|
|
143
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Capture the Stick-DE3 DMX AES-128 key from the SYMBOLIZED old HardwareManager.
|
|
2
|
+
#
|
|
3
|
+
# Target: 2024-03-21 ESA2 build (symbol-rich; NOT the stripped EsaPro2 one).
|
|
4
|
+
# Gladman AES: aes_encrypt_key128(const u8 *key, aes_encrypt_ctx *cx)
|
|
5
|
+
# x86_64 SysV at entry: $rdi = ptr to 16-byte raw key, $rsi = ctx
|
|
6
|
+
#
|
|
7
|
+
# ATTACH workflow (launch-under-lldb fails: Rosetta + hardened runtime):
|
|
8
|
+
# 1. Open ESA2 Hardware Manager normally. Do NOT connect the Stick yet.
|
|
9
|
+
# 2. PID=$(pgrep -f 'ESA2/HardwareManager/HardwareManager.app.*HardwareManager$')
|
|
10
|
+
# 3. lldb -p "$PID" -s tools/capture-aes-key.lldb
|
|
11
|
+
# 4. After it prints "continuing — connect the Stick now", in Hardware
|
|
12
|
+
# Manager connect to the Stick (192.168.96.2) and start DMX/live output.
|
|
13
|
+
#
|
|
14
|
+
# Each aes_encrypt_key128 hit prints the 16-byte key + caller backtrace
|
|
15
|
+
# (the backtrace = the per-session key-derivation chain for the plugin).
|
|
16
|
+
# IV not needed live: IV = aes_decrypt(C0) XOR P0 from a captured 576B frame.
|
|
17
|
+
|
|
18
|
+
break set -n aes_encrypt_key128
|
|
19
|
+
break command add -o "memory read --size 1 --format x --count 16 $rdi" -o "register read rdi rsi" -o "thread backtrace -c 16" -o "continue" 1
|
|
20
|
+
|
|
21
|
+
break set -n aes_encrypt_key
|
|
22
|
+
break command add -o "memory read --size 1 --format x --count 16 $rdi" -o "register read rdi rsi" -o "thread backtrace -c 16" -o "continue" 2
|
|
23
|
+
|
|
24
|
+
script print(">>> breakpoints armed; continuing — connect the Stick now <<<")
|
|
25
|
+
continue
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Capture ONE encrypted DMX frame from the live Stick stream, then try to
|
|
3
|
+
# decrypt it with the hardcoded 33-byte secret discovered via Ghidra.
|
|
4
|
+
#
|
|
5
|
+
# Prereqs (already installed):
|
|
6
|
+
# - tcpdump (built-in on macOS)
|
|
7
|
+
# - node (for the pcap parser + decrypt test)
|
|
8
|
+
# That's it. No tshark, no tsx.
|
|
9
|
+
#
|
|
10
|
+
# Setup:
|
|
11
|
+
# 1. Open Hardware Manager and connect to the Stick at 192.168.96.2.
|
|
12
|
+
# 2. Open the DMX/fader screen so HWM is actively streaming live DMX.
|
|
13
|
+
# 3. Run this script. It needs sudo for tcpdump's raw socket.
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
cd "$(dirname "$0")/.."
|
|
17
|
+
|
|
18
|
+
IFACE="${IFACE:-en0}"
|
|
19
|
+
STICK_IP="${STICK_IP:-192.168.96.2}"
|
|
20
|
+
STICK_PORT="${STICK_PORT:-2431}"
|
|
21
|
+
DUR="${DUR:-3}"
|
|
22
|
+
PCAP=/tmp/stick-dmx.pcap
|
|
23
|
+
FRAME=/tmp/stick-dmx-frame.bin
|
|
24
|
+
|
|
25
|
+
echo ">>> Capturing ${DUR}s of UDP→${STICK_IP}:${STICK_PORT} on ${IFACE}"
|
|
26
|
+
echo ">>> (HWM must be connected + streaming DMX right now)"
|
|
27
|
+
sudo tcpdump -i "$IFACE" -w "$PCAP" -G "$DUR" -W 1 \
|
|
28
|
+
"udp and host ${STICK_IP} and port ${STICK_PORT}" \
|
|
29
|
+
2>/dev/null || true
|
|
30
|
+
|
|
31
|
+
if [[ ! -s "$PCAP" ]]; then
|
|
32
|
+
echo "!!! No packets captured. Is HWM connected and streaming?"
|
|
33
|
+
exit 2
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
echo
|
|
37
|
+
echo ">>> pcap size: $(wc -c < "$PCAP") bytes"
|
|
38
|
+
|
|
39
|
+
echo
|
|
40
|
+
echo ">>> Extracting first 576-byte UDP payload"
|
|
41
|
+
STICK_IP="$STICK_IP" STICK_PORT="$STICK_PORT" \
|
|
42
|
+
node tools/extract-frame.mjs "$PCAP" "$FRAME"
|
|
43
|
+
|
|
44
|
+
echo
|
|
45
|
+
echo ">>> Running try-hardcoded-key.mjs against the captured frame"
|
|
46
|
+
node tools/try-hardcoded-key.mjs "$FRAME"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Capture a COMPLETE HWM<->Stick session: TCP/2431 (control + handshake) +
|
|
3
|
+
# UDP/2431 (encrypted DMX stream). We need the handshake from the very first
|
|
4
|
+
# byte, so HWM should be DISCONNECTED from the Stick before running this.
|
|
5
|
+
#
|
|
6
|
+
# Workflow:
|
|
7
|
+
# 1. In HWM: disconnect from the Stick (Network panel -> Disconnect).
|
|
8
|
+
# 2. Run this script. It captures for 20 sec.
|
|
9
|
+
# 3. While it's capturing: in HWM click Connect.
|
|
10
|
+
# 4. Wait for HWM to reach "Connected" state, then move a fader so DMX
|
|
11
|
+
# starts streaming. Script should record both the TCP handshake AND
|
|
12
|
+
# a few UDP DMX frames.
|
|
13
|
+
#
|
|
14
|
+
# Output: /tmp/stick-handshake.pcap (timestamped copy at ./tools/captures/<ts>.pcap)
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
cd "$(dirname "$0")/.."
|
|
18
|
+
|
|
19
|
+
IFACE="${IFACE:-en0}"
|
|
20
|
+
STICK_IP="${STICK_IP:-192.168.96.2}"
|
|
21
|
+
DUR="${DUR:-60}"
|
|
22
|
+
PCAP=/tmp/stick-handshake.pcap
|
|
23
|
+
|
|
24
|
+
mkdir -p tools/captures
|
|
25
|
+
TS=$(date +%Y%m%d-%H%M%S)
|
|
26
|
+
SAVE="tools/captures/handshake-${TS}.pcap"
|
|
27
|
+
|
|
28
|
+
echo ">>> About to capture ${DUR}s of ALL traffic to/from ${STICK_IP} on ${IFACE}"
|
|
29
|
+
echo ">>> "
|
|
30
|
+
echo ">>> BEFORE this starts: disconnect HWM from the Stick."
|
|
31
|
+
echo ">>> AS SOON as the capture starts: click HWM Connect, then once"
|
|
32
|
+
echo ">>> connected, move a fader to start DMX streaming."
|
|
33
|
+
echo ">>> "
|
|
34
|
+
read -p "Ready? (y/Enter to start, anything else to abort): " ans
|
|
35
|
+
[[ -z "$ans" || "$ans" == "y" || "$ans" == "Y" ]] || { echo "aborted"; exit 1; }
|
|
36
|
+
|
|
37
|
+
echo ">>> Capturing..."
|
|
38
|
+
sudo tcpdump -i "$IFACE" -w "$PCAP" -G "$DUR" -W 1 \
|
|
39
|
+
"host ${STICK_IP}" \
|
|
40
|
+
2>/dev/null || true
|
|
41
|
+
|
|
42
|
+
echo
|
|
43
|
+
echo ">>> pcap size: $(wc -c < "$PCAP") bytes"
|
|
44
|
+
cp "$PCAP" "$SAVE"
|
|
45
|
+
echo ">>> archived to $SAVE"
|
|
46
|
+
|
|
47
|
+
echo
|
|
48
|
+
echo ">>> Packet summary (TCP control + UDP DMX)"
|
|
49
|
+
node tools/dump-pcap-summary.mjs "$PCAP" > /tmp/stick-handshake-summary.txt
|
|
50
|
+
echo " written to /tmp/stick-handshake-summary.txt ($(wc -l < /tmp/stick-handshake-summary.txt) packets)"
|
|
51
|
+
echo
|
|
52
|
+
echo ">>> First few TCP frames (the handshake):"
|
|
53
|
+
grep ' TCP ' /tmp/stick-handshake-summary.txt | head -10
|
|
54
|
+
echo
|
|
55
|
+
echo ">>> First UDP DMX frame's first 32B (clear header):"
|
|
56
|
+
grep ' UDP .* 576B' /tmp/stick-handshake-summary.txt | head -1
|
|
57
|
+
echo
|
|
58
|
+
echo ">>> Full summary at /tmp/stick-handshake-summary.txt"
|
|
59
|
+
echo ">>> pcap at $SAVE — share this output and we'll decode the handshake"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Capture a send_dmx run: all TCP + UDP traffic to/from the Stick while
|
|
3
|
+
# send_dmx connects, handshakes and streams. The resulting pcap shows whether
|
|
4
|
+
# the Stick accepts our handshake, whether our UDP DMX frames go out, and
|
|
5
|
+
# whether the Stick errors or drops the connection.
|
|
6
|
+
#
|
|
7
|
+
# ./tools/capture-send.sh <ip> <universe,channel=value> [...]
|
|
8
|
+
# e.g. ./tools/capture-send.sh 192.168.96.2 0,6=255 0,9=255 0,10=170
|
|
9
|
+
#
|
|
10
|
+
# Needs sudo for tcpdump (it will prompt). Output: /tmp/send_dmx-run.pcap
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
cd "$(dirname "$0")/.."
|
|
14
|
+
|
|
15
|
+
IFACE="${IFACE:-en0}"
|
|
16
|
+
IP="${1:?usage: capture-send.sh <ip> <universe,channel=value>...}"
|
|
17
|
+
shift
|
|
18
|
+
PCAP=/tmp/send_dmx-run.pcap
|
|
19
|
+
rm -f "$PCAP"
|
|
20
|
+
|
|
21
|
+
echo ">>> starting tcpdump (sudo) on ${IFACE}, host ${IP}"
|
|
22
|
+
sudo tcpdump -i "$IFACE" -w "$PCAP" "host ${IP}" >/dev/null 2>&1 &
|
|
23
|
+
sleep 3 # let tcpdump come up
|
|
24
|
+
|
|
25
|
+
echo ">>> running: send_dmx ${IP} $*"
|
|
26
|
+
node tools/send_dmx.mjs "$IP" "$@" || true
|
|
27
|
+
sleep 2 # let the last frames + any Stick response land
|
|
28
|
+
|
|
29
|
+
echo ">>> stopping tcpdump"
|
|
30
|
+
sudo pkill -INT -f 'tcpdump.*send_dmx-run' 2>/dev/null || true
|
|
31
|
+
sleep 1
|
|
32
|
+
|
|
33
|
+
echo
|
|
34
|
+
echo ">>> pcap: ${PCAP} ($(wc -c < "$PCAP" 2>/dev/null || echo 0) bytes)"
|
|
35
|
+
if [[ -s "$PCAP" ]]; then
|
|
36
|
+
node tools/dump-pcap-summary.mjs "$PCAP" 2>/dev/null | head -50 || true
|
|
37
|
+
echo ">>> total packets: $(node tools/dump-pcap-summary.mjs "$PCAP" 2>/dev/null | wc -l)"
|
|
38
|
+
fi
|
|
39
|
+
echo ">>> done — tell the assistant; ${PCAP} will be analysed."
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Crack the key-derivation function: secret -> 32-byte AES-256 key.
|
|
2
|
+
//
|
|
3
|
+
// The AES key is KDF(ECDH shared secret). The cipher object stores a 32-byte
|
|
4
|
+
// block A||B at cipher_obj+0x28, right before the key at +0x48 — likely the
|
|
5
|
+
// shared secret kept resident. This slides a 32-byte "secret" window over all
|
|
6
|
+
// captured memory blobs and tries a battery of KDFs against the known key.
|
|
7
|
+
// If any KDF(window) == key, the derivation (and the secret's location) is
|
|
8
|
+
// found — and the plugin can replicate it with no further RE.
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// node tools/crack-kdf.mjs [key-hex] [blob.bin ...]
|
|
12
|
+
// key defaults to the Path C recovered key
|
|
13
|
+
// blobs default to /tmp/stick-pathC-blob-*.bin
|
|
14
|
+
|
|
15
|
+
import * as crypto from 'node:crypto';
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
|
|
18
|
+
const DEFAULT_KEY =
|
|
19
|
+
'531e1b9f097be5de5785f5bf86473da34839fabb76676e95c0e4b808b42f8ac0';
|
|
20
|
+
// hardcoded 33-byte secret deobfuscated from the binary (memory note)
|
|
21
|
+
const HC = Buffer.from(
|
|
22
|
+
'527a5b46c56f3a5e670b4f0e338727d9' +
|
|
23
|
+
'4737ec0fc4af0dba93a51d93965191d0' + '8f', 'hex');
|
|
24
|
+
|
|
25
|
+
let [, , maybeKey, ...blobArgs] = process.argv;
|
|
26
|
+
let keyHex = DEFAULT_KEY;
|
|
27
|
+
if (maybeKey && /^[0-9a-fA-F]{64}$/.test(maybeKey)) keyHex = maybeKey;
|
|
28
|
+
else if (maybeKey) blobArgs.unshift(maybeKey);
|
|
29
|
+
const KEY = Buffer.from(keyHex, 'hex');
|
|
30
|
+
|
|
31
|
+
let blobs = blobArgs;
|
|
32
|
+
if (blobs.length === 0) {
|
|
33
|
+
blobs = fs.readdirSync('/tmp')
|
|
34
|
+
.filter(f => /^stick-pathC-blob-.*\.bin$/.test(f))
|
|
35
|
+
.map(f => '/tmp/' + f);
|
|
36
|
+
}
|
|
37
|
+
if (blobs.length === 0) { console.error('no blobs found'); process.exit(1); }
|
|
38
|
+
|
|
39
|
+
const sha256 = b => crypto.createHash('sha256').update(b).digest();
|
|
40
|
+
const sha512 = b => crypto.createHash('sha512').update(b).digest();
|
|
41
|
+
const sha1 = b => crypto.createHash('sha1').update(b).digest();
|
|
42
|
+
const md5 = b => crypto.createHash('md5').update(b).digest();
|
|
43
|
+
const blake2b = b => crypto.createHash('blake2b512').update(b).digest();
|
|
44
|
+
const blake2s = b => crypto.createHash('blake2s256').update(b).digest();
|
|
45
|
+
const hmac = (k, m) => crypto.createHmac('sha256', k).update(m).digest();
|
|
46
|
+
const cat = (...xs) => Buffer.concat(xs);
|
|
47
|
+
const rev = b => Buffer.from(b).reverse();
|
|
48
|
+
const xor = (a, b) => { const o = Buffer.alloc(32); for (let i = 0; i < 32; i++) o[i] = a[i % a.length] ^ b[i % b.length]; return o; };
|
|
49
|
+
function ecb(keyB, data) {
|
|
50
|
+
const c = crypto.createCipheriv('aes-256-ecb', keyB, null);
|
|
51
|
+
c.setAutoPadding(false);
|
|
52
|
+
return Buffer.concat([c.update(data), c.final()]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const HC32 = HC.subarray(0, 32);
|
|
56
|
+
const HC16a = HC.subarray(0, 16);
|
|
57
|
+
const HC16b = HC.subarray(16, 32);
|
|
58
|
+
|
|
59
|
+
// each KDF maps a candidate secret W (32 bytes) -> a 32-byte key
|
|
60
|
+
const KDFS = [
|
|
61
|
+
['identity', W => W],
|
|
62
|
+
['sha256(W)', W => sha256(W)],
|
|
63
|
+
['sha256(sha256(W))', W => sha256(sha256(W))],
|
|
64
|
+
['sha512(W)[:32]', W => sha512(W).subarray(0, 32)],
|
|
65
|
+
['blake2b(W)[:32]', W => blake2b(W).subarray(0, 32)],
|
|
66
|
+
['blake2s(W)', W => blake2s(W)],
|
|
67
|
+
['md5(W)|md5(revW)', W => cat(md5(W), md5(rev(W)))],
|
|
68
|
+
['sha256(W|HC33)', W => sha256(cat(W, HC))],
|
|
69
|
+
['sha256(HC33|W)', W => sha256(cat(HC, W))],
|
|
70
|
+
['sha256(W|HC32)', W => sha256(cat(W, HC32))],
|
|
71
|
+
['sha256(HC32|W)', W => sha256(cat(HC32, W))],
|
|
72
|
+
['sha512(W|HC33)[:32]', W => sha512(cat(W, HC)).subarray(0, 32)],
|
|
73
|
+
['sha512(HC33|W)[:32]', W => sha512(cat(HC, W)).subarray(0, 32)],
|
|
74
|
+
['hmac(HC33,W)', W => hmac(HC, W)],
|
|
75
|
+
['hmac(W,HC33)', W => hmac(W, HC)],
|
|
76
|
+
['hmac(HC32,W)', W => hmac(HC32, W)],
|
|
77
|
+
['hmac(W,HC32)', W => hmac(W, HC32)],
|
|
78
|
+
['hmac(W,W)', W => hmac(W, W)],
|
|
79
|
+
['W xor HC32', W => xor(W, HC32)],
|
|
80
|
+
['sha256(W xor HC32)', W => sha256(xor(W, HC32))],
|
|
81
|
+
['sha256(W|W)', W => sha256(cat(W, W))],
|
|
82
|
+
['aes256ecb(HC32,W)', W => ecb(HC32, W)],
|
|
83
|
+
['aes256ecb(W,HC32)', W => ecb(W, HC32)],
|
|
84
|
+
['sha256(W|HC16a)', W => sha256(cat(W, HC16a))],
|
|
85
|
+
['sha256(W|HC16b)', W => sha256(cat(W, HC16b))],
|
|
86
|
+
['sha256(sha1(W)|sha1(HC32))', W => sha256(cat(sha1(W), sha1(HC32)))],
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const want = KEY.toString('hex');
|
|
90
|
+
let tried = 0, hit = false;
|
|
91
|
+
|
|
92
|
+
for (const path of blobs) {
|
|
93
|
+
let buf;
|
|
94
|
+
try { buf = fs.readFileSync(path); } catch { continue; }
|
|
95
|
+
const name = path.split('/').pop();
|
|
96
|
+
for (let o = 0; o + 32 <= buf.length; o++) {
|
|
97
|
+
const W = buf.subarray(o, o + 32);
|
|
98
|
+
for (const [kname, fn] of KDFS) {
|
|
99
|
+
tried++;
|
|
100
|
+
let out;
|
|
101
|
+
try { out = fn(W); } catch { continue; }
|
|
102
|
+
if (out && out.length >= 32 && out.subarray(0, 32).toString('hex') === want) {
|
|
103
|
+
hit = true;
|
|
104
|
+
console.log(`\n*** KDF MATCH ***`);
|
|
105
|
+
console.log(` key = ${want}`);
|
|
106
|
+
console.log(` KDF = ${kname}`);
|
|
107
|
+
console.log(` secret W = ${W.toString('hex')}`);
|
|
108
|
+
console.log(` location = ${name} + 0x${o.toString(16)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(`\ntried ${tried} (window, KDF) combinations across ${blobs.length} blob(s)`);
|
|
115
|
+
if (!hit) {
|
|
116
|
+
console.log('no KDF in the battery maps any 32-byte window to the key.');
|
|
117
|
+
console.log('Next: capture a handshake + memory dump in one session, recover');
|
|
118
|
+
console.log('client_priv, compute the real X25519 shared secret, and either');
|
|
119
|
+
console.log('extend this battery or trace the KDF function directly.');
|
|
120
|
+
process.exit(3);
|
|
121
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Path A, final step — brute-force the AES key out of a saved cipher-object
|
|
2
|
+
// dump by testing every window against a captured 576-byte DMX frame.
|
|
3
|
+
//
|
|
4
|
+
// The lldb scan (lldb_pathA_scan.py) saved the cipher object to
|
|
5
|
+
// /tmp/stick-pathA-obj-*.bin. The raw key is somewhere in there, but the
|
|
6
|
+
// exact offset / length / mode / byte order are uncertain. So: slide a
|
|
7
|
+
// window over the whole dump, try it as an AES key in every common mode,
|
|
8
|
+
// and score the decrypted DMX payload (DMX = mostly-zero bytes).
|
|
9
|
+
//
|
|
10
|
+
// Modes and what the IV does:
|
|
11
|
+
// ecb - no IV; every block independent
|
|
12
|
+
// cbc/cfb/cfb8 - IV only affects the first block(s); blocks 1..33 (528
|
|
13
|
+
// bytes) decrypt from the key alone -> scored IV-free
|
|
14
|
+
// ofb/ctr - keystream depends on key AND the nonce/counter, every
|
|
15
|
+
// block -> we try several nonce-derived 16-byte IVs
|
|
16
|
+
//
|
|
17
|
+
// Usage:
|
|
18
|
+
// node tools/crack-key.mjs <frame.bin> [objdump.bin ...]
|
|
19
|
+
// (objdumps default to /tmp/stick-pathA-obj-*.bin)
|
|
20
|
+
|
|
21
|
+
import * as crypto from 'node:crypto';
|
|
22
|
+
import * as fs from 'node:fs';
|
|
23
|
+
|
|
24
|
+
let [, , framePath, ...objPaths] = process.argv;
|
|
25
|
+
if (!framePath) {
|
|
26
|
+
console.error('usage: node tools/crack-key.mjs <frame.bin> [objdump.bin ...]');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
if (objPaths.length === 0) {
|
|
30
|
+
objPaths = fs.readdirSync('/tmp')
|
|
31
|
+
.filter(f => /^stick-pathA-obj-.*\.bin$/.test(f))
|
|
32
|
+
.map(f => '/tmp/' + f);
|
|
33
|
+
if (objPaths.length === 0) {
|
|
34
|
+
console.error('no /tmp/stick-pathA-obj-*.bin found — pass dumps explicitly');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const frame = fs.readFileSync(framePath);
|
|
40
|
+
if (frame.length !== 576) {
|
|
41
|
+
console.error(`frame must be 576 bytes, got ${frame.length}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const header = frame.subarray(0, 0x20);
|
|
45
|
+
const body = frame.subarray(0x20, 0x240); // 544 bytes, 34 AES blocks
|
|
46
|
+
const nonce = Buffer.from(header.subarray(0x18, 0x20)); // 8 bytes
|
|
47
|
+
|
|
48
|
+
// candidate 16-byte IV / initial-counter blocks built from the clear header
|
|
49
|
+
const z8 = Buffer.alloc(8);
|
|
50
|
+
const ivCandidates = [
|
|
51
|
+
['nonce|0', Buffer.concat([nonce, z8])],
|
|
52
|
+
['0|nonce', Buffer.concat([z8, nonce])],
|
|
53
|
+
['nonce|nonce', Buffer.concat([nonce, nonce])],
|
|
54
|
+
['nonce|0..1', Buffer.concat([nonce, Buffer.from('0000000000000001', 'hex')])],
|
|
55
|
+
['hdr[0:16]', Buffer.from(header.subarray(0, 16))],
|
|
56
|
+
['hdr[16:32]', Buffer.from(header.subarray(16, 32))],
|
|
57
|
+
['zero', Buffer.alloc(16)],
|
|
58
|
+
];
|
|
59
|
+
const ZERO_IV = Buffer.alloc(16);
|
|
60
|
+
|
|
61
|
+
// score a region of the plaintext: fraction of zero bytes
|
|
62
|
+
function zeroRatio(out, from) {
|
|
63
|
+
let zeros = 0;
|
|
64
|
+
for (let i = from; i < out.length; i++) if (out[i] === 0) zeros++;
|
|
65
|
+
return zeros / (out.length - from);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function wordswap(buf) {
|
|
69
|
+
const o = Buffer.from(buf);
|
|
70
|
+
for (let i = 0; i + 4 <= o.length; i += 4) {
|
|
71
|
+
const a = o[i], b = o[i + 1], c = o[i + 2], d = o[i + 3];
|
|
72
|
+
o[i] = d; o[i + 1] = c; o[i + 2] = b; o[i + 3] = a;
|
|
73
|
+
}
|
|
74
|
+
return o;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function tryDecrypt(algo, key, iv) {
|
|
78
|
+
const d = iv === null
|
|
79
|
+
? crypto.createDecipheriv(algo, key, null)
|
|
80
|
+
: crypto.createDecipheriv(algo, key, iv);
|
|
81
|
+
d.setAutoPadding(false);
|
|
82
|
+
return Buffer.concat([d.update(body), d.final()]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const results = [];
|
|
86
|
+
let tried = 0;
|
|
87
|
+
|
|
88
|
+
for (const objPath of objPaths) {
|
|
89
|
+
const buf = fs.readFileSync(objPath);
|
|
90
|
+
const objName = objPath.split('/').pop();
|
|
91
|
+
for (const keylen of [16, 24, 32]) {
|
|
92
|
+
const bits = keylen * 8;
|
|
93
|
+
for (let off = 0; off + keylen <= buf.length; off++) {
|
|
94
|
+
const window = buf.subarray(off, off + keylen);
|
|
95
|
+
for (const [formName, keyBuf] of [['asis', window], ['wswap', wordswap(window)]]) {
|
|
96
|
+
const note = (extra) => ({ obj: objName, off, keylen, form: formName,
|
|
97
|
+
key: Buffer.from(keyBuf).toString('hex'), ...extra });
|
|
98
|
+
|
|
99
|
+
// --- ECB: no IV, score everything --------------------------------
|
|
100
|
+
try { tried++;
|
|
101
|
+
const out = tryDecrypt(`aes-${bits}-ecb`, keyBuf, null);
|
|
102
|
+
const z = zeroRatio(out, 0);
|
|
103
|
+
if (z > 0.55) results.push(note({ mode: 'ecb', iv: '-', z,
|
|
104
|
+
sample: out.subarray(0, 32).toString('hex') }));
|
|
105
|
+
} catch { /* skip */ }
|
|
106
|
+
|
|
107
|
+
// --- CBC / CFB / CFB8: IV-free score of blocks 1.. --------------
|
|
108
|
+
for (const mode of ['cbc', 'cfb', 'cfb8']) {
|
|
109
|
+
try { tried++;
|
|
110
|
+
const out = tryDecrypt(`aes-${bits}-${mode}`, keyBuf, ZERO_IV);
|
|
111
|
+
const z = zeroRatio(out, 16);
|
|
112
|
+
if (z > 0.55) results.push(note({ mode, iv: 'n/a(blk1+)', z,
|
|
113
|
+
sample: out.subarray(16, 48).toString('hex') }));
|
|
114
|
+
} catch { /* skip */ }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- OFB / CTR: keystream depends on the nonce ------------------
|
|
118
|
+
for (const mode of ['ofb', 'ctr']) {
|
|
119
|
+
for (const [ivName, iv] of ivCandidates) {
|
|
120
|
+
try { tried++;
|
|
121
|
+
const out = tryDecrypt(`aes-${bits}-${mode}`, keyBuf, iv);
|
|
122
|
+
const z = zeroRatio(out, 0);
|
|
123
|
+
if (z > 0.55) results.push(note({ mode, iv: ivName, z,
|
|
124
|
+
sample: out.subarray(0, 32).toString('hex') }));
|
|
125
|
+
} catch { /* skip */ }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`tried ${tried} (key,mode,iv,order) combinations`);
|
|
134
|
+
console.log(`frame: seq=${frame[0x17]} nonce=${nonce.toString('hex')}`);
|
|
135
|
+
console.log();
|
|
136
|
+
|
|
137
|
+
results.sort((a, b) => b.z - a.z);
|
|
138
|
+
if (results.length === 0) {
|
|
139
|
+
console.log('❌ nothing decrypted to DMX-shaped plaintext under any of:');
|
|
140
|
+
console.log(' ecb / cbc / cfb / cfb8 / ofb / ctr (aes-128/192/256, both byte orders).');
|
|
141
|
+
console.log(' The key is not a byte-contiguous window of the cipher object,');
|
|
142
|
+
console.log(' OR it is derived (KDF) from the object bytes before use,');
|
|
143
|
+
console.log(' OR the block cipher is non-standard. Next: capture a 2nd frame');
|
|
144
|
+
console.log(' for a keystream diff, or Path C (patch the encrypt fn to log $rdi).');
|
|
145
|
+
process.exit(3);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(`top ${Math.min(15, results.length)} candidates (zero-ratio):`);
|
|
149
|
+
for (const r of results.slice(0, 15)) {
|
|
150
|
+
const pct = (r.z * 100).toFixed(1);
|
|
151
|
+
const flag = r.z > 0.80 ? ' <<< KEY FOUND' : '';
|
|
152
|
+
console.log(` ${pct}% aes-${r.keylen * 8}-${r.mode} iv=${r.iv} ${r.form}` +
|
|
153
|
+
` ${r.obj}+0x${r.off.toString(16)}${flag}`);
|
|
154
|
+
console.log(` key=${r.key}`);
|
|
155
|
+
console.log(` plaintext=${r.sample}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const best = results[0];
|
|
159
|
+
console.log();
|
|
160
|
+
if (best.z > 0.80) {
|
|
161
|
+
console.log('✅ KEY RECOVERED');
|
|
162
|
+
console.log(` algorithm : aes-${best.keylen * 8}-${best.mode}` +
|
|
163
|
+
` iv=${best.iv} (byte order: ${best.form})`);
|
|
164
|
+
console.log(` key : ${best.key}`);
|
|
165
|
+
console.log(` location : ${best.obj} +0x${best.off.toString(16)}`);
|
|
166
|
+
process.exit(0);
|
|
167
|
+
} else {
|
|
168
|
+
console.log('⚠️ best candidate is below the 80% confidence bar — inspect above.');
|
|
169
|
+
process.exit(2);
|
|
170
|
+
}
|