#!/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 "/ : sym." 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: @ size= -> callees (sub_ 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 ''}") open(out,"w").write("\n".join(L)+"\n") print(f"[+] wrote {out} ({len(blocks)}/60 roots)") PY