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
|
#!/usr/bin/env python3
"""Find copies whose LENGTH comes from memory (i.e. potentially from the request).
This is the shape behind every memory-corruption finding in this tree: HDCP's subcmd12
overflow is `ldrb` of a request byte, multiplied, straight into a copy length. So:
step 1 identify each TA's copy primitives structurally -- small leaf functions whose
body both loads and stores in a loop (`ldrb/strb` or `ldm/stm`). Naming-based
detection does not work here: the copy helper is an unnamed thunk, and keying on
"memcpy" in a name found nothing at all in an earlier attempt, while keying on
"any 3-arg call" drowned in log/printf calls.
step 2 for every call to one of those, recover how R2 (the length) was last written.
An immediate is fine. A load from memory is a lead, and the load's addressing
mode says where from -- `[rN,#imm]` off a request pointer is the dangerous case.
Reports leads with the instruction that produced the length, so each can be judged rather
than taken on faith. Boundaries come from IDA (`fw/libdumps/*.json`).
usage: ta_copy_audit.py [--prefix X]
"""
import argparse
import glob
import json
import os
import re
import capstone as cs
BASE = 0x400100
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MD = cs.Cs(cs.CS_ARCH_ARM, cs.CS_MODE_THUMB)
def decode(body, ea, size):
return list(MD.disasm(body[ea - BASE:ea - BASE + size], ea))
def is_copy_primitive(ins):
"""Loads AND stores AND a backward branch, in a small function."""
ld = sum(1 for i in ins if i.mnemonic.startswith(('ldr', 'ldm')))
st = sum(1 for i in ins if i.mnemonic.startswith(('str', 'stm')))
back = any(i.mnemonic.startswith('b') and i.op_str.startswith('#') and
int(i.op_str[1:], 0) < i.address for i in ins
if i.op_str.startswith('#') and i.mnemonic in
('b', 'bne', 'beq', 'bhi', 'bls', 'bcs', 'bcc', 'b.w', 'bne.w'))
return ld >= 1 and st >= 1 and back
def last_write(ins, n, reg, depth=14):
for k in range(n - 1, max(0, n - depth), -1):
i = ins[k]
dst = i.op_str.split(',')[0].strip()
if dst != reg:
continue
return i
return None
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--prefix')
a = ap.parse_args()
grand = 0
for p in sorted(glob.glob(os.path.join(REPO, 'fw', 'libdumps', '*.json'))):
ta = os.path.basename(p)[:-5]
if a.prefix and ta != a.prefix:
continue
fns = json.load(open(p))
meta = json.load(open(os.path.join(REPO, 'fw', 'syms', '%s.meta.json' % ta)))
body = open(meta['path'], 'rb').read()
byea = {f['ea']: f for f in fns}
prims = {}
for f in fns:
if f['size'] > 0x80:
continue
ins = decode(body, f['ea'], f['size'])
if ins and is_copy_primitive(ins):
prims[f['ea']] = f['name']
if not prims:
print('=== %s: no copy primitive identified' % ta)
continue
leads = []
for f in fns:
ins = decode(body, f['ea'], f['size'])
for n, i in enumerate(ins):
if i.mnemonic not in ('bl', 'blx') or not i.op_str.startswith('#'):
continue
try:
t = int(i.op_str[1:], 0)
except ValueError:
continue
if t not in prims:
continue
w = last_write(ins, n, 'r2')
if w is None:
leads.append((f['name'], i.address, prims[t], 'r2 unknown'))
elif w.mnemonic.startswith('ldr'):
leads.append((f['name'], i.address, prims[t],
'%s %s' % (w.mnemonic, w.op_str)))
print('=== %s: %d copy primitive(s) %s ; %d length-from-memory lead(s)'
% (ta, len(prims), [hex(x) for x in prims], len(leads)))
for nm, at, pr, how in leads:
print(' %-44s call@%#x -> %-18s len <- %s' % (nm[:44], at, pr[:18], how))
grand += len(leads)
print('\n%d lead(s) total. A load is not a bug: the value may be bounded elsewhere.'
% grand)
return 0
if __name__ == '__main__':
raise SystemExit(main())
|