1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
#!/usr/bin/env python3
"""Find every call to the DMCUB firmware's byte-copy routine and classify the
length argument, so that the host-controlled ones can be reviewed by hand.
The copy routine is located by shape, not by address: an Xtensa function with a
`loopnez` and a run of unrolled `l8ui`/`s8i` pairs (memcpy(dst=a2, src=a3, n=a4);
under call8 the caller passes dst/src/n in a10/a11/a12).
The length classifier is a backward slice inside the calling basic block:
const -- movi/movi.n, safe by construction
lit -- l32r literal
MEMLOAD -- l8ui/l16ui/l32i: the length came out of memory. These are the ones
that matter: if the memory is a mailbox command frame, the length
is host-supplied.
<op>(..) -- derived; the detail column shows the chain
usage: dmcub_copyaudit.py <image> [--all] [--target 0xVA]
"""
import argparse
import collections
import importlib.util
import os
import re
HERE = os.path.dirname(os.path.abspath(__file__))
_s = importlib.util.spec_from_file_location('dmcub_dis', os.path.join(HERE, 'dmcub_dis.py'))
dis = importlib.util.module_from_spec(_s)
_s.loader.exec_module(dis)
ARITH = ('add', 'add.n', 'addi', 'addi.n', 'sub', 'and', 'or', 'slli', 'srli',
'addx2', 'addx4', 'addx8', 'min', 'max', 'minu', 'maxu', 'moveqz',
'movnez', 'extui', 'mul16s', 'mul16u', 'mull')
def ops(x):
return [p.strip() for p in x.op_str.split(',')]
def find_memcpy(a):
"""The byte-memcpy function, by shape.
Candidates are functions with an unrolled `l8ui`/`s8i` run plus a `loop*`.
Several helpers match that loosely (a `loop`-based clear, inlined copies), so
rank by *incoming call count* first and size second -- picking merely the
smallest candidate selected a 0-caller function on dcn_3_2_1.
"""
cands = []
for f, (s, e) in a.funcs.items():
ins = a.dis(f)
pairs = sum(1 for i, x in enumerate(ins[:-1])
if x.mnemonic == 'l8ui' and ins[i + 1].mnemonic == 's8i')
if pairs >= 4 and any(x.mnemonic.startswith('loop') for x in ins):
cands.append((a.calls[f], -(e - s), f))
return max(cands)[2] if cands else None
def classify(a, ins, idx, reg, depth=0):
if depth > 6:
return ('deep', '')
for y in range(idx - 1, -1, -1):
x = ins[y]
p = ops(x)
if not p or p[0] != reg:
continue
m = x.mnemonic
if m in ('movi', 'movi.n'):
try:
return ('const', int(p[1], 0))
except ValueError:
return ('const?', x.op_str)
if m == 'l32r':
return ('lit', a.lit(x))
if m in ('mov', 'mov.n'):
return classify(a, ins, y, p[1], depth + 1)
if m.startswith(('l8ui', 'l16ui', 'l32i')):
return ('MEMLOAD', '%s %s' % (m, x.op_str))
if m in ARITH and len(p) > 1 and re.fullmatch(r'a\d+', p[1]):
k = classify(a, ins, y, p[1], depth + 1)
return ('%s(%s)' % (m, k[0]), '%s | %s' % (x.op_str, k[1]))
return (m, x.op_str)
return ('argin/unknown', '')
def main():
ap = argparse.ArgumentParser()
ap.add_argument('image')
ap.add_argument('--target', default=None, help='copy routine VA (default: detect)')
ap.add_argument('--all', action='store_true', help='also list constant-length sites')
args = ap.parse_args()
a = dis.Analysis(args.image)
tgt = int(args.target, 0) if args.target else find_memcpy(a)
if tgt is None:
print('no copy routine found')
return
sites = a.callsites.get(tgt, [])
print('%s: copy routine 0x%08x, %d call sites' % (
os.path.basename(args.image), tgt, len(sites)))
rows = []
for fva, site in sorted(sites):
ins = a.dis(fva)
i = [k for k, x in enumerate(ins) if x.address == site][0]
rows.append((fva, site,
classify(a, ins, i, 'a12'),
classify(a, ins, i, 'a10'),
classify(a, ins, i, 'a11')))
consts = collections.Counter(r[2][1] for r in rows if r[2][0] == 'const')
print('constant length: %d sites %s' % (sum(consts.values()), sorted(consts)))
print('\nnon-constant length -- review these:')
for fva, site, n, dst, src in rows:
if n[0] == 'const' and not args.all:
continue
print(' fn 0x%08x call 0x%08x' % (fva, site))
print(' n = %-22s %s' % (n[0], n[1]))
print(' dst = %-22s %s' % (dst[0], dst[1]))
print(' src = %-22s %s' % (src[0], src[1]))
if __name__ == '__main__':
main()
|