@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.
Files changed (226) hide show
  1. package/CLAUDE.md +176 -0
  2. package/README.md +146 -0
  3. package/config.schema.json +143 -0
  4. package/dist/cli/stick.d.ts +3 -0
  5. package/dist/cli/stick.d.ts.map +1 -0
  6. package/dist/cli/stick.js +176 -0
  7. package/dist/cli/stick.js.map +1 -0
  8. package/dist/color/cct.d.ts +3 -0
  9. package/dist/color/cct.d.ts.map +1 -0
  10. package/dist/color/cct.js +33 -0
  11. package/dist/color/cct.js.map +1 -0
  12. package/dist/color/dimmer.d.ts +3 -0
  13. package/dist/color/dimmer.d.ts.map +1 -0
  14. package/dist/color/dimmer.js +16 -0
  15. package/dist/color/dimmer.js.map +1 -0
  16. package/dist/color/hsvcct.d.ts +3 -0
  17. package/dist/color/hsvcct.d.ts.map +1 -0
  18. package/dist/color/hsvcct.js +52 -0
  19. package/dist/color/hsvcct.js.map +1 -0
  20. package/dist/color/index.d.ts +7 -0
  21. package/dist/color/index.d.ts.map +1 -0
  22. package/dist/color/index.js +28 -0
  23. package/dist/color/index.js.map +1 -0
  24. package/dist/color/parsers.d.ts +20 -0
  25. package/dist/color/parsers.d.ts.map +1 -0
  26. package/dist/color/parsers.js +232 -0
  27. package/dist/color/parsers.js.map +1 -0
  28. package/dist/color/rgb.d.ts +3 -0
  29. package/dist/color/rgb.d.ts.map +1 -0
  30. package/dist/color/rgb.js +32 -0
  31. package/dist/color/rgb.js.map +1 -0
  32. package/dist/color/rgbaw.d.ts +3 -0
  33. package/dist/color/rgbaw.d.ts.map +1 -0
  34. package/dist/color/rgbaw.js +48 -0
  35. package/dist/color/rgbaw.js.map +1 -0
  36. package/dist/color/rgbw.d.ts +3 -0
  37. package/dist/color/rgbw.d.ts.map +1 -0
  38. package/dist/color/rgbw.js +37 -0
  39. package/dist/color/rgbw.js.map +1 -0
  40. package/dist/color/rgbww.d.ts +3 -0
  41. package/dist/color/rgbww.d.ts.map +1 -0
  42. package/dist/color/rgbww.js +54 -0
  43. package/dist/color/rgbww.js.map +1 -0
  44. package/dist/color/types.d.ts +64 -0
  45. package/dist/color/types.d.ts.map +1 -0
  46. package/dist/color/types.js +91 -0
  47. package/dist/color/types.js.map +1 -0
  48. package/dist/config.d.ts +60 -0
  49. package/dist/config.d.ts.map +1 -0
  50. package/dist/config.js +191 -0
  51. package/dist/config.js.map +1 -0
  52. package/dist/controller.d.ts +32 -0
  53. package/dist/controller.d.ts.map +1 -0
  54. package/dist/controller.js +128 -0
  55. package/dist/controller.js.map +1 -0
  56. package/dist/index.d.ts +4 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +7 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/platform.d.ts +17 -0
  61. package/dist/platform.d.ts.map +1 -0
  62. package/dist/platform.js +96 -0
  63. package/dist/platform.js.map +1 -0
  64. package/dist/platformAccessory.d.ts +18 -0
  65. package/dist/platformAccessory.d.ts.map +1 -0
  66. package/dist/platformAccessory.js +81 -0
  67. package/dist/platformAccessory.js.map +1 -0
  68. package/dist/settings.d.ts +14 -0
  69. package/dist/settings.d.ts.map +1 -0
  70. package/dist/settings.js +15 -0
  71. package/dist/settings.js.map +1 -0
  72. package/dist/stick/frame.d.ts +16 -0
  73. package/dist/stick/frame.d.ts.map +1 -0
  74. package/dist/stick/frame.js +51 -0
  75. package/dist/stick/frame.js.map +1 -0
  76. package/dist/stick/kdf.d.ts +23 -0
  77. package/dist/stick/kdf.d.ts.map +1 -0
  78. package/dist/stick/kdf.js +90 -0
  79. package/dist/stick/kdf.js.map +1 -0
  80. package/dist/stick/protocol.d.ts +28 -0
  81. package/dist/stick/protocol.d.ts.map +1 -0
  82. package/dist/stick/protocol.js +78 -0
  83. package/dist/stick/protocol.js.map +1 -0
  84. package/dist/stick/session.d.ts +50 -0
  85. package/dist/stick/session.d.ts.map +1 -0
  86. package/dist/stick/session.js +301 -0
  87. package/dist/stick/session.js.map +1 -0
  88. package/examples/dmx.yaml +65 -0
  89. package/package.json +51 -0
  90. package/src/cli/stick.ts +198 -0
  91. package/src/color/cct.ts +40 -0
  92. package/src/color/dimmer.ts +24 -0
  93. package/src/color/hsvcct.ts +59 -0
  94. package/src/color/index.ts +34 -0
  95. package/src/color/parsers.ts +232 -0
  96. package/src/color/rgb.ts +33 -0
  97. package/src/color/rgbaw.ts +46 -0
  98. package/src/color/rgbw.ts +36 -0
  99. package/src/color/rgbww.ts +56 -0
  100. package/src/color/types.ts +150 -0
  101. package/src/config.ts +248 -0
  102. package/src/controller.ts +136 -0
  103. package/src/index.ts +9 -0
  104. package/src/platform.ts +118 -0
  105. package/src/platformAccessory.ts +102 -0
  106. package/src/settings.ts +18 -0
  107. package/src/stick/frame.ts +70 -0
  108. package/src/stick/kdf.ts +99 -0
  109. package/src/stick/protocol.ts +90 -0
  110. package/src/stick/session.ts +348 -0
  111. package/stick-de3.json +6 -0
  112. package/tools/aes-hook.c +1404 -0
  113. package/tools/analyze-pcap.mjs +143 -0
  114. package/tools/capture-aes-key.lldb +25 -0
  115. package/tools/capture-and-verify.sh +46 -0
  116. package/tools/capture-handshake.sh +59 -0
  117. package/tools/capture-send.sh +39 -0
  118. package/tools/crack-kdf.mjs +121 -0
  119. package/tools/crack-key.mjs +170 -0
  120. package/tools/decode-frame.mjs +110 -0
  121. package/tools/decrypt-011c.mjs +123 -0
  122. package/tools/decrypt-dmx.mjs +130 -0
  123. package/tools/decrypt-ssl2.ts +154 -0
  124. package/tools/derive-dmx-key.mjs +189 -0
  125. package/tools/diff-011c.mjs +127 -0
  126. package/tools/dump-pcap-summary.mjs +54 -0
  127. package/tools/dump-tcp-stream.mjs +123 -0
  128. package/tools/extract-011c.mjs +117 -0
  129. package/tools/extract-frame.mjs +78 -0
  130. package/tools/find-011c-key.mjs +140 -0
  131. package/tools/find-iv.mjs +153 -0
  132. package/tools/ghidra/README.md +136 -0
  133. package/tools/ghidra/__pycache__/dump-011c-callers.cpython-313.pyc +0 -0
  134. package/tools/ghidra/__pycache__/dump-011c-handler.cpython-313.pyc +0 -0
  135. package/tools/ghidra/__pycache__/dump-aes-candidates.cpython-313.pyc +0 -0
  136. package/tools/ghidra/__pycache__/dump-anchors.cpython-313.pyc +0 -0
  137. package/tools/ghidra/__pycache__/dump-auth2.cpython-313.pyc +0 -0
  138. package/tools/ghidra/__pycache__/dump-authenticate.cpython-313.pyc +0 -0
  139. package/tools/ghidra/__pycache__/dump-block-cipher.cpython-313.pyc +0 -0
  140. package/tools/ghidra/__pycache__/dump-cipher.cpython-313.pyc +0 -0
  141. package/tools/ghidra/__pycache__/dump-ctor-chain.cpython-313.pyc +0 -0
  142. package/tools/ghidra/__pycache__/dump-curve.cpython-313.pyc +0 -0
  143. package/tools/ghidra/__pycache__/dump-encompassing.cpython-313.pyc +0 -0
  144. package/tools/ghidra/__pycache__/dump-handshake.cpython-313.pyc +0 -0
  145. package/tools/ghidra/__pycache__/dump-kdf-chain.cpython-313.pyc +0 -0
  146. package/tools/ghidra/__pycache__/dump-kdf-context.cpython-313.pyc +0 -0
  147. package/tools/ghidra/__pycache__/dump-profile.cpython-313.pyc +0 -0
  148. package/tools/ghidra/__pycache__/dump-real-encrypt.cpython-313.pyc +0 -0
  149. package/tools/ghidra/__pycache__/dump-secret.cpython-313.pyc +0 -0
  150. package/tools/ghidra/__pycache__/dump-setsoftware.cpython-313.pyc +0 -0
  151. package/tools/ghidra/__pycache__/dump-stick3-vtable.cpython-313.pyc +0 -0
  152. package/tools/ghidra/__pycache__/dump-swxref.cpython-313.pyc +0 -0
  153. package/tools/ghidra/__pycache__/dump-vt-key-getter.cpython-313.pyc +0 -0
  154. package/tools/ghidra/__pycache__/find-cipher.cpython-313.pyc +0 -0
  155. package/tools/ghidra/__pycache__/find-ctor.cpython-313.pyc +0 -0
  156. package/tools/ghidra/__pycache__/find-md5.cpython-313.pyc +0 -0
  157. package/tools/ghidra/__pycache__/find-secret-users.cpython-313.pyc +0 -0
  158. package/tools/ghidra/__pycache__/find-stick3-crypto.cpython-313.pyc +0 -0
  159. package/tools/ghidra/__pycache__/find-stick3-device.cpython-313.pyc +0 -0
  160. package/tools/ghidra/__pycache__/find-stick3a-ctor.cpython-313.pyc +0 -0
  161. package/tools/ghidra/__pycache__/find-stick3anet-ctor.cpython-313.pyc +0 -0
  162. package/tools/ghidra/__pycache__/find-vtables.cpython-313.pyc +0 -0
  163. package/tools/ghidra/__pycache__/string-walk.cpython-313.pyc +0 -0
  164. package/tools/ghidra/dump-011c-callers.py +140 -0
  165. package/tools/ghidra/dump-011c-handler.py +158 -0
  166. package/tools/ghidra/dump-aes-candidates.py +99 -0
  167. package/tools/ghidra/dump-anchors.py +192 -0
  168. package/tools/ghidra/dump-auth2.py +50 -0
  169. package/tools/ghidra/dump-authenticate.py +71 -0
  170. package/tools/ghidra/dump-block-cipher.py +88 -0
  171. package/tools/ghidra/dump-cipher.py +106 -0
  172. package/tools/ghidra/dump-ctor-chain.py +107 -0
  173. package/tools/ghidra/dump-curve.py +112 -0
  174. package/tools/ghidra/dump-encompassing.py +146 -0
  175. package/tools/ghidra/dump-handshake.py +76 -0
  176. package/tools/ghidra/dump-kdf-chain.py +105 -0
  177. package/tools/ghidra/dump-kdf-context.py +72 -0
  178. package/tools/ghidra/dump-profile.py +59 -0
  179. package/tools/ghidra/dump-real-encrypt.py +90 -0
  180. package/tools/ghidra/dump-secret.py +84 -0
  181. package/tools/ghidra/dump-setsoftware.py +63 -0
  182. package/tools/ghidra/dump-stick3-vtable.py +182 -0
  183. package/tools/ghidra/dump-swxref.py +27 -0
  184. package/tools/ghidra/dump-vt-key-getter.py +170 -0
  185. package/tools/ghidra/find-cipher.py +210 -0
  186. package/tools/ghidra/find-ctor.py +109 -0
  187. package/tools/ghidra/find-md5.py +97 -0
  188. package/tools/ghidra/find-secret-users.py +98 -0
  189. package/tools/ghidra/find-stick3-crypto.py +190 -0
  190. package/tools/ghidra/find-stick3-device.py +111 -0
  191. package/tools/ghidra/find-stick3a-ctor.py +87 -0
  192. package/tools/ghidra/find-stick3anet-ctor.py +76 -0
  193. package/tools/ghidra/find-vtables.py +165 -0
  194. package/tools/ghidra/out-011c-callers-run.log +51 -0
  195. package/tools/ghidra/out-011c-run.log +50 -0
  196. package/tools/ghidra/out-vt-key-run.log +55 -0
  197. package/tools/ghidra/string-walk.py +180 -0
  198. package/tools/hmac-key.sh +51 -0
  199. package/tools/hwm-entitlements.plist +18 -0
  200. package/tools/inspect-plaintext.mjs +87 -0
  201. package/tools/lldb-capture-aes.py +137 -0
  202. package/tools/lldb_hmac_key.py +93 -0
  203. package/tools/lldb_pathA_scan.py +341 -0
  204. package/tools/lldb_pathC_keytrace.py +300 -0
  205. package/tools/lldb_pathD_kdf_watch.py +281 -0
  206. package/tools/pathA-launch-hwm.sh +86 -0
  207. package/tools/pathA-scan.sh +52 -0
  208. package/tools/pathC-keytrace.sh +49 -0
  209. package/tools/pathC-run.sh +71 -0
  210. package/tools/pathD-kdf-watch.sh +51 -0
  211. package/tools/quick-trigger.mjs +86 -0
  212. package/tools/run-hooked-hwmanager.sh +51 -0
  213. package/tools/scan-aes-schedule.mjs +135 -0
  214. package/tools/send_dmx.mjs +480 -0
  215. package/tools/stick-status.mjs +57 -0
  216. package/tools/test-aes-detector.mjs +100 -0
  217. package/tools/test-tcp-write.ts +274 -0
  218. package/tools/test-udp-dmx.ts +324 -0
  219. package/tools/test-x25519.mjs +87 -0
  220. package/tools/try-decrypt.ts +144 -0
  221. package/tools/try-hardcoded-key.mjs +86 -0
  222. package/tools/try-hardcoded-key.ts +103 -0
  223. package/tools/try-key.mjs +103 -0
  224. package/tools/verify-011c-key.mjs +121 -0
  225. package/tools/verify-kdf.mjs +142 -0
  226. 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
+ });