#!/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
[--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(' 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())