Files
OpenRoyale/tools/scripts/analyze_libg_callgraph.sh
T
2026-07-09 07:40:42 +02:00

65 lines
3.1 KiB
Bash

#!/usr/bin/env bash
# analyze_libg_callgraph.sh - build the FULL call graph of libg.so from its 60 JNI roots using
# radare2 in a SINGLE session (analyzing each root with `af` then `pdf`), then parse the disassembly
# by pdf's "/ <size>: sym.<name>" headers. This is ~1000x faster than reloading the binary per
# function (the whole 60-root sweep runs in ~2s instead of ~80min)
#
# Requires: radare2 + python3. Reads original_reference/native_libs_original/libg.so.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJ="$(cd "$HERE/../.." && pwd)"
LIB="$PROJ/original_reference/native_libs_original/libg.so"
OUT="$PROJ/analysis/rawdata/libg_callgraph.txt"
command -v r2 >/dev/null || { echo "radare2 not installed (apt install radare2)"; exit 2; }
[ -f "$LIB" ] || { echo "libg.so missing (run extract_apk.sh)"; exit 2; }
python3 - "$LIB" "$OUT" <<'PY'
import subprocess, re, sys
lib, out = sys.argv[1], sys.argv[2]
# JNI root addresses
rab = subprocess.run(["rabin2","-E",lib], capture_output=True, text=True).stdout.splitlines()
roots = {}
for l in rab:
if "Java_com_supercell_titan_" in l:
p = l.split()
roots[p[-1].split("Java_com_supercell_titan_")[-1]] = p[2]
# one r2 session: seek+af+pdf each root
cmds = "".join(f"s {a}; af; pdf;" for a in roots.values())
o = subprocess.run(["r2","-q","-e","scr.color=0","-e","asm.arch=arm","-e","asm.bits=16",
"-c", cmds, lib], capture_output=True, text=True, timeout=600).stdout.splitlines()
hdr = re.compile(r'^/ (\d+): sym\.Java_com_supercell_titan_(\S+?) ')
call = re.compile(r'\b(bl|blx) (sym\.imp\.[A-Za-z0-9_:]+|0x[0-9a-f]+)')
blocks, cur = {}, None
for ln in o:
m = hdr.match(ln)
if m:
cur = {"size": m.group(1), "callees": {}}; blocks[m.group(2)] = cur
elif cur:
cm = call.search(ln)
if cm:
t = cm.group(2); t = t if t.startswith("sym.imp.") else "sub_"+t[2:]
cur["callees"][t] = cur["callees"].get(t,0)+1
# any root whose af merged/anon -> analyze standalone
for name, a in roots.items():
if name in blocks: continue
oo = subprocess.run(["r2","-q","-e","scr.color=0","-e","asm.arch=arm","-e","asm.bits=16",
"-c", f"s {a}; af; afi~size:; pdf", lib], capture_output=True, text=True).stdout.splitlines()
size = next((x.split()[1] for x in oo if x.strip().startswith("size:")), "?")
cs = {}
for l in oo:
cm = call.search(l)
if cm:
t = cm.group(2); t = t if t.startswith("sym.imp.") else "sub_"+t[2:]
cs[t] = cs.get(t,0)+1
blocks[name] = {"size": size, "callees": cs}
L = ["# libg.so call graph from all 60 JNI roots (radare2 single-session af+pdf).",
"# format: <jni_method> @ <addr> size=<bytes> -> callees (sub_<addr> internal, sym.imp.* imports, xN)"]
for name in sorted(blocks):
b = blocks[name]
cs = " ".join(f"{k}(x{v})" for k,v in sorted(b["callees"].items(), key=lambda kv:-kv[1]))
L.append(f"{name:<42} @ {roots.get(name,'?')} size={b['size']}")
L.append(f" -> {cs or '<none>'}")
open(out,"w").write("\n".join(L)+"\n")
print(f"[+] wrote {out} ({len(blocks)}/60 roots)")
PY