@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,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Path D — catch the key-derivation function with a hardware watchpoint.
|
|
3
|
+
|
|
4
|
+
The 32-byte AES key lives at cipher_obj+0x48 and is rewritten when the
|
|
5
|
+
TCP/2431 handshake completes. cipher_obj is a stable heap address (reused
|
|
6
|
+
across reconnects within one HWM run). So: arm a write-watchpoint on
|
|
7
|
+
cipher_obj+0x48, let the user reconnect HWM, and the instant the handshake
|
|
8
|
+
writes the freshly-derived key, lldb stops — with the KDF call chain on the
|
|
9
|
+
backtrace and the ECDH shared secret live in registers/stack.
|
|
10
|
+
|
|
11
|
+
lldb's SBWatchpoint has no script callback, so run-control is driven here:
|
|
12
|
+
arm the watchpoint(s), then loop process.Continue() until a watched key
|
|
13
|
+
slot CHANGES to a new non-zero value (the KDF rederive — not the disconnect
|
|
14
|
+
zeroing, not a spurious same-value write).
|
|
15
|
+
|
|
16
|
+
Usage (driven by tools/pathD-kdf-watch.sh):
|
|
17
|
+
lldb -p <PID> --batch \
|
|
18
|
+
-o "command script import tools/lldb_pathD_kdf_watch.py" \
|
|
19
|
+
-o "detach" -o "quit"
|
|
20
|
+
After it prints "watchpoint(s) armed", reconnect HWM to the Stick.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import struct
|
|
26
|
+
import time
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import lldb
|
|
30
|
+
except ImportError:
|
|
31
|
+
lldb = None
|
|
32
|
+
|
|
33
|
+
CIPHER_VPTR_FILEADDR = 0x10095F838
|
|
34
|
+
KEY_OFF = 0x48
|
|
35
|
+
LOG_PATH = "/tmp/stick-pathD-kdf.log"
|
|
36
|
+
MAX_CONTINUES = 400
|
|
37
|
+
|
|
38
|
+
_logf = None
|
|
39
|
+
_blobs = []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def log(m=""):
|
|
43
|
+
print(m)
|
|
44
|
+
sys.stdout.flush()
|
|
45
|
+
if _logf:
|
|
46
|
+
_logf.write(m + "\n")
|
|
47
|
+
_logf.flush()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def hexs(b):
|
|
51
|
+
return "".join("%02x" % x for x in b)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read_mem(process, addr, size):
|
|
55
|
+
err = lldb.SBError()
|
|
56
|
+
d = process.ReadMemory(addr, size, err)
|
|
57
|
+
return d if (err.Success() and d) else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def rw_regions(process):
|
|
61
|
+
regions = process.GetMemoryRegions()
|
|
62
|
+
for i in range(regions.GetSize()):
|
|
63
|
+
info = lldb.SBMemoryRegionInfo()
|
|
64
|
+
if regions.GetMemoryRegionAtIndex(i, info) \
|
|
65
|
+
and info.IsReadable() and info.IsWritable():
|
|
66
|
+
yield info.GetRegionBase(), info.GetRegionEnd()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def find_cipher_objects(process, target):
|
|
70
|
+
mod = None
|
|
71
|
+
for m in target.module_iter():
|
|
72
|
+
if m.GetFileSpec().GetFilename() == "HardwareManager":
|
|
73
|
+
mod = m
|
|
74
|
+
break
|
|
75
|
+
if not mod:
|
|
76
|
+
return []
|
|
77
|
+
vptr = mod.ResolveFileAddress(CIPHER_VPTR_FILEADDR).GetLoadAddress(target)
|
|
78
|
+
if vptr == lldb.LLDB_INVALID_ADDRESS:
|
|
79
|
+
return []
|
|
80
|
+
pattern = struct.pack("<Q", vptr)
|
|
81
|
+
hits = []
|
|
82
|
+
CH = 8 * 1024 * 1024
|
|
83
|
+
for base, end in rw_regions(process):
|
|
84
|
+
if end - base > 4 * 1024 * 1024 * 1024:
|
|
85
|
+
continue
|
|
86
|
+
addr, carry = base, b""
|
|
87
|
+
while addr < end:
|
|
88
|
+
n = min(CH, end - addr)
|
|
89
|
+
data = read_mem(process, addr, n)
|
|
90
|
+
if data is None:
|
|
91
|
+
addr += n
|
|
92
|
+
carry = b""
|
|
93
|
+
continue
|
|
94
|
+
bb = carry + data
|
|
95
|
+
bbase = addr - len(carry)
|
|
96
|
+
s = 0
|
|
97
|
+
while True:
|
|
98
|
+
idx = bb.find(pattern, s)
|
|
99
|
+
if idx < 0:
|
|
100
|
+
break
|
|
101
|
+
hits.append(bbase + idx)
|
|
102
|
+
s = idx + 1
|
|
103
|
+
carry = bb[-7:]
|
|
104
|
+
addr += n
|
|
105
|
+
return [h for h in hits if h > 0x7000_0000_0000] # heap objects only
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def backtrace(thread):
|
|
109
|
+
log("")
|
|
110
|
+
log("backtrace (KDF call chain):")
|
|
111
|
+
for i in range(min(thread.GetNumFrames(), 28)):
|
|
112
|
+
f = thread.GetFrameAtIndex(i)
|
|
113
|
+
log(" #%-2d 0x%x %s" % (i, f.GetPC(), f.GetFunctionName() or "?"))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def capture(process, frame, label):
|
|
117
|
+
log("")
|
|
118
|
+
log("=" * 68)
|
|
119
|
+
log("CAPTURE @ %s" % label)
|
|
120
|
+
log("=" * 68)
|
|
121
|
+
regs = {}
|
|
122
|
+
for r in ("rdi rsi rdx rcx r8 r9 rax rbx rbp rsp r10 r11 r12 r13 r14 "
|
|
123
|
+
"r15 rip").split():
|
|
124
|
+
v = frame.FindRegister(r)
|
|
125
|
+
regs[r] = v.GetValueAsUnsigned() if v.IsValid() else 0
|
|
126
|
+
log(" %-4s = 0x%x" % (r, regs[r]))
|
|
127
|
+
for r in ("rdi rsi rdx rcx r8 r9 rax rbx r12 r13 r14 r15").split():
|
|
128
|
+
a = regs[r]
|
|
129
|
+
if 0x10000 < a < 0x7fffffffffff:
|
|
130
|
+
d = read_mem(process, a, 1024)
|
|
131
|
+
if d:
|
|
132
|
+
_blobs.append(("%s_0x%x" % (r, a), a, d))
|
|
133
|
+
rsp = regs["rsp"]
|
|
134
|
+
st = read_mem(process, rsp - 0x400, 0x6000)
|
|
135
|
+
if st:
|
|
136
|
+
_blobs.append(("stack_0x%x" % (rsp - 0x400), rsp - 0x400, st))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def finalize():
|
|
140
|
+
log("")
|
|
141
|
+
log("=" * 68)
|
|
142
|
+
log("saving %d blob(s) for offline KDF analysis" % len(_blobs))
|
|
143
|
+
for label, addr, data in _blobs:
|
|
144
|
+
p = "/tmp/stick-pathD-blob-%s.bin" % label
|
|
145
|
+
with open(p, "wb") as f:
|
|
146
|
+
f.write(data)
|
|
147
|
+
log(" %-22s %6d B @ 0x%x" % (label, len(data), addr))
|
|
148
|
+
log("")
|
|
149
|
+
log("NEXT: node tools/crack-kdf.mjs <new-key-hex> /tmp/stick-pathD-blob-*.bin")
|
|
150
|
+
log("done — detaching. log: %s" % LOG_PATH)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def __lldb_init_module(debugger, internal_dict):
|
|
154
|
+
global _logf
|
|
155
|
+
_logf = open(LOG_PATH, "w")
|
|
156
|
+
# async: process.Continue() returns immediately and we poll, so the
|
|
157
|
+
# script can never wedge in an unkillable synchronous kernel wait.
|
|
158
|
+
debugger.SetAsync(True)
|
|
159
|
+
|
|
160
|
+
target = debugger.GetSelectedTarget()
|
|
161
|
+
process = target.GetProcess() if target else None
|
|
162
|
+
if not process or not process.IsValid():
|
|
163
|
+
log("ERROR: no live process")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
log("=== Path D KDF watchpoint %s ===" % time.strftime("%Y-%m-%d %H:%M:%S"))
|
|
167
|
+
objs = find_cipher_objects(process, target)
|
|
168
|
+
if not objs:
|
|
169
|
+
log("ERROR: no live cipher object found")
|
|
170
|
+
return
|
|
171
|
+
log("cipher object(s): %s" % ", ".join("0x%x" % o for o in objs))
|
|
172
|
+
|
|
173
|
+
# The 32-byte key spans cipher_obj+0x48 (half C) .. +0x58 (half D). A real
|
|
174
|
+
# handshake rederive changes BOTH halves; UI writes touch only one. Watch
|
|
175
|
+
# both 8-byte halves so we get a hit once the full key is written. Active
|
|
176
|
+
# objects (non-zero key) come first so they get watchpoints within the
|
|
177
|
+
# hardware budget (4 on x86_64).
|
|
178
|
+
objs.sort(key=lambda o: 0 if any(read_mem(process, o + KEY_OFF, 32) or b"")
|
|
179
|
+
else 1)
|
|
180
|
+
# one initial 32-byte key per object
|
|
181
|
+
initials = {}
|
|
182
|
+
for o in objs:
|
|
183
|
+
initials[o] = read_mem(process, o + KEY_OFF, 32) or b"\x00" * 32
|
|
184
|
+
log(" obj 0x%x key = %s" % (o, hexs(initials[o])))
|
|
185
|
+
|
|
186
|
+
budget = 4
|
|
187
|
+
armed_objs = []
|
|
188
|
+
for o in objs:
|
|
189
|
+
ok = False
|
|
190
|
+
for half in (0x48, 0x58):
|
|
191
|
+
if budget <= 0:
|
|
192
|
+
break
|
|
193
|
+
err = lldb.SBError()
|
|
194
|
+
wp = target.WatchAddress(o + half, 8, False, True, err)
|
|
195
|
+
if err.Success() and wp and wp.IsValid():
|
|
196
|
+
budget -= 1
|
|
197
|
+
ok = True
|
|
198
|
+
log(" watchpoint id %d on 0x%x (+0x%x)"
|
|
199
|
+
% (wp.GetID(), o + half, half))
|
|
200
|
+
else:
|
|
201
|
+
log(" WARNING: watchpoint on 0x%x failed: %s"
|
|
202
|
+
% (o + half, err))
|
|
203
|
+
if ok:
|
|
204
|
+
armed_objs.append(o)
|
|
205
|
+
|
|
206
|
+
if not armed_objs:
|
|
207
|
+
log("ERROR: no watchpoints armed")
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# Force the reconnect ourselves: HWM is already stopped (from the lldb
|
|
211
|
+
# attach). Hold it stopped long enough that the Stick drops the idle
|
|
212
|
+
# TCP/2431 session (~41s sufficed in an earlier scan; 85s is safe). When
|
|
213
|
+
# we then resume, HWM finds the dead socket and reconnects — a fresh
|
|
214
|
+
# handshake + KDF — with no UI interaction needed.
|
|
215
|
+
FREEZE = 85
|
|
216
|
+
log("")
|
|
217
|
+
log(">>> watchpoint(s) armed.")
|
|
218
|
+
log(">>> Freezing HWM for %ds so the Stick drops the idle TCP session" % FREEZE)
|
|
219
|
+
log(">>> (HWM has been stopped since the lldb attach; just holding it).")
|
|
220
|
+
t0 = time.time()
|
|
221
|
+
while time.time() - t0 < FREEZE:
|
|
222
|
+
time.sleep(1.0)
|
|
223
|
+
log(">>> resuming HWM — it will detect the dead socket and reconnect.")
|
|
224
|
+
log(">>> watching for the reconnect handshake to rederive the key ...")
|
|
225
|
+
log("")
|
|
226
|
+
|
|
227
|
+
# Async poll loop, hard-bounded by a wall-clock deadline. process.Continue()
|
|
228
|
+
# returns immediately; we poll GetState() with short sleeps, so the script
|
|
229
|
+
# always self-terminates and lets lldb run `detach`/`quit` — it can never
|
|
230
|
+
# hang in an unkillable synchronous wait.
|
|
231
|
+
last_seen = dict(initials) # last key value observed per object
|
|
232
|
+
deadline = time.time() + 300.0
|
|
233
|
+
process.Continue()
|
|
234
|
+
|
|
235
|
+
while time.time() < deadline:
|
|
236
|
+
st = process.GetState()
|
|
237
|
+
if st == lldb.eStateExited:
|
|
238
|
+
log("process exited — aborting")
|
|
239
|
+
return
|
|
240
|
+
if st != lldb.eStateStopped:
|
|
241
|
+
time.sleep(0.2)
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
# stopped — a watchpoint (or signal) hit
|
|
245
|
+
thread = process.GetSelectedThread()
|
|
246
|
+
for t in process:
|
|
247
|
+
if t.GetStopReason() == lldb.eStopReasonWatchpoint:
|
|
248
|
+
thread = t
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
full = None
|
|
252
|
+
for o in armed_objs:
|
|
253
|
+
v = read_mem(process, o + KEY_OFF, 32)
|
|
254
|
+
if not v or v == last_seen[o]:
|
|
255
|
+
continue # unchanged / stale re-read
|
|
256
|
+
last_seen[o] = v
|
|
257
|
+
init = initials[o]
|
|
258
|
+
c_new = v[0:16] != init[0:16]
|
|
259
|
+
d_new = v[16:32] != init[16:32]
|
|
260
|
+
nonzero = any(b != 0 for b in v)
|
|
261
|
+
if c_new and d_new and nonzero:
|
|
262
|
+
full = (o, v)
|
|
263
|
+
else:
|
|
264
|
+
log(" (partial write: %s changed via %s — skipping)"
|
|
265
|
+
% ("C" if c_new else "D" if d_new else "?",
|
|
266
|
+
thread.GetFrameAtIndex(0).GetFunctionName() or "?"))
|
|
267
|
+
if full:
|
|
268
|
+
log("")
|
|
269
|
+
log(">>> FULL KEY REDERIVED — key@0x%x" % (full[0] + KEY_OFF))
|
|
270
|
+
log(" %s" % hexs(full[1]))
|
|
271
|
+
backtrace(thread)
|
|
272
|
+
capture(process, thread.GetFrameAtIndex(0), "full key rederive (KDF)")
|
|
273
|
+
finalize()
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
process.Continue() # resume, wait for the next write
|
|
277
|
+
time.sleep(0.25)
|
|
278
|
+
|
|
279
|
+
log("")
|
|
280
|
+
log("timed out after 5 min — no full key rederive seen. HWM left running;")
|
|
281
|
+
log("re-run and be sure to fully Disconnect THEN Connect in HWM.")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Path A, step 1 — launch a debuggable copy of the ESA2 Hardware Manager.
|
|
3
|
+
#
|
|
4
|
+
# Why a copy: the installed ESA2 HWM is signed with hardened runtime and NO
|
|
5
|
+
# `get-task-allow`, so lldb cannot attach (`attach failed: lost connection`).
|
|
6
|
+
# We copy it, adhoc-re-sign the whole bundle with tools/hwm-entitlements.plist
|
|
7
|
+
# (which now includes `get-task-allow` + `disable-library-validation`), and
|
|
8
|
+
# launch that. lldb can then attach to the copy for a passive memory scan.
|
|
9
|
+
#
|
|
10
|
+
# Source MUST be the ESA2 build — the Ghidra addresses (vptr 0x10095F838 etc.)
|
|
11
|
+
# come from the 2024-03-21 ESA2 binary, not the EsaPro2 one.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# tools/pathA-launch-hwm.sh # launch the debug copy
|
|
15
|
+
# FORCE_KILL=1 tools/pathA-launch-hwm.sh # also kill any running HWM first
|
|
16
|
+
#
|
|
17
|
+
# The Stick is single-session: only one Hardware Manager can hold the TCP/2431
|
|
18
|
+
# slot. Quit the normally-installed HWM before launching this copy.
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
23
|
+
SRC="/Applications/ESA2/HardwareManager/HardwareManager.app"
|
|
24
|
+
COPY="/tmp/HM-ESA2-dbg.app"
|
|
25
|
+
ENTS="$SCRIPT_DIR/hwm-entitlements.plist"
|
|
26
|
+
|
|
27
|
+
[ -d "$SRC" ] || { echo "ERROR: $SRC not found"; exit 1; }
|
|
28
|
+
[ -f "$ENTS" ] || { echo "ERROR: $ENTS not found"; exit 1; }
|
|
29
|
+
|
|
30
|
+
# --- single-session guard -------------------------------------------------
|
|
31
|
+
RUNNING="$(pgrep -fl 'HardwareManager.app/Contents/MacOS/HardwareManager' || true)"
|
|
32
|
+
if [ -n "$RUNNING" ]; then
|
|
33
|
+
# ignore our own debug copy when deciding
|
|
34
|
+
OTHER="$(echo "$RUNNING" | grep -v "$COPY" || true)"
|
|
35
|
+
if [ -n "$OTHER" ]; then
|
|
36
|
+
echo ">>> A Hardware Manager is already running:"
|
|
37
|
+
echo "$OTHER" | sed 's/^/ /'
|
|
38
|
+
if [ "${FORCE_KILL:-0}" = "1" ]; then
|
|
39
|
+
echo ">>> FORCE_KILL=1 — terminating it so the Stick session frees up."
|
|
40
|
+
echo "$OTHER" | awk '{print $1}' | xargs -r kill
|
|
41
|
+
sleep 2
|
|
42
|
+
else
|
|
43
|
+
echo ">>> The Stick allows only ONE HWM session. Quit it, then re-run."
|
|
44
|
+
echo ">>> (or re-run with FORCE_KILL=1)"
|
|
45
|
+
exit 2
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# --- (re)build the debug copy --------------------------------------------
|
|
51
|
+
if [ "${REBUILD:-0}" = "1" ] && [ -d "$COPY" ]; then
|
|
52
|
+
echo ">>> REBUILD=1 — removing stale $COPY"
|
|
53
|
+
rm -rf "$COPY"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if [ ! -d "$COPY" ]; then
|
|
57
|
+
echo ">>> Copying $SRC"
|
|
58
|
+
echo " -> $COPY"
|
|
59
|
+
cp -R "$SRC" "$COPY"
|
|
60
|
+
|
|
61
|
+
echo ">>> Adhoc re-signing the bundle with debug entitlements"
|
|
62
|
+
# Adhoc (--sign -), no hardened runtime, no library validation. --deep
|
|
63
|
+
# re-signs the nested Qt frameworks adhoc too so signatures stay
|
|
64
|
+
# consistent. The entitlements (incl. get-task-allow) apply to the main
|
|
65
|
+
# executable — that is what lldb attaches to.
|
|
66
|
+
codesign --force --deep --sign - --entitlements "$ENTS" "$COPY" 2>&1 \
|
|
67
|
+
| sed 's/^/ /'
|
|
68
|
+
|
|
69
|
+
echo ">>> Verifying get-task-allow landed on the main executable"
|
|
70
|
+
if codesign -d --entitlements - "$COPY" 2>/dev/null | grep -q 'get-task-allow'; then
|
|
71
|
+
echo " OK: get-task-allow present"
|
|
72
|
+
else
|
|
73
|
+
echo " WARNING: get-task-allow not found — lldb attach may still fail"
|
|
74
|
+
fi
|
|
75
|
+
else
|
|
76
|
+
echo ">>> Using existing debug copy at $COPY (REBUILD=1 to recreate)"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
echo
|
|
80
|
+
echo ">>> Launching the debug Hardware Manager"
|
|
81
|
+
echo " Next steps:"
|
|
82
|
+
echo " 1. In this HWM, connect to the Stick at 192.168.96.2"
|
|
83
|
+
echo " 2. Open the DMX / fader screen so live DMX is streaming"
|
|
84
|
+
echo " 3. In another terminal: tools/pathA-scan.sh"
|
|
85
|
+
echo
|
|
86
|
+
open -n "$COPY"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Path A, step 2 — attach lldb to the debug HWM and run the passive scan.
|
|
3
|
+
#
|
|
4
|
+
# Prereqs:
|
|
5
|
+
# 1. tools/pathA-launch-hwm.sh has launched the debug copy
|
|
6
|
+
# 2. that HWM is connected to the Stick and streaming live DMX
|
|
7
|
+
#
|
|
8
|
+
# This attaches lldb, runs tools/lldb-pathA-scan.py (no breakpoints — one
|
|
9
|
+
# brief stop while it scans), then detaches. HWM keeps running.
|
|
10
|
+
#
|
|
11
|
+
# Set STICK_HEAP_DUMP=1 to also dump rw memory for an offline heap-wide
|
|
12
|
+
# AES key-schedule search (slower; only needed if the fast scan finds nothing).
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
cd "$(dirname "$0")/.."
|
|
16
|
+
|
|
17
|
+
SCAN_PY="tools/lldb_pathA_scan.py"
|
|
18
|
+
LOG="/tmp/stick-pathA-scan.log"
|
|
19
|
+
|
|
20
|
+
# Find the debug copy's pid (NOT the installed HWM).
|
|
21
|
+
PID="$(pgrep -f '/tmp/HM-ESA2-dbg.app/Contents/MacOS/HardwareManager' | head -1)"
|
|
22
|
+
|
|
23
|
+
if [ -z "$PID" ]; then
|
|
24
|
+
echo "ERROR: debug HWM not running."
|
|
25
|
+
echo " Run tools/pathA-launch-hwm.sh first, connect it to the Stick,"
|
|
26
|
+
echo " and open the DMX screen so live DMX is streaming."
|
|
27
|
+
# show what IS running, to help diagnose
|
|
28
|
+
echo
|
|
29
|
+
echo "Currently running Hardware Managers:"
|
|
30
|
+
pgrep -fl 'HardwareManager.app/Contents/MacOS/HardwareManager' | sed 's/^/ /' \
|
|
31
|
+
|| echo " (none)"
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
echo ">>> debug HWM pid = $PID"
|
|
36
|
+
echo ">>> attaching lldb for a passive scan (one brief stop, then detach)"
|
|
37
|
+
[ "${STICK_HEAP_DUMP:-0}" = "1" ] && echo ">>> STICK_HEAP_DUMP=1 — will also dump rw memory"
|
|
38
|
+
echo
|
|
39
|
+
|
|
40
|
+
STICK_HEAP_DUMP="${STICK_HEAP_DUMP:-0}" \
|
|
41
|
+
lldb --batch \
|
|
42
|
+
-o "command script import $SCAN_PY" \
|
|
43
|
+
-o "detach" \
|
|
44
|
+
-o "quit" \
|
|
45
|
+
-p "$PID" 2>&1 | sed 's/^/ /'
|
|
46
|
+
|
|
47
|
+
echo
|
|
48
|
+
if [ -f "$LOG" ]; then
|
|
49
|
+
echo ">>> full log saved at $LOG"
|
|
50
|
+
else
|
|
51
|
+
echo "!!! no log produced — the scan script may have errored above"
|
|
52
|
+
fi
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Path C-lite — attach lldb and run the runtime key-trace.
|
|
3
|
+
#
|
|
4
|
+
# Prereqs (same as pathA-scan.sh):
|
|
5
|
+
# - the debug HWM (tools/pathA-launch-hwm.sh) is running
|
|
6
|
+
# - it is connected to the Stick and streaming live DMX
|
|
7
|
+
#
|
|
8
|
+
# Sets one-shot breakpoints on the encrypt path, lets ONE frame hit, dumps
|
|
9
|
+
# registers + stack + the cipher/device memory, runs the AES key-schedule
|
|
10
|
+
# detector, then detaches. HWM is stopped ~1-2s total.
|
|
11
|
+
|
|
12
|
+
set -uo pipefail
|
|
13
|
+
cd "$(dirname "$0")/.."
|
|
14
|
+
|
|
15
|
+
SCAN_PY="tools/lldb_pathC_keytrace.py"
|
|
16
|
+
LOG="/tmp/stick-pathC-keytrace.log"
|
|
17
|
+
|
|
18
|
+
PID="$(pgrep -f '/tmp/HM-ESA2-dbg.app/Contents/MacOS/HardwareManager' | head -1)"
|
|
19
|
+
if [ -z "$PID" ]; then
|
|
20
|
+
echo "ERROR: debug HWM not running. Run tools/pathA-launch-hwm.sh first,"
|
|
21
|
+
echo " connect it to the Stick, and stream live DMX."
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
echo ">>> debug HWM pid = $PID"
|
|
26
|
+
echo ">>> attaching lldb for the runtime key-trace"
|
|
27
|
+
echo ">>> 'continue' runs until the encrypt path fires (~40ms at 25Hz)"
|
|
28
|
+
echo
|
|
29
|
+
|
|
30
|
+
# 'continue' blocks until a breakpoint callback returns True (stop). The
|
|
31
|
+
# script's fallback (capture-at-entry after 6 frames) guarantees that always
|
|
32
|
+
# happens, so 'continue' cannot hang. timeout is just a safety net.
|
|
33
|
+
timeout 180 lldb --batch \
|
|
34
|
+
-o "command script import $SCAN_PY" \
|
|
35
|
+
-o "continue" \
|
|
36
|
+
-o "detach" \
|
|
37
|
+
-o "quit" \
|
|
38
|
+
-p "$PID" 2>&1 | sed 's/^/ /'
|
|
39
|
+
|
|
40
|
+
echo
|
|
41
|
+
if [ -f "$LOG" ]; then
|
|
42
|
+
echo ">>> full log: $LOG"
|
|
43
|
+
echo ">>> captured memory blobs: /tmp/stick-pathC-blob-*.bin"
|
|
44
|
+
echo
|
|
45
|
+
echo "If a key was recovered: node tools/try-key.mjs <frame.bin> <key-hex>"
|
|
46
|
+
echo "Otherwise brute-force: node tools/crack-key.mjs <frame.bin> /tmp/stick-pathC-blob-*.bin"
|
|
47
|
+
else
|
|
48
|
+
echo "!!! no log produced — the trace script may have errored above"
|
|
49
|
+
fi
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Atomic same-session key capture.
|
|
3
|
+
#
|
|
4
|
+
# The Stick's AES key is per-session: every TCP/2431 reconnect derives a new
|
|
5
|
+
# key. A long lldb stop (the 41s passive scan) kills the session, so the key
|
|
6
|
+
# you read no longer matches frames captured afterwards.
|
|
7
|
+
#
|
|
8
|
+
# This script avoids that:
|
|
9
|
+
# 1. start tcpdump in the background (captures encrypted frames)
|
|
10
|
+
# 2. run the Path C breakpoint key-trace — its stop is only ~1-2s, short
|
|
11
|
+
# enough that the TCP session survives
|
|
12
|
+
# 3. extract the FIRST captured frame (captured before the attach => same
|
|
13
|
+
# session as the key Path C caught)
|
|
14
|
+
# 4. crack-key / try-key against the captured memory
|
|
15
|
+
#
|
|
16
|
+
# Run it from a terminal (tcpdump needs sudo — it will prompt once):
|
|
17
|
+
# tools/pathC-run.sh
|
|
18
|
+
#
|
|
19
|
+
# Prereqs: the debug HWM is running, connected to the Stick, streaming DMX.
|
|
20
|
+
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
cd "$(dirname "$0")/.."
|
|
23
|
+
|
|
24
|
+
IFACE="${IFACE:-en0}"
|
|
25
|
+
STICK_IP="${STICK_IP:-192.168.96.2}"
|
|
26
|
+
STICK_PORT="${STICK_PORT:-2431}"
|
|
27
|
+
PCAP=/tmp/stick-pathC.pcap
|
|
28
|
+
FRAME=/tmp/stick-pathC-frame.bin
|
|
29
|
+
CAP_SECS="${CAP_SECS:-55}"
|
|
30
|
+
|
|
31
|
+
echo ">>> caching sudo credentials for tcpdump"
|
|
32
|
+
sudo -v
|
|
33
|
+
|
|
34
|
+
echo ">>> starting ${CAP_SECS}s background capture of UDP -> ${STICK_IP}:${STICK_PORT} on ${IFACE}"
|
|
35
|
+
rm -f "$PCAP" "$FRAME"
|
|
36
|
+
sudo tcpdump -i "$IFACE" -w "$PCAP" -G "$CAP_SECS" -W 1 \
|
|
37
|
+
"udp and host ${STICK_IP} and port ${STICK_PORT}" 2>/dev/null &
|
|
38
|
+
TCPDUMP_PID=$!
|
|
39
|
+
sleep 3 # let some frames land before we attach
|
|
40
|
+
|
|
41
|
+
echo ">>> running Path C key-trace (short stop — session should survive)"
|
|
42
|
+
tools/pathC-keytrace.sh
|
|
43
|
+
|
|
44
|
+
echo ">>> waiting for the background capture to finish"
|
|
45
|
+
wait "$TCPDUMP_PID" 2>/dev/null || true
|
|
46
|
+
|
|
47
|
+
if [[ ! -s "$PCAP" ]]; then
|
|
48
|
+
echo "!!! no packets captured — is HWM connected + streaming? wrong IFACE?"
|
|
49
|
+
exit 2
|
|
50
|
+
fi
|
|
51
|
+
echo ">>> pcap: $(wc -c < "$PCAP") bytes"
|
|
52
|
+
|
|
53
|
+
echo ">>> extracting the first 576-byte frame (pre-attach => same session)"
|
|
54
|
+
STICK_IP="$STICK_IP" STICK_PORT="$STICK_PORT" \
|
|
55
|
+
node tools/extract-frame.mjs "$PCAP" "$FRAME" || {
|
|
56
|
+
echo "!!! no 576-byte frame found in the capture"; exit 3; }
|
|
57
|
+
|
|
58
|
+
echo
|
|
59
|
+
echo ">>> === try any key Path C recovered from a live schedule ==="
|
|
60
|
+
shopt -s nullglob
|
|
61
|
+
KEYHIT=0
|
|
62
|
+
for kf in /tmp/stick-pathC-key-*.bin; do
|
|
63
|
+
KEYHIT=1
|
|
64
|
+
echo "--- $kf ---"
|
|
65
|
+
node tools/try-key.mjs "$FRAME" "$(xxd -p -c256 "$kf")" || true
|
|
66
|
+
done
|
|
67
|
+
[ "$KEYHIT" = 0 ] && echo "(Path C recovered no standard schedule — going to brute-force)"
|
|
68
|
+
|
|
69
|
+
echo
|
|
70
|
+
echo ">>> === brute-force the captured memory blobs against the frame ==="
|
|
71
|
+
node tools/crack-key.mjs "$FRAME" /tmp/stick-pathC-blob-*.bin
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Path D — attach lldb, arm a watchpoint on the AES key, catch the KDF.
|
|
3
|
+
#
|
|
4
|
+
# Prereqs: the debug HWM is running and currently CONNECTED to the Stick
|
|
5
|
+
# (so the cipher object exists with a live key to watch).
|
|
6
|
+
#
|
|
7
|
+
# Flow:
|
|
8
|
+
# 1. this attaches lldb and arms a write-watchpoint on cipher_obj+0x48
|
|
9
|
+
# 2. when it prints "watchpoint armed", YOU disconnect HWM from the Stick
|
|
10
|
+
# and reconnect it (Network panel -> Disconnect, then Connect)
|
|
11
|
+
# 3. the handshake rederives the key -> watchpoint fires -> lldb dumps the
|
|
12
|
+
# KDF backtrace + registers + stack, then detaches
|
|
13
|
+
#
|
|
14
|
+
# Also run a handshake capture in another terminal so the X25519 pubkeys
|
|
15
|
+
# are recorded for the same session:
|
|
16
|
+
# tools/capture-handshake.sh
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
cd "$(dirname "$0")/.."
|
|
20
|
+
|
|
21
|
+
SCAN_PY="tools/lldb_pathD_kdf_watch.py"
|
|
22
|
+
LOG="/tmp/stick-pathD-kdf.log"
|
|
23
|
+
|
|
24
|
+
PID="$(pgrep -f '/tmp/HM-ESA2-dbg.app/Contents/MacOS/HardwareManager' | head -1)"
|
|
25
|
+
if [ -z "$PID" ]; then
|
|
26
|
+
echo "ERROR: debug HWM not running — run tools/pathA-launch-hwm.sh first."
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
echo ">>> debug HWM pid = $PID"
|
|
31
|
+
echo ">>> attaching lldb and arming the KDF watchpoint"
|
|
32
|
+
echo ">>> when it says 'watchpoint armed', reconnect HWM to the Stick"
|
|
33
|
+
echo
|
|
34
|
+
|
|
35
|
+
# The import script arms the watchpoint and drives its own continue-loop
|
|
36
|
+
# (lldb watchpoints have no script callback), so there is NO -o "continue"
|
|
37
|
+
# here. Generous timeout: it waits for a human-driven reconnect.
|
|
38
|
+
timeout 600 lldb --batch \
|
|
39
|
+
-o "command script import $SCAN_PY" \
|
|
40
|
+
-o "detach" \
|
|
41
|
+
-o "quit" \
|
|
42
|
+
-p "$PID" 2>&1 | sed 's/^/ /'
|
|
43
|
+
|
|
44
|
+
echo
|
|
45
|
+
if [ -f "$LOG" ]; then
|
|
46
|
+
echo ">>> full log: $LOG"
|
|
47
|
+
echo ">>> captured blobs: /tmp/stick-pathD-blob-*.bin"
|
|
48
|
+
echo ">>> then: node tools/crack-kdf.mjs <new-key-hex> /tmp/stick-pathD-blob-*.bin"
|
|
49
|
+
else
|
|
50
|
+
echo "!!! no log produced — the trace script may have errored above"
|
|
51
|
+
fi
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Stick-DE3 "Quick Trigger" tester — documented, UNencrypted, UNauthenticated
|
|
2
|
+
// control path (STICK Remote Protocol spec, section 1).
|
|
3
|
+
//
|
|
4
|
+
// Sends a 24-byte Quick Trigger packet via UDP to the Stick on port 2430.
|
|
5
|
+
// Per the spec, UDP/2430 Quick Triggering "has no authentication layer" — so
|
|
6
|
+
// this works regardless of firmware 2.x TCP auth / "Security for Cloud Access".
|
|
7
|
+
//
|
|
8
|
+
// Packet layout (exact, from the spec; example in the spec sums to 24 bytes):
|
|
9
|
+
// [0..7] ID[8] "Stick_3A"
|
|
10
|
+
// [8..9] OpCode[2] 109 -> 0x6D 0x00 (LE)
|
|
11
|
+
// [10..11]SceneNr[2] LE (Scene Number = Page*50 + SceneInPage)
|
|
12
|
+
// [12] ZoneSyncId 1
|
|
13
|
+
// [13] Command 1 (see CMD below)
|
|
14
|
+
// [14..15]Dimmer[2] LE
|
|
15
|
+
// [16..17]Speed[2] LE
|
|
16
|
+
// [18] Unused 1
|
|
17
|
+
// [19] Unused 1
|
|
18
|
+
// [20..23]Color[4] R G B 00
|
|
19
|
+
//
|
|
20
|
+
// Usage:
|
|
21
|
+
// node tools/quick-trigger.mjs blackout-on
|
|
22
|
+
// node tools/quick-trigger.mjs blackout-off
|
|
23
|
+
// node tools/quick-trigger.mjs scene-on <sceneNr>
|
|
24
|
+
// node tools/quick-trigger.mjs scene-off <sceneNr>
|
|
25
|
+
// node tools/quick-trigger.mjs dimmer <sceneNr> <0-255>
|
|
26
|
+
// node tools/quick-trigger.mjs color <sceneNr> <r> <g> <b>
|
|
27
|
+
// node tools/quick-trigger.mjs raw <sceneNr> <cmd> [dimmer] [r] [g] [b]
|
|
28
|
+
// Env: STICK_IP (default 192.168.96.2), STICK_PORT (default 2430)
|
|
29
|
+
|
|
30
|
+
import dgram from 'node:dgram';
|
|
31
|
+
|
|
32
|
+
const STICK_IP = process.env.STICK_IP || '192.168.96.2';
|
|
33
|
+
const STICK_PORT = Number(process.env.STICK_PORT || 2430);
|
|
34
|
+
|
|
35
|
+
const CMD = {
|
|
36
|
+
'scene-off': 0, 'scene-on': 1, 'pause-off': 2, 'pause-on': 3,
|
|
37
|
+
'reset': 4, 'dimmer-set': 5, 'speed-set': 6, 'color-set': 7,
|
|
38
|
+
'blackout-off': 8, 'blackout-on': 9,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function buildPacket({ scene = 0, zoneSync = 0, command = 0, dimmer = 0, speed = 0, r = 0, g = 0, b = 0 }) {
|
|
42
|
+
const p = Buffer.alloc(24);
|
|
43
|
+
p.write('Stick_3A', 0, 'latin1'); // [0..7]
|
|
44
|
+
p.writeUInt16LE(109, 8); // [8..9] OpCode
|
|
45
|
+
p.writeUInt16LE(scene & 0xffff, 10); // [10..11] SceneNr
|
|
46
|
+
p.writeUInt8(zoneSync & 0xff, 12); // [12]
|
|
47
|
+
p.writeUInt8(command & 0xff, 13); // [13] Command
|
|
48
|
+
p.writeUInt16LE(dimmer & 0xffff, 14);// [14..15] Dimmer
|
|
49
|
+
p.writeUInt16LE(speed & 0xffff, 16); // [16..17] Speed
|
|
50
|
+
// [18],[19] unused = 0
|
|
51
|
+
p.writeUInt8(r & 0xff, 20);
|
|
52
|
+
p.writeUInt8(g & 0xff, 21);
|
|
53
|
+
p.writeUInt8(b & 0xff, 22);
|
|
54
|
+
// [23] = 0
|
|
55
|
+
return p;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const [sub, ...rest] = process.argv.slice(2);
|
|
59
|
+
if (!sub) {
|
|
60
|
+
console.error('usage: node tools/quick-trigger.mjs <blackout-on|blackout-off|scene-on|scene-off|dimmer|color|raw> [args]');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let opts;
|
|
65
|
+
switch (sub) {
|
|
66
|
+
case 'blackout-on': opts = { command: CMD['blackout-on'] }; break;
|
|
67
|
+
case 'blackout-off': opts = { command: CMD['blackout-off'] }; break;
|
|
68
|
+
case 'scene-on': opts = { command: CMD['scene-on'], scene: +rest[0] }; break;
|
|
69
|
+
case 'scene-off': opts = { command: CMD['scene-off'], scene: +rest[0] }; break;
|
|
70
|
+
case 'dimmer': opts = { command: CMD['dimmer-set'], scene: +rest[0], dimmer: +rest[1] }; break;
|
|
71
|
+
case 'color': opts = { command: CMD['color-set'], scene: +rest[0], r: +rest[1], g: +rest[2], b: +rest[3] }; break;
|
|
72
|
+
case 'raw': opts = { scene: +rest[0], command: +rest[1], dimmer: +(rest[2]||0), r: +(rest[3]||0), g: +(rest[4]||0), b: +(rest[5]||0) }; break;
|
|
73
|
+
default:
|
|
74
|
+
console.error(`unknown subcommand: ${sub}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const pkt = buildPacket(opts);
|
|
79
|
+
const sock = dgram.createSocket('udp4');
|
|
80
|
+
const ts = new Date().toISOString();
|
|
81
|
+
sock.send(pkt, STICK_PORT, STICK_IP, (err) => {
|
|
82
|
+
if (err) { console.error(`${ts} send error:`, err.message); process.exit(1); }
|
|
83
|
+
console.log(`${ts} -> ${STICK_IP}:${STICK_PORT} ${sub} ${rest.join(' ')}`);
|
|
84
|
+
console.log(` ${pkt.length}B: ${pkt.toString('hex').replace(/(..)/g, '$1 ').trim()}`);
|
|
85
|
+
sock.close();
|
|
86
|
+
});
|