@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,140 @@
1
+ # Dump the call chain around FUN_1001bbca0 (the 0x011c reply handler):
2
+ # - its 2 callers (FUN_1001bbc50, FUN_1001bc350) - see how param_3=0x00ec is set
3
+ # and what they DO with the 236-byte plaintext
4
+ # - FUN_1003f5740 (AES key install), FUN_1003f68e0 (AES-CBC decrypt)
5
+ # - FUN_1001ee4d0 (build_msg used here), FUN_1001f2c80 (register pending reply)
6
+ # - find the Stick3 device's vtable[+0xe8] function (the AES key getter)
7
+ #
8
+ # Also: dump the cross-refs to vt[+0xe8] = slot at offset 0xe8 (i.e. slot 29)
9
+ # for the Stick3 device class vtables.
10
+ #
11
+ # @category Stick
12
+
13
+ import os, re
14
+ from ghidra.app.decompiler import DecompInterface
15
+ from ghidra.util.task import ConsoleTaskMonitor
16
+
17
+ OUT = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out-011c-callers')
18
+ try: os.makedirs(OUT)
19
+ except OSError: pass
20
+
21
+ prog = currentProgram
22
+ fm = prog.getFunctionManager()
23
+ af = prog.getAddressFactory()
24
+ mem = prog.getMemory()
25
+ listing = prog.getListing()
26
+ monitor = ConsoleTaskMonitor()
27
+ decomp = DecompInterface(); decomp.openProgram(prog)
28
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
29
+
30
+ def decompile(f):
31
+ r = decomp.decompileFunction(f, 240, monitor)
32
+ return r.getDecompiledFunction().getC() if r.decompileCompleted() else '(decompile failed)\n'
33
+
34
+ def dump_fn(ep, label):
35
+ a = addr(ep)
36
+ f = fm.getFunctionAt(a) or fm.getFunctionContaining(a)
37
+ if not f:
38
+ with open(os.path.join(OUT, '%s_%x.txt' % (label, ep)), 'w') as o:
39
+ o.write('NO FUNCTION at 0x%x\n' % ep)
40
+ return None
41
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:80]
42
+ fname = os.path.join(OUT, '%s_%s_%s.txt' % (label, f.getEntryPoint(), safe))
43
+ with open(fname, 'w') as o:
44
+ o.write('==== %s @ %s (%d bytes) ====\n' %
45
+ (f.getName(True), f.getEntryPoint(), f.getBody().getNumAddresses()))
46
+ callees = sorted(set('%s @ %s' % (c.getName(True), c.getEntryPoint())
47
+ for c in f.getCalledFunctions(monitor)))
48
+ callers = sorted(set('%s @ %s' % (c.getName(True), c.getEntryPoint())
49
+ for c in f.getCallingFunctions(monitor)))
50
+ o.write('-- callees (%d) --\n ' % len(callees) + '\n '.join(callees) + '\n')
51
+ o.write('\n-- callers (%d) --\n ' % len(callers) + '\n '.join(callers) + '\n\n')
52
+ o.write('-- decompilation --\n')
53
+ o.write(decompile(f))
54
+ return f
55
+
56
+ TARGETS = [
57
+ (0x1001bbc50, 'caller1'), # caller of FUN_1001bbca0
58
+ (0x1001bc350, 'caller2'), # caller of FUN_1001bbca0
59
+ (0x1001bbca0, 'handler_011c'), # FUN_1001bbca0 itself, for reference
60
+ (0x1003f5740, 'aes_key_install'),
61
+ (0x1003f68e0, 'aes_cbc_decrypt'),
62
+ (0x1003f4530, 'aes_cfb_key_install'),
63
+ (0x1001ee4d0, 'build_msg'),
64
+ (0x1001f2c80, 'register_pending'),
65
+ (0x100195b20, 'get_reply_buf'),
66
+ (0x100195ec0, 'wait_reply'),
67
+ (0x1001b0df0, 'get_socket'),
68
+ ]
69
+
70
+ print('dumping handler-related functions ...')
71
+ for ep, label in TARGETS:
72
+ dump_fn(ep, label)
73
+
74
+ # Hunt the vtable+0xe8 entry. We look at the existing Stick3-class typeinfo
75
+ # pointers (from memory note) and try to identify the primary vtable.
76
+ # Stick3ANet typeinfo @ 0x100970160 -- vtable for Stick3ANet is at
77
+ # typeinfo - sizeof(some Itanium ABI overhead). Easier: find every place
78
+ # in code where ".vt[0xe8]" is loaded then called for that device, and
79
+ # dump those callees.
80
+ print('finding vtable[+0xe8] load sites ...')
81
+ ec_callsites = []
82
+ instr_iter = listing.getInstructions(True)
83
+ while instr_iter.hasNext():
84
+ ins = instr_iter.next()
85
+ # Pattern: "mov rax, [r?+0xe8]" followed shortly by "call rax".
86
+ # In Ghidra, easiest is to look at the mnemonic + scalar.
87
+ if ins.getMnemonicString() not in ('MOV', 'CALL', 'JMP'):
88
+ continue
89
+ for i in range(ins.getNumOperands()):
90
+ for o in ins.getOpObjects(i):
91
+ try:
92
+ v = o.getValue() if hasattr(o, 'getValue') else None
93
+ except Exception:
94
+ v = None
95
+ if v == 0xe8:
96
+ ec_callsites.append((str(ins.getAddress()), ins.toString()))
97
+ print('e8 displacement sites: %d' % len(ec_callsites))
98
+
99
+ with open(os.path.join(OUT, 'vt_e8_sites.txt'), 'w') as o:
100
+ o.write('=== instructions with 0xe8 as a scalar operand ===\n')
101
+ for a, t in ec_callsites:
102
+ f = fm.getFunctionContaining(addr(int(a, 16)))
103
+ fn = f.getName(True) if f else '?'
104
+ o.write(' %s %s in %s\n' % (a, t, fn))
105
+
106
+ # Dump FUN_1001bbca0's caller of vt[+0xe8] specifically.
107
+ # Use the bytes pattern: search the binary for instruction sequences
108
+ # that look like "callq [reg+0xe8]" -- in x86-64 that's FF 90 e8 00 00 00
109
+ # or FF 50 + sign-extended -- "FF 90 E8 00 00 00".
110
+ print('searching for byte pattern FF 90 E8 00 00 00 (call [reg+0xe8]) ...')
111
+ pattern = bytes([0xff, 0x90, 0xe8, 0x00, 0x00, 0x00])
112
+ mem_blocks = [b for b in mem.getBlocks() if b.isInitialized() and b.isExecute()]
113
+ hits = []
114
+ for block in mem_blocks:
115
+ start = block.getStart().getOffset()
116
+ end = block.getEnd().getOffset()
117
+ size = end - start + 1
118
+ if size > 64*1024*1024: continue
119
+ buf = bytearray(size)
120
+ try:
121
+ block.getBytes(block.getStart(), buf)
122
+ except Exception:
123
+ continue
124
+ i = 0
125
+ while True:
126
+ idx = buf.find(pattern, i)
127
+ if idx == -1: break
128
+ hits.append(start + idx)
129
+ i = idx + 1
130
+ print('found %d call-via-[reg+0xe8] sites' % len(hits))
131
+
132
+ with open(os.path.join(OUT, 'vt_e8_calls.txt'), 'w') as o:
133
+ o.write('=== call [reg+0xe8] sites (FF 90 E8 00 00 00) ===\n')
134
+ for h in hits:
135
+ a = addr(h)
136
+ f = fm.getFunctionContaining(a)
137
+ fn = f.getName(True) if f else '?'
138
+ o.write(' 0x%x in %s\n' % (h, fn))
139
+
140
+ print('dump-011c-callers: done -> %s' % OUT)
@@ -0,0 +1,158 @@
1
+ # Find HWM's opcode-0x011c receive-handler + the 0x011 (go-live) precondition
2
+ # check.
3
+ #
4
+ # Strategy: scan every instruction for immediate operands matching the
5
+ # interesting constants and group by containing function:
6
+ # * 0x011c (= 284) -- the opcode we send + the reply length, almost
7
+ # certainly appears in HWM as a switch-case or `cmp r, 0x11c`.
8
+ # * 0x00ec (= 236) -- a 2-byte marker inside the 0x011c reply's
9
+ # 258-byte payload at relative offset +0x12. A parser that examines
10
+ # the payload will compare against this constant.
11
+ # * 0x011 (= 17) -- the go-live opcode. Very common immediate, so we
12
+ # ONLY take hits whose containing function ALSO references 0x011c or
13
+ # a Stick3 magic string, to filter false positives.
14
+ #
15
+ # Output goes to out-011c/.
16
+ #
17
+ # @category Stick
18
+
19
+ import os, re
20
+ from ghidra.app.decompiler import DecompInterface
21
+ from ghidra.util.task import ConsoleTaskMonitor
22
+
23
+ OUT = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out-011c')
24
+ try: os.makedirs(OUT)
25
+ except OSError: pass
26
+
27
+ prog = currentProgram
28
+ fm = prog.getFunctionManager()
29
+ af = prog.getAddressFactory()
30
+ mem = prog.getMemory()
31
+ listing = prog.getListing()
32
+ rm = prog.getReferenceManager()
33
+ monitor = ConsoleTaskMonitor()
34
+ decomp = DecompInterface(); decomp.openProgram(prog)
35
+
36
+ def decompile(f):
37
+ r = decomp.decompileFunction(f, 240, monitor)
38
+ return r.getDecompiledFunction().getC() if r.decompileCompleted() else '(decompile failed)\n'
39
+
40
+ def safe_name(f):
41
+ return re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:80]
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # 1. Walk every instruction once; collect operand scalars we care about.
45
+ # ---------------------------------------------------------------------------
46
+ TARGETS = {0x011c, 0x00ec, 0x0011, 0x0010, 0x000f}
47
+ hits = {v: {} for v in TARGETS} # value -> { func_entry -> [addrs] }
48
+
49
+ print('scanning instructions ...')
50
+ total = 0
51
+ instr_iter = listing.getInstructions(True)
52
+ while instr_iter.hasNext():
53
+ ins = instr_iter.next()
54
+ total += 1
55
+ for i in range(ins.getNumOperands()):
56
+ objs = ins.getOpObjects(i)
57
+ for o in objs:
58
+ try:
59
+ v = o.getValue() if hasattr(o, 'getValue') else None
60
+ except Exception:
61
+ v = None
62
+ if v in TARGETS:
63
+ f = fm.getFunctionContaining(ins.getAddress())
64
+ if f is None: continue
65
+ ep = str(f.getEntryPoint())
66
+ hits[v].setdefault(ep, []).append(str(ins.getAddress()))
67
+ print('scanned %d instructions' % total)
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # 2. Identify "interesting" functions: any function that references 0x011c
71
+ # OR 0x00ec is a prime candidate (these constants are rare). 0x10/0x11/0xf
72
+ # are kept only if the SAME function also references 0x011c or 0x00ec.
73
+ # ---------------------------------------------------------------------------
74
+ prime = set(hits[0x011c].keys()) | set(hits[0x00ec].keys())
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # 3. Also xref the Stick magic strings -- so we capture the network-receive
78
+ # dispatch even if it doesn't use 0x011c as a literal.
79
+ # ---------------------------------------------------------------------------
80
+ magic_xrefs = set()
81
+ needles = ['Stick_3A', 'LSAG_ALL', 'Stick_U1', 'Siudi_7B']
82
+ for d in listing.getDefinedData(True):
83
+ try:
84
+ if not d.hasStringValue(): continue
85
+ s = str(d.getValue())
86
+ except Exception:
87
+ continue
88
+ if s not in needles: continue
89
+ for r in rm.getReferencesTo(d.getAddress()):
90
+ f = fm.getFunctionContaining(r.getFromAddress())
91
+ if f: magic_xrefs.add(str(f.getEntryPoint()))
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # 4. Dump.
95
+ # ---------------------------------------------------------------------------
96
+ summary = open(os.path.join(OUT, 'SUMMARY.txt'), 'w')
97
+ summary.write('=== immediate-operand hit counts ===\n')
98
+ for v in sorted(TARGETS):
99
+ summary.write(' 0x%04x : %d functions, %d sites\n' %
100
+ (v, len(hits[v]), sum(len(x) for x in hits[v].values())))
101
+ summary.write(' magic-string xref funcs: %d\n' % len(magic_xrefs))
102
+ summary.write('\n=== functions referencing 0x011c ===\n')
103
+ for ep in sorted(hits[0x011c].keys()):
104
+ f = fm.getFunctionAt(af.getDefaultAddressSpace().getAddress(int(ep, 16)))
105
+ if not f: continue
106
+ summary.write(' %s %s sites=%d size=%d also_0xec=%s magic_xref=%s\n' % (
107
+ ep, f.getName(True), len(hits[0x011c][ep]),
108
+ f.getBody().getNumAddresses(),
109
+ 'Y' if ep in hits[0x00ec] else 'n',
110
+ 'Y' if ep in magic_xrefs else 'n'))
111
+
112
+ summary.write('\n=== functions referencing 0x00ec ===\n')
113
+ for ep in sorted(hits[0x00ec].keys()):
114
+ f = fm.getFunctionAt(af.getDefaultAddressSpace().getAddress(int(ep, 16)))
115
+ if not f: continue
116
+ summary.write(' %s %s sites=%d size=%d also_0x11c=%s magic_xref=%s\n' % (
117
+ ep, f.getName(True), len(hits[0x00ec][ep]),
118
+ f.getBody().getNumAddresses(),
119
+ 'Y' if ep in hits[0x011c] else 'n',
120
+ 'Y' if ep in magic_xrefs else 'n'))
121
+
122
+ # Anchor list: every prime func, plus magic-xref funcs that also touch 0x11
123
+ # (those are the network-dispatch loops).
124
+ dump_set = set(prime)
125
+ for ep in magic_xrefs:
126
+ if ep in hits[0x0011] or ep in hits[0x0010] or ep in hits[0x000f]:
127
+ dump_set.add(ep)
128
+
129
+ summary.write('\n=== dumping %d candidate functions ===\n' % len(dump_set))
130
+ for ep in sorted(dump_set):
131
+ addr = af.getDefaultAddressSpace().getAddress(int(ep, 16))
132
+ f = fm.getFunctionAt(addr) or fm.getFunctionContaining(addr)
133
+ if not f: continue
134
+ name = safe_name(f)
135
+ sites_11c = hits[0x011c].get(ep, [])
136
+ sites_ec = hits[0x00ec].get(ep, [])
137
+ fname = os.path.join(OUT, 'fn_%s_%s.txt' % (ep, name))
138
+ with open(fname, 'w') as o:
139
+ o.write('==== %s @ %s (%d bytes) ====\n' %
140
+ (f.getName(True), f.getEntryPoint(), f.getBody().getNumAddresses()))
141
+ o.write('sites of 0x011c : %s\n' % ', '.join(sites_11c))
142
+ o.write('sites of 0x00ec : %s\n' % ', '.join(sites_ec))
143
+ o.write('sites of 0x0011 : %s\n' % ', '.join(hits[0x0011].get(ep, [])))
144
+ o.write('sites of 0x0010 : %s\n' % ', '.join(hits[0x0010].get(ep, [])))
145
+ o.write('sites of 0x000f : %s\n' % ', '.join(hits[0x000f].get(ep, [])))
146
+ o.write('magic-string xref: %s\n' % ('YES' if ep in magic_xrefs else 'no'))
147
+ callees = sorted(set('%s @ %s' % (c.getName(True), c.getEntryPoint())
148
+ for c in f.getCalledFunctions(monitor)))
149
+ callers = sorted(set('%s @ %s' % (c.getName(True), c.getEntryPoint())
150
+ for c in f.getCallingFunctions(monitor)))
151
+ o.write('\n-- callees (%d) --\n ' % len(callees) + '\n '.join(callees) + '\n')
152
+ o.write('\n-- callers (%d) --\n ' % len(callers) + '\n '.join(callers) + '\n\n')
153
+ o.write('-- decompilation --\n')
154
+ o.write(decompile(f))
155
+ summary.write(' dumped %s -> fn_%s_%s.txt\n' % (f.getName(True), ep, name))
156
+
157
+ summary.close()
158
+ print('dump-011c-handler: %d functions -> %s' % (len(dump_set), OUT))
@@ -0,0 +1,99 @@
1
+ # Dump all candidate AES encrypt method addresses + walk one hop deep into
2
+ # the most-promising primary candidate (DasEccStreamCryptography vt[3] @
3
+ # 0x100108200) so we see the actual AES round logic / S-box.
4
+ #
5
+ # Vtables under inspection:
6
+ # AesOStream primary @ vptr 0x100851B20: vt[0]=0x100105900, vt[1]=0x1001059B0
7
+ # AesOStream secondary @ vptr 0x100851B48: vt[0]=0x100105950, vt[1]=0x100105A10
8
+ # DasEccStreamCryptography primary @ vptr 0x100851F38:
9
+ # vt[0]=0x100107BA0, vt[1]=0x100107BD0, vt[2]=0x100106290, vt[3]=0x100108200
10
+
11
+ # @category Stick
12
+
13
+ import os, re
14
+ from ghidra.app.decompiler import DecompInterface
15
+ from ghidra.util.task import ConsoleTaskMonitor
16
+
17
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out8')
18
+ try: os.makedirs(OUT_DIR)
19
+ except OSError: pass
20
+
21
+ prog = currentProgram
22
+ fm = prog.getFunctionManager()
23
+ af = prog.getAddressFactory()
24
+ monitor = ConsoleTaskMonitor()
25
+ decomp = DecompInterface()
26
+ decomp.openProgram(prog)
27
+
28
+
29
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
30
+
31
+
32
+ def dump(func, out):
33
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
34
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
35
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
36
+ for f in func.getCalledFunctions(monitor)))
37
+ out.write('\n-- callees (%d) --\n' % len(callees))
38
+ for c in callees: out.write(' ' + c + '\n')
39
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
40
+ for f in func.getCallingFunctions(monitor)))
41
+ out.write('\n-- callers (%d) --\n' % len(callers))
42
+ for c in callers: out.write(' ' + c + '\n')
43
+ out.write('\n-- decompilation --\n')
44
+ res = decomp.decompileFunction(func, 240, monitor)
45
+ if res.decompileCompleted():
46
+ out.write(res.getDecompiledFunction().getC())
47
+ else:
48
+ out.write('(decompilation failed: %s)\n' % res.getErrorMessage())
49
+ out.write('\n')
50
+
51
+
52
+ CANDIDATES = {
53
+ 'AesOStream_primary_vt0': 0x100105900,
54
+ 'AesOStream_primary_vt1': 0x1001059B0,
55
+ 'AesOStream_secondary_vt0': 0x100105950,
56
+ 'AesOStream_secondary_vt1': 0x100105A10,
57
+ 'DasEccStreamCrypto_vt0': 0x100107BA0,
58
+ 'DasEccStreamCrypto_vt1': 0x100107BD0,
59
+ 'DasEccStreamCrypto_vt2': 0x100106290,
60
+ 'DasEccStreamCrypto_vt3_ENC?': 0x100108200,
61
+ }
62
+
63
+ seen = set()
64
+ summary = open(os.path.join(OUT_DIR, 'SUMMARY.txt'), 'w')
65
+
66
+ for label, fp in CANDIDATES.items():
67
+ f = fm.getFunctionAt(addr(fp)) or fm.getFunctionContaining(addr(fp))
68
+ if f is None:
69
+ summary.write(' %s 0x%x (no fn)\n' % (label, fp))
70
+ continue
71
+ summary.write(' %-32s = %s @ %s size=%d callees=%d\n' %
72
+ (label, f.getName(True), f.getEntryPoint(),
73
+ f.getBody().getNumAddresses(),
74
+ len(set(f.getCalledFunctions(monitor)))))
75
+ if f.getEntryPoint() in seen: continue
76
+ seen.add(f.getEntryPoint())
77
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
78
+ with open(os.path.join(OUT_DIR, '%s_%s_%s.txt' %
79
+ (label, f.getEntryPoint(), safe)), 'w') as o:
80
+ dump(f, o)
81
+
82
+ # Walk one deeper into the candidate encrypt (DasEccStreamCrypto vt[3])
83
+ candidate_enc = fm.getFunctionAt(addr(0x100108200))
84
+ if candidate_enc:
85
+ summary.write('\n=== depth-1 callees of DasEccStreamCrypto vt[3] (candidate ENCRYPT) ===\n')
86
+ for c in sorted(candidate_enc.getCalledFunctions(monitor), key=lambda f: f.getEntryPoint()):
87
+ summary.write(' callee: %s @ %s (size %d)\n' %
88
+ (c.getName(True), c.getEntryPoint(),
89
+ c.getBody().getNumAddresses()))
90
+ if c.getEntryPoint() in seen: continue
91
+ seen.add(c.getEntryPoint())
92
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', c.getName(True))[:120]
93
+ with open(os.path.join(OUT_DIR, 'depth1_%s_%s.txt' %
94
+ (c.getEntryPoint(), safe)), 'w') as o:
95
+ dump(c, o)
96
+
97
+ summary.write('\n=== total: %d ===\n' % len(seen))
98
+ summary.close()
99
+ print('dump-aes-candidates: %d functions dumped to %s' % (len(seen), OUT_DIR))
@@ -0,0 +1,192 @@
1
+ # Ghidra headless post-script — dump the named anchor functions in the
2
+ # 2024-03-21 ESA2 HardwareManager so we can locate the real DMX cipher.
3
+ #
4
+ # This is Jython 2.7 (Ghidra script API). Run via analyzeHeadless:
5
+ #
6
+ # $(brew --prefix)/share/ghidra/support/analyzeHeadless \
7
+ # tools/ghidra HwmESA2 \
8
+ # -import /Applications/ESA2/HardwareManager/HardwareManager.app/Contents/MacOS/HardwareManager \
9
+ # -scriptPath tools/ghidra \
10
+ # -postScript dump-anchors.py
11
+ #
12
+ # Output lands in tools/ghidra/out/.
13
+ #
14
+ # Anchors (from stick-aes-symbolized-breakthrough memory):
15
+ # * DmxUniversePage::onTimerSendReceive @ 0x100074090 (NOT live path)
16
+ # * DmxWidget::onTimerSendAndReceive (live path; posts QtConcurrent task)
17
+ # * aes_encrypt_key128 / aes_encrypt / fcrypt_* — Gladman (NOT DMX cipher)
18
+ # * CryptDmxUniverse / AesOStream / AesIStream — RTTI-only (no method names)
19
+ # * DasEccAesCryptography / DasDhRsaCryptography / DasEccStreamCryptography
20
+ #
21
+ # Goal: for each named function, write its decompilation + callers/callees so
22
+ # we can walk: DmxWidget::onTimerSendAndReceive -> QtConcurrent::run target
23
+ # -> custom AES encrypt -> sendto(2). Also note xrefs into the RTTI strings
24
+ # (CryptDmxUniverse etc.) — those reach the vtables; vtable entries are the
25
+ # method addresses even when symbol names are absent.
26
+
27
+ # @category Stick
28
+
29
+ import os, re
30
+
31
+ from ghidra.app.decompiler import DecompInterface
32
+ from ghidra.util.task import ConsoleTaskMonitor
33
+
34
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out')
35
+ try:
36
+ os.makedirs(OUT_DIR)
37
+ except OSError:
38
+ pass
39
+
40
+ # Symbols we want decompiled (substring match, case-sensitive).
41
+ WANTED_SUBSTRINGS = [
42
+ # Live DMX path
43
+ 'onTimerSendAndReceive',
44
+ 'onTimerSendReceive',
45
+ 'sendDmx',
46
+ 'receiveDmx',
47
+ # Qt async dispatch
48
+ 'QtConcurrent',
49
+ # Known-not-DMX Gladman (sanity check + showfile/cloud crypto)
50
+ 'aes_encrypt_key128',
51
+ 'aes_encrypt_key',
52
+ 'aes_encrypt',
53
+ 'aes_decrypt',
54
+ 'fcrypt_init',
55
+ 'fcrypt_encrypt',
56
+ 'fcrypt_decrypt',
57
+ # RTTI-anchored classes (likely only as data; we still report addresses)
58
+ 'CryptDmxUniverse',
59
+ 'AesOStream',
60
+ 'AesIStream',
61
+ 'DasEccAesCryptography',
62
+ 'DasDhRsaCryptography',
63
+ 'DasEccStreamCryptography',
64
+ 'DasCryptography',
65
+ # libc sinks worth seeing callers of
66
+ 'sendto',
67
+ ]
68
+
69
+ # Known explicit addresses (file vmaddr) to dump if symbol lookup misses them.
70
+ EXPLICIT_ADDRS = [
71
+ 0x100074090, # DmxUniversePage::onTimerSendReceive (per memory)
72
+ 0x1006d2150, # aes_encrypt_key128 (per memory)
73
+ 0x1006d2d30, # aes_encrypt_key
74
+ 0x1006d0600, # aes_encrypt
75
+ 0x1006d1380, # aes_decrypt
76
+ 0x1006d4990, # aes_init
77
+ 0x1006d49f0, # fcrypt_init
78
+ 0x1006d4b10, # fcrypt_encrypt
79
+ 0x1006d4c10, # fcrypt_decrypt
80
+ ]
81
+
82
+ prog = currentProgram
83
+ fm = prog.getFunctionManager()
84
+ st = prog.getSymbolTable()
85
+ af = prog.getAddressFactory()
86
+ listing = prog.getListing()
87
+ monitor = ConsoleTaskMonitor()
88
+
89
+ decomp = DecompInterface()
90
+ decomp.openProgram(prog)
91
+
92
+
93
+ def addr(a):
94
+ return af.getDefaultAddressSpace().getAddress(a)
95
+
96
+
97
+ def dump_function(func, out):
98
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
99
+ out.write('signature: %s\n' % func.getPrototypeString(True, True))
100
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
101
+
102
+ # Callees
103
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
104
+ for f in func.getCalledFunctions(monitor)))
105
+ out.write('\n-- callees (%d) --\n' % len(callees))
106
+ for c in callees:
107
+ out.write(' ' + c + '\n')
108
+
109
+ # Callers
110
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
111
+ for f in func.getCallingFunctions(monitor)))
112
+ out.write('\n-- callers (%d) --\n' % len(callers))
113
+ for c in callers:
114
+ out.write(' ' + c + '\n')
115
+
116
+ # Decompilation
117
+ out.write('\n-- decompilation --\n')
118
+ res = decomp.decompileFunction(func, 120, monitor)
119
+ if res.decompileCompleted():
120
+ out.write(res.getDecompiledFunction().getC())
121
+ else:
122
+ out.write('(decompilation failed: %s)\n' % res.getErrorMessage())
123
+ out.write('\n')
124
+
125
+
126
+ def find_funcs_by_substring(subs):
127
+ hits = {}
128
+ # Walk all defined functions
129
+ for f in fm.getFunctions(True):
130
+ name = f.getName(True) # includes namespace
131
+ for s in subs:
132
+ if s in name:
133
+ hits.setdefault(s, []).append(f)
134
+ # Also walk symbol table for non-function symbols (RTTI strings etc.)
135
+ extra = []
136
+ for sym in st.getAllSymbols(False):
137
+ name = sym.getName(True)
138
+ for s in subs:
139
+ if s in name and not fm.getFunctionAt(sym.getAddress()):
140
+ extra.append((s, sym))
141
+ return hits, extra
142
+
143
+
144
+ hits, extras = find_funcs_by_substring(WANTED_SUBSTRINGS)
145
+
146
+ summary_path = os.path.join(OUT_DIR, 'SUMMARY.txt')
147
+ with open(summary_path, 'w') as summary:
148
+ summary.write('Ghidra anchor dump for ESA2 HardwareManager (2024-03-21)\n')
149
+ summary.write('Binary: %s\n\n' % prog.getExecutablePath())
150
+
151
+ summary.write('=== Function hits by substring ===\n')
152
+ for s in WANTED_SUBSTRINGS:
153
+ funcs = hits.get(s, [])
154
+ summary.write('%-32s %d function(s)\n' % (s, len(funcs)))
155
+ for f in funcs:
156
+ summary.write(' %s @ %s\n' % (f.getName(True), f.getEntryPoint()))
157
+ summary.write('\n=== Non-function symbols (RTTI, data, etc.) ===\n')
158
+ for s, sym in extras:
159
+ summary.write('%-32s %s @ %s\n' % (s, sym.getName(True), sym.getAddress()))
160
+
161
+ # Dump each matched function to its own file
162
+ seen = set()
163
+ for s, funcs in hits.items():
164
+ for f in funcs:
165
+ ep = f.getEntryPoint()
166
+ if ep in seen:
167
+ continue
168
+ seen.add(ep)
169
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
170
+ path = os.path.join(OUT_DIR, '%s_%s.txt' % (ep, safe))
171
+ with open(path, 'w') as out:
172
+ dump_function(f, out)
173
+
174
+ # Also dump the explicit addresses (in case symbol lookup missed any)
175
+ for raw in EXPLICIT_ADDRS:
176
+ a = addr(raw)
177
+ f = fm.getFunctionAt(a)
178
+ if f is None:
179
+ f = fm.getFunctionContaining(a)
180
+ if f is None:
181
+ continue
182
+ ep = f.getEntryPoint()
183
+ if ep in seen:
184
+ continue
185
+ seen.add(ep)
186
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
187
+ path = os.path.join(OUT_DIR, '%s_%s.txt' % (ep, safe))
188
+ with open(path, 'w') as out:
189
+ dump_function(f, out)
190
+
191
+ print('dump-anchors: %d functions dumped to %s' % (len(seen), OUT_DIR))
192
+ print('dump-anchors: summary at %s' % summary_path)
@@ -0,0 +1,50 @@
1
+ # Decompile the 0x48 body builder (FUN_1001ee530), the 0x47 body builder
2
+ # (FUN_1001ee730), the packet wrapper (FUN_1001e3890), and authenticate()'s
3
+ # caller (FUN_10019e4d0) — to see exactly what the 0x48 auth message contains.
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-auth2')
11
+ try: os.makedirs(OUT)
12
+ except OSError: pass
13
+
14
+ prog = currentProgram
15
+ fm = prog.getFunctionManager()
16
+ af = prog.getAddressFactory()
17
+ monitor = ConsoleTaskMonitor()
18
+ decomp = DecompInterface(); decomp.openProgram(prog)
19
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
20
+
21
+ def decompile(f):
22
+ r = decomp.decompileFunction(f, 240, monitor)
23
+ return r.getDecompiledFunction().getC() if r.decompileCompleted() else '(fail)\n'
24
+
25
+ seen = set()
26
+ def dump(fp, label, depth=0):
27
+ f = fm.getFunctionAt(addr(fp)) or fm.getFunctionContaining(addr(fp))
28
+ if f is None or str(f.getEntryPoint()) in seen: return
29
+ seen.add(str(f.getEntryPoint()))
30
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:60]
31
+ callees = sorted(set(f.getCalledFunctions(monitor)), key=lambda x: str(x.getEntryPoint()))
32
+ with open(os.path.join(OUT, '%s_%s.txt' % (label, f.getEntryPoint())), 'w') as o:
33
+ o.write('==== %s @ %s (%d bytes) ====\n' %
34
+ (f.getName(True), f.getEntryPoint(), f.getBody().getNumAddresses()))
35
+ o.write('callees: ' + ', '.join('%s' % c.getEntryPoint() for c in callees) + '\n\n')
36
+ o.write(decompile(f))
37
+ # one level of non-libc callees for the body builders
38
+ if depth > 0:
39
+ for c in callees:
40
+ nm = c.getName(True)
41
+ if nm.startswith('_') or 'std::' in nm or 'operator' in nm: continue
42
+ dump(int(str(c.getEntryPoint()), 16), label + '_c', depth - 1)
43
+
44
+ for fp, label in [(0x1001ee530, 'build_0x48'),
45
+ (0x1001ee730, 'build_0x47'),
46
+ (0x1001e3890, 'packet_wrap'),
47
+ (0x10019e4d0, 'auth_caller')]:
48
+ dump(fp, label, depth=1)
49
+
50
+ print('dump-auth2: %d functions -> %s' % (len(seen), OUT))