aboutsummaryrefslogtreecommitdiffstats
path: root/fw/ta_liveness_audit.py
blob: 7f10dcbfab8146f2e27e6568fee36f985f0ab56b (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
#!/usr/bin/env python3
"""Find slot lookups that never test whether the slot is live.

Generalises GCP FE0F (`fw/FINDING-GCP-FE0F-STALE-SLOT-LOOKUP.md`): it walks a 64-entry
array comparing the caller's bytes against a slot field and returns the slot's handle,
without ever reading the slot's state word. Combined with a destroy path that clears only
that state word, destroyed objects stay findable.

Shape of the detector, per function:
  * a **slot scan**: a loop whose back-edge is preceded by a constant stride added to a
    pointer register (`add rX, #N` / `add.w rX, rX, #N`, 8 <= N <= 256), or an index
    multiply by such a stride;
  * a **liveness test**: a `cmp` of a loaded value against a small constant (1..8) -- the
    state words in these TAs are 1 (allocated) or 2 (ready);
  * report scans that have **no** liveness test in the same function.

This is a lead generator. A scan may validate liveness in a callee, or the field it matches
on may itself be cleared on destroy (which is the correct fix and makes the scan safe). Each
hit needs the destroy path checked, exactly as FE0F needed FE0D.

usage: ta_liveness_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)
STRIDE_ADD = re.compile(r'^(add|adds|add\.w)$')
BRANCH = ('b', 'bne', 'beq', 'bhi', 'bls', 'bcs', 'bcc', 'blo', 'bge', 'blt',
          'b.w', 'bne.w', 'beq.w', 'blo.w', 'bhi.w')


def strides_and_state(ins):
    strides, states, back = [], [], False
    for n, i in enumerate(ins):
        if STRIDE_ADD.match(i.mnemonic) and '#' in i.op_str and 'sp' not in i.op_str:
            try:
                v = int(i.op_str.split('#')[1], 0)
            except ValueError:
                continue
            if 8 <= v <= 256:
                strides.append((i.address, v))
        if i.mnemonic in ('mul', 'muls', 'mla', 'mlane') or \
           (i.mnemonic.startswith('add') and 'lsl' in i.op_str):
            strides.append((i.address, -1))          # index arithmetic
        if i.mnemonic.startswith('cmp') and '#' in i.op_str:
            try:
                v = int(i.op_str.split('#')[1], 0)
            except ValueError:
                continue
            if 1 <= v <= 8:
                states.append((i.address, v))
        if i.mnemonic in BRANCH and i.op_str.startswith('#'):
            try:
                if int(i.op_str[1:], 0) < i.address:
                    back = True
            except ValueError:
                pass
    return strides, states, back


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--prefix')
    a = ap.parse_args()
    total = 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()
        rows = []
        for f in fns:
            if 'lib_' in f['name']:
                continue
            ins = list(MD.disasm(body[f['ea'] - BASE:f['ea'] - BASE + f['size']], f['ea']))
            if not ins:
                continue
            strides, states, back = strides_and_state(ins)
            if back and strides and not states:
                rows.append((f['name'], f['ea'],
                             sorted({v for _, v in strides if v > 0})))
        if rows:
            print('=== %s' % ta)
            for nm, ea, sv in rows:
                print('   %-44s @%#x  strides=%s  NO liveness test'
                      % (nm[:44], ea, sv))
            total += len(rows)
    print('\n%d scan-without-liveness lead(s). Each needs its destroy path checked.'
          % total)
    return 0


if __name__ == '__main__':
    raise SystemExit(main())