@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,78 @@
|
|
|
1
|
+
// Extract one 576-byte DMX payload from a tcpdump pcap file.
|
|
2
|
+
//
|
|
3
|
+
// pcap format (libpcap, little-endian):
|
|
4
|
+
// 24-byte global header (magic d4c3b2a1 or a1b2c3d4)
|
|
5
|
+
// per-packet records:
|
|
6
|
+
// 16-byte record header (ts_sec, ts_usec, cap_len, orig_len)
|
|
7
|
+
// <cap_len> bytes of raw frame (ethernet+IP+UDP+payload)
|
|
8
|
+
//
|
|
9
|
+
// We look for any packet whose IP/UDP demux says: dst=STICK_IP:STICK_PORT
|
|
10
|
+
// and UDP payload length == 576.
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
|
|
14
|
+
const STICK_IP = process.env.STICK_IP || '192.168.96.2';
|
|
15
|
+
const STICK_PORT = Number(process.env.STICK_PORT || 2431);
|
|
16
|
+
|
|
17
|
+
const [, , pcapPath, outPath] = process.argv;
|
|
18
|
+
if (!pcapPath || !outPath) {
|
|
19
|
+
console.error('usage: node tools/extract-frame.mjs <input.pcap> <output.bin>');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const buf = fs.readFileSync(pcapPath);
|
|
24
|
+
if (buf.length < 24) { console.error('pcap too small'); process.exit(2); }
|
|
25
|
+
|
|
26
|
+
// Magic constant `0xa1b2c3d4` is written in the writer's native byte order.
|
|
27
|
+
// Reading the first 4 bytes as LE: 0xa1b2c3d4 -> file is LE
|
|
28
|
+
// 0xd4c3b2a1 -> file is BE
|
|
29
|
+
const magic = buf.readUInt32LE(0);
|
|
30
|
+
let le;
|
|
31
|
+
if (magic === 0xa1b2c3d4) le = true;
|
|
32
|
+
else if (magic === 0xd4c3b2a1) le = false;
|
|
33
|
+
else { console.error(`unknown pcap magic 0x${magic.toString(16)}`); process.exit(2); }
|
|
34
|
+
|
|
35
|
+
const linkType = le ? buf.readUInt32LE(20) : buf.readUInt32BE(20);
|
|
36
|
+
// linkType 1 = Ethernet, 0 = BSD loopback / NULL header (4 bytes)
|
|
37
|
+
const ethSkip = (linkType === 1) ? 14 : (linkType === 0 ? 4 : 14);
|
|
38
|
+
|
|
39
|
+
let off = 24;
|
|
40
|
+
let pkts = 0, matched = 0;
|
|
41
|
+
while (off + 16 <= buf.length) {
|
|
42
|
+
const tsSec = le ? buf.readUInt32LE(off) : buf.readUInt32BE(off);
|
|
43
|
+
const tsUsec = le ? buf.readUInt32LE(off + 4) : buf.readUInt32BE(off + 4);
|
|
44
|
+
const capLen = le ? buf.readUInt32LE(off + 8) : buf.readUInt32BE(off + 8);
|
|
45
|
+
off += 16;
|
|
46
|
+
if (off + capLen > buf.length) break;
|
|
47
|
+
pkts++;
|
|
48
|
+
const pkt = buf.subarray(off, off + capLen);
|
|
49
|
+
off += capLen;
|
|
50
|
+
|
|
51
|
+
// Parse Ethernet/Null + IPv4 + UDP
|
|
52
|
+
if (pkt.length < ethSkip + 28) continue;
|
|
53
|
+
const ip = pkt.subarray(ethSkip);
|
|
54
|
+
const ihl = (ip[0] & 0x0f) * 4;
|
|
55
|
+
const proto = ip[9];
|
|
56
|
+
if (proto !== 17) continue; // not UDP
|
|
57
|
+
|
|
58
|
+
const srcIp = `${ip[12]}.${ip[13]}.${ip[14]}.${ip[15]}`;
|
|
59
|
+
const dstIp = `${ip[16]}.${ip[17]}.${ip[18]}.${ip[19]}`;
|
|
60
|
+
const udp = ip.subarray(ihl);
|
|
61
|
+
if (udp.length < 8) continue;
|
|
62
|
+
const srcPort = udp.readUInt16BE(0);
|
|
63
|
+
const dstPort = udp.readUInt16BE(2);
|
|
64
|
+
const udpLen = udp.readUInt16BE(4);
|
|
65
|
+
const payload = udp.subarray(8, udpLen);
|
|
66
|
+
|
|
67
|
+
if (dstIp === STICK_IP && dstPort === STICK_PORT) {
|
|
68
|
+
matched++;
|
|
69
|
+
console.error(` match #${matched}: ${srcIp}:${srcPort} -> ${dstIp}:${dstPort} payload=${payload.length}B`);
|
|
70
|
+
if (payload.length === 576) {
|
|
71
|
+
fs.writeFileSync(outPath, payload);
|
|
72
|
+
console.error(`OK: wrote 576-byte payload to ${outPath}`);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
console.error(`scanned ${pkts} packets, ${matched} matched dst, no 576B payload found`);
|
|
78
|
+
process.exit(3);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Brute-force search for the AES-256 key that decrypts the 0x011c reply.
|
|
2
|
+
//
|
|
3
|
+
// We have constraints on the plaintext:
|
|
4
|
+
// - 240 bytes = 15 AES-CBC blocks decrypts to 236-byte useful body + 4 padding
|
|
5
|
+
// - Padding is either PKCS#7 (`04 04 04 04`) or zero-fill (`00 00 00 00`)
|
|
6
|
+
// - Per HWM (FUN_1001676a0), plaintext has a self-consistency check:
|
|
7
|
+
// plaintext[0..4] == ~plaintext[0x64..0x68] (bitwise-complement)
|
|
8
|
+
//
|
|
9
|
+
// Strategy: walk every 32-byte aligned offset of the HardwareManager binary
|
|
10
|
+
// and try that 32 bytes as an AES-256 key against a captured (IV, ciphertext).
|
|
11
|
+
// Pre-filter by padding tail; secondary-filter by the complement check.
|
|
12
|
+
//
|
|
13
|
+
// node tools/find-011c-key.mjs <pcap> <hwm-binary>
|
|
14
|
+
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
|
|
18
|
+
const [, , pcapPath, hwmPath] = process.argv;
|
|
19
|
+
if (!pcapPath || !hwmPath) {
|
|
20
|
+
console.error('usage: node tools/find-011c-key.mjs <pcap> <hwm-binary>');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// --- extract one 0x011c reply ---------------------------------------------
|
|
25
|
+
function extractFirstReply(pcap) {
|
|
26
|
+
const buf = fs.readFileSync(pcap);
|
|
27
|
+
const le = buf.readUInt32LE(0) === 0xa1b2c3d4;
|
|
28
|
+
function ipFromEth(p) {
|
|
29
|
+
if (p.length < 14) return null;
|
|
30
|
+
let o = 12, e = p.readUInt16BE(o);
|
|
31
|
+
for (let i = 0; i < 2 && e === 0x8100; i++) { o += 4; if (o+2>p.length) return null; e = p.readUInt16BE(o); }
|
|
32
|
+
return e === 0x0800 ? p.subarray(o + 2) : null;
|
|
33
|
+
}
|
|
34
|
+
const streams = new Map();
|
|
35
|
+
let off = 24;
|
|
36
|
+
while (off + 16 <= buf.length) {
|
|
37
|
+
const capLen = le ? buf.readUInt32LE(off + 8) : buf.readUInt32BE(off + 8);
|
|
38
|
+
off += 16; if (off + capLen > buf.length) break;
|
|
39
|
+
const pkt = buf.subarray(off, off + capLen); off += capLen;
|
|
40
|
+
const ip = ipFromEth(pkt); if (!ip || ip.length < 20 || ((ip[0]>>4)&0xf)!==4) continue;
|
|
41
|
+
const ihl = (ip[0]&0x0f)*4; if (ip[9]!==6) continue;
|
|
42
|
+
const tcp = ip.subarray(ihl);
|
|
43
|
+
if (tcp.readUInt16BE(0) !== 2431) continue;
|
|
44
|
+
const seq = tcp.readUInt32BE(4);
|
|
45
|
+
const dataOff = (tcp[12]>>4)*4;
|
|
46
|
+
const payload = tcp.subarray(dataOff);
|
|
47
|
+
if (!payload.length) continue;
|
|
48
|
+
const key = `${ip[12]}.${ip[13]}.${ip[14]}.${ip[15]}:${tcp.readUInt16BE(0)}->${ip[16]}.${ip[17]}.${ip[18]}.${ip[19]}:${tcp.readUInt16BE(2)}`;
|
|
49
|
+
if (!streams.has(key)) streams.set(key, []);
|
|
50
|
+
streams.get(key).push({ seq, payload });
|
|
51
|
+
}
|
|
52
|
+
for (const segs of streams.values()) {
|
|
53
|
+
segs.sort((a,b)=>(a.seq-b.seq+0x100000000)%0x100000000);
|
|
54
|
+
const seen = new Set(); const out = [];
|
|
55
|
+
const base = segs[0].seq;
|
|
56
|
+
for (const s of segs) {
|
|
57
|
+
const k = `${s.seq}:${s.payload.length}`; if (seen.has(k)) continue; seen.add(k);
|
|
58
|
+
const o = (s.seq-base+0x100000000)%0x100000000;
|
|
59
|
+
while (out.length < o + s.payload.length) out.push(0);
|
|
60
|
+
for (let i = 0; i < s.payload.length; i++) out[o+i] = s.payload[i];
|
|
61
|
+
}
|
|
62
|
+
const r = Buffer.from(out);
|
|
63
|
+
for (let i = 0; i + 24 <= r.length; i++) {
|
|
64
|
+
if (r.subarray(i,i+8).toString() !== 'Stick_3A') continue;
|
|
65
|
+
if (r.readUInt16LE(i+8) !== 0x011c) continue;
|
|
66
|
+
const len = r.readUInt32LE(i+0x14);
|
|
67
|
+
if (len !== 284) continue;
|
|
68
|
+
const msg = r.subarray(i, i + 284);
|
|
69
|
+
return { iv: Buffer.from(msg.subarray(0x1a, 0x2a)), ct: Buffer.from(msg.subarray(0x2c, 0x2c + 240)) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const reply = extractFirstReply(pcapPath);
|
|
76
|
+
if (!reply) { console.error('no 0x011c reply in', pcapPath); process.exit(2); }
|
|
77
|
+
console.log(`reply: IV=${reply.iv.toString('hex')}`);
|
|
78
|
+
console.log(`ct[0:32]=${reply.ct.subarray(0,32).toString('hex')}`);
|
|
79
|
+
|
|
80
|
+
// --- candidate key generator ----------------------------------------------
|
|
81
|
+
const bin = fs.readFileSync(hwmPath);
|
|
82
|
+
console.log(`binary: ${hwmPath} ${bin.length} bytes`);
|
|
83
|
+
|
|
84
|
+
// Try every 32-byte aligned offset. Also try byte-aligned (stride 1) for the
|
|
85
|
+
// last-pass. Reject keys that are obviously not credible (all zero, all FF).
|
|
86
|
+
function isBoring(k) {
|
|
87
|
+
let z = 0, ff = 0, distinct = new Set();
|
|
88
|
+
for (const b of k) { if (b===0) z++; if (b===0xff) ff++; distinct.add(b); }
|
|
89
|
+
if (z > 20 || ff > 20) return true;
|
|
90
|
+
if (distinct.size < 6) return true;
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Decrypt one block trial: returns Buffer or null on AES error
|
|
95
|
+
function dec(keyBuf, iv, ct) {
|
|
96
|
+
try {
|
|
97
|
+
const d = crypto.createDecipheriv('aes-256-cbc', keyBuf, iv);
|
|
98
|
+
d.setAutoPadding(false);
|
|
99
|
+
return Buffer.concat([d.update(ct), d.final()]);
|
|
100
|
+
} catch { return null; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Validation:
|
|
104
|
+
// 1. last 4 bytes of plaintext must be 04 04 04 04 OR 00 00 00 00
|
|
105
|
+
// 2. plaintext[0..4] XOR plaintext[0x64..0x68] == 0xff,0xff,0xff,0xff
|
|
106
|
+
function isValid(pt) {
|
|
107
|
+
if (!pt) return null;
|
|
108
|
+
const t0 = pt[236], t1 = pt[237], t2 = pt[238], t3 = pt[239];
|
|
109
|
+
const isPkcs7 = t0===4 && t1===4 && t2===4 && t3===4;
|
|
110
|
+
const isZero = t0===0 && t1===0 && t2===0 && t3===0;
|
|
111
|
+
if (!isPkcs7 && !isZero) return null;
|
|
112
|
+
// self-consistency check
|
|
113
|
+
let comp = true;
|
|
114
|
+
for (let i = 0; i < 4; i++) {
|
|
115
|
+
if (((pt[i] ^ pt[0x64+i]) & 0xff) !== 0xff) { comp = false; break; }
|
|
116
|
+
}
|
|
117
|
+
return { padKind: isPkcs7 ? 'pkcs7' : 'zero', comp };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('\n=== pass 1: 32-byte aligned candidates ===');
|
|
121
|
+
let tested = 0, hits = 0;
|
|
122
|
+
for (let i = 0; i + 32 <= bin.length; i += 8) { // 8-byte alignment is reasonable
|
|
123
|
+
const k = bin.subarray(i, i + 32);
|
|
124
|
+
if (isBoring(k)) continue;
|
|
125
|
+
const pt = dec(k, reply.iv, reply.ct);
|
|
126
|
+
tested++;
|
|
127
|
+
const v = isValid(pt);
|
|
128
|
+
if (v) {
|
|
129
|
+
hits++;
|
|
130
|
+
console.log(` HIT @ 0x${i.toString(16)} pad=${v.padKind} comp=${v.comp}`);
|
|
131
|
+
console.log(` key = ${k.toString('hex')}`);
|
|
132
|
+
console.log(` pt[0..32] = ${pt.subarray(0,32).toString('hex')}`);
|
|
133
|
+
console.log(` pt[0x60..] = ${pt.subarray(0x60,0x80).toString('hex')}`);
|
|
134
|
+
if (v.comp) console.log(` *** complement check PASSED -- almost certainly the key ***`);
|
|
135
|
+
}
|
|
136
|
+
if (tested % 500000 === 0) {
|
|
137
|
+
console.log(` ... tested ${tested}, hits=${hits}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
console.log(`done: tested=${tested} hits=${hits}`);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Recover the AES-256-CBC first-block IV construction from a pcap of frames.
|
|
2
|
+
//
|
|
3
|
+
// With the key known, for every frame: D(key,C0) = IV XOR P0.
|
|
4
|
+
// If the DMX state is constant across the capture, P0 is the same in every
|
|
5
|
+
// frame, so for the CORRECT IV construction H(header):
|
|
6
|
+
// D(key,C0) XOR H(header) == P0 == the same constant in every frame.
|
|
7
|
+
// We test each candidate H and look for one that yields a frame-invariant
|
|
8
|
+
// residual. That residual is P0 (block-0 plaintext); H is the IV rule.
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// node tools/find-iv.mjs <capture.pcap> [key-hex]
|
|
12
|
+
|
|
13
|
+
import * as crypto from 'node:crypto';
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_KEY =
|
|
17
|
+
'531e1b9f097be5de5785f5bf86473da34839fabb76676e95c0e4b808b42f8ac0';
|
|
18
|
+
|
|
19
|
+
const [, , pcapPath, keyArg] = process.argv;
|
|
20
|
+
if (!pcapPath) { console.error('usage: node tools/find-iv.mjs <capture.pcap> [key-hex]'); process.exit(1); }
|
|
21
|
+
const key = Buffer.from((keyArg || DEFAULT_KEY).replace(/\s/g, ''), 'hex');
|
|
22
|
+
if (key.length !== 32) { console.error('key must be 32 bytes'); process.exit(1); }
|
|
23
|
+
|
|
24
|
+
// --- parse pcap, collect every 576-byte UDP payload to the Stick ----------
|
|
25
|
+
const STICK_IP = process.env.STICK_IP || '192.168.96.2';
|
|
26
|
+
const STICK_PORT = Number(process.env.STICK_PORT || 2431);
|
|
27
|
+
const buf = fs.readFileSync(pcapPath);
|
|
28
|
+
const magic = buf.readUInt32LE(0);
|
|
29
|
+
const le = magic === 0xa1b2c3d4;
|
|
30
|
+
if (!le && magic !== 0xd4c3b2a1) { console.error('bad pcap magic'); process.exit(2); }
|
|
31
|
+
const linkType = le ? buf.readUInt32LE(20) : buf.readUInt32BE(20);
|
|
32
|
+
const ethSkip = linkType === 1 ? 14 : linkType === 0 ? 4 : 14;
|
|
33
|
+
|
|
34
|
+
const frames = [];
|
|
35
|
+
let off = 24;
|
|
36
|
+
while (off + 16 <= buf.length) {
|
|
37
|
+
const capLen = le ? buf.readUInt32LE(off + 8) : buf.readUInt32BE(off + 8);
|
|
38
|
+
off += 16;
|
|
39
|
+
if (off + capLen > buf.length) break;
|
|
40
|
+
const pkt = buf.subarray(off, off + capLen);
|
|
41
|
+
off += capLen;
|
|
42
|
+
if (pkt.length < ethSkip + 28) continue;
|
|
43
|
+
const ip = pkt.subarray(ethSkip);
|
|
44
|
+
if (ip[9] !== 17) continue; // not UDP
|
|
45
|
+
const dstIp = `${ip[16]}.${ip[17]}.${ip[18]}.${ip[19]}`;
|
|
46
|
+
const udp = ip.subarray((ip[0] & 0x0f) * 4);
|
|
47
|
+
if (udp.length < 8) continue;
|
|
48
|
+
const dstPort = udp.readUInt16BE(2);
|
|
49
|
+
const payload = udp.subarray(8, udp.readUInt16BE(4));
|
|
50
|
+
if (dstIp === STICK_IP && dstPort === STICK_PORT && payload.length === 576)
|
|
51
|
+
frames.push(Buffer.from(payload));
|
|
52
|
+
}
|
|
53
|
+
console.log(`parsed ${frames.length} encrypted 576-byte frames from ${pcapPath}`);
|
|
54
|
+
if (frames.length < 2) { console.error('need at least 2 frames'); process.exit(3); }
|
|
55
|
+
|
|
56
|
+
// --- D(key,C0) for each frame --------------------------------------------
|
|
57
|
+
function decBlock0(frame) {
|
|
58
|
+
const d = crypto.createDecipheriv('aes-256-cbc', key, Buffer.alloc(16));
|
|
59
|
+
d.setAutoPadding(false);
|
|
60
|
+
return d.update(frame.subarray(0x20, 0x30)); // D(key,C0), IV=0
|
|
61
|
+
}
|
|
62
|
+
function ecb(block) {
|
|
63
|
+
const c = crypto.createCipheriv('aes-256-ecb', key, null);
|
|
64
|
+
c.setAutoPadding(false);
|
|
65
|
+
return Buffer.concat([c.update(block), c.final()]);
|
|
66
|
+
}
|
|
67
|
+
function xor(a, b) {
|
|
68
|
+
const o = Buffer.alloc(16);
|
|
69
|
+
for (let i = 0; i < 16; i++) o[i] = a[i] ^ b[i];
|
|
70
|
+
return o;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const recs = frames.map(f => ({
|
|
74
|
+
hdr: f.subarray(0, 0x20),
|
|
75
|
+
nonce: Buffer.from(f.subarray(0x18, 0x20)),
|
|
76
|
+
fieldA: Buffer.from(f.subarray(0x0a, 0x12)),
|
|
77
|
+
dt0: decBlock0(f),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
// also reuse the per-session key material A||B from the Path C device dump,
|
|
81
|
+
// if present — the IV might be keyed on that instead.
|
|
82
|
+
let AB = null;
|
|
83
|
+
try {
|
|
84
|
+
const dev = fs.readFileSync(
|
|
85
|
+
fs.readdirSync('/tmp').filter(f => /^stick-pathC-blob-device_/.test(f))
|
|
86
|
+
.map(f => '/tmp/' + f)[0]);
|
|
87
|
+
// cipher_obj = device + 0x1618 ; A||B at cipher_obj + 0x28
|
|
88
|
+
AB = Buffer.from(dev.subarray(0x1618 + 0x28, 0x1618 + 0x28 + 32));
|
|
89
|
+
console.log(`loaded A||B from device dump: ${AB.toString('hex')}`);
|
|
90
|
+
} catch { /* optional */ }
|
|
91
|
+
|
|
92
|
+
const z8 = Buffer.alloc(8);
|
|
93
|
+
function blocks(r) {
|
|
94
|
+
const b = [
|
|
95
|
+
['nonce|0', Buffer.concat([r.nonce, z8])],
|
|
96
|
+
['0|nonce', Buffer.concat([z8, r.nonce])],
|
|
97
|
+
['nonce|nonce', Buffer.concat([r.nonce, r.nonce])],
|
|
98
|
+
['fieldA|nonce', Buffer.concat([r.fieldA, r.nonce])],
|
|
99
|
+
['nonce|fieldA', Buffer.concat([r.nonce, r.fieldA])],
|
|
100
|
+
['hdr[0:16]', Buffer.from(r.hdr.subarray(0, 16))],
|
|
101
|
+
['hdr[16:32]', Buffer.from(r.hdr.subarray(16, 32))],
|
|
102
|
+
['hdr[10:26]', Buffer.from(r.hdr.subarray(0x0a, 0x1a))],
|
|
103
|
+
['nonce|nonceR', Buffer.concat([r.nonce, Buffer.from(r.nonce).reverse()])],
|
|
104
|
+
];
|
|
105
|
+
return b;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- test each construction for a frame-invariant residual ---------------
|
|
109
|
+
function evaluate(label, hOf) {
|
|
110
|
+
const tally = new Map();
|
|
111
|
+
for (const r of recs) {
|
|
112
|
+
const resid = xor(r.dt0, hOf(r)).toString('hex');
|
|
113
|
+
tally.set(resid, (tally.get(resid) || 0) + 1);
|
|
114
|
+
}
|
|
115
|
+
let best = null;
|
|
116
|
+
for (const [resid, n] of tally)
|
|
117
|
+
if (!best || n > best.n) best = { resid, n };
|
|
118
|
+
return { label, frac: best.n / recs.length, resid: best.resid, distinct: tally.size };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const names = blocks(recs[0]).map(([n]) => n);
|
|
122
|
+
const results = [];
|
|
123
|
+
for (let i = 0; i < names.length; i++) {
|
|
124
|
+
const name = names[i];
|
|
125
|
+
results.push(evaluate(`IV = ${name}`, r => blocks(r)[i][1]));
|
|
126
|
+
results.push(evaluate(`IV = AES(key, ${name})`, r => ecb(blocks(r)[i][1])));
|
|
127
|
+
if (AB) {
|
|
128
|
+
const abEcb = (blk) => {
|
|
129
|
+
const c = crypto.createCipheriv('aes-256-ecb', AB, null);
|
|
130
|
+
c.setAutoPadding(false);
|
|
131
|
+
return Buffer.concat([c.update(blk), c.final()]);
|
|
132
|
+
};
|
|
133
|
+
results.push(evaluate(`IV = AES(A||B, ${name})`, r => abEcb(blocks(r)[i][1])));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
results.sort((a, b) => b.frac - a.frac);
|
|
138
|
+
console.log();
|
|
139
|
+
console.log('IV construction — fraction of frames sharing one residual (=P0):');
|
|
140
|
+
for (const r of results.slice(0, 12))
|
|
141
|
+
console.log(` ${(r.frac * 100).toFixed(1).padStart(5)}% ${r.label.padEnd(26)}` +
|
|
142
|
+
` ${r.frac > 0.8 ? 'P0=' + r.resid : `(${r.distinct} distinct)`}`);
|
|
143
|
+
|
|
144
|
+
console.log();
|
|
145
|
+
const win = results[0];
|
|
146
|
+
if (win.frac > 0.8) {
|
|
147
|
+
console.log(`✅ IV CONSTRUCTION: ${win.label}`);
|
|
148
|
+
console.log(` block-0 plaintext P0 = ${win.resid}`);
|
|
149
|
+
} else {
|
|
150
|
+
console.log('⚠️ no construction gives a frame-invariant residual.');
|
|
151
|
+
console.log(' Either the DMX state changed during capture, or the IV uses');
|
|
152
|
+
console.log(' inputs not tested here. The residual counts above hint which.');
|
|
153
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Stick-DE3 DMX cipher RE — Ghidra static analysis
|
|
2
|
+
|
|
3
|
+
Headless pyghidra scripts that reverse-engineer the DMX cipher in the
|
|
4
|
+
**2024-03-21 ESA2 HardwareManager** binary at:
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
/Applications/ESA2/HardwareManager/HardwareManager.app/Contents/MacOS/HardwareManager
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Static RE *replaces* the prior interactive-lldb attempts (see
|
|
11
|
+
`memory/stick-aes-symbolized-breakthrough.md` for what didn't work, and
|
|
12
|
+
`memory/stick-cipher-is-stream-not-aes.md` for what's now known).
|
|
13
|
+
|
|
14
|
+
## TL;DR — the cipher IS AES-128 (with mode dispatch)
|
|
15
|
+
|
|
16
|
+
The Stick3 DMX cipher is **AES-128** (custom-inlined, not Gladman), running
|
|
17
|
+
in CBC or CFB mode depending on a flag in the cipher state. Earlier in this
|
|
18
|
+
RE session I briefly concluded "stream cipher, not AES" — that was wrong;
|
|
19
|
+
I had been looking at a sibling class (`XHL_DasEccStreamCryptography`, used
|
|
20
|
+
for USB devices) whose encrypt path uses a small-state stream cipher. The
|
|
21
|
+
*Stick3 ANet* path doesn't go through that. The real AES path is via
|
|
22
|
+
`FUN_100107950` → `FUN_1003f6790` (CBC) / `FUN_1003f5140` (CFB) → block
|
|
23
|
+
cipher at `FUN_1003f5980` / `FUN_1003f4690` with S-box at `DAT_1007c0010`.
|
|
24
|
+
|
|
25
|
+
576-byte DMX frame format:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
+0x00 (8B) session_magic table @ DAT_100d11a90 + idx*0x0E
|
|
29
|
+
+0x08 (2B) 0x0019 constant
|
|
30
|
+
+0x0A (8B) 8-byte from vtable slot 0x78
|
|
31
|
+
+0x12 (2B) port16
|
|
32
|
+
+0x14 (2B) channel count (≤512)
|
|
33
|
+
+0x16 (1B) 100 constant
|
|
34
|
+
+0x17 (1B) seq_counter++
|
|
35
|
+
+0x18 (8B) 8 random bytes nonce / first-block IV seed
|
|
36
|
+
+0x20 (544B) ENCRYPTED AES-128 (CBC or CFB) — 34 blocks of 16 bytes
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Key addresses
|
|
40
|
+
|
|
41
|
+
| Addr | Function |
|
|
42
|
+
|-------------|------------------------------------------------------|
|
|
43
|
+
| 0x10006C8A0 | `DmxWidget::onTimerSendAndReceive` (Qt timer slot) |
|
|
44
|
+
| 0x100074090 | `DmxUniversePage::onTimerSendReceive` |
|
|
45
|
+
| 0x100073BD0 | `DmxUniversePage::sendDmx` (QtConcurrent worker fn) |
|
|
46
|
+
| 0x1001a9d50 | `Stick3CryptDmxUniverse` "send DMX" (vt[22] primary) |
|
|
47
|
+
| 0x1001eeac0 | Frame-build wrapper |
|
|
48
|
+
| 0x1001ee990 | Build encrypted frame |
|
|
49
|
+
| **0x100107950** | **Real encrypt entry** (mode-dispatch CBC vs CFB) |
|
|
50
|
+
| 0x1003f5140 | AES-CFB outer (uses IV at +0xB0) |
|
|
51
|
+
| **0x1003f4690** | **AES-128 block cipher (CFB path, 1153 B)** |
|
|
52
|
+
| 0x1003f6790 | AES-CBC outer (chains with previous ciphertext) |
|
|
53
|
+
| **0x1003f5980** | **AES-128 block cipher (CBC path, 1872 B)** |
|
|
54
|
+
| **DAT_1007c0010** | **AES S-box** (256 bytes) |
|
|
55
|
+
| 0x1001b0340 | `XHL_UdpSocket::send` |
|
|
56
|
+
| 0x100677060 | `sendto` wrapper |
|
|
57
|
+
|
|
58
|
+
The earlier "stream cipher" addresses (`FUN_100108200`, `FUN_1005e0530`) are
|
|
59
|
+
for `XHL_DasEccStreamCryptography` — a sibling class for USB devices, NOT
|
|
60
|
+
on the Stick3 DMX path.
|
|
61
|
+
|
|
62
|
+
Typeinfo addresses (for further vtable hunting via `dyld_info -fixups`):
|
|
63
|
+
|
|
64
|
+
| Class | TypeInfo @ |
|
|
65
|
+
|--------------------------------|----------------|
|
|
66
|
+
| XHL_Stick3CryptDmxUniverse | 0x100970DC0 |
|
|
67
|
+
| XHL_DasEccStreamCryptography | 0x100852080 |
|
|
68
|
+
| XHL_DasEccAesCryptography | 0x100851ED8 |
|
|
69
|
+
| XHL_AesOStream | 0x100851C48 |
|
|
70
|
+
|
|
71
|
+
## Reproduce
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Install (one-time)
|
|
75
|
+
brew install ghidra
|
|
76
|
+
|
|
77
|
+
# Run any script (-noanalysis once initial import has been done)
|
|
78
|
+
/opt/homebrew/Cellar/ghidra/12.1/bin/pyghidraRun -H \
|
|
79
|
+
tools/ghidra HwmESA2 \
|
|
80
|
+
-import /Applications/ESA2/HardwareManager/HardwareManager.app/Contents/MacOS/HardwareManager \
|
|
81
|
+
-scriptPath tools/ghidra \
|
|
82
|
+
-postScript dump-anchors.py
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
After the first run, omit `-import` and add `-process HardwareManager
|
|
86
|
+
-noanalysis` to re-run scripts against the saved project (fast).
|
|
87
|
+
|
|
88
|
+
## Scripts
|
|
89
|
+
|
|
90
|
+
- `dump-anchors.py` — dump every named DMX/AES anchor function (Gladman, sendto, RTTI strings)
|
|
91
|
+
- `find-cipher.py` — chase XHL_UdpSocket::sendto callers and any RTTI vtable
|
|
92
|
+
- `find-vtables.py` — scan for typeinfo bytes in `__const` (mostly empty: Mach-O chained fixups)
|
|
93
|
+
- `string-walk.py` — find typeinfo-name strings then follow their data xrefs
|
|
94
|
+
- `find-stick3-crypto.py` — search Ghidra symbols by class-name keyword (mostly empty)
|
|
95
|
+
- `dump-stick3-vtable.py` — dump every Stick3 primary + secondary vtable slot fn
|
|
96
|
+
- `dump-cipher.py` — dump the encrypt chain rooted at `FUN_1001a9d50`
|
|
97
|
+
- `dump-aes-candidates.py` — dump candidate encrypt fn vt[3] across classes
|
|
98
|
+
|
|
99
|
+
## Side-channel: `dyld_info -fixups`
|
|
100
|
+
|
|
101
|
+
Mach-O chained fixups aren't materialized by Ghidra for unsymbolized
|
|
102
|
+
typeinfos. Use `xcrun dyld_info -fixups <binary>` to dump every rebase /
|
|
103
|
+
bind. Vtables for hidden classes are reached by grepping for rebases
|
|
104
|
+
targeting a known typeinfo struct address. Example:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
xcrun dyld_info -fixups <binary> | grep 'rebase 0x100970DC0$'
|
|
108
|
+
# Each result is a vtable's typeinfo_ptr field; vt[0] is +8.
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## KDF — SOLVED & VERIFIED (2026-05-21)
|
|
112
|
+
|
|
113
|
+
The per-session AES-256 key derivation is fully recovered by static RE and
|
|
114
|
+
**verified end-to-end** against a real captured session.
|
|
115
|
+
Scripts `dump-kdf-chain.py`, `dump-curve.py`, `dump-secret.py` decompile the
|
|
116
|
+
chain `FUN_100178030 → FUN_100107650`. Result:
|
|
117
|
+
|
|
118
|
+
- **Curve = NIST P-256.** `p`, `n`, `G`, and the `b` constant all byte-match.
|
|
119
|
+
- **KDF:** `key = LE( X(d·S) XOR X(d·Q) )` — `d` ephemeral, `S` a hardcoded
|
|
120
|
+
static point (raw 33-byte SEC1 point at `0x1007b0490`), `Q` the Stick
|
|
121
|
+
pubkey from the opcode-0x0F handshake.
|
|
122
|
+
- **Reference impl:** `tools/derive-dmx-key.mjs` (passing self-test).
|
|
123
|
+
- **Proof:** `tools/verify-kdf.mjs` recomputes a real session's installed
|
|
124
|
+
AES key byte-for-byte (using an lldb-dumped `d` + the handshake pcap).
|
|
125
|
+
|
|
126
|
+
Ghidra shows the curve-constant DATs as zero — its `__const`/`__data` blocks
|
|
127
|
+
weren't materialized. Read them straight from the Mach-O file:
|
|
128
|
+
`file_offset = vmaddr − 0x100000000`. Full write-up:
|
|
129
|
+
`memory/stick-cipher-is-stream-not-aes.md`.
|
|
130
|
+
|
|
131
|
+
## What's left
|
|
132
|
+
|
|
133
|
+
1. **Session-magic table** — `DAT_100d11a90` is a table of 14-byte entries.
|
|
134
|
+
Decode it (frame `+0x00` magic + handshake validation).
|
|
135
|
+
2. **Random nonce source** — `FUN_100108670` (likely `std::mt19937`).
|
|
136
|
+
Either reproduce or pick our own.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|