#!/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()