aboutsummaryrefslogtreecommitdiffstats
path: root/dmcub/dmcub_copyaudit.py
blob: 86eedbeddb06f48965e7cdce1968d3e2d6a380f2 (plain) (blame)
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()