#!/usr/bin/env python3 """Define the functions IDA missed, found via call sites outside its function set. Discovered while classifying the annotation residue: GCP has `bl` instructions at `0x402540..0x40259a` that call *known* functions yet lie outside every IDA function -- they are inside `sub_402524`, the MAC driver analysed by hand earlier, which IDA never turned into a function. So coverage percentages computed over IDA's function list used an incomplete denominator, and every detector that iterated IDA functions skipped this code. Method: sweep the image; wherever a `bl`/`blx` to a known function start sits outside all IDA functions, walk **back** to the nearest plausible prologue (`push{...,lr}` or `sub sp,#imm`) and define a function there. Re-dumping afterwards shows the new count. Deliberately conservative: only regions that *call a known function* are considered, which is a strong code signal. A permissive "does it decode as Thumb" test was tried first and rejected -- it claimed 75% of these images were unanalysed code, when most of that is data (OPM embeds XML certificates). That is the same fallacy as judging an ISA by decode percentage, which this project already recorded. usage: IDATUI_RPC_SOCK=... ta_define_missed.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))) b = open(a.body, 'rb').read() starts = {f['ea'] for f in fns} inside = set() for f in fns: inside.update(range(f['ea'], f['ea'] + f['size'])) # 1. unmarked call sites into known functions sites, off = [], 0x401000 - BASE while off < len(b) - 2: addr = BASE + off got = None for i in MD.disasm(b[off:off + 4], addr): got = i break if not got: off += 2 continue if addr not in inside and got.mnemonic in ('bl', 'blx') and \ got.op_str.startswith('#'): try: t = int(got.op_str[1:], 0) except ValueError: t = None if t in starts: sites.append(addr) off += got.size # 2. walk back from each site to a prologue cands = set() for s in sites: for back in range(2, 0x300, 2): a0 = s - back if a0 in inside or a0 < 0x401000: break got = None for i in MD.disasm(b[a0 - BASE:a0 - BASE + 4], a0): got = i break if not got: continue is_push = got.mnemonic.startswith('push') and 'lr' in got.op_str is_sub = (got.mnemonic in ('sub', 'sub.w') and 'sp' in got.op_str and '#' in got.op_str) if is_push or is_sub: # a `sub sp` is usually the SECOND instruction of the prologue; prefer the # push in front of it, or the function starts mid-prologue (GCP's real # start is 0x402524 `push`, not 0x402528 `sub sp`). if is_sub: for delta in (2, 4): pa = a0 - delta g2 = None for i2 in MD.disasm(b[pa - BASE:pa - BASE + 4], pa): g2 = i2 break if g2 and g2.mnemonic.startswith('push') and 'lr' in g2.op_str: a0 = pa break cands.add(a0) break print('%s: %d unmarked call sites -> %d candidate prologues %s' % (a.prefix, len(sites), len(cands), sorted(hex(x) for x in cands)[:8])) if a.apply and cands: from idatui.rpcclient import RpcClient c = RpcClient(os.environ['IDATUI_RPC_SOCK'], timeout=60) c.connect() ok = 0 for ea in sorted(cands): try: c.call('define', kind='func', target=hex(ea)) ok += 1 except Exception as e: print(' define %#x failed: %s' % (ea, str(e)[:50])) c.call('save') print(' defined %d/%d' % (ok, len(cands))) return 0 if __name__ == '__main__': raise SystemExit(main())