#!/usr/bin/env python3
"""Identify shared runtime routines across the TAs and propagate one name to all of them.
These twelve TAs are built against the same PSP TEE runtime, so the same helper appears
byte-identical in many of them. Naming such a routine once and propagating it by content
hash is far better leverage than renaming 384 anonymous functions one at a time, and it is
verifiable: a cluster is only accepted when the bytes match exactly.
Two modes:
dump --prefix X --body B : record IDA's function list + per-function content hash
report : cluster across all dumps, rank by how many TAs share it
apply --prefix X : rename that TA's members of every named cluster
Cluster names live in fw/libnames.json ({hash: name}), filled in from inspection, so the
mapping is auditable rather than implicit.
usage: IDATUI_RPC_SOCK=... ta_libmatch.py dump --prefix gcp --body
ta_libmatch.py report
IDATUI_RPC_SOCK=... ta_libmatch.py apply --prefix gcp
"""
import argparse
import glob
import hashlib
import json
import os
import sys
import tempfile
CWD = os.path.expanduser('~/dev/ida-tui-maybe')
sys.path.insert(0, CWD)
from idatui.rpcclient import RpcClient # noqa: E402
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DUMPS = os.path.join(REPO, 'fw', 'libdumps')
NAMES = os.path.join(REPO, 'fw', 'libnames.json')
BASE = 0x400100
def connect():
return RpcClient(os.environ['IDATUI_RPC_SOCK'], timeout=60)
def cmd_dump(a):
c = connect()
c.connect()
fs = c.call('functions', filter='', limit=2000)
body = open(a.body, 'rb').read()
out = []
for f in fs:
ea, sz, nm = f.get('ea'), f.get('size') or 0, f.get('name')
off = ea - BASE
if off < 0 or sz <= 0 or off + sz > len(body):
continue
h = hashlib.sha256(body[off:off + sz]).hexdigest()
out.append(dict(ea=ea, size=sz, name=nm, hash=h))
os.makedirs(DUMPS, exist_ok=True)
json.dump(out, open(os.path.join(DUMPS, '%s.json' % a.prefix), 'w'), indent=1)
print('%s: %d functions dumped (%d hashable)' % (a.prefix, len(fs), len(out)))
return 0
def load_dumps():
d = {}
for p in glob.glob(os.path.join(DUMPS, '*.json')):
d[os.path.basename(p)[:-5]] = json.load(open(p))
return d
def cmd_report(a):
dumps = load_dumps()
clusters = {}
for ta, fns in dumps.items():
for f in fns:
clusters.setdefault(f['hash'], []).append((ta, f))
shared = {h: v for h, v in clusters.items() if len({x[0] for x in v}) >= 2}
print('TAs dumped: %d ; functions: %d ; distinct bodies: %d ; shared across >=2 TAs: %d'
% (len(dumps), sum(len(v) for v in dumps.values()), len(clusters), len(shared)))
named = json.load(open(NAMES)) if os.path.exists(NAMES) else {}
rows = sorted(shared.items(), key=lambda kv: -len({x[0] for x in kv[1]}))
print('\n%-12s %-5s %-5s %-8s %s' % ('hash', 'TAs', 'size', 'named?', 'example names'))
for h, v in rows[:40]:
tas = sorted({x[0] for x in v})
ex = sorted({x[1]['name'] for x in v})[:3]
print('%-12s %-5d %-5d %-8s %s'
% (h[:10], len(tas), v[0][1]['size'],
named.get(h, '-')[:8], ', '.join(ex)[:58]))
unnamed = [h for h, v in rows if h not in named]
print('\nshared clusters still unnamed: %d (covering %d function instances)'
% (len(unnamed), sum(len(clusters[h]) for h in unnamed)))
return 0
def cmd_apply(a):
names = json.load(open(NAMES))
dumps = load_dumps()
fns = dumps.get(a.prefix, [])
syms = [dict(addr=f['ea'], name='%s_%s' % (a.prefix, names[f['hash']]))
for f in fns if f['hash'] in names and f['name'].startswith('sub_')]
if not syms:
print('%s: nothing to apply' % a.prefix)
return 0
c = connect()
c.connect()
with tempfile.NamedTemporaryFile('w', suffix='.json', delete=False) as fh:
json.dump(syms, fh)
p = fh.name
r = c.call('rename_many', file=p) or {}
m = r.get('rename_many', r)
os.unlink(p)
c.call('save')
print('%s: applied %s/%s shared-library names' % (a.prefix, m.get('ok'),
m.get('requested')))
return 0
def main():
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest='mode', required=True)
d = sub.add_parser('dump')
d.add_argument('--prefix', required=True)
d.add_argument('--body', required=True)
d.set_defaults(fn=cmd_dump)
r = sub.add_parser('report')
r.set_defaults(fn=cmd_report)
p = sub.add_parser('apply')
p.add_argument('--prefix', required=True)
p.set_defaults(fn=cmd_apply)
a = ap.parse_args()
return a.fn(a)
if __name__ == '__main__':
raise SystemExit(main())