@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,109 @@
1
+ # Locate the constructor(s) of XHL_Stick3CryptDmxUniverse and the
2
+ # DasEccStreamCryptography subobject it embeds.
3
+ #
4
+ # Method: in x86_64 PIC code, a constructor sets the object's vptr via
5
+ # lea rax, [rip + delta] ; rax = address of vt slot 0
6
+ # mov [rdi], rax ; obj->vptr = rax
7
+ # Ghidra creates a Reference of type DATA from the lea instruction's address
8
+ # to the vtable's slot-0 address.
9
+ #
10
+ # So xrefs to vptr values from CODE locations = constructor / VTT setup.
11
+ #
12
+ # Vptrs of interest (per our fixup mining):
13
+ # Stick3CryptDmxUniverse primary vptr = 0x100962BE8 (= vt site + 8)
14
+ # Stick3CryptDmxUniverse sub1 vptr = 0x100962CC8
15
+ # Stick3CryptDmxUniverse sub2 vptr = 0x100962CF0
16
+ # Stick3CryptDmxUniverse sub3 vptr = 0x100962D30
17
+ # Stick3CryptDmxUniverse sub4 vptr = 0x100962D68
18
+ # Stick3CryptDmxUniverse sub5 vptr = 0x100962DA8
19
+ # DasEccStreamCryptography vptr = 0x100851F38
20
+ # DasEccAesCryptography vptr = 0x100851D88
21
+ # AesOStream vptr = 0x100851B20
22
+
23
+ # @category Stick
24
+
25
+ import os, re
26
+ from ghidra.app.decompiler import DecompInterface
27
+ from ghidra.util.task import ConsoleTaskMonitor
28
+
29
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out9')
30
+ try: os.makedirs(OUT_DIR)
31
+ except OSError: pass
32
+
33
+ prog = currentProgram
34
+ fm = prog.getFunctionManager()
35
+ af = prog.getAddressFactory()
36
+ ref_mgr = prog.getReferenceManager()
37
+ mem = prog.getMemory()
38
+ monitor = ConsoleTaskMonitor()
39
+ decomp = DecompInterface()
40
+ decomp.openProgram(prog)
41
+
42
+
43
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
44
+
45
+
46
+ def dump(func, out):
47
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
48
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
49
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
50
+ for f in func.getCalledFunctions(monitor)))
51
+ out.write('\n-- callees (%d) --\n' % len(callees))
52
+ for c in callees: out.write(' ' + c + '\n')
53
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
54
+ for f in func.getCallingFunctions(monitor)))
55
+ out.write('\n-- callers (%d) --\n' % len(callers))
56
+ for c in callers: out.write(' ' + c + '\n')
57
+ out.write('\n-- decompilation --\n')
58
+ res = decomp.decompileFunction(func, 240, monitor)
59
+ if res.decompileCompleted():
60
+ out.write(res.getDecompiledFunction().getC())
61
+ else:
62
+ out.write('(decompilation failed: %s)\n' % res.getErrorMessage())
63
+ out.write('\n')
64
+
65
+
66
+ VPTRS = {
67
+ 'Stick3_primary': 0x100962BE8,
68
+ 'Stick3_sub1': 0x100962CC8,
69
+ 'Stick3_sub2': 0x100962CF0,
70
+ 'Stick3_sub3': 0x100962D30,
71
+ 'Stick3_sub4': 0x100962D68,
72
+ 'Stick3_sub5': 0x100962DA8,
73
+ 'DasEccStreamCryptography': 0x100851F38,
74
+ 'DasEccAesCryptography': 0x100851D88,
75
+ 'AesOStream': 0x100851B20,
76
+ 'AesOStream_sec': 0x100851B48,
77
+ }
78
+
79
+ seen = set()
80
+ summary = open(os.path.join(OUT_DIR, 'SUMMARY.txt'), 'w')
81
+
82
+ for label, vptr in VPTRS.items():
83
+ a = addr(vptr)
84
+ summary.write('\n#### %s vptr @ 0x%x\n' % (label, vptr))
85
+ refs = list(ref_mgr.getReferencesTo(a))
86
+ code_callers = set()
87
+ for r in refs:
88
+ fa = r.getFromAddress()
89
+ f = fm.getFunctionContaining(fa)
90
+ if f is not None:
91
+ code_callers.add(f.getEntryPoint())
92
+ else:
93
+ summary.write(' data ref @ %s (no fn)\n' % fa)
94
+ summary.write(' code refs from %d function(s):\n' % len(code_callers))
95
+ for ep in sorted(code_callers):
96
+ f = fm.getFunctionAt(ep)
97
+ summary.write(' %s @ %s (size %d)\n' %
98
+ (f.getName(True), f.getEntryPoint(),
99
+ f.getBody().getNumAddresses()))
100
+ if f.getEntryPoint() in seen: continue
101
+ seen.add(f.getEntryPoint())
102
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
103
+ with open(os.path.join(OUT_DIR, '%s_%s_%s.txt' %
104
+ (label, f.getEntryPoint(), safe)), 'w') as o:
105
+ dump(f, o)
106
+
107
+ summary.write('\n=== total dumped: %d ===\n' % len(seen))
108
+ summary.close()
109
+ print('find-ctor: %d functions dumped to %s' % (len(seen), OUT_DIR))
@@ -0,0 +1,97 @@
1
+ # Find every function that references the MD5 init constants table
2
+ # at 0x1007C0290. That table is the H0 = {A,B,C,D} = {0x67452301, ...}.
3
+ # The function reading it IS the MD5 init / compute function. From there
4
+ # we can find callers and identify the KDF that produces the AES key.
5
+
6
+ # @category Stick
7
+
8
+ import os, re
9
+ from ghidra.app.decompiler import DecompInterface
10
+ from ghidra.util.task import ConsoleTaskMonitor
11
+
12
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out18')
13
+ try: os.makedirs(OUT_DIR)
14
+ except OSError: pass
15
+
16
+ prog = currentProgram
17
+ fm = prog.getFunctionManager()
18
+ af = prog.getAddressFactory()
19
+ ref_mgr = prog.getReferenceManager()
20
+ monitor = ConsoleTaskMonitor()
21
+ decomp = DecompInterface()
22
+ decomp.openProgram(prog)
23
+
24
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
25
+
26
+ def dump(func, out):
27
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
28
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
29
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
30
+ for f in func.getCalledFunctions(monitor)))
31
+ out.write('\n-- callees (%d) --\n' % len(callees))
32
+ for c in callees: out.write(' ' + c + '\n')
33
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
34
+ for f in func.getCallingFunctions(monitor)))
35
+ out.write('\n-- callers (%d) --\n' % len(callers))
36
+ for c in callers: out.write(' ' + c + '\n')
37
+ out.write('\n-- decompilation --\n')
38
+ res = decomp.decompileFunction(func, 240, monitor)
39
+ if res.decompileCompleted():
40
+ out.write(res.getDecompiledFunction().getC())
41
+ else:
42
+ out.write('(decompilation failed: %s)\n' % res.getErrorMessage())
43
+ out.write('\n')
44
+
45
+
46
+ seen = set()
47
+ summary = open(os.path.join(OUT_DIR, 'SUMMARY.txt'), 'w')
48
+
49
+ # Find any function that references the MD5 init table at 0x1007C0290.
50
+ MD5_ADDR = 0x1007C0290
51
+ summary.write('Refs to MD5 init constants @ 0x%x\n\n' % MD5_ADDR)
52
+ refs = list(ref_mgr.getReferencesTo(addr(MD5_ADDR)))
53
+ summary.write('total refs: %d\n' % len(refs))
54
+ code_callers = set()
55
+ for r in refs:
56
+ fa = r.getFromAddress()
57
+ rt = r.getReferenceType().getName()
58
+ f = fm.getFunctionContaining(fa)
59
+ if f is not None:
60
+ code_callers.add(f.getEntryPoint())
61
+ summary.write(' ref @ %s (%s) -> fn %s @ %s\n' %
62
+ (fa, rt, f.getName(True), f.getEntryPoint()))
63
+ else:
64
+ summary.write(' ref @ %s (%s) - no fn\n' % (fa, rt))
65
+
66
+ # Dump each MD5-using function + its callers (one hop UP)
67
+ summary.write('\n=== MD5-init users + their callers ===\n')
68
+ for ep in sorted(code_callers):
69
+ f = fm.getFunctionAt(ep)
70
+ if f is None: continue
71
+ summary.write('\n MD5_user: %s @ %s (size %d)\n' %
72
+ (f.getName(True), f.getEntryPoint(),
73
+ f.getBody().getNumAddresses()))
74
+ summary.write(' callers of this fn:\n')
75
+ for c in sorted(f.getCallingFunctions(monitor), key=lambda f: f.getEntryPoint()):
76
+ summary.write(' %s @ %s (size %d)\n' %
77
+ (c.getName(True), c.getEntryPoint(),
78
+ c.getBody().getNumAddresses()))
79
+ # Dump the MD5 user itself
80
+ if ep not in seen:
81
+ seen.add(ep)
82
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
83
+ with open(os.path.join(OUT_DIR, 'md5_user_%s_%s.txt' %
84
+ (f.getEntryPoint(), safe)), 'w') as o:
85
+ dump(f, o)
86
+ # Dump each caller too
87
+ for c in f.getCallingFunctions(monitor):
88
+ if c.getEntryPoint() in seen: continue
89
+ seen.add(c.getEntryPoint())
90
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', c.getName(True))[:120]
91
+ with open(os.path.join(OUT_DIR, 'md5_caller_%s_%s.txt' %
92
+ (c.getEntryPoint(), safe)), 'w') as o:
93
+ dump(c, o)
94
+
95
+ summary.write('\n=== total: %d ===\n' % len(seen))
96
+ summary.close()
97
+ print('find-md5: %d functions dumped to %s' % (len(seen), OUT_DIR))
@@ -0,0 +1,98 @@
1
+ # Find every function that reads the deobfuscated hardcoded 33-byte secret.
2
+ #
3
+ # The chain is:
4
+ # obfuscated raw bytes @ 0x1007B0490 (33 bytes in __TEXT __const)
5
+ # FUN_100182c80(&DAT_100cf8cf0, &DAT_1007b0490) loads them to __DATA
6
+ # cipher vt[7] (FUN_100180E90) XORs with mask 0x51..0x71, leaves decoded
7
+ # buffer at &DAT_100cf8d11, returns its address
8
+ # <somebody> calls vt[7], reads 33 bytes, uses for cipher state seeding
9
+ #
10
+ # Anyone calling vt[7] via `(*((cipher)->vptr + 0x38))(cipher)` would not
11
+ # show as a direct ref to FUN_100180E90. But anyone READING addresses
12
+ # 0x100cf8cf0 / 0x100cf8d11 / 0x1007b0490 directly would.
13
+
14
+ # @category Stick
15
+
16
+ import os, re
17
+ from ghidra.app.decompiler import DecompInterface
18
+ from ghidra.util.task import ConsoleTaskMonitor
19
+
20
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out17')
21
+ try: os.makedirs(OUT_DIR)
22
+ except OSError: pass
23
+
24
+ prog = currentProgram
25
+ fm = prog.getFunctionManager()
26
+ af = prog.getAddressFactory()
27
+ ref_mgr = prog.getReferenceManager()
28
+ monitor = ConsoleTaskMonitor()
29
+ decomp = DecompInterface()
30
+ decomp.openProgram(prog)
31
+
32
+
33
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
34
+
35
+
36
+ def dump(func, out):
37
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
38
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
39
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
40
+ for f in func.getCalledFunctions(monitor)))
41
+ out.write('\n-- callees (%d) --\n' % len(callees))
42
+ for c in callees: out.write(' ' + c + '\n')
43
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
44
+ for f in func.getCallingFunctions(monitor)))
45
+ out.write('\n-- callers (%d) --\n' % len(callers))
46
+ for c in callers: out.write(' ' + c + '\n')
47
+ out.write('\n-- decompilation --\n')
48
+ res = decomp.decompileFunction(func, 240, monitor)
49
+ if res.decompileCompleted():
50
+ out.write(res.getDecompiledFunction().getC())
51
+ else:
52
+ out.write('(decompilation failed: %s)\n' % res.getErrorMessage())
53
+ out.write('\n')
54
+
55
+
56
+ # Addresses involved in the hardcoded-secret deobfuscation
57
+ TARGETS = {
58
+ 'raw_obfuscated': 0x1007B0490, # 33 bytes XOR-masked
59
+ 'staging_buffer': 0x100CF8CF0, # bytes loaded here, then XOR'd
60
+ 'decoded_pointer': 0x100CF8D11, # vt[7] returns this; decoded bytes start
61
+ 'guard_byte': 0x100CF8D38, # __cxa_guard for one-time init
62
+ 'deobfuscator_fn': 0x100180E90,
63
+ 'loader_fn': 0x100182C80,
64
+ }
65
+
66
+ seen = set()
67
+ summary = open(os.path.join(OUT_DIR, 'SUMMARY.txt'), 'w')
68
+
69
+ for label, a in TARGETS.items():
70
+ summary.write('\n#### %s @ 0x%x\n' % (label, a))
71
+ code_refs = set()
72
+ refs = list(ref_mgr.getReferencesTo(addr(a)))
73
+ summary.write(' total refs: %d\n' % len(refs))
74
+ for r in refs:
75
+ fa = r.getFromAddress()
76
+ rtype = r.getReferenceType().getName()
77
+ f = fm.getFunctionContaining(fa)
78
+ if f is not None:
79
+ code_refs.add(f.getEntryPoint())
80
+ else:
81
+ summary.write(' %s @ %s (%s, not in any fn)\n' %
82
+ (label, fa, rtype))
83
+ summary.write(' code refs from %d fn(s):\n' % len(code_refs))
84
+ for ep in sorted(code_refs):
85
+ f = fm.getFunctionAt(ep)
86
+ summary.write(' %s @ %s (size %d)\n' %
87
+ (f.getName(True), f.getEntryPoint(),
88
+ f.getBody().getNumAddresses()))
89
+ if ep in seen: continue
90
+ seen.add(ep)
91
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
92
+ with open(os.path.join(OUT_DIR, '%s_%s_%s.txt' %
93
+ (label, f.getEntryPoint(), safe)), 'w') as o:
94
+ dump(f, o)
95
+
96
+ summary.write('\n=== total dumped: %d ===\n' % len(seen))
97
+ summary.close()
98
+ print('find-secret-users: %d functions dumped to %s' % (len(seen), OUT_DIR))
@@ -0,0 +1,190 @@
1
+ # Locate XHL_Stick3CryptDmxUniverse + related crypto vtables and dump methods.
2
+ #
3
+ # Now that we know the real class names (with XHL_ prefix and namespaces):
4
+ # XHL_Stick3ANet::XHL_Stick3CryptDmxUniverse ← DE3 DMX cipher class
5
+ # XHL_DasNetCryptDmxUniverse ← base class
6
+ # XHL_AesOStream / XHL_AesIStream ← block cipher streams
7
+ # XHL_AesOStreamBuffer / XHL_AesIStreamBuffer ← buffered variants
8
+ # XHL_DasDhRsaCryptography<1024>/<512> ← DH+RSA handshake
9
+ # XHL_DasEccAesCryptography ← ECC+AES
10
+ # XHL_DasEccStreamCryptography
11
+ # XHL_DasCryptography(KeySize)
12
+ #
13
+ # We dump the full method table of each, plus every called function — that
14
+ # walks us into the actual AES inlining and the per-session key derivation.
15
+
16
+ # @category Stick
17
+
18
+ import os, re
19
+ from ghidra.app.decompiler import DecompInterface
20
+ from ghidra.util.task import ConsoleTaskMonitor
21
+
22
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out3')
23
+ try:
24
+ os.makedirs(OUT_DIR)
25
+ except OSError:
26
+ pass
27
+
28
+ prog = currentProgram
29
+ fm = prog.getFunctionManager()
30
+ st = prog.getSymbolTable()
31
+ af = prog.getAddressFactory()
32
+ mem = prog.getMemory()
33
+ ref_mgr = prog.getReferenceManager()
34
+ monitor = ConsoleTaskMonitor()
35
+ decomp = DecompInterface()
36
+ decomp.openProgram(prog)
37
+
38
+ # Substrings to match in mangled or demangled symbol names
39
+ KEYWORDS = [
40
+ 'Stick3CryptDmxUniverse',
41
+ 'Stick5CryptDmxUniverse',
42
+ 'DasNetCryptDmxUniverse',
43
+ 'AesOStream',
44
+ 'AesIStream',
45
+ 'DasDhRsaCryptography',
46
+ 'DasEccAesCryptography',
47
+ 'DasEccStreamCryptography',
48
+ 'DasCryptography',
49
+ # Also: any function whose source-file string contains these
50
+ 'XHL_Stick3',
51
+ 'XHL_DasNet',
52
+ ]
53
+
54
+
55
+ def addr(a):
56
+ return af.getDefaultAddressSpace().getAddress(a)
57
+
58
+
59
+ def get_ptr(a):
60
+ try:
61
+ return mem.getLong(a) & 0xFFFFFFFFFFFFFFFF
62
+ except Exception:
63
+ return None
64
+
65
+
66
+ def dump_function_full(func, out, depth=0):
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, 180, 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
+ # Find all symbols (any) whose name contains any keyword.
90
+ all_hits = [] # list of (kw, symbol)
91
+ for sym in st.getAllSymbols(False):
92
+ n = sym.getName(True)
93
+ for kw in KEYWORDS:
94
+ if kw in n:
95
+ all_hits.append((kw, sym))
96
+ break
97
+
98
+ summary_path = os.path.join(OUT_DIR, 'SUMMARY.txt')
99
+ out_summary = open(summary_path, 'w')
100
+ out_summary.write('Stick3 crypto search — ESA2 HardwareManager (2024-03-21)\n\n')
101
+
102
+ # Bucket by kind
103
+ funcs = [] # symbols that are functions
104
+ vtables = [] # vtable symbols (__ZTV)
105
+ typeinfos = [] # __ZTI
106
+ typenames = [] # __ZTS
107
+ others = []
108
+ for kw, sym in all_hits:
109
+ n = sym.getName(False) or ''
110
+ nfull = sym.getName(True)
111
+ if fm.getFunctionAt(sym.getAddress()):
112
+ funcs.append(sym)
113
+ elif n.startswith('__ZTV') or 'vtable' in nfull:
114
+ vtables.append(sym)
115
+ elif n.startswith('__ZTI') or 'typeinfo' in nfull and 'name' not in nfull:
116
+ typeinfos.append(sym)
117
+ elif n.startswith('__ZTS') or 'typeinfo-name' in nfull:
118
+ typenames.append(sym)
119
+ else:
120
+ others.append(sym)
121
+
122
+ out_summary.write('=== Summary counts ===\n')
123
+ out_summary.write(' functions: %d\n' % len(funcs))
124
+ out_summary.write(' vtables: %d\n' % len(vtables))
125
+ out_summary.write(' typeinfos: %d\n' % len(typeinfos))
126
+ out_summary.write(' type-names: %d\n' % len(typenames))
127
+ out_summary.write(' other: %d\n\n' % len(others))
128
+
129
+ out_summary.write('=== Functions (named methods) ===\n')
130
+ for s in sorted(funcs, key=lambda s: s.getAddress()):
131
+ out_summary.write(' %s @ %s\n' % (s.getName(True), s.getAddress()))
132
+
133
+ out_summary.write('\n=== Vtables ===\n')
134
+ for s in sorted(vtables, key=lambda s: s.getAddress()):
135
+ out_summary.write(' %s @ %s\n' % (s.getName(True), s.getAddress()))
136
+
137
+ out_summary.write('\n=== Typeinfos ===\n')
138
+ for s in sorted(typeinfos, key=lambda s: s.getAddress()):
139
+ out_summary.write(' %s @ %s\n' % (s.getName(True), s.getAddress()))
140
+
141
+ out_summary.write('\n=== Type-name strings ===\n')
142
+ for s in sorted(typenames, key=lambda s: s.getAddress()):
143
+ out_summary.write(' %s @ %s\n' % (s.getName(True), s.getAddress()))
144
+
145
+ # Walk every vtable: dump fn-pointer slots and full decomp of each
146
+ seen = set()
147
+ out_summary.write('\n\n=== Vtable layouts ===\n')
148
+ for s in sorted(vtables, key=lambda s: s.getAddress()):
149
+ name = s.getName(True)
150
+ a = s.getAddress()
151
+ out_summary.write('\n--- %s @ %s ---\n' % (name, a))
152
+ base = int(a.getOffset())
153
+ for slot in range(0, 32):
154
+ slot_off = 0x10 + slot * 8 # skip offset-to-top + typeinfo
155
+ slot_addr = addr(base + slot_off)
156
+ fp = get_ptr(slot_addr)
157
+ if fp is None or fp == 0:
158
+ break
159
+ # Heuristic to stop at end of vtable: if the qword is itself another
160
+ # vtable symbol's address or clearly not code, bail.
161
+ fa = addr(fp)
162
+ f = fm.getFunctionAt(fa) or fm.getFunctionContaining(fa)
163
+ if f is None:
164
+ out_summary.write(' +0x%02x: 0x%x (no fn)\n' % (slot_off, fp))
165
+ continue
166
+ out_summary.write(' +0x%02x: %s @ %s\n' %
167
+ (slot_off, f.getName(True), f.getEntryPoint()))
168
+ if f.getEntryPoint() in seen:
169
+ continue
170
+ seen.add(f.getEntryPoint())
171
+ safe_cls = re.sub(r'[^A-Za-z0-9._-]', '_', name)[:80]
172
+ safe_fn = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
173
+ path = os.path.join(OUT_DIR, '%s_+%02x_%s.txt' % (safe_cls, slot_off, safe_fn))
174
+ with open(path, 'w') as o:
175
+ dump_function_full(f, o)
176
+
177
+ # Also dump every named function we matched directly
178
+ for s in sorted(funcs, key=lambda s: s.getAddress()):
179
+ f = fm.getFunctionAt(s.getAddress())
180
+ if f is None or f.getEntryPoint() in seen:
181
+ continue
182
+ seen.add(f.getEntryPoint())
183
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:140]
184
+ path = os.path.join(OUT_DIR, 'fn_%s_%s.txt' % (f.getEntryPoint(), safe))
185
+ with open(path, 'w') as o:
186
+ dump_function_full(f, o)
187
+
188
+ out_summary.close()
189
+ print('find-stick3-crypto: dumped %d functions to %s' % (len(seen), OUT_DIR))
190
+ print('find-stick3-crypto: summary at %s' % summary_path)
@@ -0,0 +1,111 @@
1
+ # Trace UP from the Stick3CryptDmxUniverse factory to find the XHL_Stick3
2
+ # device class — where the cipher state is actually initialized.
3
+ #
4
+ # Known facts:
5
+ # - FUN_100178bb0 = Stick3CryptDmxUniverse factory
6
+ # - Called by FUN_100179050
7
+ # - The cipher object lives at (device + 0x1618), embedded inside the
8
+ # device class. The device's ctor writes the embedded cipher's vptr.
9
+ #
10
+ # Goal: dump FUN_100179050 + its callers (probably the XHL_Stick3 device's
11
+ # methods), and dump the 7 big constructors (2.7-8KB) that we identified as
12
+ # touching the encrypt-fn vtables — one of them is XHL_Stick3 itself.
13
+
14
+ # @category Stick
15
+
16
+ import os, re
17
+ from ghidra.app.decompiler import DecompInterface
18
+ from ghidra.util.task import ConsoleTaskMonitor
19
+
20
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out12')
21
+ try: os.makedirs(OUT_DIR)
22
+ except OSError: pass
23
+
24
+ prog = currentProgram
25
+ fm = prog.getFunctionManager()
26
+ af = prog.getAddressFactory()
27
+ mem = prog.getMemory()
28
+ ref_mgr = prog.getReferenceManager()
29
+ monitor = ConsoleTaskMonitor()
30
+ decomp = DecompInterface()
31
+ decomp.openProgram(prog)
32
+
33
+
34
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
35
+
36
+
37
+ def dump(func, out):
38
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
39
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
40
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
41
+ for f in func.getCalledFunctions(monitor)))
42
+ out.write('\n-- callees (%d) --\n' % len(callees))
43
+ for c in callees: out.write(' ' + c + '\n')
44
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
45
+ for f in func.getCallingFunctions(monitor)))
46
+ out.write('\n-- callers (%d) --\n' % len(callers))
47
+ for c in callers: out.write(' ' + c + '\n')
48
+ out.write('\n-- decompilation --\n')
49
+ res = decomp.decompileFunction(func, 240, monitor)
50
+ if res.decompileCompleted():
51
+ out.write(res.getDecompiledFunction().getC())
52
+ else:
53
+ out.write('(decompilation failed: %s)\n' % res.getErrorMessage())
54
+ out.write('\n')
55
+
56
+
57
+ # 1. Caller of Stick3 factory + its callers (one more hop up)
58
+ factory_caller = 0x100179050
59
+ roots = {'caller_of_factory': factory_caller}
60
+ seen = set()
61
+ summary = open(os.path.join(OUT_DIR, 'SUMMARY.txt'), 'w')
62
+
63
+ f = fm.getFunctionAt(addr(factory_caller))
64
+ if f:
65
+ seen.add(f.getEntryPoint())
66
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
67
+ with open(os.path.join(OUT_DIR, 'A_factorycaller_%s_%s.txt' %
68
+ (f.getEntryPoint(), safe)), 'w') as o:
69
+ dump(f, o)
70
+ summary.write('factory_caller = %s @ %s (size %d)\n' %
71
+ (f.getName(True), f.getEntryPoint(),
72
+ f.getBody().getNumAddresses()))
73
+ summary.write(' one-up callers:\n')
74
+ for c in sorted(f.getCallingFunctions(monitor), key=lambda f: f.getEntryPoint()):
75
+ summary.write(' %s @ %s (size %d)\n' %
76
+ (c.getName(True), c.getEntryPoint(),
77
+ c.getBody().getNumAddresses()))
78
+ if c.getEntryPoint() in seen: continue
79
+ seen.add(c.getEntryPoint())
80
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', c.getName(True))[:120]
81
+ with open(os.path.join(OUT_DIR, 'B_oneUp_%s_%s.txt' %
82
+ (c.getEntryPoint(), safe)), 'w') as o:
83
+ dump(c, o)
84
+
85
+ # 2. The 7 big constructors that touched encrypt-fn vtables
86
+ BIG_CTORS = [
87
+ 0x1002385b0,
88
+ 0x10027d350,
89
+ 0x100283f60,
90
+ 0x1002b7360,
91
+ 0x1002c1920,
92
+ 0x1002c8ac0,
93
+ 0x1002ceaf0,
94
+ ]
95
+ summary.write('\n=== Big constructors touching encrypt-fn vtables ===\n')
96
+ for fp in BIG_CTORS:
97
+ f = fm.getFunctionAt(addr(fp))
98
+ if f is None: continue
99
+ summary.write(' %s @ %s (size %d)\n' %
100
+ (f.getName(True), f.getEntryPoint(),
101
+ f.getBody().getNumAddresses()))
102
+ if f.getEntryPoint() in seen: continue
103
+ seen.add(f.getEntryPoint())
104
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
105
+ with open(os.path.join(OUT_DIR, 'C_bigctor_%s_%s.txt' %
106
+ (f.getEntryPoint(), safe)), 'w') as o:
107
+ dump(f, o)
108
+
109
+ summary.write('\n=== total: %d ===\n' % len(seen))
110
+ summary.close()
111
+ print('find-stick3-device: %d functions dumped to %s' % (len(seen), OUT_DIR))