@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,76 @@
1
+ # Find + decompile HWM's LSAG_ALL / 0x47 / 0x48 TCP-auth handshake builder.
2
+ # Goal: learn what HWM puts in the 0x48 message — is the X25519 key a fresh
3
+ # ephemeral, or derived from a stored credential? That decides whether the
4
+ # auth is RE-able or needs "Security for Cloud Access" disabled.
5
+ # @category Stick
6
+
7
+ import os, re
8
+ from ghidra.app.decompiler import DecompInterface
9
+ from ghidra.util.task import ConsoleTaskMonitor
10
+
11
+ OUT = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out-handshake')
12
+ try: os.makedirs(OUT)
13
+ except OSError: pass
14
+
15
+ prog = currentProgram
16
+ fm = prog.getFunctionManager()
17
+ af = prog.getAddressFactory()
18
+ mem = prog.getMemory()
19
+ monitor = ConsoleTaskMonitor()
20
+ decomp = DecompInterface(); decomp.openProgram(prog)
21
+ listing = prog.getListing()
22
+
23
+ def decompile(f):
24
+ r = decomp.decompileFunction(f, 240, monitor)
25
+ return r.getDecompiledFunction().getC() if r.decompileCompleted() else '(fail)\n'
26
+
27
+ summary = open(os.path.join(OUT, 'SUMMARY.txt'), 'w')
28
+
29
+ # 1. find interesting strings
30
+ needles = ['LSAG_ALL', 'EccAesCryptography', 'DasNetEcc', 'Cloud', 'Security',
31
+ 'curve25519', 'Curve25519', '25519', 'softwareName', 'authenticat']
32
+ hits = {}
33
+ for d in listing.getDefinedData(True):
34
+ v = d.getValue()
35
+ s = None
36
+ try:
37
+ if d.hasStringValue(): s = str(v)
38
+ except Exception:
39
+ pass
40
+ if not s: continue
41
+ for n in needles:
42
+ if n in s:
43
+ hits.setdefault(n, []).append((d.getAddress(), s[:90]))
44
+
45
+ summary.write('=== string hits ===\n')
46
+ for n, lst in hits.items():
47
+ for a, s in lst[:12]:
48
+ summary.write(' [%s] %s : %r\n' % (n, a, s))
49
+
50
+ # 2. xref the LSAG_ALL / Ecc strings -> referencing functions -> decompile
51
+ seen = set()
52
+ def dump_fn(f, tag):
53
+ ep = str(f.getEntryPoint())
54
+ if ep in seen: return
55
+ seen.add(ep)
56
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:70]
57
+ with open(os.path.join(OUT, '%s_%s_%s.txt' % (tag, ep, safe)), 'w') as o:
58
+ o.write('==== %s @ %s (%d bytes) ====\n' %
59
+ (f.getName(True), ep, f.getBody().getNumAddresses()))
60
+ callees = sorted(set('%s @ %s' % (c.getName(True), c.getEntryPoint())
61
+ for c in f.getCalledFunctions(monitor)))
62
+ o.write('callees:\n ' + '\n '.join(callees) + '\n\n')
63
+ o.write(decompile(f))
64
+ summary.write(' dumped %s : %s @ %s\n' % (tag, f.getName(True), ep))
65
+
66
+ rm = prog.getReferenceManager()
67
+ summary.write('\n=== functions referencing handshake strings ===\n')
68
+ for n in ['LSAG_ALL', 'EccAesCryptography', 'DasNetEcc']:
69
+ for a, s in hits.get(n, []):
70
+ for r in rm.getReferencesTo(a):
71
+ f = fm.getFunctionContaining(r.getFromAddress())
72
+ if f: dump_fn(f, 'ref_' + n[:8])
73
+
74
+ summary.write('\n=== total dumped: %d ===\n' % len(seen))
75
+ summary.close()
76
+ print('dump-handshake: %d functions -> %s' % (len(seen), OUT))
@@ -0,0 +1,105 @@
1
+ # Decompile the X25519 -> AES-256-key KDF call chain.
2
+ # Chain (lldb Path D, ESA2 file addrs):
3
+ # on_listWidgetDevice_itemSelectionChanged
4
+ # -> 0x1005e1bd6 -> 0x1005e267b -> 0x1001780d1
5
+ # -> 0x10010780e -> 0x1003f57c6 (key installer)
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()), 'out-kdf')
13
+ try: os.makedirs(OUT_DIR)
14
+ except OSError: pass
15
+
16
+ prog = currentProgram
17
+ fm = prog.getFunctionManager()
18
+ af = prog.getAddressFactory()
19
+ monitor = ConsoleTaskMonitor()
20
+ decomp = DecompInterface()
21
+ decomp.openProgram(prog)
22
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
23
+
24
+ def func_for(a):
25
+ ad = addr(a)
26
+ return fm.getFunctionAt(ad) or fm.getFunctionContaining(ad)
27
+
28
+ def decompile(func):
29
+ res = decomp.decompileFunction(func, 240, monitor)
30
+ if res.decompileCompleted():
31
+ return res.getDecompiledFunction().getC()
32
+ return '(decompilation failed: %s)\n' % res.getErrorMessage()
33
+
34
+ def dump(func, out, with_decomp=True):
35
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
36
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
37
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
38
+ for f in func.getCalledFunctions(monitor)))
39
+ out.write('\n-- callees (%d) --\n' % len(callees))
40
+ for c in callees: out.write(' ' + c + '\n')
41
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
42
+ for f in func.getCallingFunctions(monitor)))
43
+ out.write('\n-- callers (%d) --\n' % len(callers))
44
+ for c in callers: out.write(' ' + c + '\n')
45
+ if with_decomp:
46
+ out.write('\n-- decompilation --\n')
47
+ out.write(decompile(func))
48
+ out.write('\n')
49
+
50
+ # Chain call sites -> the function each address sits inside.
51
+ CHAIN = [
52
+ ('callsite_1005e1bd6', 0x1005e1bd6),
53
+ ('callsite_1005e267b', 0x1005e267b),
54
+ ('kdf_outer_1001780d1', 0x1001780d1),
55
+ ('kdf_1001780d1_fn', 0x1001780d1),
56
+ ('kdf_inner_10010780e', 0x10010780e),
57
+ ('key_installer_1003f57c6', 0x1003f57c6),
58
+ ]
59
+
60
+ summary = open(os.path.join(OUT_DIR, 'SUMMARY.txt'), 'w')
61
+ seen = set()
62
+
63
+ # 1. Dump every function in the chain.
64
+ chain_funcs = []
65
+ for label, a in CHAIN:
66
+ f = func_for(a)
67
+ if f is None:
68
+ summary.write(' %-26s 0x%x (NO FUNCTION)\n' % (label, a))
69
+ continue
70
+ ep = f.getEntryPoint()
71
+ summary.write(' %-26s 0x%x -> %s @ %s size=%d callees=%d\n' %
72
+ (label, a, f.getName(True), ep,
73
+ f.getBody().getNumAddresses(),
74
+ len(set(f.getCalledFunctions(monitor)))))
75
+ if str(ep) in seen:
76
+ continue
77
+ seen.add(str(ep))
78
+ chain_funcs.append((label, f))
79
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:80]
80
+ with open(os.path.join(OUT_DIR, '1_%s_%s.txt' % (label, safe)), 'w') as o:
81
+ dump(f, o)
82
+
83
+ # 2. Decompile every direct callee of the two KDF-critical functions, so the
84
+ # actual hash/curve arithmetic is visible.
85
+ KDF_CRITICAL = [0x1001780d1, 0x10010780e]
86
+ callee_seen = set(seen)
87
+ for a in KDF_CRITICAL:
88
+ f = func_for(a)
89
+ if f is None: continue
90
+ for callee in sorted(f.getCalledFunctions(monitor), key=lambda x: str(x.getEntryPoint())):
91
+ ep = str(callee.getEntryPoint())
92
+ if ep in callee_seen: continue
93
+ callee_seen.add(ep)
94
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', callee.getName(True))[:80]
95
+ with open(os.path.join(OUT_DIR, '2_callee_%s_%s.txt' %
96
+ (callee.getEntryPoint(), safe)), 'w') as o:
97
+ dump(callee, o)
98
+ summary.write(' callee %s @ %s size=%d\n' %
99
+ (callee.getName(True), callee.getEntryPoint(),
100
+ callee.getBody().getNumAddresses()))
101
+
102
+ summary.write('\n=== chain funcs: %d, callees dumped: %d ===\n' %
103
+ (len(chain_funcs), len(callee_seen) - len(seen)))
104
+ summary.close()
105
+ print('dump-kdf-chain: done -> %s' % OUT_DIR)
@@ -0,0 +1,72 @@
1
+ # Dump everything around the SHA-1 KDF candidate to confirm role.
2
+
3
+ # @category Stick
4
+
5
+ import os, re
6
+ from ghidra.app.decompiler import DecompInterface
7
+ from ghidra.util.task import ConsoleTaskMonitor
8
+
9
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out19')
10
+ try: os.makedirs(OUT_DIR)
11
+ except OSError: pass
12
+
13
+ prog = currentProgram
14
+ fm = prog.getFunctionManager()
15
+ af = prog.getAddressFactory()
16
+ monitor = ConsoleTaskMonitor()
17
+ decomp = DecompInterface()
18
+ decomp.openProgram(prog)
19
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
20
+
21
+ def dump(func, out):
22
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
23
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
24
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
25
+ for f in func.getCalledFunctions(monitor)))
26
+ out.write('\n-- callees (%d) --\n' % len(callees))
27
+ for c in callees: out.write(' ' + c + '\n')
28
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
29
+ for f in func.getCallingFunctions(monitor)))
30
+ out.write('\n-- callers (%d) --\n' % len(callers))
31
+ for c in callers: out.write(' ' + c + '\n')
32
+ out.write('\n-- decompilation --\n')
33
+ res = decomp.decompileFunction(func, 240, monitor)
34
+ if res.decompileCompleted():
35
+ out.write(res.getDecompiledFunction().getC())
36
+ else:
37
+ out.write('(decompilation failed: %s)\n' % res.getErrorMessage())
38
+ out.write('\n')
39
+
40
+
41
+ ROOTS = {
42
+ 'sha1_init_FUN_100407940': 0x100407940, # 39B — should be a tiny init
43
+ 'sha1_update_FUN_100407970': 0x100407970, # update
44
+ 'sha1_final_FUN_100407a60': 0x100407a60, # 615B
45
+ 'kdf_candidate_FUN_10065aad0': 0x10065aad0, # KDF candidate using sha1 + salt
46
+ 'dest_FUN_1006420d0': 0x1006420d0, # what kdf writes via
47
+ 'getter_FUN_1006aebb0': 0x1006aebb0, # what supplies KDF input
48
+ 'kdf_caller_FUN_10065ac20': 0x10065ac20,
49
+ 'kdf_caller_FUN_10065d160': 0x10065d160,
50
+ }
51
+
52
+ seen = set()
53
+ summary = open(os.path.join(OUT_DIR, 'SUMMARY.txt'), 'w')
54
+
55
+ for label, fp in ROOTS.items():
56
+ f = fm.getFunctionAt(addr(fp)) or fm.getFunctionContaining(addr(fp))
57
+ if f is None:
58
+ summary.write(' %s 0x%x (no fn)\n' % (label, fp)); continue
59
+ summary.write(' %-32s = %s @ %s size=%d callees=%d\n' %
60
+ (label, f.getName(True), f.getEntryPoint(),
61
+ f.getBody().getNumAddresses(),
62
+ len(set(f.getCalledFunctions(monitor)))))
63
+ if f.getEntryPoint() in seen: continue
64
+ seen.add(f.getEntryPoint())
65
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
66
+ with open(os.path.join(OUT_DIR, '%s_%s_%s.txt' %
67
+ (label, f.getEntryPoint(), safe)), 'w') as o:
68
+ dump(f, o)
69
+
70
+ summary.write('\n=== total: %d ===\n' % len(seen))
71
+ summary.close()
72
+ print('dump-kdf-context: %d functions dumped to %s' % (len(seen), OUT_DIR))
@@ -0,0 +1,59 @@
1
+ # Decompile HmSoftwareProfile::HmSoftwareProfile (+ its callees) — the
2
+ # constructor that hardcodes the software name + the auth key used as the
3
+ # HMAC-SHA256 key for the 0x48 handshake.
4
+ # @category Stick
5
+
6
+ import os, re
7
+ from ghidra.app.decompiler import DecompInterface
8
+ from ghidra.util.task import ConsoleTaskMonitor
9
+
10
+ OUT = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out-profile')
11
+ try: os.makedirs(OUT)
12
+ except OSError: pass
13
+
14
+ prog = currentProgram
15
+ fm = prog.getFunctionManager()
16
+ st = prog.getSymbolTable()
17
+ monitor = ConsoleTaskMonitor()
18
+ decomp = DecompInterface(); decomp.openProgram(prog)
19
+
20
+ def decompile(f):
21
+ r = decomp.decompileFunction(f, 240, monitor)
22
+ return r.getDecompiledFunction().getC() if r.decompileCompleted() else '(fail)\n'
23
+
24
+ seen = set()
25
+ def dump(f, tag, depth=0):
26
+ if f is None or str(f.getEntryPoint()) in seen: return
27
+ seen.add(str(f.getEntryPoint()))
28
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:70]
29
+ cs = sorted(set(f.getCalledFunctions(monitor)), key=lambda x: str(x.getEntryPoint()))
30
+ with open(os.path.join(OUT, '%s_%s_%s.txt' % (tag, f.getEntryPoint(), safe)), 'w') as o:
31
+ o.write('==== %s @ %s (%d bytes) ====\n' %
32
+ (f.getName(True), f.getEntryPoint(), f.getBody().getNumAddresses()))
33
+ o.write('callees: ' + ', '.join(str(c.getEntryPoint()) for c in cs) + '\n\n')
34
+ o.write(decompile(f))
35
+ if depth > 0:
36
+ for c in cs:
37
+ nm = c.getName(True)
38
+ if nm.startswith('_') or 'std::' in nm or 'operator' in nm or 'QArray' in nm \
39
+ or 'QString' in nm or 'QDir' in nm: continue
40
+ dump(c, tag + '_c', depth - 1)
41
+
42
+ summary = open(os.path.join(OUT, 'SUMMARY.txt'), 'w')
43
+
44
+ # find any symbol mentioning HmSoftwareProfile
45
+ hits = []
46
+ for s in st.getAllSymbols(True):
47
+ n = s.getName(True)
48
+ if 'HmSoftwareProfile' in n or 'SoftwareProfile' in n:
49
+ hits.append((n, s.getAddress()))
50
+ summary.write('=== HmSoftwareProfile symbols ===\n')
51
+ for n, a in hits[:40]:
52
+ summary.write(' %s @ %s\n' % (n, a))
53
+ f = fm.getFunctionContaining(a)
54
+ if f and ('HmSoftwareProfile' in f.getName(True)):
55
+ dump(f, 'profile', depth=2)
56
+
57
+ summary.write('\n=== dumped %d ===\n' % len(seen))
58
+ summary.close()
59
+ print('dump-profile: %d functions, %d symbol hits -> %s' % (len(seen), len(hits), OUT))
@@ -0,0 +1,90 @@
1
+ # Dump the ACTUAL encrypt function at FUN_100107950 (Stick3 cipher vt[3]).
2
+ # Plus walk callees to find the keystream generator.
3
+
4
+ # @category Stick
5
+
6
+ import os, re
7
+ from ghidra.app.decompiler import DecompInterface
8
+ from ghidra.util.task import ConsoleTaskMonitor
9
+
10
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out15')
11
+ try: os.makedirs(OUT_DIR)
12
+ except OSError: pass
13
+
14
+ prog = currentProgram
15
+ fm = prog.getFunctionManager()
16
+ af = prog.getAddressFactory()
17
+ monitor = ConsoleTaskMonitor()
18
+ decomp = DecompInterface()
19
+ decomp.openProgram(prog)
20
+
21
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
22
+
23
+ def dump(func, out):
24
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
25
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
26
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
27
+ for f in func.getCalledFunctions(monitor)))
28
+ out.write('\n-- callees (%d) --\n' % len(callees))
29
+ for c in callees: out.write(' ' + c + '\n')
30
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
31
+ for f in func.getCallingFunctions(monitor)))
32
+ out.write('\n-- callers (%d) --\n' % len(callers))
33
+ for c in callers: out.write(' ' + c + '\n')
34
+ out.write('\n-- decompilation --\n')
35
+ res = decomp.decompileFunction(func, 240, monitor)
36
+ if res.decompileCompleted():
37
+ out.write(res.getDecompiledFunction().getC())
38
+ else:
39
+ out.write('(decompilation failed: %s)\n' % res.getErrorMessage())
40
+ out.write('\n')
41
+
42
+ # Full Stick3 cipher vtable @ 0x10095F830 (typeinfo_ptr):
43
+ ROOTS = {
44
+ 'vt0_FUN_100177990': 0x100177990,
45
+ 'vt1_FUN_100177CB0': 0x100177CB0,
46
+ 'vt2_FUN_100106290': 0x100106290,
47
+ 'vt3_REAL_ENCRYPT_FUN_100107950': 0x100107950,
48
+ 'vt4_FUN_1001C0650': 0x1001C0650,
49
+ 'vt5_FUN_1001C0A50': 0x1001C0A50,
50
+ 'vt6_FUN_1001C0CF0': 0x1001C0CF0,
51
+ 'vt7_FUN_100180E90': 0x100180E90,
52
+ }
53
+
54
+ seen = set()
55
+ summary = open(os.path.join(OUT_DIR, 'SUMMARY.txt'), 'w')
56
+
57
+ for label, fp in ROOTS.items():
58
+ f = fm.getFunctionAt(addr(fp)) or fm.getFunctionContaining(addr(fp))
59
+ if f is None:
60
+ summary.write(' %s 0x%x (no fn)\n' % (label, fp))
61
+ continue
62
+ summary.write(' %-40s = %s @ %s size=%d callees=%d\n' %
63
+ (label, f.getName(True), f.getEntryPoint(),
64
+ f.getBody().getNumAddresses(),
65
+ len(set(f.getCalledFunctions(monitor)))))
66
+ if f.getEntryPoint() in seen: continue
67
+ seen.add(f.getEntryPoint())
68
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
69
+ with open(os.path.join(OUT_DIR, '%s_%s_%s.txt' %
70
+ (label, f.getEntryPoint(), safe)), 'w') as o:
71
+ dump(f, o)
72
+
73
+ # Walk into real-encrypt's callees
74
+ enc = fm.getFunctionAt(addr(0x100107950))
75
+ if enc:
76
+ summary.write('\n=== depth-1 callees of real encrypt fn ===\n')
77
+ for c in sorted(enc.getCalledFunctions(monitor), key=lambda f: f.getEntryPoint()):
78
+ summary.write(' callee: %s @ %s (size %d)\n' %
79
+ (c.getName(True), c.getEntryPoint(),
80
+ c.getBody().getNumAddresses()))
81
+ if c.getEntryPoint() in seen: continue
82
+ seen.add(c.getEntryPoint())
83
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', c.getName(True))[:120]
84
+ with open(os.path.join(OUT_DIR, 'depth1_%s_%s.txt' %
85
+ (c.getEntryPoint(), safe)), 'w') as o:
86
+ dump(c, o)
87
+
88
+ summary.write('\n=== total: %d ===\n' % len(seen))
89
+ summary.close()
90
+ print('dump-real-encrypt: %d functions dumped to %s' % (len(seen), OUT_DIR))
@@ -0,0 +1,84 @@
1
+ # Decompile the 33-byte-secret deobfuscator + locate its obfuscated source,
2
+ # and find the initializer that populates the P-256 curve constants.
3
+ # @category Stick
4
+
5
+ import os, struct
6
+ from ghidra.app.decompiler import DecompInterface
7
+ from ghidra.util.task import ConsoleTaskMonitor
8
+
9
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out-secret')
10
+ try: os.makedirs(OUT_DIR)
11
+ except OSError: pass
12
+
13
+ prog = currentProgram
14
+ fm = prog.getFunctionManager()
15
+ af = prog.getAddressFactory()
16
+ mem = prog.getMemory()
17
+ rm = prog.getReferenceManager()
18
+ monitor = ConsoleTaskMonitor()
19
+ decomp = DecompInterface()
20
+ decomp.openProgram(prog)
21
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
22
+
23
+ def rd(a, n):
24
+ buf = bytearray(n)
25
+ try:
26
+ mem.getBytes(addr(a), buf); return bytes(buf)
27
+ except Exception:
28
+ return None
29
+
30
+ def decompile(func):
31
+ res = decomp.decompileFunction(func, 240, monitor)
32
+ if res.decompileCompleted():
33
+ return res.getDecompiledFunction().getC()
34
+ return '(decompile failed: %s)\n' % res.getErrorMessage()
35
+
36
+ out = open(os.path.join(OUT_DIR, 'REPORT.txt'), 'w')
37
+
38
+ # memory blocks
39
+ out.write('=== MEMORY BLOCKS ===\n')
40
+ for b in mem.getBlocks():
41
+ out.write(' %-20s %s - %s init=%s %s\n' %
42
+ (b.getName(), b.getStart(), b.getEnd(),
43
+ b.isInitialized(), 'rwx'[0] if b.isRead() else '-'))
44
+
45
+ # deobfuscators + network exchange + state getter
46
+ out.write('\n=== DEOBFUSCATORS / NETWORK / STATE ===\n')
47
+ for fp, label in [(0x100180E90, 'vt38_deobf_180E90'),
48
+ (0x100180D40, 'deobf_180D40'),
49
+ (0x1001C0650, 'vt20_net_exchange_1C0650'),
50
+ (0x1001C0A50, 'vt28_state_getter_1C0A50'),
51
+ (0x1001C0CF0, 'vt30_1C0CF0'),
52
+ (0x1004029b0, 'EC_decomp_rhs_4029b0')]:
53
+ f = fm.getFunctionAt(addr(fp)) or fm.getFunctionContaining(addr(fp))
54
+ if f is None:
55
+ out.write('%s: no fn\n' % label); continue
56
+ out.write('\n---- %s : %s @ %s ----\n' % (label, f.getName(True), f.getEntryPoint()))
57
+ out.write(decompile(f))
58
+
59
+ # hunt the obfuscated 33-byte source: scan __const for plausible region,
60
+ # and read candidate addresses
61
+ out.write('\n=== CANDIDATE OBFUSCATED-SECRET BYTES ===\n')
62
+ for a in [0x1007b0490, 0x1007b04a0, 0x1007af9c0, 0x1007c0240, 0x1007c0250]:
63
+ b = rd(a, 0x21)
64
+ out.write(' 0x%x : %s\n' % (a, b.hex() if b else '(unreadable)'))
65
+
66
+ # references to curve-constant addresses -> find the initializer
67
+ out.write('\n=== XREFS TO CURVE CONSTANTS ===\n')
68
+ for ca, label in [(0x100cf5bf0, 'prime_p'), (0x100cf5c10, 'order_n'),
69
+ (0x100cf5c30, 'generator_G'), (0x100cf8d11, 'decoded_secret')]:
70
+ out.write('\n-- %s @ 0x%x --\n' % (label, ca))
71
+ refs = rm.getReferencesTo(addr(ca))
72
+ cnt = 0
73
+ for r in refs:
74
+ cnt += 1
75
+ if cnt > 30:
76
+ out.write(' ... (more)\n'); break
77
+ fa = r.getFromAddress()
78
+ f = fm.getFunctionContaining(fa)
79
+ out.write(' %s %s from %s\n' %
80
+ (r.getReferenceType(), fa,
81
+ (f.getName(True) + ' @ ' + str(f.getEntryPoint())) if f else '(no fn)'))
82
+
83
+ out.close()
84
+ print('dump-secret: done -> %s' % OUT_DIR)
@@ -0,0 +1,63 @@
1
+ # Find XHL_XHardwareLibrary::setSoftware() and its caller — the caller passes
2
+ # the hardcoded software-code / software-name / software-KEY. That key is the
3
+ # HMAC-SHA256 key for the 0x48 auth handshake. Also decompile the hash init
4
+ # (FUN_100408420) to confirm the hash algorithm.
5
+ # @category Stick
6
+
7
+ import os, re
8
+ from ghidra.app.decompiler import DecompInterface
9
+ from ghidra.util.task import ConsoleTaskMonitor
10
+
11
+ OUT = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out-sw')
12
+ try: os.makedirs(OUT)
13
+ except OSError: pass
14
+
15
+ prog = currentProgram
16
+ fm = prog.getFunctionManager()
17
+ af = prog.getAddressFactory()
18
+ rm = prog.getReferenceManager()
19
+ mem = prog.getMemory()
20
+ monitor = ConsoleTaskMonitor()
21
+ decomp = DecompInterface(); decomp.openProgram(prog)
22
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
23
+
24
+ def decompile(f):
25
+ r = decomp.decompileFunction(f, 240, monitor)
26
+ return r.getDecompiledFunction().getC() if r.decompileCompleted() else '(fail)\n'
27
+
28
+ seen = set()
29
+ def dump(f, tag, callers=False):
30
+ if f is None or str(f.getEntryPoint()) in seen: return
31
+ seen.add(str(f.getEntryPoint()))
32
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:60]
33
+ with open(os.path.join(OUT, '%s_%s_%s.txt' % (tag, f.getEntryPoint(), safe)), 'w') as o:
34
+ o.write('==== %s @ %s (%d bytes) ====\n' %
35
+ (f.getName(True), f.getEntryPoint(), f.getBody().getNumAddresses()))
36
+ cs = sorted(set(f.getCalledFunctions(monitor)), key=lambda x: str(x.getEntryPoint()))
37
+ o.write('callees: ' + ', '.join(str(c.getEntryPoint()) for c in cs) + '\n\n')
38
+ o.write(decompile(f))
39
+ if callers:
40
+ for c in sorted(set(f.getCallingFunctions(monitor)), key=lambda x: str(x.getEntryPoint())):
41
+ dump(c, tag + '_caller', False)
42
+
43
+ summary = open(os.path.join(OUT, 'SUMMARY.txt'), 'w')
44
+
45
+ # 1. setSoftware — xref the __PRETTY_FUNCTION__ string at 0x10081a947
46
+ summary.write('=== setSoftware (xref 0x10081a947) ===\n')
47
+ for r in rm.getReferencesTo(addr(0x10081a947)):
48
+ f = fm.getFunctionContaining(r.getFromAddress())
49
+ if f:
50
+ summary.write(' setSoftware = %s @ %s\n' % (f.getName(True), f.getEntryPoint()))
51
+ dump(f, '1_setSoftware', callers=True)
52
+
53
+ # 2. the hash init/update/final used by the HMAC
54
+ summary.write('\n=== HMAC hash primitives ===\n')
55
+ for fp, t in [(0x100408420, '2_hash_init'), (0x10040aee0, '2_hash_update'),
56
+ (0x10040afb0, '2_hash_final'), (0x10040b290, '2_hash_oneshot')]:
57
+ f = fm.getFunctionAt(addr(fp))
58
+ if f: dump(f, t, False)
59
+
60
+ # 3. also dump bytes around the "software" cstring region for nearby key data
61
+ summary.write('\n=== done, %d fns ===\n' % len(seen))
62
+ summary.close()
63
+ print('dump-setsoftware: %d functions -> %s' % (len(seen), OUT))