Skip to content

Instantly share code, notes, and snippets.

@erev0s
Created December 23, 2025 14:01
Show Gist options
  • Select an option

  • Save erev0s/2fda747cc47dd520ee8e7c7d4bde0d46 to your computer and use it in GitHub Desktop.

Select an option

Save erev0s/2fda747cc47dd520ee8e7c7d4bde0d46 to your computer and use it in GitHub Desktop.
How to find which functions are being called in a native library in Android when the application has anti-instrumentation?
'use strict';
/*
App Native Lib Sorter + Hotspot Tracer
Goals:
- Track ALL app-origin native libs in the current process (split APK paths + extracted /data/app libs).
- Hook “boundary APIs” (libc) once, attribute each hit to the calling module + callsite (returnAddress).
- Produce “hotspots” per module (immediate libc considered boundary)
- Optionally install second-stage detailed hooks on top hotspots.
Notes:
- “App-origin” libs are those under /data/app, /data/data, /data/user, or loaded from "*.apk!/lib/".
- Tested only on Android 16/Frida 17
Author:
- "erev0s <projects@erev0s.com>"
*/
///////////////////////////////
// Config
///////////////////////////////
const LOG_NEW_MODULES = true;
// Summary timing
const SUMMARY_AFTER_MS = 10000;
const SUMMARY_EVERY_MS = 30000;
// How many hotspots to show per module in summary
const TOP_CALLSITES_PER_MODULE = 20;
// Cap hotspot map growth per module
const MAX_CALLSITES_PER_MODULE = 3000;
// Second-stage detailed callsite hooks - by default DISABLED!
const ENABLE_DETAILED_HOTSPOT_HOOKS = false;
const DETAILED_HOOK_AFTER_MS = 35000; // when to install (window to collect hotspots first)
const DETAILED_TOP_MODULES = 3; // install detailed hooks only for top N modules
const DETAILED_TOP_CALLSITES = 25; // per module
const DETAILED_LOG_FIRST = 20; // log first N hits per callsite
const DETAILED_LOG_EVERY = 500; // then every N hits
// App lib rescans (in case dlopen hook is not available/reliable)
const POLL_SCAN_MS = 2000;
///////////////////////////////
// Timer polyfill (setInterval)
///////////////////////////////
(function installTimerPolyfills() {
if (typeof setTimeout !== "function") {
throw new Error("setTimeout is not available in this Frida JS runtime");
}
if (typeof setInterval !== "function") {
const _intervals = new Map();
let _nextId = 1;
globalThis.setInterval = function (fn, ms) {
const id = _nextId++;
function tick() {
if (!_intervals.has(id)) return;
try { fn(); } catch (e) { console.error(e.stack || e); }
_intervals.set(id, setTimeout(tick, ms));
}
_intervals.set(id, setTimeout(tick, ms));
return id;
};
globalThis.clearInterval = function (id) {
const t = _intervals.get(id);
if (t) {
try { clearTimeout(t); } catch (_) {}
}
_intervals.delete(id);
};
}
if (typeof clearInterval !== "function") {
globalThis.clearInterval = function (_) {};
}
})();
///////////////////////////////
// Helpers / compat
///////////////////////////////
function enumerateModulesCompat() {
if (typeof Process.enumerateModules === "function") return Process.enumerateModules();
if (typeof Process.enumerateModulesSync === "function") return Process.enumerateModulesSync();
throw new Error("No Process.enumerateModules* API available");
}
function safeReadCString(p) {
try { return p.readCString(); } catch (_) { return null; }
}
function safeDebugSym(addr) {
try { return DebugSymbol.fromAddress(addr).toString(); }
catch (_) { return addr.toString(); }
}
function moduleId(m) {
return m.name + "@" + m.base.toString();
}
function splitApkPath(path) {
const bang = path.indexOf("!/");
if (bang === -1) return { apkPath: null, innerPath: null };
return { apkPath: path.slice(0, bang), innerPath: path.slice(bang + 2) };
}
function isAppOriginSo(path) {
if (!path || !path.endsWith(".so")) return false;
// Exclude OS/runtime
const sysPrefixes = ["/apex/", "/system/", "/vendor/", "/product/", "/odm/", "/system_ext/"];
for (const p of sysPrefixes) if (path.startsWith(p)) return false;
// Include app-origin - am i missing any?
return path.startsWith("/data/app/") ||
path.startsWith("/data/data/") ||
path.startsWith("/data/user/") ||
path.includes(".apk!/lib/");
}
///////////////////////////////
// Process name (best-effort)
///////////////////////////////
let PROCESS_LABEL = null;
function trySetProcessLabel() {
// 1) If Process.name exists
try {
if (typeof Process !== "undefined" && Process && Process.name) {
PROCESS_LABEL = Process.name;
return;
}
} catch (_) {}
// 2) Try Java (ActivityThread)
try {
if (typeof Java === "undefined" || !Java.available) return;
Java.perform(function () {
try {
const ActivityThread = Java.use("android.app.ActivityThread");
// currentProcessName exists on newer Android
if (ActivityThread.currentProcessName) {
const n = ActivityThread.currentProcessName();
if (n) PROCESS_LABEL = String(n);
return;
}
} catch (_) {}
try {
const ActivityThread = Java.use("android.app.ActivityThread");
const app = ActivityThread.currentApplication();
if (app) {
const ctx = app.getApplicationContext();
if (ctx) {
const pn = ctx.getPackageName();
if (pn) PROCESS_LABEL = String(pn);
}
}
} catch (_) {}
});
} catch (_) {}
}
function procLabel() {
if (PROCESS_LABEL) return PROCESS_LABEL;
return "unknown";
}
///////////////////////////////
// State: app libs + stats
///////////////////////////////
const appModulesById = Object.create(null);
const appModuleIds = new Set();
// id -> { total, apiCounts, callsites, callsiteCount }
const statsById = Object.create(null);
function ensureStats(id) {
if (!statsById[id]) {
statsById[id] = {
total: 0,
apiCounts: Object.create(null),
callsites: Object.create(null), // addrStr -> { count, sym, apis:Set }
callsiteCount: 0,
};
}
return statsById[id];
}
function registerIfAppLib(m, reason) {
if (!m || !m.path) return;
if (!isAppOriginSo(m.path)) return;
const id = moduleId(m);
if (appModuleIds.has(id)) return;
const sp = splitApkPath(m.path);
appModuleIds.add(id);
appModulesById[id] = {
name: m.name,
base: m.base.toString(),
size: (m.size >>> 0),
path: m.path,
apkPath: sp.apkPath,
innerPath: sp.innerPath,
firstSeen: Date.now(),
reason: reason || "unknown",
};
ensureStats(id);
if (LOG_NEW_MODULES) {
console.log(`[+] App lib loaded: ${m.name} @ ${m.base} (size=${m.size})`);
if (sp.apkPath) {
console.log(` split: ${sp.apkPath}`);
console.log(` path : ${sp.innerPath}`);
} else {
console.log(` path : ${m.path}`);
}
}
}
function scanLoadedModules(reason) {
const mods = enumerateModulesCompat();
for (const m of mods) registerIfAppLib(m, reason || "scan");
}
///////////////////////////////
// dlopen hook (best-effort)
///////////////////////////////
function hookDlopenAny() {
// android_dlopen_ext should live in linker/linker64; resolve globally.
let dlopenExt = null;
try { dlopenExt = Module.findExportByName(null, "android_dlopen_ext"); } catch (_) {}
if (!dlopenExt) {
console.log("[!] android_dlopen_ext not found; relying on polling scans.");
return false;
}
console.log("[*] Hooking android_dlopen_ext @", dlopenExt);
Interceptor.attach(dlopenExt, {
onEnter(args) {
this.path = safeReadCString(args[0]);
},
onLeave() {
const p = this.path;
if (!p || !p.endsWith(".so")) return;
setTimeout(() => scanLoadedModules("dlopen-scan"), 50);
}
});
return true;
}
///////////////////////////////
// libc symbol resolution
///////////////////////////////
let libcMap = null;
let libcMapBuilt = false;
function buildLibcExportMapIfPossible() {
if (libcMapBuilt) return libcMap;
libcMapBuilt = true;
libcMap = Object.create(null);
let libc = null;
try { libc = Process.getModuleByName("libc.so"); } catch (_) { libc = null; }
if (!libc) {
// libc not found (yet)
return libcMap;
}
if (typeof libc.enumerateExports !== "function") {
// enumerateExports missing in this runtime
return libcMap;
}
try {
const exps = libc.enumerateExports();
for (const e of exps) {
if (e.type === "function") libcMap[e.name] = e.address;
}
} catch (_) {
// ignore
}
return libcMap;
}
const resolver = (() => {
try { return new ApiResolver("module"); } catch (_) { return null; }
})();
function resolveAnySymbol(symbolNames) {
// Returns first address found for any of symbolNames in libc / global space.
// Strategy:
// 1) libc.enumerateExports map (fast, if available)
// 2) ApiResolver('module') exports:libc.so!sym
// 3) Module.findExportByName("libc.so", sym) then global
buildLibcExportMapIfPossible();
for (const s of symbolNames) {
if (libcMap && libcMap[s]) return libcMap[s];
}
if (resolver) {
for (const s of symbolNames) {
try {
const ms = resolver.enumerateMatches(`exports:libc.so!${s}`);
if (ms && ms.length > 0) return ms[0].address;
} catch (_) {}
}
}
for (const s of symbolNames) {
try {
const a1 = Module.findExportByName("libc.so", s);
if (a1) return a1;
} catch (_) {}
try {
const a2 = Module.findExportByName(null, s);
if (a2) return a2;
} catch (_) {}
}
return null;
}
///////////////////////////////
// Boundary API hook + per-module hotspots
///////////////////////////////
const BOUNDARY_TARGETS = [
// file I/O
{ label: "open", syms: ["open", "open64", "__open_2"] },
{ label: "openat", syms: ["openat", "__openat", "__openat2", "openat2"] },
{ label: "read", syms: ["read", "__read_chk"] },
{ label: "pread64", syms: ["pread64", "pread"] },
{ label: "readv", syms: ["readv"] },
{ label: "write", syms: ["write", "__write_chk"] },
{ label: "pwrite64", syms: ["pwrite64", "pwrite"] },
{ label: "writev", syms: ["writev"] },
// socket I/O
{ label: "connect", syms: ["connect"] },
{ label: "send", syms: ["send", "sendto", "sendmsg", "sendmmsg"] },
{ label: "recv", syms: ["recv", "recvfrom", "recvmsg", "recvmmsg"] },
// misc
{ label: "ioctl", syms: ["ioctl"] },
{ label: "prctl", syms: ["prctl"] },
{ label: "process_vm_readv", syms: ["process_vm_readv"] },
{ label: "process_vm_writev", syms: ["process_vm_writev"] },
];
let hookedBoundaryCount = 0;
let attemptedBoundaryCount = 0;
function hookBoundaryApis() {
hookedBoundaryCount = 0;
attemptedBoundaryCount = 0;
for (const t of BOUNDARY_TARGETS) {
attemptedBoundaryCount++;
const addr = resolveAnySymbol(t.syms);
if (!addr) continue;
hookedBoundaryCount++;
Interceptor.attach(addr, {
onEnter() {
const ra = this.returnAddress;
if (!ra) return;
let m = null;
try { m = Process.findModuleByAddress(ra); } catch (_) { return; }
if (!m) return;
const id = moduleId(m);
// Lazy register if it’s app-origin
if (!appModuleIds.has(id) && isAppOriginSo(m.path)) registerIfAppLib(m, "lazy");
if (!appModuleIds.has(id)) return;
const st = ensureStats(id);
st.total++;
st.apiCounts[t.label] = (st.apiCounts[t.label] || 0) + 1;
const addrStr = ra.toString();
let cs = st.callsites[addrStr];
if (!cs) {
if (st.callsiteCount >= MAX_CALLSITES_PER_MODULE) return;
cs = st.callsites[addrStr] = {
count: 0,
sym: safeDebugSym(ra),
apis: new Set()
};
st.callsiteCount++;
}
cs.count++;
cs.apis.add(t.label);
}
});
}
console.log(`[*] Hooked ${hookedBoundaryCount}/${attemptedBoundaryCount} boundary targets (libc-resolved)`);
}
///////////////////////////////
// Summary / sorting
///////////////////////////////
function sortedModules(sortMode) {
const list = [];
for (const id of appModuleIds) {
const mi = appModulesById[id];
const st = statsById[id] || { total: 0, apiCounts: {} };
list.push({
id,
name: mi.name,
base: mi.base,
size: mi.size,
calls: st.total || 0,
path: mi.path,
apkPath: mi.apkPath,
innerPath: mi.innerPath,
});
}
if (sortMode === "size") {
list.sort((a, b) => (b.size - a.size) || (b.calls - a.calls));
} else {
list.sort((a, b) => (b.calls - a.calls) || (b.size - a.size));
}
return list;
}
function topCallsitesForModule(id, n) {
const st = statsById[id];
if (!st) return [];
const entries = Object.keys(st.callsites).map(k => {
const h = st.callsites[k];
return { addrStr: k, sym: h.sym, count: h.count, apis: Array.from(h.apis) };
});
entries.sort((a, b) => b.count - a.count);
return entries.slice(0, n);
}
function printSummary(sortMode) {
scanLoadedModules("periodic-scan");
const mods = sortedModules(sortMode || "calls");
console.log("\n================ Native libs summary ================");
console.log(`Proc: ${procLabel()} | libsTracked=${mods.length} | boundaryHooks=${hookedBoundaryCount}/${attemptedBoundaryCount}`);
if (mods.length === 0) {
console.log("No app-origin .so modules observed yet (or wrong process).");
console.log("====================================================\n");
return;
}
for (const m of mods) {
console.log(`[*] ${m.name} @ ${m.base} | size=${m.size} | boundaryCalls=${m.calls}`);
if (m.apkPath) {
console.log(` split: ${m.apkPath}`);
console.log(` path : ${m.innerPath}`);
} else {
console.log(` path : ${m.path}`);
}
const hs = topCallsitesForModule(m.id, TOP_CALLSITES_PER_MODULE);
if (hs.length > 0) {
for (const h of hs) {
console.log(` [H] ${h.count} calls @ ${h.addrStr} -> ${h.sym} | APIs: ${h.apis.join(", ")}`);
}
}
}
console.log("====================================================\n");
}
///////////////////////////////
// Second-stage detailed hotspot hooks
///////////////////////////////
function installDetailedHooksOnTopHotspots() {
if (!ENABLE_DETAILED_HOTSPOT_HOOKS) return;
const mods = sortedModules("calls").filter(m => m.calls > 0);
if (mods.length === 0) {
console.log("[*] Detailed hooks: no hotspot data collected (no boundary calls observed).");
return;
}
const chosen = mods.slice(0, Math.min(DETAILED_TOP_MODULES, mods.length));
let totalHooks = 0;
console.log(`[*] Installing detailed hooks for top ${chosen.length} modules`);
for (const m of chosen) {
const hs = topCallsitesForModule(m.id, DETAILED_TOP_CALLSITES);
if (hs.length === 0) continue;
console.log(`[*] Module ${m.name}: installing ${hs.length} callsite hooks`);
for (const e of hs) {
const addr = ptr(e.addrStr);
const label = e.sym;
const apis = e.apis.slice();
let hits = 0;
totalHooks++;
Interceptor.attach(addr, {
onEnter() {
hits++;
if (hits > DETAILED_LOG_FIRST && (hits % DETAILED_LOG_EVERY) !== 0) return;
const tid = (typeof Process.getCurrentThreadId === "function")
? Process.getCurrentThreadId()
: -1;
console.log(`\n[HOT] ${m.name} :: ${label} @ ${addr} | hit #${hits} | thread ${tid} | APIs: ${apis.join(", ")}`);
// Generic arm64 registers (best-effort)
try {
console.log(
" x0=", this.context.x0,
"x1=", this.context.x1,
"x2=", this.context.x2,
"x3=", this.context.x3
);
} catch (_) {}
}
});
}
}
console.log(`[*] Detailed hooks installed: ${totalHooks}`);
}
///////////////////////////////
// Main
///////////////////////////////
console.log("[*] Starting app-native-libs hotspot tracer…");
trySetProcessLabel();
scanLoadedModules("initial-scan");
hookDlopenAny();
// Hook boundary APIs after a short delay (libc/linker may not be fully ready at very early spawn)
setTimeout(() => {
// refresh process label if Java is available
trySetProcessLabel();
hookBoundaryApis();
}, 800);
// Periodic rescans and summaries
setInterval(() => scanLoadedModules("poll-scan"), POLL_SCAN_MS);
setTimeout(() => printSummary("calls"), SUMMARY_AFTER_MS);
setInterval(() => printSummary("calls"), SUMMARY_EVERY_MS);
// Install detailed hooks after collection window
setTimeout(() => installDetailedHooksOnTopHotspots(), DETAILED_HOOK_AFTER_MS);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment