@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,71 @@
1
+ # Decompile HWM's XHL_DasNetDevice::authenticate() and the LSAG_ALL handshake
2
+ # builder (LSAG_ALL is inline in code at 0x1001f233b). This is the 0x47/0x48
3
+ # TCP-auth handshake our send_dmx is failing (0x48 reply status 100 vs 0).
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-auth')
11
+ try: os.makedirs(OUT)
12
+ except OSError: pass
13
+
14
+ prog = currentProgram
15
+ fm = prog.getFunctionManager()
16
+ af = prog.getAddressFactory()
17
+ rm = prog.getReferenceManager()
18
+ monitor = ConsoleTaskMonitor()
19
+ decomp = DecompInterface(); decomp.openProgram(prog)
20
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
21
+
22
+ def decompile(f):
23
+ r = decomp.decompileFunction(f, 240, monitor)
24
+ return r.getDecompiledFunction().getC() if r.decompileCompleted() else '(decompile failed)\n'
25
+
26
+ seen = set()
27
+ summary = open(os.path.join(OUT, 'SUMMARY.txt'), 'w')
28
+
29
+ def dump(f, tag, with_callees=False):
30
+ if f is None: return
31
+ ep = str(f.getEntryPoint())
32
+ if ep in seen: return
33
+ seen.add(ep)
34
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:70]
35
+ callees = sorted(set(f.getCalledFunctions(monitor)),
36
+ key=lambda x: str(x.getEntryPoint()))
37
+ with open(os.path.join(OUT, '%s_%s_%s.txt' % (tag, ep, safe)), 'w') as o:
38
+ o.write('==== %s @ %s (%d bytes) ====\n' %
39
+ (f.getName(True), ep, f.getBody().getNumAddresses()))
40
+ o.write('callees:\n')
41
+ for c in callees:
42
+ o.write(' %s @ %s\n' % (c.getName(True), c.getEntryPoint()))
43
+ o.write('\ncallers:\n')
44
+ for c in sorted(set(f.getCallingFunctions(monitor)), key=lambda x: str(x.getEntryPoint())):
45
+ o.write(' %s @ %s\n' % (c.getName(True), c.getEntryPoint()))
46
+ o.write('\n' + decompile(f))
47
+ summary.write(' %s : %s @ %s (%d callees)\n' % (tag, f.getName(True), ep, len(callees)))
48
+ if with_callees:
49
+ for c in callees:
50
+ nm = c.getName(True)
51
+ if nm.startswith('_') or 'std::' in nm or 'operator' in nm: continue
52
+ dump(c, tag + '_callee', False)
53
+
54
+ # 1. the LSAG_ALL builder — function containing the inline "LSAG_ALL" bytes
55
+ f = fm.getFunctionContaining(addr(0x1001f233b))
56
+ summary.write('=== LSAG_ALL inline @ 0x1001f233b ===\n')
57
+ dump(f, '1_lsag_builder', with_callees=True)
58
+
59
+ # 2. XHL_DasNetDevice::authenticate() — xref the assert string at 0x1007ffeef
60
+ summary.write('\n=== authenticate() (string xref 0x1007ffeef) ===\n')
61
+ for r in rm.getReferencesTo(addr(0x1007ffeef)):
62
+ dump(fm.getFunctionContaining(r.getFromAddress()), '2_authenticate', with_callees=True)
63
+
64
+ # 3. "Configuring Capsens Security" string xref @ 0x1007b84b4
65
+ summary.write('\n=== Capsens Security (0x1007b84b4) ===\n')
66
+ for r in rm.getReferencesTo(addr(0x1007b84b4)):
67
+ dump(fm.getFunctionContaining(r.getFromAddress()), '3_capsens', with_callees=True)
68
+
69
+ summary.write('\n=== total: %d ===\n' % len(seen))
70
+ summary.close()
71
+ print('dump-authenticate: %d functions -> %s' % (len(seen), OUT))
@@ -0,0 +1,88 @@
1
+ # Dump the actual block cipher (AES?) under CBC and CFB modes.
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()), 'out16')
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
+
20
+
21
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
22
+
23
+
24
+ def dump(func, out):
25
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
26
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
27
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
28
+ for f in func.getCalledFunctions(monitor)))
29
+ out.write('\n-- callees (%d) --\n' % len(callees))
30
+ for c in callees: out.write(' ' + c + '\n')
31
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
32
+ for f in func.getCallingFunctions(monitor)))
33
+ out.write('\n-- callers (%d) --\n' % len(callers))
34
+ for c in callers: out.write(' ' + c + '\n')
35
+ out.write('\n-- decompilation --\n')
36
+ res = decomp.decompileFunction(func, 360, monitor)
37
+ if res.decompileCompleted():
38
+ out.write(res.getDecompiledFunction().getC())
39
+ else:
40
+ out.write('(decompilation failed: %s)\n' % res.getErrorMessage())
41
+ out.write('\n')
42
+
43
+
44
+ ROOTS = {
45
+ 'CFB_block_FUN_1003f4690': 0x1003f4690,
46
+ 'CBC_block_FUN_1003f5980': 0x1003f5980,
47
+ 'CFB_helper_FUN_1003f4660': 0x1003f4660,
48
+ 'preencrypt_FUN_100107a30': 0x100107a30,
49
+ }
50
+
51
+ seen = set()
52
+ summary = open(os.path.join(OUT_DIR, 'SUMMARY.txt'), 'w')
53
+
54
+ for label, fp in ROOTS.items():
55
+ f = fm.getFunctionAt(addr(fp)) or fm.getFunctionContaining(addr(fp))
56
+ if f is None:
57
+ summary.write(' %s 0x%x (no fn)\n' % (label, fp))
58
+ continue
59
+ summary.write(' %-30s = %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
+ # Walk into each block cipher to depth 1
71
+ for fp in (0x1003f4690, 0x1003f5980):
72
+ f = fm.getFunctionAt(addr(fp))
73
+ if f is None: continue
74
+ summary.write('\n=== depth-1 callees of 0x%x ===\n' % fp)
75
+ for c in sorted(f.getCalledFunctions(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
+ if c.getEntryPoint() in seen: continue
80
+ seen.add(c.getEntryPoint())
81
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', c.getName(True))[:120]
82
+ with open(os.path.join(OUT_DIR, 'depth1_0x%x_%s_%s.txt' %
83
+ (fp, c.getEntryPoint(), safe)), 'w') as o:
84
+ dump(c, o)
85
+
86
+ summary.write('\n=== total: %d ===\n' % len(seen))
87
+ summary.close()
88
+ print('dump-block-cipher: %d functions dumped to %s' % (len(seen), OUT_DIR))
@@ -0,0 +1,106 @@
1
+ # Dump the actual cipher function chain.
2
+ #
3
+ # From FUN_1001a9d50 (the "send DMX" virtual method):
4
+ # FUN_1001eeac0 = builds 576-byte AES frame (the ENCRYPT function)
5
+ # FUN_1001b0340 = XHL_UdpSocket::send (verify it routes to sendto)
6
+ # FUN_100333f40 = sequence-number bookkeeping (param_1 -> &seqnum_out)
7
+ # FUN_100333ae0 = pre-encrypt setup (maybe IV refresh)
8
+ #
9
+ # Also dump callers of FUN_1001eeac0 (the encrypt) and all its callees:
10
+ # AES tables and round logic live in there.
11
+
12
+ # @category Stick
13
+
14
+ import os, re
15
+ from ghidra.app.decompiler import DecompInterface
16
+ from ghidra.util.task import ConsoleTaskMonitor
17
+
18
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out7')
19
+ try: os.makedirs(OUT_DIR)
20
+ except OSError: pass
21
+
22
+ prog = currentProgram
23
+ fm = prog.getFunctionManager()
24
+ af = prog.getAddressFactory()
25
+ monitor = ConsoleTaskMonitor()
26
+ decomp = DecompInterface()
27
+ decomp.openProgram(prog)
28
+
29
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
30
+
31
+ def dump(func, out):
32
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
33
+ out.write('signature: %s\n' % func.getPrototypeString(True, True))
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
+ # Functions to dump (label -> address)
52
+ ROOTS = {
53
+ 'sendDmx_FUN_1001a9d50': 0x1001a9d50, # outer; for context
54
+ 'encrypt_FUN_1001eeac0': 0x1001eeac0, # THE ENCRYPT FUNCTION
55
+ 'udpsend_FUN_1001b0340': 0x1001b0340, # UDP send wrapper
56
+ 'pre_FUN_100333ae0': 0x100333ae0,
57
+ 'getseqno_FUN_100333f40': 0x100333f40,
58
+ 'getip_FUN_1001b1780': 0x1001b1780, # gets IP from socket?
59
+ 'unknown_FUN_1006a70a0': 0x1006a70a0, # ??? (mutex variant)
60
+ 'unknown_FUN_1006a6f70': 0x1006a6f70, # ??? (mutex variant)
61
+ 'unknown_FUN_10060b8e0': 0x10060b8e0, # mutex lock?
62
+ 'unknown_FUN_10060b980': 0x10060b980, # mutex unlock?
63
+ }
64
+
65
+ # Also: recursively descend into encrypt to depth 1 — its callees are the
66
+ # round logic / sbox lookups.
67
+ ENCRYPT_ADDR = 0x1001eeac0
68
+
69
+ seen = set()
70
+ summary = open(os.path.join(OUT_DIR, 'SUMMARY.txt'), 'w')
71
+ summary.write('Cipher dump — chain rooted at FUN_1001a9d50\n\n')
72
+
73
+ for label, fp in ROOTS.items():
74
+ f = fm.getFunctionAt(addr(fp)) or fm.getFunctionContaining(addr(fp))
75
+ if f is None:
76
+ summary.write(' %s 0x%x (no function)\n' % (label, fp))
77
+ continue
78
+ summary.write(' %s = %s @ %s (size %d, callees=%d)\n' %
79
+ (label, f.getName(True), f.getEntryPoint(),
80
+ f.getBody().getNumAddresses(),
81
+ len(set(f.getCalledFunctions(monitor)))))
82
+ if f.getEntryPoint() in seen: continue
83
+ seen.add(f.getEntryPoint())
84
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
85
+ with open(os.path.join(OUT_DIR, '%s_%s_%s.txt' %
86
+ (label, f.getEntryPoint(), safe)), 'w') as o:
87
+ dump(f, o)
88
+
89
+ # Descend into encrypt fn's callees (depth 1)
90
+ enc = fm.getFunctionAt(addr(ENCRYPT_ADDR))
91
+ if enc:
92
+ summary.write('\n=== encrypt fn callees (depth 1) ===\n')
93
+ for c in sorted(enc.getCalledFunctions(monitor), key=lambda f: f.getEntryPoint()):
94
+ summary.write(' callee: %s @ %s (size %d)\n' %
95
+ (c.getName(True), c.getEntryPoint(),
96
+ c.getBody().getNumAddresses()))
97
+ if c.getEntryPoint() in seen: continue
98
+ seen.add(c.getEntryPoint())
99
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', c.getName(True))[:120]
100
+ with open(os.path.join(OUT_DIR, 'enccallee_%s_%s.txt' %
101
+ (c.getEntryPoint(), safe)), 'w') as o:
102
+ dump(c, o)
103
+
104
+ summary.write('\n=== total dumped: %d ===\n' % len(seen))
105
+ summary.close()
106
+ print('dump-cipher: %d functions dumped to %s' % (len(seen), OUT_DIR))
@@ -0,0 +1,107 @@
1
+ # Walk the constructor chain to find where the 8-word cipher state is
2
+ # initialized. Top-level: FUN_100178f10 (Stick3CryptDmxUniverse::ctor)
3
+ # delegates the heavy lifting to FUN_1001a9a00 (base-class ctor).
4
+ #
5
+ # Targets:
6
+ # FUN_1001a9a00 base constructor (sets up sub-objects incl. cipher)
7
+ # FUN_100178bb0 caller of Stick3 ctor (one level UP — who creates these?)
8
+ # FUN_100105770 AesOStream constructor
9
+ # the s_XHL_Stick3ANet... string is at 0x1007ff054 — dump bytes too
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()), 'out10')
18
+ try: os.makedirs(OUT_DIR)
19
+ except OSError: pass
20
+
21
+ prog = currentProgram
22
+ fm = prog.getFunctionManager()
23
+ af = prog.getAddressFactory()
24
+ mem = prog.getMemory()
25
+ monitor = ConsoleTaskMonitor()
26
+ decomp = DecompInterface()
27
+ decomp.openProgram(prog)
28
+
29
+
30
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
31
+
32
+
33
+ def dump(func, out):
34
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
35
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
36
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
37
+ for f in func.getCalledFunctions(monitor)))
38
+ out.write('\n-- callees (%d) --\n' % len(callees))
39
+ for c in callees: out.write(' ' + c + '\n')
40
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
41
+ for f in func.getCallingFunctions(monitor)))
42
+ out.write('\n-- callers (%d) --\n' % len(callers))
43
+ for c in callers: out.write(' ' + c + '\n')
44
+ out.write('\n-- decompilation --\n')
45
+ res = decomp.decompileFunction(func, 240, monitor)
46
+ if res.decompileCompleted():
47
+ out.write(res.getDecompiledFunction().getC())
48
+ else:
49
+ out.write('(decompilation failed: %s)\n' % res.getErrorMessage())
50
+ out.write('\n')
51
+
52
+
53
+ ROOTS = {
54
+ 'base_ctor_FUN_1001a9a00': 0x1001a9a00,
55
+ 'caller_of_Stick3_ctor_100178bb0': 0x100178bb0,
56
+ 'AesOStream_ctor_100105770': 0x100105770,
57
+ }
58
+
59
+ seen = set()
60
+ summary = open(os.path.join(OUT_DIR, 'SUMMARY.txt'), 'w')
61
+
62
+ # Read and print the source-tag string at 0x1007ff054
63
+ summary.write('=== Source-tag string at 0x1007ff054 ===\n')
64
+ try:
65
+ sb = []
66
+ a = addr(0x1007ff054)
67
+ for i in range(120):
68
+ b = mem.getByte(a.add(i)) & 0xff
69
+ if b == 0: break
70
+ sb.append(chr(b))
71
+ summary.write(' ' + repr(''.join(sb)) + '\n\n')
72
+ except Exception as e:
73
+ summary.write(' (read failed: %s)\n' % e)
74
+
75
+ for label, fp in ROOTS.items():
76
+ f = fm.getFunctionAt(addr(fp)) or fm.getFunctionContaining(addr(fp))
77
+ if f is None:
78
+ summary.write(' %s 0x%x (no fn)\n' % (label, fp))
79
+ continue
80
+ summary.write(' %s = %s @ %s size=%d\n' %
81
+ (label, f.getName(True), f.getEntryPoint(),
82
+ f.getBody().getNumAddresses()))
83
+ if f.getEntryPoint() in seen: continue
84
+ seen.add(f.getEntryPoint())
85
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
86
+ with open(os.path.join(OUT_DIR, '%s_%s_%s.txt' %
87
+ (label, f.getEntryPoint(), safe)), 'w') as o:
88
+ dump(f, o)
89
+
90
+ # Also descend one hop into base ctor
91
+ base_ctor = fm.getFunctionAt(addr(0x1001a9a00))
92
+ if base_ctor:
93
+ summary.write('\n=== base ctor depth-1 callees ===\n')
94
+ for c in sorted(base_ctor.getCalledFunctions(monitor), key=lambda f: f.getEntryPoint()):
95
+ summary.write(' callee: %s @ %s (size %d)\n' %
96
+ (c.getName(True), c.getEntryPoint(),
97
+ c.getBody().getNumAddresses()))
98
+ if c.getEntryPoint() in seen: continue
99
+ seen.add(c.getEntryPoint())
100
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', c.getName(True))[:120]
101
+ with open(os.path.join(OUT_DIR, 'basector_d1_%s_%s.txt' %
102
+ (c.getEntryPoint(), safe)), 'w') as o:
103
+ dump(c, o)
104
+
105
+ summary.write('\n=== total dumped: %d ===\n' % len(seen))
106
+ summary.close()
107
+ print('dump-ctor-chain: %d functions dumped to %s' % (len(seen), OUT_DIR))
@@ -0,0 +1,112 @@
1
+ # Dump the EC curve constants + remaining KDF primitives.
2
+ # KDF (FUN_100107650): key = X(priv*staticPt) XOR X(priv*stickPub)
3
+ # priv generated by FUN_1004032b0 (random mod n, n at DAT_100cf5c10)
4
+ # scalarmult = FUN_1003ff3c0, prime p at DAT_100cf5bf0, gen G at DAT_100cf5c30
5
+ # @category Stick
6
+
7
+ import os, re, struct
8
+ from ghidra.app.decompiler import DecompInterface
9
+ from ghidra.util.task import ConsoleTaskMonitor
10
+
11
+ OUT_DIR = os.path.join(os.path.dirname(getSourceFile().getAbsolutePath()), 'out-curve')
12
+ try: os.makedirs(OUT_DIR)
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()
21
+ decomp.openProgram(prog)
22
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
23
+
24
+ def rd(a, n):
25
+ buf = bytearray(n)
26
+ try:
27
+ mem.getBytes(addr(a), buf)
28
+ except Exception as e:
29
+ return None
30
+ return bytes(buf)
31
+
32
+ def hexdump(label, a, n, out):
33
+ b = rd(a, n)
34
+ out.write('\n== %s @ 0x%x (%d bytes) ==\n' % (label, a, n))
35
+ if b is None:
36
+ out.write(' (unreadable)\n'); return
37
+ for i in range(0, n, 16):
38
+ row = b[i:i+16]
39
+ hx = ' '.join('%02x' % x for x in row)
40
+ out.write(' +%03x %-47s\n' % (i, hx))
41
+ # also as little-endian 256-bit ints, 32B at a time
42
+ for i in range(0, n - 31, 32):
43
+ words = struct.unpack('<4Q', b[i:i+32])
44
+ be = (words[3] << 192) | (words[2] << 128) | (words[1] << 64) | words[0]
45
+ out.write(' [%03x] LE-as-256bit = 0x%064x\n' % (i, be))
46
+
47
+ def decompile(func):
48
+ res = decomp.decompileFunction(func, 240, monitor)
49
+ if res.decompileCompleted():
50
+ return res.getDecompiledFunction().getC()
51
+ return '(decompile failed: %s)\n' % res.getErrorMessage()
52
+
53
+ def dump_fn(fp, label, out):
54
+ f = fm.getFunctionAt(addr(fp)) or fm.getFunctionContaining(addr(fp))
55
+ if f is None:
56
+ out.write('\n==== %s 0x%x : NO FUNCTION ====\n' % (label, fp)); return
57
+ out.write('\n==== %s : %s @ %s (%d bytes) ====\n' %
58
+ (label, f.getName(True), f.getEntryPoint(),
59
+ f.getBody().getNumAddresses()))
60
+ callees = sorted(set('%s@%s' % (c.getName(True), c.getEntryPoint())
61
+ for c in f.getCalledFunctions(monitor)))
62
+ out.write('callees: %s\n' % ', '.join(callees))
63
+ out.write(decompile(f))
64
+
65
+ # ---- 1. constants ----
66
+ const = open(os.path.join(OUT_DIR, 'CONSTANTS.txt'), 'w')
67
+ # wide region covering prime, order, generator, a/b coefficients
68
+ hexdump('region_cf5b00', 0x100cf5b00, 0x300, const)
69
+ hexdump('DAT_100cf5bf0_prime?', 0x100cf5bf0, 0x20, const)
70
+ hexdump('DAT_100cf5c10_order_lo', 0x100cf5c10, 0x20, const)
71
+ hexdump('DAT_100cf5c30_generator',0x100cf5c30, 0x40, const)
72
+ hexdump('decoded_secret_cf8d11', 0x100cf8d11, 0x21, const)
73
+ hexdump('obfuscated_src_7b0490', 0x1007b0490, 0x21, const)
74
+ const.close()
75
+
76
+ # ---- 2. EC primitives + static-point builder ----
77
+ prim = open(os.path.join(OUT_DIR, 'PRIMITIVES.txt'), 'w')
78
+ for fp, label in [
79
+ (0x100402560, 'EC_op_402560'),
80
+ (0x100404fc0, 'EC_op_404fc0'),
81
+ (0x1003ffc40, 'EC_pt_add_3ffc40'),
82
+ (0x100400fd0, 'EC_pt_dbl_400fd0'),
83
+ (0x1004061d0, 'EC_reduce_4061d0'),
84
+ (0x100401c50, 'EC_op_401c50'),
85
+ (0x100402ab0, 'static_point_builder_402ab0'),
86
+ (0x1003f4530, 'cfb_key_install_3f4530'),
87
+ ]:
88
+ dump_fn(fp, label, prim)
89
+ prim.close()
90
+
91
+ # ---- 3. cipher vtable slots (vptr value 0x10095F838) ----
92
+ vt = open(os.path.join(OUT_DIR, 'VTABLE.txt'), 'w')
93
+ VTBL = 0x10095F838
94
+ vt.write('cipher secondary vtable @ 0x%x\n' % VTBL)
95
+ slots = {}
96
+ for off in range(0, 0x60, 8):
97
+ b = rd(VTBL + off, 8)
98
+ if b is None:
99
+ vt.write(' +0x%02x : (unreadable)\n' % off); continue
100
+ ptr = struct.unpack('<Q', b)[0]
101
+ slots[off] = ptr
102
+ f = fm.getFunctionAt(addr(ptr)) if ptr else None
103
+ vt.write(' +0x%02x : 0x%x %s\n' % (off, ptr, f.getName(True) if f else ''))
104
+ for off, label in [(0x20, 'vt20_net_exchange'),
105
+ (0x28, 'vt28_state_getter'),
106
+ (0x38, 'vt38_static_getter')]:
107
+ ptr = slots.get(off)
108
+ if ptr:
109
+ dump_fn(ptr, label, vt)
110
+ vt.close()
111
+
112
+ print('dump-curve: done -> %s' % OUT_DIR)
@@ -0,0 +1,146 @@
1
+ # Walk one more level up the constructor chain — find the device-level
2
+ # factory that calls FUN_100179050 (caller of the Stick3CryptDmxUniverse
3
+ # factory FUN_100178bb0) and dump constructors that touch offset +0x1618
4
+ # (where the cipher object lives).
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()), 'out11')
13
+ try: os.makedirs(OUT_DIR)
14
+ except OSError: pass
15
+
16
+ prog = currentProgram
17
+ fm = prog.getFunctionManager()
18
+ af = prog.getAddressFactory()
19
+ mem = prog.getMemory()
20
+ ref_mgr = prog.getReferenceManager()
21
+ monitor = ConsoleTaskMonitor()
22
+ decomp = DecompInterface()
23
+ decomp.openProgram(prog)
24
+
25
+
26
+ def addr(a): return af.getDefaultAddressSpace().getAddress(a)
27
+
28
+
29
+ def dump(func, out):
30
+ out.write('==== %s @ %s ====\n' % (func.getName(True), func.getEntryPoint()))
31
+ out.write('size: %d body bytes\n' % func.getBody().getNumAddresses())
32
+ callees = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
33
+ for f in func.getCalledFunctions(monitor)))
34
+ out.write('\n-- callees (%d) --\n' % len(callees))
35
+ for c in callees: out.write(' ' + c + '\n')
36
+ callers = sorted(set('%s @ %s' % (f.getName(True), f.getEntryPoint())
37
+ for f in func.getCallingFunctions(monitor)))
38
+ out.write('\n-- callers (%d) --\n' % len(callers))
39
+ for c in callers: out.write(' ' + c + '\n')
40
+ out.write('\n-- decompilation --\n')
41
+ res = decomp.decompileFunction(func, 240, monitor)
42
+ if res.decompileCompleted():
43
+ out.write(res.getDecompiledFunction().getC())
44
+ else:
45
+ out.write('(decompilation failed: %s)\n' % res.getErrorMessage())
46
+ out.write('\n')
47
+
48
+
49
+ # Read C string at addr
50
+ def cstr(a, maxlen=200):
51
+ out = []
52
+ for i in range(maxlen):
53
+ try:
54
+ b = mem.getByte(a.add(i)) & 0xff
55
+ except Exception:
56
+ break
57
+ if b == 0: break
58
+ out.append(chr(b))
59
+ return ''.join(out)
60
+
61
+
62
+ # All 26 vtables containing FUN_100108200 at slot vt[3] (= site = vt_base+0x18)
63
+ # So: vt_base = site - 0x18 = vptr - 0x00 (vptr is at vt_base+8 conceptually,
64
+ # but the way we labelled in fixup output, vptr = vt_base+8... let me recompute.
65
+ #
66
+ # Convention: vtable struct layout:
67
+ # site(typeinfo_ptr) @ X ← typeinfo rebase row
68
+ # vt[0] @ X + 0x08
69
+ # vt[1] @ X + 0x10
70
+ # vt[2] @ X + 0x18
71
+ # vt[3] @ X + 0x20
72
+ #
73
+ # So a row "rebase FUN_100108200" at addr Y means Y stores the encrypt fn.
74
+ # If vt[3], then Y = X + 0x20, so X (typeinfo_ptr addr) = Y - 0x20.
75
+ # Slot 0 (vptr) is at X + 0x08 = Y - 0x18.
76
+
77
+ # This was wrong before; the relationship is:
78
+ # if Y = vtable_struct_base + 0x20 (slot 3), and vptr is at +0x08,
79
+ # then vptr = Y - 0x18
80
+
81
+ ENCRYPT_SLOT_ADDRS = [
82
+ 0x100851F50, 0x100A1F168, 0x100A239D0, 0x100A23A70, 0x100B289E0,
83
+ 0x100B2EDC8, 0x100B2EE68, 0x100B35D48, 0x100B3C0F8, 0x100B3C198,
84
+ 0x100B95940, 0x100B959E0, 0x100B9D7A8, 0x100BA25A8, 0x100BA2648,
85
+ 0x100BA9008, 0x100BB0E18, 0x100BB0EB8, 0x100BB7E00, 0x100BBCBC8,
86
+ 0x100BBCC68, 0x100BC1920, 0x100BC7D48, 0x100BC7DE8, 0x100C14170,
87
+ 0x100C14260,
88
+ ]
89
+
90
+ summary = open(os.path.join(OUT_DIR, 'SUMMARY.txt'), 'w')
91
+ seen = set()
92
+
93
+ # For each, read the typeinfo_ptr (at slot_addr - 0x20), then the name string
94
+ # referenced from that typeinfo struct (at typeinfo_addr + 0x08).
95
+ # Since chained fixups aren't materialized, we have to find the typeinfo via
96
+ # Ghidra refs. Try: getReferencesFrom(typeinfo_ptr_field).
97
+ summary.write('=== 26 vtables sharing FUN_100108200 as vt[3] ===\n')
98
+ for slot3 in ENCRYPT_SLOT_ADDRS:
99
+ typeinfo_field = slot3 - 0x20
100
+ vptr = slot3 - 0x18
101
+ summary.write('\n slot[3] @ 0x%x vptr=0x%x typeinfo_ptr_field=0x%x\n' %
102
+ (slot3, vptr, typeinfo_field))
103
+ # Look at outgoing refs from the typeinfo_field — Ghidra applies refs
104
+ # from chained-fixup rebases for SOME of these.
105
+ refs = list(ref_mgr.getReferencesFrom(addr(typeinfo_field)))
106
+ for r in refs:
107
+ ta = r.getToAddress()
108
+ summary.write(' -> %s\n' % ta)
109
+ # Then refs FROM typeinfo addr's name-ptr field (+8) give the name string
110
+ name_ptr_field = ta.add(8)
111
+ for r2 in ref_mgr.getReferencesFrom(name_ptr_field):
112
+ nstr = cstr(r2.getToAddress(), 200)
113
+ summary.write(' name @ %s : %r\n' % (r2.getToAddress(), nstr))
114
+ # Code refs to vptr = the constructor that uses this vtable
115
+ code_callers = set()
116
+ for r in ref_mgr.getReferencesTo(addr(vptr)):
117
+ f = fm.getFunctionContaining(r.getFromAddress())
118
+ if f is not None:
119
+ code_callers.add(f.getEntryPoint())
120
+ summary.write(' code refs to vptr from %d fn(s):\n' % len(code_callers))
121
+ for ep in sorted(code_callers):
122
+ f = fm.getFunctionAt(ep)
123
+ summary.write(' %s @ %s (size %d)\n' %
124
+ (f.getName(True), f.getEntryPoint(),
125
+ f.getBody().getNumAddresses()))
126
+ if ep in seen: continue
127
+ seen.add(ep)
128
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
129
+ with open(os.path.join(OUT_DIR, 'ctor_vt0x%x_%s_%s.txt' %
130
+ (vptr, f.getEntryPoint(), safe)), 'w') as o:
131
+ dump(f, o)
132
+
133
+ # Also: dump FUN_100179050 (caller of Stick3 factory)
134
+ for fp in [0x100179050]:
135
+ f = fm.getFunctionAt(addr(fp))
136
+ if f is None: continue
137
+ if f.getEntryPoint() in seen: continue
138
+ seen.add(f.getEntryPoint())
139
+ safe = re.sub(r'[^A-Za-z0-9._-]', '_', f.getName(True))[:120]
140
+ with open(os.path.join(OUT_DIR, 'caller_%s_%s.txt' %
141
+ (f.getEntryPoint(), safe)), 'w') as o:
142
+ dump(f, o)
143
+
144
+ summary.write('\n=== total dumped: %d ===\n' % len(seen))
145
+ summary.close()
146
+ print('dump-encompassing: %d functions dumped to %s' % (len(seen), OUT_DIR))