aboutsummaryrefslogtreecommitdiffstats
path: root/dmcub/dmcub_dpaux_audit.py
blob: ca5b51207001974afdc0c27f4b24e03f58d43672 (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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#!/usr/bin/env python3
"""Cross-version evidence for FINDING-11: the DPIA `DP_AUX_ACCESS` (type 70)
unchecked `instance`.

For every image, locate the type-70 handler, decide stub vs implemented, and for
the implemented ones extract the shape the finding rests on:

  * the DPIA branch on the 32-bit `type` field at frame+0xc
  * the writing function, its struct-base literal slot and the two offset
    literals, giving the write address `base + off + 0x80 + instance`
  * the store sites that put the host byte (frame+0x5) at that address
  * whether the *sibling* on the same path applies the `instance < 4` bound the
    writer omits
  * which adjacent struct offsets fall inside `instance in [4,255]`, i.e. the
    blast radius

Run under the capstone-6 interpreter:
    /home/user/.venvs/cs6/bin/python dmcub_dpaux_audit.py fw/*.bin
"""
import collections
import sys

import dmcub_cmdaudit as ca
import dmcub_dis as dis


def ops(x):
    return [p.strip() for p in x.op_str.split(',')]


def analyse(path):
    out = {'image': path.split('/')[-1]}
    c = ca.CmdAudit(path)
    hmap, _ = c.handler_map()
    if 70 not in hmap:
        out['verdict'] = 'type 70 not implemented (not in dispatch table)'
        return out
    stub, end, handlers = hmap[70]
    if not handlers:
        out['verdict'] = 'type 70 has no called handler'
        return out
    entry = handlers[0]
    lo, hi = c.a.funcs[entry]
    out['entry'] = entry
    out['entry_size'] = hi - lo
    eins = c.a.dis(entry)

    # A stub is the memset-and-return shape: no frame-relative reads past the
    # header, one call, under ~32 bytes.
    reads = [x for x in eins if x.mnemonic.startswith('l8ui') and ops(x)[1] == 'a2']
    if out['entry_size'] < 0x30 and not reads:
        out['verdict'] = 'STUB (memset stack, return 1)'
        return out

    # the DPIA branch: bnei aN, 1 after assembling frame[0xc..0xf]. Without it the
    # build implements only the legacy leg (reads `instance`, fills the stack reply,
    # posts the outbox) and has no indexed write -- e.g. dcn_3_1_6.
    out['dpia_branch'] = any(x.mnemonic == 'bnei' and ops(x)[1] == '1' for x in eins)
    if not out['dpia_branch']:
        out['verdict'] = 'IMPLEMENTED, legacy leg only (no DPIA branch) -- NOT affected'
        return out
    # the call taken on the DPIA leg
    target = None
    for i, x in enumerate(eins):
        if x.mnemonic == 'bnei' and ops(x)[1] == '1':
            for y in eins[i + 1:]:
                if y.mnemonic.startswith('call'):
                    target = c.a.target(y)
                    break
            break
    if target is None or target not in c.a.funcs:
        out['verdict'] = 'IMPLEMENTED, DPIA callee not resolved'
        return out
    out['writer'] = target
    wins = c.a.dis(target)

    # base literal slot + offset literals
    lits = [c.a.lit(x) for x in wins if x.mnemonic == 'l32r']
    out['base_slot'] = next((v for v in lits if v and v > 0x60000000), None)
    offs = sorted(v for v in lits if v and v < 0x60000000)
    out['offsets'] = offs[:2]
    # stores of the host byte (a3) at +0x80 -> the OOB write sites
    out['store_sites'] = [x.address for x in wins
                          if x.mnemonic == 's8i' and ops(x)[0] == 'a3'
                          and ops(x)[2] in ('0x80', '0x9c')]
    if out['offsets']:
        disp = int(ops(next(x for x in wins if x.mnemonic == 's8i'
                            and ops(x)[0] == 'a3'))[2], 0)
        out['write_addr'] = 'base + 0x%x + instance' % (out['offsets'][0] + disp)
        out['array_off'] = out['offsets'][0] + disp

    # sibling bound: any callee of the writer that does `bgeui aN, 4`
    out['sibling_bounds'] = []
    for x in wins:
        if not x.mnemonic.startswith('call') or x.mnemonic.startswith('callx'):
            continue
        t = c.a.target(x)
        if t not in c.a.funcs:
            continue
        n = sum(1 for y in c.a.dis(t)
                if y.mnemonic == 'bgeui' and ops(y)[1] == '4')
        if n:
            out['sibling_bounds'].append((t, n))
    # writer's own bound on the index (should be none)
    out['writer_bounds'] = sum(1 for y in wins
                               if y.mnemonic == 'bgeui' and ops(y)[1] == '4')

    # blast radius: struct offsets referenced elsewhere that land in [4,255]
    if out.get('array_off'):
        a = out['array_off']
        refs = collections.Counter()
        for f in c.a.funcs:
            for x in c.a.dis(f):
                if x.mnemonic == 'l32r':
                    v = c.a.lit(x)
                    if v is not None and a + 4 <= v <= a + 255:
                        refs[v] += 1
        out['blast'] = sorted(refs.items())
    out['verdict'] = 'IMPLEMENTED'
    return out


def main():
    for path in sys.argv[1:]:
        r = analyse(path)
        print('=' * 78)
        print('%-26s %s' % (r['image'], r['verdict']))
        if r['verdict'] != 'IMPLEMENTED':
            continue
        print('  entry            0x%08x (%d bytes), DPIA branch on frame+0xc: %s'
              % (r['entry'], r['entry_size'], r['dpia_branch']))
        print('  writing fn       0x%08x' % r['writer'])
        print('  base slot        0x%08x   offsets %s'
              % (r['base_slot'], ' '.join('0x%x' % o for o in r['offsets'])))
        print('  write address    %s' % r.get('write_addr'))
        print('  host-byte stores %s'
              % ' '.join('0x%08x' % s for s in r['store_sites']))
        print('  bound in writer  bgeui-4 count = %d  %s'
              % (r['writer_bounds'],
                 '<-- MISSING' if r['writer_bounds'] == 0 else ''))
        for fn, n in r['sibling_bounds']:
            print('  sibling bound    0x%08x applies `instance < 4` %dx (too late)'
                  % (fn, n))
        if r.get('blast'):
            print('  blast radius (offsets referenced elsewhere, instance 4..255):')
            for off, n in r['blast']:
                print('      0x%05x  referenced %2dx  -> instance = %d'
                      % (off, n, off - r['array_off']))


if __name__ == '__main__':
    main()