@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,182 @@
1
+ # Dump every Stick3CryptDmxUniverse vtable slot function + every caller of
2
+ # XHL_UdpSocket::send. Finds the actual DMX encrypt routine.
3
+ #
4
+ # Vtable sites (from dyld_info -fixups, typeinfo @ 0x100970DC0):
5
+ # Primary: 0x100962BE0 (slot[0] @ +0x08, primary virtuals)
6
+ # Sub1: 0x100962CC0 (5 methods, secondary base #1)
7
+ # Sub2: 0x100962CE8 (5 methods, secondary base #2)
8
+ # Sub3: 0x100962D28 (5 methods, secondary base #3)
9
+ # Sub4: 0x100962D60 (5 methods, secondary base #4)
10
+ # Sub5: 0x100962DA0 (last group, also has VTT entries)
11
+ #
12
+ # UdpSocket sends are FUN_100677060 (sendUnicast?) and FUN_100677330
13
+ # (sendBroadcast or send-to-many?). 26 + 6 callers.
14
+
15
+ # @category Stick
16
+
17
+ import os, re
18
+ from ghidra.app.decompiler import DecompInterface
19
+ from ghidra.util.task import ConsoleTaskMonitor
20
+
21
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out6')
22
+ try:
23
+ os.makedirs(OUT_DIR)
24
+ except OSError:
25
+ pass
26
+
27
+ prog = currentProgram
28
+ fm = prog.getFunctionManager()
29
+ af = prog.getAddressFactory()
30
+ mem = prog.getMemory()
31
+ ref_mgr = prog.getReferenceManager()
32
+ monitor = ConsoleTaskMonitor()
33
+ decomp = DecompInterface()
34
+ decomp.openProgram(prog)
35
+
36
+
37
+ def addr(a):
38
+ return af.getDefaultAddressSpace().getAddress(a)
39
+
40
+
41
+ def dump_function(func, out):
42
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
43
+ out.write('signature: %s\n' % func.getPrototypeString(True, True))
44
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
45
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
46
+ for f in func.getCalledFunctions(monitor)))
47
+ out.write('\n-- callees (%d) --\n' % len(callees))
48
+ for c in callees:
49
+ out.write(' ' + c + '\n')
50
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
51
+ for f in func.getCallingFunctions(monitor)))
52
+ out.write('\n-- callers (%d) --\n' % len(callers))
53
+ for c in callers:
54
+ out.write(' ' + c + '\n')
55
+ out.write('\n-- decompilation --\n')
56
+ res = decomp.decompileFunction(func, 180, monitor)
57
+ if res.decompileCompleted():
58
+ out.write(res.getDecompiledFunction().getC())
59
+ else:
60
+ out.write('(decompilation failed: %s)\n' % res.getErrorMessage())
61
+ out.write('\n')
62
+
63
+
64
+ # Hardcoded vtable slot function addresses extracted from dyld_info -fixups
65
+ VTABLE_SLOTS = {
66
+ 'Stick3CryptDmxUniverse_PRIMARY': [
67
+ # primary @ 0x100962BE0; slot 0 is the byte just after typeinfo_ptr.
68
+ # (Itanium: slot 0 typically = D0/D1 destructors, slots 2+ = virtuals.)
69
+ (0, 0x1001818E0),
70
+ (1, 0x100181A30),
71
+ (2, 0x1001AA240),
72
+ (3, 0x1005EF5D0),
73
+ (4, 0x100181FC0),
74
+ (5, 0x100181BB0),
75
+ (6, 0x1001AA410),
76
+ (7, 0x1005EF0E0),
77
+ (8, 0x1005EF0F0),
78
+ (9, 0x1005EF250),
79
+ (10, 0x100181CB0),
80
+ (11, 0x1005EF2A0),
81
+ (12, 0x1001C0340),
82
+ (13, 0x1005EF4A0),
83
+ (14, 0x1001AA2B0),
84
+ (15, 0x1001AA110),
85
+ (16, 0x1005EF700),
86
+ (17, 0x100181FB0),
87
+ (18, 0x100334140),
88
+ (19, 0x1005EF800),
89
+ (20, 0x1005EF8F0),
90
+ (21, 0x1003342B0),
91
+ (22, 0x1001A9D50),
92
+ (23, 0x100181BC0),
93
+ ],
94
+ 'Stick3CryptDmxUniverse_SUB1_at_0x100962CC0': [
95
+ (0, 0x1001A9F00),
96
+ (1, 0x100181910),
97
+ (2, 0x100181A60),
98
+ ],
99
+ 'Stick3CryptDmxUniverse_SUB2_at_0x100962CE8': [
100
+ (0, 0x100181CA0),
101
+ (1, 0x100181940),
102
+ (2, 0x100181AA0),
103
+ (3, 0x1005F0510),
104
+ (4, 0x1005F0520),
105
+ ],
106
+ 'Stick3CryptDmxUniverse_SUB3_at_0x100962D28': [
107
+ (0, 0x100181970),
108
+ (1, 0x100181AE0),
109
+ (2, 0x1001C0410),
110
+ (3, 0x1001C0540),
111
+ (4, 0x100421210),
112
+ ],
113
+ 'Stick3CryptDmxUniverse_SUB4_at_0x100962D60': [
114
+ (0, 0x1001819B0),
115
+ (1, 0x100181B20),
116
+ (2, 0x100334300),
117
+ ],
118
+ 'Stick3CryptDmxUniverse_SUB5_at_0x100962DA0': [
119
+ (0, 0x1001819F0),
120
+ (1, 0x100181B60),
121
+ (2, 0x10062E1A0),
122
+ (3, 0x10062E2A0),
123
+ ],
124
+ }
125
+
126
+ # UdpSocket send functions — every caller is a candidate
127
+ UDP_SEND_FNS = {
128
+ 'XHL_UdpSocket_sendA_FUN_100677060': 0x100677060,
129
+ 'XHL_UdpSocket_sendB_FUN_100677330': 0x100677330,
130
+ }
131
+
132
+ summary = open(os.path.join(OUT_DIR, 'SUMMARY.txt'), 'w')
133
+ summary.write('Stick3 vtable + UDP-send callers — ESA2 HardwareManager\n\n')
134
+
135
+ seen = set()
136
+
137
+ # --- Dump vtable slots ---
138
+ for label, slots in VTABLE_SLOTS.items():
139
+ summary.write('\n=== %s ===\n' % label)
140
+ for slot_idx, fp in slots:
141
+ fa = addr(fp)
142
+ f = fm.getFunctionAt(fa) or fm.getFunctionContaining(fa)
143
+ if f is None:
144
+ summary.write(' vt[%d] @ 0x%x (no fn)\n' % (slot_idx, fp))
145
+ continue
146
+ summary.write(' vt[%d] @ 0x%x = %s @ %s (size %d)\n' %
147
+ (slot_idx, fp, f.getName(True),
148
+ f.getEntryPoint(), f.getBody().getNumAddresses()))
149
+ if f.getEntryPoint() in seen:
150
+ continue
151
+ seen.add(f.getEntryPoint())
152
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
153
+ path = os.path.join(OUT_DIR, '%s_vt%02d_%s_%s.txt' %
154
+ (label, slot_idx, f.getEntryPoint(), safe))
155
+ with open(path, 'w') as o:
156
+ dump_function(f, o)
157
+
158
+ # --- Dump every caller of UdpSocket::send ---
159
+ for label, fp in UDP_SEND_FNS.items():
160
+ summary.write('\n=== Callers of %s @ 0x%x ===\n' % (label, fp))
161
+ fn = fm.getFunctionAt(addr(fp))
162
+ if fn is None:
163
+ summary.write(' (function not found)\n')
164
+ continue
165
+ callers = sorted(set(f.getEntryPoint() for f in fn.getCallingFunctions(monitor)))
166
+ for ep in callers:
167
+ f = fm.getFunctionAt(ep)
168
+ summary.write(' %s @ %s (size %d)\n' %
169
+ (f.getName(True), f.getEntryPoint(),
170
+ f.getBody().getNumAddresses()))
171
+ if f.getEntryPoint() in seen:
172
+ continue
173
+ seen.add(f.getEntryPoint())
174
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
175
+ path = os.path.join(OUT_DIR, 'udpcaller_%s_%s.txt' %
176
+ (f.getEntryPoint(), safe))
177
+ with open(path, 'w') as o:
178
+ dump_function(f, o)
179
+
180
+ summary.write('\n=== total functions dumped: %d ===\n' % len(seen))
181
+ summary.close()
182
+ print('dump-stick3-vtable: dumped %d functions to %s' % (len(seen), OUT_DIR))
@@ -0,0 +1,27 @@
1
+ # xref the "software" cstring (0x1007e4f83) — the function that assigns it to
2
+ # a device also assigns the adjacent auth key.
3
+ # @category Stick
4
+ import os, re
5
+ from ghidra.app.decompiler import DecompInterface
6
+ from ghidra.util.task import ConsoleTaskMonitor
7
+ OUT = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out-swxref')
8
+ try: os.makedirs(OUT)
9
+ except OSError: pass
10
+ prog = currentProgram; fm = prog.getFunctionManager(); af = prog.getAddressFactory()
11
+ rm = prog.getReferenceManager(); monitor = ConsoleTaskMonitor()
12
+ decomp = DecompInterface(); decomp.openProgram(prog)
13
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
14
+ def dec(f):
15
+ r = decomp.decompileFunction(f, 240, monitor)
16
+ return r.getDecompiledFunction().getC() if r.decompileCompleted() else '(fail)\n'
17
+ seen=set()
18
+ for target in [0x1007e4f83]:
19
+ for r in rm.getReferencesTo(addr(target)):
20
+ f = fm.getFunctionContaining(r.getFromAddress())
21
+ if f is None or str(f.getEntryPoint()) in seen: continue
22
+ seen.add(str(f.getEntryPoint()))
23
+ safe = re.sub(r'[^A-Za-z0-9._-]','_',f.getName(True))[:70]
24
+ with open(os.path.join(OUT,'xref_%s_%s.txt'%(f.getEntryPoint(),safe)),'w') as o:
25
+ o.write('==== %s @ %s (from %s) ====\n'%(f.getName(True),f.getEntryPoint(),r.getFromAddress()))
26
+ o.write(dec(f))
27
+ print('dump-swxref: %d functions -> %s'%(len(seen),OUT))
@@ -0,0 +1,170 @@
1
+ # Identify the vtable+0xe8 entry (AES-256 key getter) for the Stick3 device
2
+ # class, plus the sibling crypto functions FUN_1001bb3a0 and FUN_1001bb860
3
+ # which also use the same key path -- so we can correlate.
4
+ #
5
+ # Strategy: pyghidra has the binary indexed, so dump the function pointer
6
+ # in the device's __const vtable at slot +0xe8. Stick3ANet typeinfo at
7
+ # 0x100970160 -- vtable_top should be at typeinfo - 0x10 or via __const xref.
8
+ #
9
+ # Also: dump FUN_1001bb3a0 (sibling that also uses vt+0xe8 then aes_install)
10
+ # and FUN_1001bb860 (sibling crypto fn).
11
+ #
12
+ # @category Stick
13
+
14
+ import os, re
15
+ from ghidra.app.decompiler import DecompInterface
16
+ from ghidra.util.task import ConsoleTaskMonitor
17
+
18
+ OUT = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out-vt-key')
19
+ try: os.makedirs(OUT)
20
+ except OSError: pass
21
+
22
+ prog = currentProgram
23
+ fm = prog.getFunctionManager()
24
+ af = prog.getAddressFactory()
25
+ mem = prog.getMemory()
26
+ listing = prog.getListing()
27
+ monitor = ConsoleTaskMonitor()
28
+ decomp = DecompInterface(); decomp.openProgram(prog)
29
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
30
+
31
+ def decompile(f):
32
+ r = decomp.decompileFunction(f, 240, monitor)
33
+ return r.getDecompiledFunction().getC() if r.decompileCompleted() else '(decompile failed)\n'
34
+
35
+ def dump_fn(ep, label):
36
+ a = addr(ep)
37
+ f = fm.getFunctionAt(a) or fm.getFunctionContaining(a)
38
+ if not f:
39
+ return
40
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:80]
41
+ fname = os.path.join(OUT, '%s_%s_%s.txt' % (label, f.getEntryPoint(), safe))
42
+ with open(fname, 'w') as o:
43
+ o.write('==== %s @ %s (%d bytes) ====\n' %
44
+ (f.getName(True), f.getEntryPoint(), f.getBody().getNumAddresses()))
45
+ callees = sorted(set('%s @ %s' % (c.getName(True), c.getEntryPoint())
46
+ for c in f.getCalledFunctions(monitor)))
47
+ callers = sorted(set('%s @ %s' % (c.getName(True), c.getEntryPoint())
48
+ for c in f.getCallingFunctions(monitor)))
49
+ o.write('-- callees (%d) --\n ' % len(callees) + '\n '.join(callees) + '\n')
50
+ o.write('\n-- callers (%d) --\n ' % len(callers) + '\n '.join(callers) + '\n\n')
51
+ o.write('-- decompilation --\n')
52
+ o.write(decompile(f))
53
+
54
+ # Sibling crypto functions also using AES key install.
55
+ SIBLINGS = [
56
+ (0x1001bb3a0, 'sibling_bb3a0'), # also calls FUN_1003f5740
57
+ (0x1001bb860, 'sibling_bb860'), # vt+0xe8 user
58
+ (0x100353b20, 'sibling_353b20'), # another decrypt user
59
+ (0x100594ba0, 'sibling_594ba0'),
60
+ (0x100595a40, 'sibling_595a40'),
61
+ (0x1001bc0a0, 'unencrypted_011c'), # the non-encrypted path -- shows what data layout the encrypted version equals
62
+ (0x1001f89f0, 'register_pending'), # used in handler
63
+ (0x100195b20, 'extract_reply'), # used in handler
64
+ ]
65
+
66
+ for ep, label in SIBLINGS:
67
+ dump_fn(ep, label)
68
+
69
+ # Find the function called at offset 0x1001bbee5 -- the indirect vt[+0xe8] call.
70
+ # Walk references FROM that instruction to find the resolved target (if Ghidra
71
+ # resolved it via dyld_info / objc / pointer-authentication). If not resolved,
72
+ # we'll have to find the device class vtable by another means.
73
+ ins_addr = addr(0x1001bbee5)
74
+ print('--- references at 0x1001bbee5 ---')
75
+ rm = prog.getReferenceManager()
76
+ for r in rm.getReferencesFrom(ins_addr):
77
+ print(' ', r.getReferenceType(), r.getToAddress())
78
+
79
+ # Find Stick3ANet typeinfo @ 0x100970160 and the vtable preceding it.
80
+ ti_addr = addr(0x100970160)
81
+ print('\n--- references TO Stick3ANet typeinfo @ 0x100970160 ---')
82
+ for r in rm.getReferencesTo(ti_addr):
83
+ print(' ', r.getFromAddress(), r.getReferenceType())
84
+
85
+ # Also dump the bytes around the typeinfo -- the vtable is just before it (in
86
+ # Itanium ABI, vtable layout = [offset-to-top][typeinfo*][vfn0][vfn1]...).
87
+ print('\n--- bytes near typeinfo 0x100970160 (vtable + typeinfo) ---')
88
+ buf = bytearray(0x800)
89
+ try:
90
+ base = addr(0x10096fa00)
91
+ mem.getBytes(base, buf)
92
+ # interpret as 8-byte LE pointers; show first 256
93
+ for i in range(0, len(buf), 8):
94
+ v = int.from_bytes(buf[i:i+8], 'little')
95
+ if 0x100000000 <= v < 0x110000000:
96
+ print(' +0x%04x : 0x%016x' % (i, v))
97
+ except Exception as e:
98
+ print(' err:', e)
99
+
100
+ # Search the entire binary's __DATA segments for any pointer = 0x100970160
101
+ # (typeinfo of Stick3ANet) -- the location 8 bytes BEFORE that hit is the
102
+ # vtable start; +0xe8 + 0x10 = slot.
103
+ print('\n--- searching for pointer 0x100970160 in __DATA ---')
104
+ target_bytes = (0x100970160).to_bytes(8, 'little')
105
+ for block in mem.getBlocks():
106
+ if not block.isInitialized(): continue
107
+ if block.isExecute(): continue # data only
108
+ start = block.getStart().getOffset()
109
+ size = block.getSize()
110
+ if size > 64*1024*1024: continue
111
+ bb = bytearray(size)
112
+ try:
113
+ block.getBytes(block.getStart(), bb)
114
+ except Exception:
115
+ continue
116
+ i = 0
117
+ while True:
118
+ idx = bb.find(target_bytes, i)
119
+ if idx == -1: break
120
+ ptr_addr = start + idx
121
+ # In Itanium ABI, this offset = vtable + 0x8 (the typeinfo pointer slot).
122
+ # So vtable starts at ptr_addr - 0x8 (the offset-to-top slot before it).
123
+ # Slot index N is at vtable + 0x10 + N*8.
124
+ # vfn at offset +0xe8 in vptr-space = raw addr ptr_addr + 0x8 + 0xe8
125
+ # because vptr = vtable + 0x10, and slot at +0xe8 in vptr-space
126
+ # is at vtable + 0x10 + 0xe8 = ptr_addr - 0x8 + 0x10 + 0xe8 = ptr_addr + 0xf0.
127
+ slot_addr = ptr_addr + 0xf0
128
+ try:
129
+ slot_buf = bytearray(8)
130
+ mem.getBytes(addr(slot_addr), slot_buf)
131
+ slot_val = int.from_bytes(slot_buf, 'little')
132
+ print(' typeinfo* found at 0x%x -> vt[+0xe8] = 0x%016x (fn @ 0x%016x)'
133
+ % (ptr_addr, slot_val, slot_val))
134
+ # Dump that function.
135
+ f = fm.getFunctionAt(addr(slot_val)) or fm.getFunctionContaining(addr(slot_val))
136
+ if f:
137
+ dump_fn(slot_val, 'stick3anet_vt_e8')
138
+ except Exception as e:
139
+ print(' err reading slot at 0x%x: %s' % (slot_addr, e))
140
+ i = idx + 1
141
+
142
+ # Also try Stick3A typeinfo
143
+ print('\n--- searching for pointer 0x100C820F0 (Stick3A typeinfo) ---')
144
+ target2 = (0x100C820F0).to_bytes(8, 'little')
145
+ for block in mem.getBlocks():
146
+ if not block.isInitialized() or block.isExecute(): continue
147
+ size = block.getSize()
148
+ if size > 64*1024*1024: continue
149
+ bb = bytearray(size)
150
+ try: block.getBytes(block.getStart(), bb)
151
+ except: continue
152
+ i = 0
153
+ start = block.getStart().getOffset()
154
+ while True:
155
+ idx = bb.find(target2, i)
156
+ if idx == -1: break
157
+ ptr_addr = start + idx
158
+ slot_addr = ptr_addr + 0xf0
159
+ try:
160
+ slot_buf = bytearray(8); mem.getBytes(addr(slot_addr), slot_buf)
161
+ slot_val = int.from_bytes(slot_buf, 'little')
162
+ print(' Stick3A typeinfo @ 0x%x -> vt[+0xe8] = 0x%016x' % (ptr_addr, slot_val))
163
+ f = fm.getFunctionAt(addr(slot_val)) or fm.getFunctionContaining(addr(slot_val))
164
+ if f:
165
+ dump_fn(slot_val, 'stick3a_vt_e8')
166
+ except Exception as e:
167
+ print(' err: %s' % e)
168
+ i = idx + 1
169
+
170
+ print('dump-vt-key-getter: done -> %s' % OUT)
@@ -0,0 +1,210 @@
1
+ # Locate the real DMX cipher path in the symbolized ESA2 HardwareManager.
2
+ #
3
+ # What we know from dump-anchors:
4
+ # - DmxUniversePage::sendDmx (worker fn dispatched via QtConcurrent::run) calls
5
+ # (*((**(this+0x48))[2]))((this+0x48)+0x10, this+0x58)
6
+ # i.e. a virtual on an object held at DmxUniversePage::m_field_0x48,
7
+ # passing the 488-byte raw DMX buffer at this+0x58.
8
+ # - receiveDmx calls slot +0x18 of the same vtable.
9
+ # - That object's class is one of the RTTI-anchored ones (CryptDmxUniverse,
10
+ # AesOStream, ...) — only RTTI strings exist (no symbolized methods).
11
+ #
12
+ # Plan:
13
+ # 1. Find every RTTI typeinfo-name string that contains "CryptDmxUniverse"
14
+ # or "AesOStream"/"AesIStream"/"Dmx" (broader net).
15
+ # 2. For each, walk xrefs back to the corresponding __ZTV* (vtable) — the
16
+ # vtable is a sequence of function pointers and its 2nd qword points to
17
+ # the typeinfo. So for each xref site that looks like a vtable, list the
18
+ # function pointers at +0x10, +0x18, +0x20, +0x28 and dump those functions.
19
+ # 3. Also: cross-ref _sendto (0x1006ed9c6) callers — the encrypt+send fn
20
+ # should appear among them. Dump every caller.
21
+ # 4. Print everything to tools/ghidra/out2/.
22
+
23
+ # @category Stick
24
+
25
+ import os, re
26
+ from ghidra.app.decompiler import DecompInterface
27
+ from ghidra.util.task import ConsoleTaskMonitor
28
+ from ghidra.program.model.symbol import RefType
29
+
30
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out2')
31
+ try:
32
+ os.makedirs(OUT_DIR)
33
+ except OSError:
34
+ pass
35
+
36
+ prog = currentProgram
37
+ fm = prog.getFunctionManager()
38
+ st = prog.getSymbolTable()
39
+ af = prog.getAddressFactory()
40
+ mem = prog.getMemory()
41
+ listing = prog.getListing()
42
+ ref_mgr = prog.getReferenceManager()
43
+ monitor = ConsoleTaskMonitor()
44
+
45
+ decomp = DecompInterface()
46
+ decomp.openProgram(prog)
47
+
48
+
49
+ def addr(a):
50
+ return af.getDefaultAddressSpace().getAddress(a)
51
+
52
+
53
+ def get_ptr(a):
54
+ """Read an 8-byte LE pointer from address a."""
55
+ try:
56
+ return mem.getLong(a) & 0xFFFFFFFFFFFFFFFF
57
+ except Exception:
58
+ return None
59
+
60
+
61
+ def dump_function_brief(func, out):
62
+ out.write(' FUNC %s @ %s (size %d)\n' % (func.getName(True),
63
+ func.getEntryPoint(), func.getBody().getNumAddresses()))
64
+
65
+
66
+ def dump_function_full(func, out):
67
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
68
+ out.write('signature: %s\n' % func.getPrototypeString(True, True))
69
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
70
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
71
+ for f in func.getCalledFunctions(monitor)))
72
+ out.write('\n-- callees (%d) --\n' % len(callees))
73
+ for c in callees:
74
+ out.write(' ' + c + '\n')
75
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
76
+ for f in func.getCallingFunctions(monitor)))
77
+ out.write('\n-- callers (%d) --\n' % len(callers))
78
+ for c in callers:
79
+ out.write(' ' + c + '\n')
80
+ out.write('\n-- decompilation --\n')
81
+ res = decomp.decompileFunction(func, 120, monitor)
82
+ if res.decompileCompleted():
83
+ out.write(res.getDecompiledFunction().getC())
84
+ else:
85
+ out.write('(decompilation failed: %s)\n' % res.getErrorMessage())
86
+ out.write('\n')
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # 1. Find RTTI typeinfo-name strings for DMX/Crypto classes
91
+ # ---------------------------------------------------------------------------
92
+ target_class_substrings = [
93
+ 'CryptDmxUniverse', 'AesOStream', 'AesIStream',
94
+ 'DasEccAesCryptography', 'DasDhRsaCryptography',
95
+ 'DasEccStreamCryptography', 'DasCryptography',
96
+ 'DmxUniverse', # base class candidates
97
+ ]
98
+
99
+ rtti_strings = [] # list of (substring, name, address)
100
+ for sym in st.getAllSymbols(False):
101
+ name = sym.getName(True)
102
+ if '__ZTS' in name or 'typeinfo-name' in name:
103
+ for sub in target_class_substrings:
104
+ if sub in name:
105
+ rtti_strings.append((sub, name, sym.getAddress()))
106
+ break
107
+ elif '__ZTI' in name or 'typeinfo' in name:
108
+ for sub in target_class_substrings:
109
+ if sub in name:
110
+ rtti_strings.append((sub, name, sym.getAddress()))
111
+ break
112
+ elif '__ZTV' in name or 'vtable' in name:
113
+ for sub in target_class_substrings:
114
+ if sub in name:
115
+ rtti_strings.append((sub, name, sym.getAddress()))
116
+ break
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # 2. Dump summary, vtable layouts, and full decomps of every vtable slot fn
120
+ # ---------------------------------------------------------------------------
121
+ summary_path = os.path.join(OUT_DIR, 'SUMMARY.txt')
122
+ seen_funcs = set()
123
+ out_summary = open(summary_path, 'w')
124
+ out_summary.write('Cipher-anchor search — ESA2 HardwareManager (2024-03-21)\n\n')
125
+
126
+ out_summary.write('=== RTTI/vtable symbols for DMX/Crypto classes ===\n')
127
+ for sub, name, a in rtti_strings:
128
+ out_summary.write(' [%s] %s @ %s\n' % (sub, name, a))
129
+ out_summary.write('\n')
130
+
131
+ # For every vtable symbol (__ZTV*), the layout is:
132
+ # [+0x00] offset-to-top (signed 64)
133
+ # [+0x08] typeinfo pointer
134
+ # [+0x10] virtual fn 0
135
+ # [+0x18] virtual fn 1
136
+ # [+0x20] virtual fn 2
137
+ # ...
138
+ # We saw sendDmx do ((vt+0x10))(...) — that's the *second function pointer*
139
+ # in the vtable structure if you measure from the typeinfo pointer; actually
140
+ # *((vptr)[2])(...) at C-source level. In the Ghidra decomp it's the value
141
+ # obtained as **((vptr+0x10) cast to ptr-to-ptr), which means vtable[+0x10]
142
+ # IS the first function-pointer slot (Itanium ABI: vptr points at the first
143
+ # fn slot, NOT the typeinfo header). So:
144
+ # - The address Ghidra calls "vtable @ X" is the START of the structure
145
+ # (offset-to-top); typical layout above.
146
+ # - "vptr" stored in objects = X + 0x10
147
+ # - vt[0] = mem[X+0x10], vt[1] = mem[X+0x18], ...
148
+ # We dump fn pointers at offsets +0x10 through +0x80 (16 slots) of any vtable.
149
+
150
+ vtable_syms = [(s, n, a) for (s, n, a) in rtti_strings if '__ZTV' in n or 'vtable' in n]
151
+ out_summary.write('=== Vtable layouts (first 16 fn slots) ===\n')
152
+ for sub, name, a in vtable_syms:
153
+ out_summary.write('\n--- %s @ %s ---\n' % (name, a))
154
+ base = int(a.getOffset())
155
+ # Skip first two qwords (offset-to-top, typeinfo); slots start at +0x10
156
+ for slot in range(0, 16):
157
+ slot_addr = addr(base + 0x10 + slot * 8)
158
+ fp = get_ptr(slot_addr)
159
+ if fp is None:
160
+ out_summary.write(' +0x%02x: <unmapped>\n' % (0x10 + slot * 8))
161
+ continue
162
+ if fp == 0:
163
+ out_summary.write(' +0x%02x: NULL — stop\n' % (0x10 + slot * 8))
164
+ break
165
+ fa = addr(fp)
166
+ f = fm.getFunctionAt(fa) or fm.getFunctionContaining(fa)
167
+ if f is None:
168
+ out_summary.write(' +0x%02x: 0x%x (no function)\n' %
169
+ (0x10 + slot * 8, fp))
170
+ continue
171
+ out_summary.write(' +0x%02x: %s @ %s\n' %
172
+ (0x10 + slot * 8, f.getName(True), f.getEntryPoint()))
173
+ if f.getEntryPoint() in seen_funcs:
174
+ continue
175
+ seen_funcs.add(f.getEntryPoint())
176
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
177
+ path = os.path.join(OUT_DIR, 'vt_%s_+%02x_%s.txt' %
178
+ (re.sub(r'[^A-Za-z0-9._-]', '_', name)[:60],
179
+ 0x10 + slot * 8, safe))
180
+ with open(path, 'w') as o:
181
+ dump_function_full(f, o)
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # 3. Cross-reference _sendto callers — the encrypt+send fn must be among them
185
+ # ---------------------------------------------------------------------------
186
+ out_summary.write('\n=== Callers of _sendto (0x1006ed9c6) ===\n')
187
+ sendto_addr = addr(0x1006ed9c6)
188
+ sendto_fn = fm.getFunctionAt(sendto_addr)
189
+ sendto_callers = set()
190
+ if sendto_fn is not None:
191
+ for ref in ref_mgr.getReferencesTo(sendto_addr):
192
+ from_addr = ref.getFromAddress()
193
+ caller = fm.getFunctionContaining(from_addr)
194
+ if caller is not None:
195
+ sendto_callers.add(caller.getEntryPoint())
196
+
197
+ for ep in sorted(sendto_callers):
198
+ f = fm.getFunctionAt(ep)
199
+ out_summary.write(' %s @ %s\n' % (f.getName(True), f.getEntryPoint()))
200
+ if ep in seen_funcs:
201
+ continue
202
+ seen_funcs.add(ep)
203
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
204
+ path = os.path.join(OUT_DIR, 'sendto_caller_%s_%s.txt' % (f.getEntryPoint(), safe))
205
+ with open(path, 'w') as o:
206
+ dump_function_full(f, o)
207
+
208
+ out_summary.close()
209
+ print('find-cipher: dumped %d functions to %s' % (len(seen_funcs), OUT_DIR))
210
+ print('find-cipher: summary at %s' % summary_path)