aboutsummaryrefslogtreecommitdiffstats
path: root/fw/ta_triage.py
blob: 38b0cded6f2637d9f2c7cce120afd272f54ade64 (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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#!/usr/bin/env python3
"""First-pass structural triage of a PSP TA body: entry points and command surface.

Every TA in this family shares a layout, established on HDCP 23.0.4.49 in idatui and
re-checked here against the four TAs this project already knows:

  info header at body+0 (maps to 0x400100)
      +0x04 image base   +0x10 entry trampoline   +0x14 'TA'/'DR' class magic
      +0x1c session table   +0x20 &__stack_chk_guard

  the trampoline dispatches on request[0] through a pointer table:
      1 CreateEntryPoint   2 DestroyEntryPoint   3 OpenSessionEntryPoint
      4 CloseSessionEntryPoint   5 InvokeCommandEntryPoint  (4 args)

`InvokeCommandEntryPoint` is the host-facing command surface. Its dispatcher is either a
`cmp`-chain or a `tbb`/`tbh` jump table; both are enumerated here.

usage: ta_triage.py <body.bin> [...]        (add --commands for the command dump)
"""
import argparse
import os
import re
import struct
import sys

import capstone as cs

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


MD_ARM = cs.Cs(cs.CS_ARCH_ARM, cs.CS_MODE_ARM)


def sweep_arm(b, start_va, n=0x40):
    """The entry trampoline is ARM, not Thumb (4-byte encodings), unlike the
    rest of the image. Decoding it as Thumb yields convincing garbage."""
    off = start_va - BASE
    return list(MD_ARM.disasm(b[off:off + n * 4], start_va))


def sweep(b, start_va, end_va=None):
    """Linear sweep that resumes past undecodable words (data in .text)."""
    out = []
    off = start_va - BASE
    end = (end_va - BASE) if end_va else len(b) - 2
    while off < end:
        got = None
        for i in MD.disasm(b[off:off + 4], BASE + off):
            got = i
            break
        if got:
            out.append(got)
            off += got.size
        else:
            off += 2
    return out


def header(b):
    g = lambda o: struct.unpack_from('<I', b, o)[0]
    return dict(base=g(0x04), entry=g(0x10), magic=b[0x14:0x18],
                session_tbl=g(0x1c), guard=g(0x20))


def entry_points(b, h):
    """Walk the trampoline, collecting the literal pointers it jumps through."""
    ins = sweep_arm(b, h['entry'], 0x30)
    ptrs = []
    for i in ins:
        if i.mnemonic.startswith('ldr') and '[pc' in i.op_str:
            m = re.search(r'#(-?0x[0-9a-f]+|-?\d+)', i.op_str.split('[pc')[1])
            if not m:
                continue
            lit = ((i.address + 4) & ~3) + int(m.group(1), 0)
            if 0 <= lit - BASE < len(b) - 4:
                v = struct.unpack_from('<I', b, lit - BASE)[0]
                if h['base'] < (v & ~1) < h['base'] + len(b):
                    ptrs.append(v & ~1)
    # de-dup preserving order; the trampoline emits them in case order 1..5
    seen, out = set(), []
    for p in ptrs:
        if p not in seen:
            seen.add(p)
            out.append(p)
    return out


def commands(b, fn, limit=0x600):
    """Enumerate the command ids a dispatcher tests."""
    ins = sweep(b, fn, fn + limit)
    ids, tables = set(), []
    for n, i in enumerate(ins):
        if i.mnemonic in ('cmp', 'cmp.w') and '#' in i.op_str:
            try:
                ids.add(int(i.op_str.split('#')[1], 0))
            except ValueError:
                pass
        if i.mnemonic in ('subs', 'sub.w', 'subw') and '#' in i.op_str:
            pass
        if i.mnemonic in ('tbb', 'tbh'):
            tables.append(i.address)
    return sorted(ids), tables


def tb_entries(b, tb_va, is_half, n):
    base = tb_va + 4
    out = []
    for k in range(n):
        o = (base + (2 * k if is_half else k)) - BASE
        if o + 2 > len(b):
            break
        e = struct.unpack_from('<H', b, o)[0] if is_half else b[o]
        out.append(base + 2 * e)
    return out


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('files', nargs='+')
    ap.add_argument('--commands', action='store_true')
    args = ap.parse_args()
    for p in args.files:
        b = open(p, 'rb').read()
        h = header(b)
        eps = entry_points(b, h)
        name = os.path.basename(p)
        print('\n=== %s  (%#x bytes)' % (name, len(b)))
        print('    base=%#x  entry=%#x  class=%s  session_tbl=%#x  guard=%#x'
              % (h['base'], h['entry'], h['magic'][:2].decode('latin1'),
                 h['session_tbl'], h['guard']))
        labels = ['Create', 'Destroy', 'OpenSession', 'CloseSession', 'Invoke']
        for lab, ep in zip(labels, eps):
            print('    %-13s %#x' % (lab, ep))
        if len(eps) >= 5 and args.commands:
            ids, tbs = commands(b, eps[4])
            print('    invoke dispatcher @%#x' % eps[4])
            print('      cmp constants: %s' % [hex(x) for x in ids if x < 0x200])
            for t in tbs:
                print('      jump table @%#x' % t)
    return 0


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