@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,180 @@
1
+ # Find crypto/cipher code via string xrefs.
2
+ #
3
+ # Ghidra didn't auto-create RTTI symbols for XHL_Stick3CryptDmxUniverse etc.,
4
+ # but the binary clearly contains:
5
+ # - typeinfo-name strings like "26XHL_DasNetCryptDmxUniverse"
6
+ # "N14XHL_Stick3ANet26XHL_Stick3CryptDmxUniverseE"
7
+ # - leaked source paths "../source/common/.../XHL_DasNetCryptDmxUniverse.cpp"
8
+ # - method-name strings in error logs like
9
+ # "[XHL_DasNetCryptDmxUniverse::setIoMode] pSubscribeUdpPort failed"
10
+ #
11
+ # Strategy: for each interesting string, locate its bytes in memory and find
12
+ # every function that references it. That function IS the method whose name
13
+ # the string mentions (since the string is its diagnostic log message), or
14
+ # AT LEAST a function in the crypto path.
15
+
16
+ # @category Stick
17
+
18
+ import os, re
19
+ from ghidra.app.decompiler import DecompInterface
20
+ from ghidra.util.task import ConsoleTaskMonitor
21
+ from ghidra.program.model.address import AddressSet
22
+ from ghidra.program.model.listing import CodeUnit
23
+
24
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out4')
25
+ try:
26
+ os.makedirs(OUT_DIR)
27
+ except OSError:
28
+ pass
29
+
30
+ prog = currentProgram
31
+ fm = prog.getFunctionManager()
32
+ mem = prog.getMemory()
33
+ listing = prog.getListing()
34
+ ref_mgr = prog.getReferenceManager()
35
+ monitor = ConsoleTaskMonitor()
36
+ decomp = DecompInterface()
37
+ decomp.openProgram(prog)
38
+
39
+
40
+ def find_string_addrs(needle):
41
+ """Return all addresses in memory where the literal bytes of `needle`
42
+ occur (followed by a NUL byte for safety)."""
43
+ pattern = needle.encode('utf-8') + b'\x00'
44
+ hits = []
45
+ # Use Memory.findBytes
46
+ addr = mem.findBytes(prog.getMinAddress(), pattern, None, True, monitor)
47
+ while addr is not None and len(hits) < 50:
48
+ hits.append(addr)
49
+ nxt = addr.add(1)
50
+ try:
51
+ addr = mem.findBytes(nxt, pattern, None, True, monitor)
52
+ except Exception:
53
+ break
54
+ return hits
55
+
56
+
57
+ def dump_function(func, out):
58
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
59
+ out.write('signature: %s\n' % func.getPrototypeString(True, True))
60
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
61
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
62
+ for f in func.getCalledFunctions(monitor)))
63
+ out.write('\n-- callees (%d) --\n' % len(callees))
64
+ for c in callees:
65
+ out.write(' ' + c + '\n')
66
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
67
+ for f in func.getCallingFunctions(monitor)))
68
+ out.write('\n-- callers (%d) --\n' % len(callers))
69
+ for c in callers:
70
+ out.write(' ' + c + '\n')
71
+ out.write('\n-- decompilation --\n')
72
+ res = decomp.decompileFunction(func, 180, monitor)
73
+ if res.decompileCompleted():
74
+ out.write(res.getDecompiledFunction().getC())
75
+ else:
76
+ out.write('(decompilation failed: %s)\n' % res.getErrorMessage())
77
+ out.write('\n')
78
+
79
+
80
+ # (needle, suggested filename slug)
81
+ NEEDLES = [
82
+ # Source file paths leak class boundaries
83
+ ('XHL_DasNetCryptDmxUniverse.cpp', 'XHL_DasNetCryptDmxUniverse_cpp'),
84
+ ('XHL_DasEccAesCryptography.cpp', 'XHL_DasEccAesCryptography_cpp'),
85
+ ('XHL_DasEccStreamCryptography.cpp', 'XHL_DasEccStreamCryptography_cpp'),
86
+ ('XHL_DasDhRsaCryptography.hpp', 'XHL_DasDhRsaCryptography_hpp'),
87
+
88
+ # Error/debug strings that contain method names
89
+ ('XHL_DasNetCryptDmxUniverse::setIoMode', 'setIoMode'),
90
+
91
+ # RTTI strings — anything that references them is the typeinfo struct
92
+ # for that class, and the vtable points at it.
93
+ ('N14XHL_Stick3ANet26XHL_Stick3CryptDmxUniverseE', 'Stick3CryptDmxUniverse_rtti'),
94
+ ('N14XHL_Stick5ANet26XHL_Stick5CryptDmxUniverseE', 'Stick5CryptDmxUniverse_rtti'),
95
+ ('26XHL_DasNetCryptDmxUniverse', 'DasNetCryptDmxUniverse_rtti'),
96
+ ('14XHL_AesOStream', 'AesOStream_rtti'),
97
+ ('14XHL_AesIStream', 'AesIStream_rtti'),
98
+ ('20XHL_AesOStreamBuffer', 'AesOStreamBuffer_rtti'),
99
+ ('20XHL_AesIStreamBuffer', 'AesIStreamBuffer_rtti'),
100
+ ('25XHL_DasEccAesCryptography', 'DasEccAesCryptography_rtti'),
101
+ ('24XHL_DasDhRsaCryptographyILj1024EE', 'DasDhRsaCryptography_1024_rtti'),
102
+ ('24XHL_DasDhRsaCryptographyILj512EE', 'DasDhRsaCryptography_512_rtti'),
103
+ ('28XHL_DasEccStreamCryptography', 'DasEccStreamCryptography_rtti'),
104
+ ('19XHL_DasCryptography', 'DasCryptography_rtti'),
105
+ ]
106
+
107
+ summary = open(os.path.join(OUT_DIR, 'SUMMARY.txt'), 'w')
108
+ summary.write('String-walk anchors — ESA2 HardwareManager (2024-03-21)\n\n')
109
+
110
+ seen_funcs = set()
111
+
112
+ for needle, slug in NEEDLES:
113
+ summary.write('\n#### needle: %r\n' % needle)
114
+ str_addrs = find_string_addrs(needle)
115
+ summary.write(' found %d occurrence(s) of string\n' % len(str_addrs))
116
+ if not str_addrs:
117
+ continue
118
+
119
+ for str_addr in str_addrs:
120
+ summary.write(' @ %s\n' % str_addr)
121
+
122
+ # Walk one ref-hop: who references this string?
123
+ L1_refs = list(ref_mgr.getReferencesTo(str_addr))
124
+ L1_funcs = set()
125
+ L1_data_addrs = set()
126
+ for r in L1_refs:
127
+ fa = r.getFromAddress()
128
+ f = fm.getFunctionContaining(fa)
129
+ if f is not None:
130
+ L1_funcs.add(f.getEntryPoint())
131
+ else:
132
+ L1_data_addrs.add(fa)
133
+
134
+ summary.write(' direct fn refs: %d\n' % len(L1_funcs))
135
+ for ep in sorted(L1_funcs):
136
+ f = fm.getFunctionAt(ep)
137
+ summary.write(' %s @ %s\n' % (f.getName(True), f.getEntryPoint()))
138
+
139
+ summary.write(' direct data refs: %d\n' % len(L1_data_addrs))
140
+ for da in sorted(L1_data_addrs):
141
+ summary.write(' data @ %s\n' % da)
142
+ # Walk one more hop: who references THAT data location?
143
+ L2_refs = list(ref_mgr.getReferencesTo(da))
144
+ for r2 in L2_refs:
145
+ fa2 = r2.getFromAddress()
146
+ f2 = fm.getFunctionContaining(fa2)
147
+ if f2 is not None:
148
+ L1_funcs.add(f2.getEntryPoint())
149
+ summary.write(' via %s -> fn %s @ %s\n' %
150
+ (da, f2.getName(True), f2.getEntryPoint()))
151
+ else:
152
+ # Could be the vtable structure itself
153
+ summary.write(' via %s -> data @ %s\n' % (da, fa2))
154
+ # And one more hop (vtable -> referencing fn)
155
+ for r3 in ref_mgr.getReferencesTo(fa2):
156
+ fa3 = r3.getFromAddress()
157
+ f3 = fm.getFunctionContaining(fa3)
158
+ if f3 is not None:
159
+ L1_funcs.add(f3.getEntryPoint())
160
+ summary.write(' via %s -> fn %s @ %s\n' %
161
+ (fa2, f3.getName(True),
162
+ f3.getEntryPoint()))
163
+
164
+ # Dump every collected function
165
+ for ep in sorted(L1_funcs):
166
+ if ep in seen_funcs:
167
+ continue
168
+ seen_funcs.add(ep)
169
+ f = fm.getFunctionAt(ep)
170
+ if f is None:
171
+ continue
172
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:140]
173
+ path = os.path.join(OUT_DIR, '%s_%s_%s.txt' %
174
+ (slug, f.getEntryPoint(), safe))
175
+ with open(path, 'w') as o:
176
+ dump_function(f, o)
177
+
178
+ summary.write('\n=== total functions dumped: %d ===\n' % len(seen_funcs))
179
+ summary.close()
180
+ print('string-walk: dumped %d functions to %s' % (len(seen_funcs), OUT_DIR))
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ # Extract the 0x48 TCP-auth HMAC key from a running debug Hardware Manager.
3
+ #
4
+ # The 0x48 handshake is authenticated with HMAC-SHA256(key, msg[0:82]); the
5
+ # key is an internal HWM constant. This attaches lldb, breakpoints the HMAC
6
+ # routine, and dumps the key the instant HWM builds a 0x48 (one hit, then
7
+ # detaches — not a per-frame breakpoint, so HWM stays healthy).
8
+ #
9
+ # Steps:
10
+ # 1. Quit the normal Hardware Manager (it holds the Stick's single session).
11
+ # 2. Launch the debug copy: tools/pathA-launch-hwm.sh
12
+ # Leave it DISCONNECTED from the Stick for now.
13
+ # 3. Run this script.
14
+ # 4. When it prints "waiting for the 0x48 handshake", connect the debug HWM
15
+ # to the Stick (Connection -> Add/Connect). The key is dumped + logged.
16
+
17
+ set -uo pipefail
18
+ cd "$(dirname "$0")/.."
19
+
20
+ SCAN_PY="tools/lldb_hmac_key.py"
21
+ LOG="/tmp/stick-hmac-key.log"
22
+
23
+ PID="$(pgrep -f '/tmp/HM-ESA2-dbg.app/Contents/MacOS/HardwareManager' | head -1)"
24
+ if [ -z "$PID" ]; then
25
+ echo "ERROR: the debug Hardware Manager is not running."
26
+ echo " 1. Quit the normal Hardware Manager."
27
+ echo " 2. Run: tools/pathA-launch-hwm.sh"
28
+ echo " 3. Leave it DISCONNECTED from the Stick, then re-run this script."
29
+ exit 1
30
+ fi
31
+
32
+ echo ">>> debug HWM pid = $PID"
33
+ echo ">>> attaching lldb and arming the HMAC breakpoint ..."
34
+ echo ">>> when it says 'waiting for the 0x48 handshake', CONNECT HWM to the Stick."
35
+ echo
36
+
37
+ timeout 600 lldb --batch \
38
+ -o "command script import $SCAN_PY" \
39
+ -o "detach" \
40
+ -o "quit" \
41
+ -p "$PID" 2>&1 | sed 's/^/ /'
42
+
43
+ echo
44
+ if grep -q 'auth HMAC caught' "$LOG" 2>/dev/null; then
45
+ echo ">>> SUCCESS — the auth key:"
46
+ grep -E 'key length|key \(hex\)|key \(text\)' "$LOG" | sed 's/^/ /'
47
+ echo ">>> full log: $LOG"
48
+ else
49
+ echo "!!! the 0x48 HMAC was not caught — did HWM connect to the Stick?"
50
+ echo " log: $LOG"
51
+ fi
@@ -0,0 +1,18 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.get-task-allow</key>
6
+ <true/>
7
+ <key>com.apple.security.cs.disable-library-validation</key>
8
+ <true/>
9
+ <key>com.apple.security.cs.allow-dyld-environment-variables</key>
10
+ <true/>
11
+ <key>com.apple.security.cs.disable-executable-page-protection</key>
12
+ <true/>
13
+ <key>com.apple.security.cs.allow-jit</key>
14
+ <true/>
15
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
16
+ <true/>
17
+ </dict>
18
+ </plist>
@@ -0,0 +1,87 @@
1
+ // Decrypt every 576-byte UDP DMX frame in a pcap and dump the FULL 544-byte
2
+ // plaintext layout: [0:16] P0 header, [16:528] 512 DMX, [528:544] trailer.
3
+ // The question: is the 16-byte trailer zero (what send_dmx sends) or a
4
+ // per-frame checksum the Stick validates?
5
+ //
6
+ // node tools/inspect-plaintext.mjs <pcap> <key-hex>
7
+
8
+ import fs from 'node:fs';
9
+ import crypto from 'node:crypto';
10
+
11
+ const [, , pcapPath, keyHex] = process.argv;
12
+ if (!pcapPath || !keyHex) { console.error('usage: inspect-plaintext.mjs <pcap> <key-hex>'); process.exit(1); }
13
+ const key = Buffer.from(keyHex.replace(/\s/g, ''), 'hex');
14
+ if (key.length !== 32) { console.error('key must be 32 bytes'); process.exit(1); }
15
+
16
+ const buf = fs.readFileSync(pcapPath);
17
+ const le = buf.readUInt32LE(0) === 0xa1b2c3d4;
18
+ const linkType = le ? buf.readUInt32LE(20) : buf.readUInt32BE(20);
19
+ const ethSkip = linkType === 1 ? 14 : 4;
20
+
21
+ const frames = [];
22
+ let off = 24;
23
+ while (off + 16 <= buf.length) {
24
+ const capLen = le ? buf.readUInt32LE(off + 8) : buf.readUInt32BE(off + 8);
25
+ off += 16;
26
+ if (off + capLen > buf.length) break;
27
+ const pkt = buf.subarray(off, off + capLen);
28
+ off += capLen;
29
+ if (pkt.length < ethSkip + 28) continue;
30
+ const ip = pkt.subarray(ethSkip);
31
+ if (ip[9] !== 17) continue;
32
+ const udp = ip.subarray((ip[0] & 0x0f) * 4);
33
+ const payload = udp.subarray(8, udp.readUInt16BE(4));
34
+ if (payload.length === 576 && payload.subarray(0, 8).toString() === 'Stick_3A')
35
+ frames.push(Buffer.from(payload));
36
+ }
37
+ console.log(`${frames.length} 576-byte frames in ${pcapPath}\n`);
38
+
39
+ function decrypt(frame) {
40
+ const h = frame.subarray(0, 32);
41
+ const iv = Buffer.concat([h.subarray(0x0a, 0x12), h.subarray(0x18, 0x20)]);
42
+ const d = crypto.createDecipheriv('aes-256-cbc', key, iv);
43
+ d.setAutoPadding(false);
44
+ return Buffer.concat([d.update(frame.subarray(32)), d.final()]);
45
+ }
46
+ const zr = (b) => { let z = 0; for (const x of b) if (x === 0) z++; return z / b.length; };
47
+
48
+ let decoded = 0;
49
+ const p0set = new Map(), trailerSet = new Map();
50
+ const trailerSamples = [];
51
+ for (let i = 0; i < frames.length; i++) {
52
+ const pt = decrypt(frames[i]);
53
+ const dmx = pt.subarray(16, 528);
54
+ if (zr(dmx) < 0.5) continue; // not this session's key
55
+ decoded++;
56
+ const p0 = pt.subarray(0, 16).toString('hex');
57
+ const tr = pt.subarray(528, 544).toString('hex');
58
+ p0set.set(p0, (p0set.get(p0) || 0) + 1);
59
+ trailerSet.set(tr, (trailerSet.get(tr) || 0) + 1);
60
+ if (decoded <= 6) {
61
+ const h = frames[i].subarray(0, 32);
62
+ const lit = [];
63
+ for (let c = 0; c < 512; c++) if (dmx[c]) lit.push(`ch${c+1}=${dmx[c]}`);
64
+ trailerSamples.push({
65
+ fieldA: h.readUInt32LE(0x0a), seq: h[0x17],
66
+ p0, tr, nLit: lit.length, lit: lit.slice(0, 8).join(' '),
67
+ });
68
+ }
69
+ }
70
+
71
+ console.log(`frames that decrypt with this key: ${decoded}/${frames.length}\n`);
72
+ if (!decoded) { console.log('key does not match any frame in this pcap'); process.exit(1); }
73
+
74
+ console.log('first 6 decrypted frames:');
75
+ for (const s of trailerSamples) {
76
+ console.log(` fieldA=${String(s.fieldA).padEnd(6)} seq=${String(s.seq).padEnd(4)} lit=${s.nLit}`);
77
+ console.log(` P0 = ${s.p0}`);
78
+ console.log(` trailer = ${s.tr}`);
79
+ if (s.lit) console.log(` dmx = ${s.lit}`);
80
+ }
81
+ console.log(`\nP0 distinct values : ${p0set.size}`);
82
+ for (const [v, n] of p0set) console.log(` ${v} x${n}`);
83
+ console.log(`trailer distinct values: ${trailerSet.size}`);
84
+ for (const [v, n] of [...trailerSet].slice(0, 8)) console.log(` ${v} x${n}`);
85
+ console.log(trailerSet.size === 1
86
+ ? '\n => trailer is CONSTANT — send_dmx sending zeros is wrong only if this constant != 0'
87
+ : '\n => trailer VARIES per frame — it is a checksum/MAC; send_dmx sending zeros is FATAL');
@@ -0,0 +1,137 @@
1
+ """
2
+ lldb script to capture AES keys from HardwareManager.
3
+ Attach to HardwareManager, then source this script.
4
+
5
+ Usage:
6
+ lldb -p <PID>
7
+ (lldb) command script import /path/to/lldb-capture-aes.py
8
+ (lldb) continue
9
+
10
+ Then connect to the Stick in the HardwareManager UI.
11
+ Keys will be printed and saved to /tmp/stick-aes-keys.log
12
+ """
13
+
14
+ import lldb
15
+ import os
16
+
17
+ LOG_PATH = "/tmp/stick-aes-keys.log"
18
+ key_count = 0
19
+
20
+ def log(msg):
21
+ with open(LOG_PATH, "a") as f:
22
+ f.write(msg + "\n")
23
+ print(msg)
24
+
25
+ def on_setkey_enc(frame, bp_loc, dict):
26
+ global key_count
27
+ key_count += 1
28
+ thread = frame.GetThread()
29
+ process = thread.GetProcess()
30
+
31
+ # x86_64 calling convention: rdi=ctx, rsi=key, edx=keybits
32
+ keybits = frame.FindRegister("edx").GetValueAsUnsigned()
33
+ key_ptr = frame.FindRegister("rsi").GetValueAsUnsigned()
34
+ key_len = keybits // 8
35
+
36
+ err = lldb.SBError()
37
+ key_bytes = process.ReadMemory(key_ptr, key_len, err)
38
+
39
+ if err.Success() and key_bytes:
40
+ hex_key = " ".join(f"{b:02x}" for b in key_bytes)
41
+ log(f"[{key_count}] AES_SETKEY_ENC bits={keybits} key={hex_key}")
42
+
43
+ # Save raw key to file
44
+ with open("/tmp/stick-aes-key.bin", "wb") as f:
45
+ f.write(key_bytes)
46
+ log(f" Raw key saved to /tmp/stick-aes-key.bin")
47
+ else:
48
+ log(f"[{key_count}] AES_SETKEY_ENC bits={keybits} (failed to read key: {err})")
49
+
50
+ # Don't stop, continue running
51
+ return False
52
+
53
+ def on_setkey_dec(frame, bp_loc, dict):
54
+ global key_count
55
+ key_count += 1
56
+ thread = frame.GetThread()
57
+ process = thread.GetProcess()
58
+
59
+ keybits = frame.FindRegister("edx").GetValueAsUnsigned()
60
+ key_ptr = frame.FindRegister("rsi").GetValueAsUnsigned()
61
+ key_len = keybits // 8
62
+
63
+ err = lldb.SBError()
64
+ key_bytes = process.ReadMemory(key_ptr, key_len, err)
65
+
66
+ if err.Success() and key_bytes:
67
+ hex_key = " ".join(f"{b:02x}" for b in key_bytes)
68
+ log(f"[{key_count}] AES_SETKEY_DEC bits={keybits} key={hex_key}")
69
+ else:
70
+ log(f"[{key_count}] AES_SETKEY_DEC bits={keybits} (failed to read key: {err})")
71
+
72
+ return False
73
+
74
+ def on_crypt_cbc(frame, bp_loc, dict):
75
+ thread = frame.GetThread()
76
+ process = thread.GetProcess()
77
+
78
+ # rdi=ctx, esi=mode, rdx=length, rcx=iv, r8=input, r9=output
79
+ mode = frame.FindRegister("esi").GetValueAsUnsigned()
80
+ length = frame.FindRegister("rdx").GetValueAsUnsigned()
81
+
82
+ mode_str = "ENC" if mode == 1 else "DEC"
83
+ log(f" AES_CBC {mode_str} len={length}")
84
+
85
+ return False
86
+
87
+ def __lldb_init_module(debugger, internal_dict):
88
+ target = debugger.GetSelectedTarget()
89
+ if not target:
90
+ print("ERROR: No target. Attach to HardwareManager first.")
91
+ return
92
+
93
+ # Clear old log
94
+ with open(LOG_PATH, "w") as f:
95
+ f.write("=== lldb AES key capture started ===\n")
96
+
97
+ # Find the mbedcrypto module
98
+ found_module = None
99
+ for module in target.module_iter():
100
+ name = module.GetFileSpec().GetFilename()
101
+ if name and "mbedcrypto" in name:
102
+ found_module = module
103
+ break
104
+
105
+ if not found_module:
106
+ print("WARNING: libmbedcrypto not found in loaded modules")
107
+ print("Available modules:")
108
+ for module in target.module_iter():
109
+ print(f" {module.GetFileSpec()}")
110
+ return
111
+
112
+ print(f"Found: {found_module.GetFileSpec()}")
113
+
114
+ # Set breakpoints on the symbols
115
+ bp_enc = target.BreakpointCreateByName("mbedtls_aes_setkey_enc", found_module.GetFileSpec().GetFilename())
116
+ if bp_enc.IsValid():
117
+ bp_enc.SetScriptCallbackFunction("lldb_capture_aes.on_setkey_enc")
118
+ bp_enc.SetAutoContinue(True)
119
+ print(f"Breakpoint on mbedtls_aes_setkey_enc: {bp_enc}")
120
+ else:
121
+ print("ERROR: Could not set breakpoint on mbedtls_aes_setkey_enc")
122
+
123
+ bp_dec = target.BreakpointCreateByName("mbedtls_aes_setkey_dec", found_module.GetFileSpec().GetFilename())
124
+ if bp_dec.IsValid():
125
+ bp_dec.SetScriptCallbackFunction("lldb_capture_aes.on_setkey_dec")
126
+ bp_dec.SetAutoContinue(True)
127
+ print(f"Breakpoint on mbedtls_aes_setkey_dec: {bp_dec}")
128
+
129
+ bp_cbc = target.BreakpointCreateByName("mbedtls_aes_crypt_cbc", found_module.GetFileSpec().GetFilename())
130
+ if bp_cbc.IsValid():
131
+ bp_cbc.SetScriptCallbackFunction("lldb_capture_aes.on_crypt_cbc")
132
+ bp_cbc.SetAutoContinue(True)
133
+ print(f"Breakpoint on mbedtls_aes_crypt_cbc: {bp_cbc}")
134
+
135
+ log("Breakpoints set. Connect to the Stick in the UI, then check this log.")
136
+ print(f"\nReady! Keys will be logged to {LOG_PATH}")
137
+ print("Now type 'continue' and connect to the Stick in the UI.")
@@ -0,0 +1,93 @@
1
+ """
2
+ Extract the 0x48 TCP-auth HMAC key from a running Hardware Manager.
3
+
4
+ The 0x48 handshake message ends with HMAC-SHA256(key, msg[0:82]). The key is
5
+ an internal HWM constant we can't derive offline. But the HMAC routine
6
+ FUN_1004080b0 receives it directly:
7
+
8
+ FUN_1004080b0(rdi=key, rsi=keylen, rdx=msg, rcx=msglen, r8=out, r9=32)
9
+
10
+ When HWM builds the 0x48 it calls this with rcx (msglen) == 0x52. We set one
11
+ breakpoint on FUN_1004080b0, and on the call where rcx==0x52 we dump rdi/rsi
12
+ — that's the key. One hit, then detach. Not a per-frame breakpoint, so it
13
+ won't starve HWM the way the old encrypt-path breakpoints did.
14
+
15
+ Usage (driven by tools/hmac-key.sh):
16
+ lldb -p <PID> --batch \
17
+ -o "command script import tools/lldb_hmac_key.py" -o "detach" -o "quit"
18
+ Then in HWM: disconnect from the Stick and reconnect.
19
+ """
20
+
21
+ import sys
22
+
23
+ try:
24
+ import lldb
25
+ except ImportError:
26
+ lldb = None
27
+
28
+ HMAC_FN_FILEADDR = 0x1004080b0 # FUN_1004080b0 — HMAC-SHA256
29
+ LOG_PATH = "/tmp/stick-hmac-key.log"
30
+ _logf = None
31
+
32
+
33
+ def log(m=""):
34
+ print(m)
35
+ sys.stdout.flush()
36
+ if _logf:
37
+ _logf.write(m + "\n")
38
+ _logf.flush()
39
+
40
+
41
+ def hexs(b):
42
+ return "".join("%02x" % x for x in bytearray(b))
43
+
44
+
45
+ def hmac_bp_callback(frame, bp_loc, internal_dict):
46
+ """Fires on every FUN_1004080b0 call; reports the one for the 0x48 (msglen 0x52)."""
47
+ process = frame.GetThread().GetProcess()
48
+ reg = lambda n: frame.FindRegister(n).GetValueAsUnsigned()
49
+ key, keylen, msglen = reg("rdi"), reg("rsi"), reg("rcx")
50
+ if msglen != 0x52:
51
+ return False # not the 0x48 auth HMAC — keep going
52
+ err = lldb.SBError()
53
+ n = keylen if 0 < keylen <= 512 else 64
54
+ kd = process.ReadMemory(key, n, err)
55
+ md = process.ReadMemory(reg("rdx"), 0x52, err)
56
+ log("")
57
+ log(">>> 0x48 auth HMAC caught")
58
+ log(" key length : %d" % keylen)
59
+ log(" key (hex) : %s" % (hexs(kd) if kd else "(unreadable)"))
60
+ if kd:
61
+ try:
62
+ log(" key (text) : %r" % bytearray(kd).decode("utf-8"))
63
+ except Exception:
64
+ log(" key (text) : (not utf-8)")
65
+ log(" msg[0:82] : %s" % (hexs(md) if md else "(unreadable)"))
66
+ log("")
67
+ log(">>> got it — detaching.")
68
+ return True # stop so the batch script can detach
69
+
70
+
71
+ def __lldb_init_module(debugger, internal_dict):
72
+ global _logf
73
+ _logf = open(LOG_PATH, "w")
74
+ log("=== HMAC-key extraction %s ===" % __import__("time").ctime())
75
+ target = debugger.GetSelectedTarget()
76
+ process = target.GetProcess()
77
+
78
+ saddr = target.ResolveFileAddress(HMAC_FN_FILEADDR)
79
+ laddr = saddr.GetLoadAddress(target)
80
+ log("HMAC fn file 0x%x -> load 0x%x" % (HMAC_FN_FILEADDR, laddr))
81
+
82
+ bp = target.BreakpointCreateByAddress(laddr)
83
+ bp.SetScriptCallbackFunction("lldb_hmac_key.hmac_bp_callback")
84
+ if bp.GetNumLocations() == 0:
85
+ log("!!! breakpoint resolved 0 locations — wrong address/module")
86
+ return
87
+ log("breakpoint armed on the HMAC function (%d location)" % bp.GetNumLocations())
88
+ log("")
89
+ log(">>> NOW: in HWM, disconnect from the Stick and reconnect.")
90
+ log(">>> waiting for the 0x48 handshake ...")
91
+
92
+ process.Continue() # blocks until the 0x48 HMAC call stops us
93
+ log(">>> done — log at %s" % LOG_PATH)