@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,341 @@
1
+ """
2
+ Path A — passive lldb memory scan for the Stick3 DMX AES key.
3
+
4
+ NO breakpoints. lldb attach stops the process; this script does a fast
5
+ in-memory scan and detaches. Common case = a few seconds stopped.
6
+
7
+ What it does:
8
+ 1. Resolves the cipher vtable runtime address (file addr 0x10095F838 + slide).
9
+ 2. Scans every readable+writable region for that 8-byte LE pointer value.
10
+ Each hit is the start of a cipher subobject (XHL_Stick3ANet device + 0x1618).
11
+ 3. For each hit: dumps 0x800 bytes, reads the documented candidate fields,
12
+ and runs an AES-128 key-schedule auto-detector over the whole object.
13
+ 4. Writes a log + raw object dumps + any recovered keys to /tmp.
14
+
15
+ If STICK_HEAP_DUMP=1 is set in the environment, it also dumps every rw region
16
+ to /tmp/stick-heap/ with a manifest, so tools/scan-aes-schedule.mjs can do a
17
+ heap-wide schedule search offline (fallback if step 3 finds nothing).
18
+
19
+ Usage (driven by tools/pathA-scan.sh):
20
+ lldb -p <PID> --batch \
21
+ -o "command script import tools/lldb_pathA_scan.py" \
22
+ -o "detach" -o "quit"
23
+ """
24
+
25
+ import os
26
+ import struct
27
+ import time
28
+
29
+ # lldb is only present when run inside the debugger; guard the import so the
30
+ # pure-Python detector functions below stay unit-testable standalone.
31
+ try:
32
+ import lldb
33
+ except ImportError:
34
+ lldb = None
35
+
36
+ # --- known addresses (ESA2 2024-03-21 build, image vmaddrs) ---------------
37
+ CIPHER_VPTR_FILEADDR = 0x10095F838 # value stored at cipher_obj+0x00
38
+ CIPHER_OBJ_DUMP_LEN = 0x800 # how much of each cipher object to grab
39
+
40
+ # documented offsets within the cipher object (cipher_obj = device+0x1618)
41
+ OFF_MODE = 0x20 # 1 byte: 0=CBC, 1=CFB
42
+ OFF_KEY = 0x48 # candidate raw AES-128 key (16 bytes)
43
+ OFF_CBC_IV = 0x60 # candidate CBC chaining IV (16 bytes)
44
+ OFF_CFB_IV = 0xB0 # candidate CFB IV (16 bytes)
45
+
46
+ LOG_PATH = "/tmp/stick-pathA-scan.log"
47
+ HEAP_DIR = "/tmp/stick-heap"
48
+
49
+ # --- AES-128 key schedule detector ---------------------------------------
50
+ SBOX = bytes.fromhex(
51
+ "637c777bf26b6fc53001672bfed7ab76ca82c97dfa5947f0add4a2af9ca472c0"
52
+ "b7fd9326363ff7cc34a5e5f171d8311504c723c31896059a071280e2eb27b275"
53
+ "09832c1a1b6e5aa0523bd6b329e32f8453d100ed20fcb15b6acbbe394a4c58cf"
54
+ "d0efaafb434d338545f9027f503c9fa851a3408f929d38f5bcb6da2110fff3d2"
55
+ "cd0c13ec5f974417c4a77e3d645d197360814fdc222a908846eeb814de5e0bdb"
56
+ "e0323a0a4906245cc2d3ac629195e479e7c8376d8dd54ea96c56f4ea657aae08"
57
+ "ba78252e1ca6b4c6e8dd741f4bbd8b8a703eb5664803f60e613557b986c11d9e"
58
+ "e1f8981169d98e949b1e87e9ce5528df8ca1890dbfe6426841992d0fb054bb16"
59
+ )
60
+ RCON = (0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36)
61
+
62
+
63
+ def aes128_schedule_check(blob):
64
+ """If blob[0:176] is a valid AES-128 forward key schedule, return the
65
+ 16-byte master key; else None."""
66
+ if len(blob) < 176:
67
+ return None
68
+ w = [blob[4 * i:4 * i + 4] for i in range(44)]
69
+ for i in range(4, 44):
70
+ t = w[i - 1]
71
+ if i % 4 == 0:
72
+ # RotWord -> SubWord -> xor Rcon
73
+ t = bytes((SBOX[t[1]], SBOX[t[2]], SBOX[t[3]], SBOX[t[0]]))
74
+ t = bytes((t[0] ^ RCON[i // 4 - 1], t[1], t[2], t[3]))
75
+ exp = bytes((w[i - 4][k] ^ t[k] for k in range(4)))
76
+ if exp != w[i]:
77
+ return None
78
+ return blob[0:16]
79
+
80
+
81
+ def find_schedules(blob, base_label):
82
+ """Slide a 176-byte window over blob; return list of (offset, key)."""
83
+ hits = []
84
+ limit = len(blob) - 176
85
+ o = 0
86
+ while o <= limit:
87
+ key = aes128_schedule_check(blob[o:o + 176])
88
+ if key is not None:
89
+ hits.append((o, key))
90
+ o += 1
91
+ return hits
92
+
93
+
94
+ # --- helpers --------------------------------------------------------------
95
+ _logf = None
96
+
97
+
98
+ def log(msg=""):
99
+ global _logf
100
+ print(msg)
101
+ if _logf:
102
+ _logf.write(msg + "\n")
103
+ _logf.flush()
104
+
105
+
106
+ def hexsp(b):
107
+ return " ".join("%02x" % x for x in b)
108
+
109
+
110
+ def read_mem(process, addr, size):
111
+ err = lldb.SBError()
112
+ data = process.ReadMemory(addr, size, err)
113
+ if err.Success() and data:
114
+ return data
115
+ return None
116
+
117
+
118
+ def find_main_module(target):
119
+ for m in target.module_iter():
120
+ fs = m.GetFileSpec()
121
+ name = fs.GetFilename()
122
+ if name == "HardwareManager":
123
+ return m
124
+ # fall back to module 0 (the main executable)
125
+ return target.GetModuleAtIndex(0) if target.GetNumModules() else None
126
+
127
+
128
+ def rw_regions(process):
129
+ """Yield (base, end) for readable+writable memory regions."""
130
+ regions = process.GetMemoryRegions()
131
+ for i in range(regions.GetSize()):
132
+ info = lldb.SBMemoryRegionInfo()
133
+ if not regions.GetMemoryRegionAtIndex(i, info):
134
+ continue
135
+ if info.IsReadable() and info.IsWritable():
136
+ yield info.GetRegionBase(), info.GetRegionEnd()
137
+
138
+
139
+ def scan_for_pattern(process, pattern):
140
+ """Return list of runtime addresses where the 8-byte pattern occurs in
141
+ rw memory."""
142
+ hits = []
143
+ CHUNK = 8 * 1024 * 1024
144
+ plen = len(pattern)
145
+ for base, end in rw_regions(process):
146
+ size = end - base
147
+ if size <= 0 or size > 4 * 1024 * 1024 * 1024:
148
+ continue
149
+ addr = base
150
+ carry = b""
151
+ while addr < end:
152
+ n = min(CHUNK, end - addr)
153
+ data = read_mem(process, addr, n)
154
+ if data is None:
155
+ addr += n
156
+ carry = b""
157
+ continue
158
+ # buf[0] sits at memory address (addr - len(carry))
159
+ buf_base = addr - len(carry)
160
+ buf = carry + data
161
+ start = 0
162
+ while True:
163
+ idx = buf.find(pattern, start)
164
+ if idx < 0:
165
+ break
166
+ hits.append(buf_base + idx)
167
+ start = idx + 1
168
+ carry = buf[-(plen - 1):] if plen > 1 else b""
169
+ addr += n
170
+ return hits
171
+
172
+
173
+ def dump_heap(process):
174
+ """Dump every rw region to HEAP_DIR with a manifest (fallback path)."""
175
+ try:
176
+ os.makedirs(HEAP_DIR, exist_ok=True)
177
+ except Exception as e:
178
+ log(" heap dump: mkdir failed: %s" % e)
179
+ return
180
+ manifest = open(os.path.join(HEAP_DIR, "manifest.txt"), "w")
181
+ total = 0
182
+ idx = 0
183
+ CHUNK = 16 * 1024 * 1024
184
+ for base, end in rw_regions(process):
185
+ size = end - base
186
+ if size <= 0 or size > 2 * 1024 * 1024 * 1024:
187
+ continue
188
+ path = os.path.join(HEAP_DIR, "region-%03d-0x%x.bin" % (idx, base))
189
+ f = open(path, "wb")
190
+ got = 0
191
+ addr = base
192
+ while addr < end:
193
+ n = min(CHUNK, end - addr)
194
+ data = read_mem(process, addr, n)
195
+ if data is None:
196
+ break
197
+ f.write(data)
198
+ got += len(data)
199
+ addr += len(data)
200
+ f.close()
201
+ manifest.write("0x%x %d %s\n" % (base, got, os.path.basename(path)))
202
+ total += got
203
+ idx += 1
204
+ manifest.close()
205
+ log(" heap dump: %d regions, %.1f MiB -> %s" %
206
+ (idx, total / 1048576.0, HEAP_DIR))
207
+
208
+
209
+ def inspect_cipher_obj(process, addr, n):
210
+ """Dump + analyse one cipher object found at runtime address `addr`."""
211
+ log("")
212
+ log("=" * 70)
213
+ log("CIPHER OBJECT @ 0x%x" % addr)
214
+ log("=" * 70)
215
+
216
+ blob = read_mem(process, addr, CIPHER_OBJ_DUMP_LEN)
217
+ if blob is None:
218
+ log(" !! could not read object memory")
219
+ return []
220
+
221
+ # raw dump of the first 0x200 bytes (16 bytes/line)
222
+ log(" raw bytes 0x000..0x200:")
223
+ for off in range(0, 0x200, 16):
224
+ log(" +0x%03x %s" % (off, hexsp(blob[off:off + 16])))
225
+
226
+ # documented candidate fields
227
+ mode = blob[OFF_MODE]
228
+ log("")
229
+ log(" documented candidate fields:")
230
+ log(" +0x%02x mode flag = 0x%02x (%s)" %
231
+ (OFF_MODE, mode, "CFB" if mode == 1 else "CBC" if mode == 0 else "??"))
232
+ log(" +0x%02x key(16) = %s" % (OFF_KEY, hexsp(blob[OFF_KEY:OFF_KEY + 16])))
233
+ log(" +0x%02x CBC IV(16)= %s" % (OFF_CBC_IV, hexsp(blob[OFF_CBC_IV:OFF_CBC_IV + 16])))
234
+ log(" +0x%02x CFB IV(16)= %s" % (OFF_CFB_IV, hexsp(blob[OFF_CFB_IV:OFF_CFB_IV + 16])))
235
+
236
+ # AES-128 key schedule auto-detect over the whole object
237
+ scheds = find_schedules(blob, "obj@0x%x" % addr)
238
+ log("")
239
+ if scheds:
240
+ log(" *** AES-128 KEY SCHEDULE DETECTED (%d) ***" % len(scheds))
241
+ for off, key in scheds:
242
+ log(" +0x%03x master key = %s" % (off, hexsp(key)))
243
+ else:
244
+ log(" no valid AES-128 key schedule in this object")
245
+ log(" (impl may store only the 16-byte raw key — see +0x%02x above," % OFF_KEY)
246
+ log(" or expand the schedule on the fly per frame)")
247
+ return scheds
248
+
249
+
250
+ def __lldb_init_module(debugger, internal_dict):
251
+ global _logf
252
+ _logf = open(LOG_PATH, "w")
253
+
254
+ target = debugger.GetSelectedTarget()
255
+ if not target or not target.IsValid():
256
+ log("ERROR: no target — attach with `lldb -p <PID>` first")
257
+ return
258
+ process = target.GetProcess()
259
+ if not process or not process.IsValid():
260
+ log("ERROR: no live process")
261
+ return
262
+
263
+ log("=== Path A passive scan %s ===" % time.strftime("%Y-%m-%d %H:%M:%S"))
264
+ log("pid=%d state=%s" % (process.GetProcessID(), process.GetState()))
265
+
266
+ mod = find_main_module(target)
267
+ if not mod:
268
+ log("ERROR: HardwareManager module not found")
269
+ return
270
+
271
+ # resolve cipher vptr file address -> runtime address (handles ASLR slide)
272
+ sb = mod.ResolveFileAddress(CIPHER_VPTR_FILEADDR)
273
+ vptr_runtime = sb.GetLoadAddress(target)
274
+ if vptr_runtime == lldb.LLDB_INVALID_ADDRESS:
275
+ log("ERROR: could not resolve cipher vptr 0x%x to a runtime address"
276
+ % CIPHER_VPTR_FILEADDR)
277
+ log(" module=%s" % mod.GetFileSpec())
278
+ return
279
+ slide = vptr_runtime - CIPHER_VPTR_FILEADDR
280
+ log("module : %s" % mod.GetFileSpec())
281
+ log("ASLR slide : 0x%x" % slide)
282
+ log("cipher vptr : fileaddr 0x%x -> runtime 0x%x"
283
+ % (CIPHER_VPTR_FILEADDR, vptr_runtime))
284
+
285
+ pattern = struct.pack("<Q", vptr_runtime)
286
+ log("scan pattern : %s" % hexsp(pattern))
287
+ log("")
288
+ log("scanning rw memory for the cipher vtable pointer ...")
289
+
290
+ t0 = time.time()
291
+ hits = scan_for_pattern(process, pattern)
292
+ log("scan done in %.1fs — %d hit(s)" % (time.time() - t0, len(hits)))
293
+
294
+ all_keys = []
295
+ for i, addr in enumerate(hits):
296
+ scheds = inspect_cipher_obj(process, addr, CIPHER_OBJ_DUMP_LEN)
297
+ # save the raw object dump
298
+ blob = read_mem(process, addr, CIPHER_OBJ_DUMP_LEN)
299
+ if blob:
300
+ objpath = "/tmp/stick-pathA-obj-%d-0x%x.bin" % (i, addr)
301
+ with open(objpath, "wb") as f:
302
+ f.write(blob)
303
+ log(" raw object saved -> %s" % objpath)
304
+ for off, key in scheds:
305
+ all_keys.append((addr, off, key))
306
+
307
+ # save any recovered keys
308
+ log("")
309
+ log("=" * 70)
310
+ if all_keys:
311
+ log("RECOVERED %d AES-128 KEY(S):" % len(all_keys))
312
+ seen = set()
313
+ n = 0
314
+ for addr, off, key in all_keys:
315
+ hexk = key.hex()
316
+ if hexk in seen:
317
+ continue
318
+ seen.add(hexk)
319
+ kp = "/tmp/stick-pathA-key-%d.bin" % n
320
+ with open(kp, "wb") as f:
321
+ f.write(key)
322
+ log(" key %d: %s (obj 0x%x +0x%x) -> %s"
323
+ % (n, hexsp(key), addr, off, kp))
324
+ n += 1
325
+ log("")
326
+ log("NEXT: capture a 576B frame and verify, e.g.:")
327
+ log(" node tools/try-key.mjs <frame.bin> $(xxd -p /tmp/stick-pathA-key-0.bin)")
328
+ else:
329
+ log("No AES-128 key schedule auto-detected.")
330
+ log("Inspect the +0x48 raw-key candidates in the per-object dumps above,")
331
+ log("or set STICK_HEAP_DUMP=1 and re-run for a heap-wide offline scan.")
332
+
333
+ if os.environ.get("STICK_HEAP_DUMP") == "1":
334
+ log("")
335
+ log("STICK_HEAP_DUMP=1 — dumping rw regions for offline scan ...")
336
+ dump_heap(process)
337
+ log(" then run: node tools/scan-aes-schedule.mjs %s" % HEAP_DIR)
338
+
339
+ log("")
340
+ log("scan complete — detaching. log: %s" % LOG_PATH)
341
+ _logf.close()
@@ -0,0 +1,300 @@
1
+ """
2
+ Path C-lite — runtime key-trace via one-shot lldb breakpoints.
3
+
4
+ When the passive scan can't find the key (it isn't a byte-contiguous window
5
+ of the cipher object), the key/round-schedule is either expanded transiently
6
+ on the stack per frame, or stored somewhere a register points to during
7
+ encryption. This catches it: breakpoint the encrypt path, and the instant it
8
+ fires, dump every register + the memory it points at + the stack, then run
9
+ the AES key-schedule detector over all of it.
10
+
11
+ NOT a persistent breakpoint. It fires, captures once, deletes all
12
+ breakpoints, and the batch detaches — HWM is stopped only ~1-2s total
13
+ (same as the passive scan).
14
+
15
+ Breakpoints (ESA2 2024-03-21 file addrs, from the Ghidra RE):
16
+ 0x100107950 encrypt entry (mode dispatch) -> capture device, keep going
17
+ 0x1003f5980 AES block, CBC path -> capture stack+regs, stop
18
+ 0x1003f4690 AES block, CFB path -> capture stack+regs, stop
19
+ If no block bp fires within 6 frames, capture at the entry and stop anyway,
20
+ so the batch always terminates cleanly (no HWM kill on hang).
21
+
22
+ Usage (driven by tools/pathC-keytrace.sh):
23
+ lldb -p <PID> --batch \
24
+ -o "command script import tools/lldb_pathC_keytrace.py" \
25
+ -o "continue" -o "detach" -o "quit"
26
+ """
27
+
28
+ import os
29
+ import time
30
+
31
+ try:
32
+ import lldb
33
+ except ImportError:
34
+ lldb = None
35
+
36
+ # --- breakpoint file addresses (image vmaddrs) ----------------------------
37
+ FA_ENTRY = 0x100107950
38
+ FA_CBC_BLOCK = 0x1003F5980
39
+ FA_CFB_BLOCK = 0x1003F4690
40
+ CIPHER_VPTR_FILEADDR = 0x10095F838
41
+
42
+ LOG_PATH = "/tmp/stick-pathC-keytrace.log"
43
+
44
+ # --- AES key-schedule detectors (AES-128 and AES-256) --------------------
45
+ SBOX = bytes.fromhex(
46
+ "637c777bf26b6fc53001672bfed7ab76ca82c97dfa5947f0add4a2af9ca472c0"
47
+ "b7fd9326363ff7cc34a5e5f171d8311504c723c31896059a071280e2eb27b275"
48
+ "09832c1a1b6e5aa0523bd6b329e32f8453d100ed20fcb15b6acbbe394a4c58cf"
49
+ "d0efaafb434d338545f9027f503c9fa851a3408f929d38f5bcb6da2110fff3d2"
50
+ "cd0c13ec5f974417c4a77e3d645d197360814fdc222a908846eeb814de5e0bdb"
51
+ "e0323a0a4906245cc2d3ac629195e479e7c8376d8dd54ea96c56f4ea657aae08"
52
+ "ba78252e1ca6b4c6e8dd741f4bbd8b8a703eb5664803f60e613557b986c11d9e"
53
+ "e1f8981169d98e949b1e87e9ce5528df8ca1890dbfe6426841992d0fb054bb16"
54
+ )
55
+ RCON = (0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36)
56
+
57
+
58
+ def aes128_schedule_check(b):
59
+ if len(b) < 176:
60
+ return None
61
+ w = [b[4 * i:4 * i + 4] for i in range(44)]
62
+ for i in range(4, 44):
63
+ t = w[i - 1]
64
+ if i % 4 == 0:
65
+ t = bytes((SBOX[t[1]], SBOX[t[2]], SBOX[t[3]], SBOX[t[0]]))
66
+ t = bytes((t[0] ^ RCON[i // 4 - 1], t[1], t[2], t[3]))
67
+ if bytes((w[i - 4][k] ^ t[k] for k in range(4))) != w[i]:
68
+ return None
69
+ return b[0:16]
70
+
71
+
72
+ def aes256_schedule_check(b):
73
+ if len(b) < 240:
74
+ return None
75
+ w = [b[4 * i:4 * i + 4] for i in range(60)]
76
+ for i in range(8, 60):
77
+ t = w[i - 1]
78
+ if i % 8 == 0:
79
+ t = bytes((SBOX[t[1]], SBOX[t[2]], SBOX[t[3]], SBOX[t[0]]))
80
+ t = bytes((t[0] ^ RCON[i // 8 - 1], t[1], t[2], t[3]))
81
+ elif i % 8 == 4:
82
+ t = bytes((SBOX[t[0]], SBOX[t[1]], SBOX[t[2]], SBOX[t[3]]))
83
+ if bytes((w[i - 8][k] ^ t[k] for k in range(4))) != w[i]:
84
+ return None
85
+ return b[0:32]
86
+
87
+
88
+ def find_schedules(blob):
89
+ """Return list of (offset, bits, key) for every AES schedule in blob."""
90
+ hits = []
91
+ for o in range(0, len(blob) - 176):
92
+ k = aes128_schedule_check(blob[o:o + 176])
93
+ if k is not None:
94
+ hits.append((o, 128, k))
95
+ if o < len(blob) - 240:
96
+ k2 = aes256_schedule_check(blob[o:o + 240])
97
+ if k2 is not None:
98
+ hits.append((o, 256, k2))
99
+ return hits
100
+
101
+
102
+ # --- state ---------------------------------------------------------------
103
+ _logf = None
104
+ _entry_hits = 0
105
+ _done = False
106
+ _blobs = [] # list of (label, addr, bytes)
107
+
108
+
109
+ def log(msg=""):
110
+ global _logf
111
+ print(msg)
112
+ if _logf:
113
+ _logf.write(msg + "\n")
114
+ _logf.flush()
115
+
116
+
117
+ def hexsp(b):
118
+ return " ".join("%02x" % x for x in b)
119
+
120
+
121
+ def read_mem(process, addr, size):
122
+ err = lldb.SBError()
123
+ d = process.ReadMemory(addr, size, err)
124
+ return d if (err.Success() and d) else None
125
+
126
+
127
+ def cipher_vptr_runtime(target):
128
+ for m in target.module_iter():
129
+ if m.GetFileSpec().GetFilename() == "HardwareManager":
130
+ a = m.ResolveFileAddress(CIPHER_VPTR_FILEADDR).GetLoadAddress(target)
131
+ return a if a != lldb.LLDB_INVALID_ADDRESS else 0
132
+ return 0
133
+
134
+
135
+ def capture(frame, where):
136
+ """Dump registers + pointed-to memory + stack + (if found) the cipher
137
+ object / device struct. Accumulate into _blobs."""
138
+ thread = frame.GetThread()
139
+ process = thread.GetProcess()
140
+ target = process.GetTarget()
141
+ vptr = cipher_vptr_runtime(target)
142
+
143
+ log("")
144
+ log("=" * 68)
145
+ log("CAPTURE @ %s" % where)
146
+ log("=" * 68)
147
+
148
+ gpr = ["rdi", "rsi", "rdx", "rcx", "r8", "r9", "rax", "rbx",
149
+ "rbp", "rsp", "r10", "r11", "r12", "r13", "r14", "r15", "rip"]
150
+ regs = {}
151
+ for r in gpr:
152
+ v = frame.FindRegister(r)
153
+ regs[r] = v.GetValueAsUnsigned() if v.IsValid() else 0
154
+ log(" %-4s = 0x%x" % (r, regs[r]))
155
+
156
+ # dump memory each plausible pointer register points at
157
+ for r in ["rdi", "rsi", "rdx", "rcx", "r8", "r9", "rax", "rbx",
158
+ "r12", "r13", "r14", "r15"]:
159
+ a = regs[r]
160
+ if 0x10000 < a < 0x7fffffffffff:
161
+ d = read_mem(process, a, 768)
162
+ if d:
163
+ _blobs.append(("%s_ptr_0x%x" % (r, a), a, d))
164
+ # if this points at the cipher object, also grab the whole
165
+ # device struct (cipher subobject is at device + 0x1618)
166
+ if vptr and len(d) >= 8 and int.from_bytes(d[0:8], "little") == vptr:
167
+ dev = a - 0x1618
168
+ big = read_mem(process, dev, 0x3800)
169
+ if big:
170
+ _blobs.append(("device_0x%x" % dev, dev, big))
171
+ log(" %s -> cipher object; dumped device struct @ 0x%x" % (r, dev))
172
+
173
+ # the stack — a transiently-expanded schedule lives here during encrypt
174
+ rsp = regs["rsp"]
175
+ st = read_mem(process, rsp - 0x400, 0x4400)
176
+ if st:
177
+ _blobs.append(("stack_0x%x" % (rsp - 0x400), rsp - 0x400, st))
178
+ log(" dumped stack 0x%x .. 0x%x" % (rsp - 0x400, rsp - 0x400 + len(st)))
179
+
180
+
181
+ def finalize():
182
+ global _done
183
+ if _done:
184
+ return
185
+ _done = True
186
+
187
+ log("")
188
+ log("=" * 68)
189
+ log("ANALYSIS — %d memory blob(s) captured" % len(_blobs))
190
+ log("=" * 68)
191
+
192
+ # save every blob; run the schedule detector across all of them
193
+ found = []
194
+ for label, addr, data in _blobs:
195
+ path = "/tmp/stick-pathC-blob-%s.bin" % label
196
+ with open(path, "wb") as f:
197
+ f.write(data)
198
+ scheds = find_schedules(data)
199
+ log(" %-26s %6d B @ 0x%x -> %s"
200
+ % (label, len(data), addr,
201
+ "%d schedule(s)!" % len(scheds) if scheds else "saved"))
202
+ for off, bits, key in scheds:
203
+ log(" *** AES-%d KEY @ %s+0x%x = %s"
204
+ % (bits, label, off, hexsp(key)))
205
+ found.append((bits, key))
206
+
207
+ log("")
208
+ if found:
209
+ log("*** KEY(S) RECOVERED FROM A LIVE ROUND-KEY SCHEDULE ***")
210
+ seen = set()
211
+ n = 0
212
+ for bits, key in found:
213
+ h = key.hex()
214
+ if h in seen:
215
+ continue
216
+ seen.add(h)
217
+ with open("/tmp/stick-pathC-key-%d.bin" % n, "wb") as f:
218
+ f.write(key)
219
+ log(" key %d (AES-%d): %s" % (n, bits, hexsp(key)))
220
+ n += 1
221
+ log("")
222
+ log("verify: node tools/try-key.mjs <frame.bin> <key-hex>")
223
+ else:
224
+ log("No standard AES schedule in the captured memory.")
225
+ log("The raw key may still be in these blobs in a non-standard layout.")
226
+ log("Brute-force them against a captured frame:")
227
+ log(" node tools/crack-key.mjs <frame.bin> /tmp/stick-pathC-blob-*.bin")
228
+ log("")
229
+ log("done — detaching. log: %s" % LOG_PATH)
230
+
231
+
232
+ # --- breakpoint callbacks ------------------------------------------------
233
+ def on_entry(frame, bp_loc, internal_dict):
234
+ global _entry_hits
235
+ _entry_hits += 1
236
+ if _entry_hits == 1:
237
+ capture(frame, "encrypt-entry FUN_100107950 (frame 1)")
238
+ if _entry_hits >= 6:
239
+ log("")
240
+ log("no AES-block breakpoint fired in %d frames — finalizing at entry"
241
+ % _entry_hits)
242
+ finalize()
243
+ return True # stop
244
+ return False # keep running so a block bp can fire
245
+
246
+
247
+ def on_block(frame, bp_loc, internal_dict):
248
+ capture(frame, "aes-block (CBC/CFB path)")
249
+ finalize()
250
+ return True # stop
251
+
252
+
253
+ def __lldb_init_module(debugger, internal_dict):
254
+ global _logf
255
+ _logf = open(LOG_PATH, "w")
256
+
257
+ target = debugger.GetSelectedTarget()
258
+ if not target or not target.IsValid():
259
+ log("ERROR: no target")
260
+ return
261
+ process = target.GetProcess()
262
+ if not process or not process.IsValid():
263
+ log("ERROR: no live process")
264
+ return
265
+
266
+ log("=== Path C key-trace %s ===" % time.strftime("%Y-%m-%d %H:%M:%S"))
267
+
268
+ mod = None
269
+ for m in target.module_iter():
270
+ if m.GetFileSpec().GetFilename() == "HardwareManager":
271
+ mod = m
272
+ break
273
+ if not mod:
274
+ log("ERROR: HardwareManager module not found")
275
+ return
276
+
277
+ def runtime(fa):
278
+ return mod.ResolveFileAddress(fa).GetLoadAddress(target)
279
+
280
+ plan = [
281
+ ("encrypt-entry", FA_ENTRY, on_entry),
282
+ ("cbc-block", FA_CBC_BLOCK, on_block),
283
+ ("cfb-block", FA_CFB_BLOCK, on_block),
284
+ ]
285
+ for name, fa, cb in plan:
286
+ addr = runtime(fa)
287
+ if addr == lldb.LLDB_INVALID_ADDRESS:
288
+ log(" WARNING: could not resolve %s (0x%x)" % (name, fa))
289
+ continue
290
+ bp = target.BreakpointCreateByAddress(addr)
291
+ if bp.IsValid():
292
+ bp.SetScriptCallbackFunction("lldb_pathC_keytrace.%s" % cb.__name__)
293
+ bp.SetAutoContinue(False)
294
+ log(" bp %-14s file 0x%x -> runtime 0x%x id=%d"
295
+ % (name, fa, addr, bp.GetID()))
296
+ else:
297
+ log(" ERROR: breakpoint at 0x%x failed" % addr)
298
+
299
+ log("")
300
+ log("breakpoints armed — `continue` now; will capture on first hit.")