aboutsummaryrefslogtreecommitdiffstats
path: root/fw/ta_document_residue.py
blob: 5aed2f6cef9260771badd2e4f5e517d12779aee9 (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
#!/usr/bin/env python3
"""Document every still-anonymous function with facts, instead of inventing a name.

After name propagation, call-graph attribution and pointer-table recovery, ~14% of
functions remain `sub_*`. They are in two classes, both established
(`fw/AUDIT-NEW-TAS-RE.md`): helpers with *several* named owners, where picking one owner
would misattribute; and functions reached only indirectly or not at all.

Naming those would be invention. Documenting them is not: for each one this writes an IDA
function comment stating its size, its callers, its callees (named where known), and which
class it falls into. That makes the database self-describing for the residue too — a reader
lands on `sub_401ADA` and immediately sees "no named caller; calls X and Y; island member"
rather than nothing.

Facts come from IDA's own function boundaries (`fw/libdumps/*.json`) plus a capstone decode,
so nothing here depends on a guess.

usage: IDATUI_RPC_SOCK=... ta_document_residue.py --prefix gcp --body <body.bin> [--apply]
"""
import argparse
import json
import os
import sys

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)
CWD = os.path.expanduser('~/dev/ida-tui-maybe')
sys.path.insert(0, CWD)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--prefix', required=True)
    ap.add_argument('--body', required=True)
    ap.add_argument('--apply', action='store_true')
    a = ap.parse_args()

    fns = json.load(open(os.path.join(REPO, 'fw', 'libdumps', '%s.json' % a.prefix)))
    body = open(a.body, 'rb').read()
    name = {f['ea']: f['name'] for f in fns}
    starts = set(name)

    callees, callers = {}, {}
    for f in fns:
        out = set()
        for i in MD.disasm(body[f['ea'] - BASE:f['ea'] - BASE + f['size']], f['ea']):
            if i.mnemonic in ('bl', 'blx') and i.op_str.startswith('#'):
                try:
                    t = int(i.op_str[1:], 0)
                except ValueError:
                    continue
                if t in starts:
                    out.add(t)
                    callers.setdefault(t, set()).add(f['ea'])
        callees[f['ea']] = out

    anon = [f for f in fns if f['name'].startswith('sub_')]
    # data-referenced?
    import struct
    words = set()
    for off in range(0, len(body) - 4, 2):
        words.add(struct.unpack_from('<I', body, off)[0])

    notes = []
    for f in anon:
        ea = f['ea']
        cs_ = sorted(callers.get(ea, set()))
        named_owners = {name[c].split('__h')[0] for c in cs_
                        if not name[c].startswith('sub_')}
        cls = ('helper with several named owners (attribution would misattribute)'
               if len(named_owners) > 1 else
               'no named caller; not referenced as data -> indirect or dead'
               if not cs_ and (ea | 1) not in words and ea not in words else
               'no bl caller, but referenced as data -> reached indirectly'
               if not cs_ else
               'called only from still-anonymous code')
        lines = [
            'Residue function, documented rather than named (naming would be invention).',
            '',
            'size: %d bytes' % f['size'],
            'class: %s' % cls,
            'callers: %s' % (', '.join(name[c] for c in cs_) or 'none found'),
            'callees: %s' % (', '.join(sorted(name[t] for t in callees[ea])) or 'none'),
            '',
            'Basis: IDA function boundaries + capstone decode of this function only;',
            'see fw/AUDIT-NEW-TAS-RE.md for how the residue was classified.',
        ]
        notes.append((ea, '\\n'.join(lines)))

    print('%s: %d anonymous functions to document' % (a.prefix, len(notes)))
    for ea, _ in notes[:4]:
        print('   %#x %s' % (ea, name[ea]))

    if a.apply and notes:
        # Headless via the Nexus client. The TUI RPC path (goto + comment) depends on the
        # cursor landing on the right token and cannot be verified without scraping the
        # screen; set_comments takes the address directly and reports per-item status.
        import sys as _sys
        _sys.path.insert(0, os.path.expanduser('~/dev/ida-tui-maybe'))
        from idatui import remote_ops
        from idatui.nexus_client import NexusClient
        elf = json.load(open(os.path.join(REPO, 'fw', 'syms', 'index.json')))[a.prefix]['elf']
        c = NexusClient(os.path.join(REPO, elf))
        c.connect(timeout=600)
        items = [{'addr': hex(ea), 'comment': txt.replace('\\n', '\n')} for ea, txt in notes]
        r = c.call(remote_ops.set_comments, items=items)
        rows = r.get('result', []) if isinstance(r, dict) else []
        ok = sum(1 for x in rows if not x.get('error'))
        print('   documented %d/%d' % (ok, len(notes)))
        c.call_save = getattr(c, 'save_database', None)
        if c.call_save:
            print('   save: %s' % json.dumps(c.save_database())[:60])
        c.close()
    return 0


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