@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.
- package/CLAUDE.md +176 -0
- package/README.md +146 -0
- package/config.schema.json +143 -0
- package/dist/cli/stick.d.ts +3 -0
- package/dist/cli/stick.d.ts.map +1 -0
- package/dist/cli/stick.js +176 -0
- package/dist/cli/stick.js.map +1 -0
- package/dist/color/cct.d.ts +3 -0
- package/dist/color/cct.d.ts.map +1 -0
- package/dist/color/cct.js +33 -0
- package/dist/color/cct.js.map +1 -0
- package/dist/color/dimmer.d.ts +3 -0
- package/dist/color/dimmer.d.ts.map +1 -0
- package/dist/color/dimmer.js +16 -0
- package/dist/color/dimmer.js.map +1 -0
- package/dist/color/hsvcct.d.ts +3 -0
- package/dist/color/hsvcct.d.ts.map +1 -0
- package/dist/color/hsvcct.js +52 -0
- package/dist/color/hsvcct.js.map +1 -0
- package/dist/color/index.d.ts +7 -0
- package/dist/color/index.d.ts.map +1 -0
- package/dist/color/index.js +28 -0
- package/dist/color/index.js.map +1 -0
- package/dist/color/parsers.d.ts +20 -0
- package/dist/color/parsers.d.ts.map +1 -0
- package/dist/color/parsers.js +232 -0
- package/dist/color/parsers.js.map +1 -0
- package/dist/color/rgb.d.ts +3 -0
- package/dist/color/rgb.d.ts.map +1 -0
- package/dist/color/rgb.js +32 -0
- package/dist/color/rgb.js.map +1 -0
- package/dist/color/rgbaw.d.ts +3 -0
- package/dist/color/rgbaw.d.ts.map +1 -0
- package/dist/color/rgbaw.js +48 -0
- package/dist/color/rgbaw.js.map +1 -0
- package/dist/color/rgbw.d.ts +3 -0
- package/dist/color/rgbw.d.ts.map +1 -0
- package/dist/color/rgbw.js +37 -0
- package/dist/color/rgbw.js.map +1 -0
- package/dist/color/rgbww.d.ts +3 -0
- package/dist/color/rgbww.d.ts.map +1 -0
- package/dist/color/rgbww.js +54 -0
- package/dist/color/rgbww.js.map +1 -0
- package/dist/color/types.d.ts +64 -0
- package/dist/color/types.d.ts.map +1 -0
- package/dist/color/types.js +91 -0
- package/dist/color/types.js.map +1 -0
- package/dist/config.d.ts +60 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +191 -0
- package/dist/config.js.map +1 -0
- package/dist/controller.d.ts +32 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +128 -0
- package/dist/controller.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +17 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +96 -0
- package/dist/platform.js.map +1 -0
- package/dist/platformAccessory.d.ts +18 -0
- package/dist/platformAccessory.d.ts.map +1 -0
- package/dist/platformAccessory.js +81 -0
- package/dist/platformAccessory.js.map +1 -0
- package/dist/settings.d.ts +14 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +15 -0
- package/dist/settings.js.map +1 -0
- package/dist/stick/frame.d.ts +16 -0
- package/dist/stick/frame.d.ts.map +1 -0
- package/dist/stick/frame.js +51 -0
- package/dist/stick/frame.js.map +1 -0
- package/dist/stick/kdf.d.ts +23 -0
- package/dist/stick/kdf.d.ts.map +1 -0
- package/dist/stick/kdf.js +90 -0
- package/dist/stick/kdf.js.map +1 -0
- package/dist/stick/protocol.d.ts +28 -0
- package/dist/stick/protocol.d.ts.map +1 -0
- package/dist/stick/protocol.js +78 -0
- package/dist/stick/protocol.js.map +1 -0
- package/dist/stick/session.d.ts +50 -0
- package/dist/stick/session.d.ts.map +1 -0
- package/dist/stick/session.js +301 -0
- package/dist/stick/session.js.map +1 -0
- package/examples/dmx.yaml +65 -0
- package/package.json +51 -0
- package/src/cli/stick.ts +198 -0
- package/src/color/cct.ts +40 -0
- package/src/color/dimmer.ts +24 -0
- package/src/color/hsvcct.ts +59 -0
- package/src/color/index.ts +34 -0
- package/src/color/parsers.ts +232 -0
- package/src/color/rgb.ts +33 -0
- package/src/color/rgbaw.ts +46 -0
- package/src/color/rgbw.ts +36 -0
- package/src/color/rgbww.ts +56 -0
- package/src/color/types.ts +150 -0
- package/src/config.ts +248 -0
- package/src/controller.ts +136 -0
- package/src/index.ts +9 -0
- package/src/platform.ts +118 -0
- package/src/platformAccessory.ts +102 -0
- package/src/settings.ts +18 -0
- package/src/stick/frame.ts +70 -0
- package/src/stick/kdf.ts +99 -0
- package/src/stick/protocol.ts +90 -0
- package/src/stick/session.ts +348 -0
- package/stick-de3.json +6 -0
- package/tools/aes-hook.c +1404 -0
- package/tools/analyze-pcap.mjs +143 -0
- package/tools/capture-aes-key.lldb +25 -0
- package/tools/capture-and-verify.sh +46 -0
- package/tools/capture-handshake.sh +59 -0
- package/tools/capture-send.sh +39 -0
- package/tools/crack-kdf.mjs +121 -0
- package/tools/crack-key.mjs +170 -0
- package/tools/decode-frame.mjs +110 -0
- package/tools/decrypt-011c.mjs +123 -0
- package/tools/decrypt-dmx.mjs +130 -0
- package/tools/decrypt-ssl2.ts +154 -0
- package/tools/derive-dmx-key.mjs +189 -0
- package/tools/diff-011c.mjs +127 -0
- package/tools/dump-pcap-summary.mjs +54 -0
- package/tools/dump-tcp-stream.mjs +123 -0
- package/tools/extract-011c.mjs +117 -0
- package/tools/extract-frame.mjs +78 -0
- package/tools/find-011c-key.mjs +140 -0
- package/tools/find-iv.mjs +153 -0
- package/tools/ghidra/README.md +136 -0
- package/tools/ghidra/__pycache__/dump-011c-callers.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-011c-handler.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-aes-candidates.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-anchors.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-auth2.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-authenticate.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-block-cipher.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-cipher.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-ctor-chain.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-curve.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-encompassing.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-handshake.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-kdf-chain.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-kdf-context.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-profile.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-real-encrypt.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-secret.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-setsoftware.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-stick3-vtable.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-swxref.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-vt-key-getter.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-cipher.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-ctor.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-md5.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-secret-users.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-stick3-crypto.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-stick3-device.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-stick3a-ctor.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-stick3anet-ctor.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-vtables.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/string-walk.cpython-313.pyc +0 -0
- package/tools/ghidra/dump-011c-callers.py +140 -0
- package/tools/ghidra/dump-011c-handler.py +158 -0
- package/tools/ghidra/dump-aes-candidates.py +99 -0
- package/tools/ghidra/dump-anchors.py +192 -0
- package/tools/ghidra/dump-auth2.py +50 -0
- package/tools/ghidra/dump-authenticate.py +71 -0
- package/tools/ghidra/dump-block-cipher.py +88 -0
- package/tools/ghidra/dump-cipher.py +106 -0
- package/tools/ghidra/dump-ctor-chain.py +107 -0
- package/tools/ghidra/dump-curve.py +112 -0
- package/tools/ghidra/dump-encompassing.py +146 -0
- package/tools/ghidra/dump-handshake.py +76 -0
- package/tools/ghidra/dump-kdf-chain.py +105 -0
- package/tools/ghidra/dump-kdf-context.py +72 -0
- package/tools/ghidra/dump-profile.py +59 -0
- package/tools/ghidra/dump-real-encrypt.py +90 -0
- package/tools/ghidra/dump-secret.py +84 -0
- package/tools/ghidra/dump-setsoftware.py +63 -0
- package/tools/ghidra/dump-stick3-vtable.py +182 -0
- package/tools/ghidra/dump-swxref.py +27 -0
- package/tools/ghidra/dump-vt-key-getter.py +170 -0
- package/tools/ghidra/find-cipher.py +210 -0
- package/tools/ghidra/find-ctor.py +109 -0
- package/tools/ghidra/find-md5.py +97 -0
- package/tools/ghidra/find-secret-users.py +98 -0
- package/tools/ghidra/find-stick3-crypto.py +190 -0
- package/tools/ghidra/find-stick3-device.py +111 -0
- package/tools/ghidra/find-stick3a-ctor.py +87 -0
- package/tools/ghidra/find-stick3anet-ctor.py +76 -0
- package/tools/ghidra/find-vtables.py +165 -0
- package/tools/ghidra/out-011c-callers-run.log +51 -0
- package/tools/ghidra/out-011c-run.log +50 -0
- package/tools/ghidra/out-vt-key-run.log +55 -0
- package/tools/ghidra/string-walk.py +180 -0
- package/tools/hmac-key.sh +51 -0
- package/tools/hwm-entitlements.plist +18 -0
- package/tools/inspect-plaintext.mjs +87 -0
- package/tools/lldb-capture-aes.py +137 -0
- package/tools/lldb_hmac_key.py +93 -0
- package/tools/lldb_pathA_scan.py +341 -0
- package/tools/lldb_pathC_keytrace.py +300 -0
- package/tools/lldb_pathD_kdf_watch.py +281 -0
- package/tools/pathA-launch-hwm.sh +86 -0
- package/tools/pathA-scan.sh +52 -0
- package/tools/pathC-keytrace.sh +49 -0
- package/tools/pathC-run.sh +71 -0
- package/tools/pathD-kdf-watch.sh +51 -0
- package/tools/quick-trigger.mjs +86 -0
- package/tools/run-hooked-hwmanager.sh +51 -0
- package/tools/scan-aes-schedule.mjs +135 -0
- package/tools/send_dmx.mjs +480 -0
- package/tools/stick-status.mjs +57 -0
- package/tools/test-aes-detector.mjs +100 -0
- package/tools/test-tcp-write.ts +274 -0
- package/tools/test-udp-dmx.ts +324 -0
- package/tools/test-x25519.mjs +87 -0
- package/tools/try-decrypt.ts +144 -0
- package/tools/try-hardcoded-key.mjs +86 -0
- package/tools/try-hardcoded-key.ts +103 -0
- package/tools/try-key.mjs +103 -0
- package/tools/verify-011c-key.mjs +121 -0
- package/tools/verify-kdf.mjs +142 -0
- 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))
|