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()
|