aboutsummaryrefslogtreecommitdiffstats
path: root/fw/ta_svc_scan.py
blob: 2ba5de6368137056f6eb7c195ef43d55988c8bcb (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
#!/usr/bin/env python3
"""What can each TA ask the PSP kernel / SYS_DRV to do? (`SVC` surface per TA)

This is the analysis that decided reachability for ASD: an 89-site scan of `SVC 0xF2`
call sites across the four known TAs showed every service opcode is an immediate and no
TA forwards a host-controlled opcode. The twelve newly-hittable TAs have never been
scanned, and they are host-invokable, so their privilege reach matters.

For each body:
  * every `svc #N` site, grouped by N (0xF2 = service RPC, others = kernel calls);
  * for `svc 0xF2`, the service id (r0) and opcode, recovered by walking backwards for
    the immediate that last wrote r0/r1 — and **flagging any site where the opcode is
    not an immediate**, which is the dangerous shape (a TA forwarding a caller-chosen
    opcode into SYS_DRV).

usage: ta_svc_scan.py <body.bin> [...]
"""
import argparse
import collections
import os
import re
import struct
import sys

import capstone as cs

BASE = 0x400100
MT = cs.Cs(cs.CS_ARCH_ARM, cs.CS_MODE_THUMB)


def sweep(b, start=0x401000):
    out, off = [], start - BASE
    while off < len(b) - 2:
        got = None
        for i in MT.disasm(b[off:off + 4], BASE + off):
            got = i
            break
        if got:
            out.append(got)
            off += got.size
        else:
            off += 2
    return out


def last_imm(ins, n, reg, depth=18):
    """Nearest preceding immediate write to `reg`; None if it came from memory/register."""
    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
        if i.mnemonic in ('mov', 'movs', 'movw', 'mov.w') and '#' in i.op_str:
            try:
                return int(i.op_str.split('#')[1], 0)
            except ValueError:
                return None
        return None          # written from something that is not an immediate
    return None


def stored_opcode(ins, n, depth=26):
    """The service opcode is not in a register: GUARD-INIT-CHAIN.md shows the ABI is
    `req[0] = opcode; r0 = service id; r1 = &req; svc 0xF2`. So recover the immediate
    most recently stored at offset 0 of some buffer before the call."""
    imm = {}
    for k in range(max(0, n - depth), n):
        i = ins[k]
        parts = [x.strip() for x in i.op_str.split(',')]
        if i.mnemonic in ('mov', 'movs', 'movw', 'mov.w') and '#' in i.op_str:
            try:
                imm[parts[0]] = int(i.op_str.split('#')[1], 0)
            except ValueError:
                pass
        if i.mnemonic.startswith('str') and len(parts) >= 2:
            mem = ','.join(parts[1:])
            if re.fullmatch(r'\[\w+\]', mem) or re.fullmatch(r'\[\w+, #0\]', mem):
                if parts[0] in imm:
                    return imm[parts[0]]
    return None


def scan(path):
    b = open(path, 'rb').read()
    ins = sweep(b)
    svcs = collections.Counter()
    rpc = collections.Counter()
    nonimm = []
    for n, i in enumerate(ins):
        if i.mnemonic != 'svc':
            continue
        try:
            num = int(i.op_str.strip('#'), 0)
        except ValueError:
            continue
        svcs[num] += 1
        if num == 0xF2:
            svc_id = last_imm(ins, n, 'r0')
            op = stored_opcode(ins, n)
            if op is None:
                nonimm.append(i.address)
            rpc[(svc_id, op)] += 1
    return svcs, rpc, nonimm, len(ins)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('bodies', nargs='+')
    a = ap.parse_args()
    for p in a.bodies:
        svcs, rpc, nonimm, n = scan(p)
        print('\n=== %s  (%d insns)' % (os.path.basename(p), n))
        print('    svc numbers: %s'
              % ', '.join('%#x x%d' % (k, v) for k, v in sorted(svcs.items())))
        if rpc:
            print('    svc 0xF2 sites: %d' % sum(rpc.values()))
            for (sid, op), c in sorted(rpc.items(), key=lambda kv: (str(kv[0][0]), str(kv[0][1]))):
                print('       service=%-6s opcode=%-8s x%d'
                      % (sid if sid is None else hex(sid),
                         op if op is None else hex(op), c))
        print('    sites whose OPCODE is not an immediate: %d %s'
              % (len(nonimm), [hex(x) for x in nonimm[:8]]))
    return 0


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