Common Frida Hooks
推荐运行环境
- Node.js >= 25(原生执行 TypeScript,无需 ts-node)
- frida > 17
- frida-java-bridge(hook 脚本中
import Java from "frida-java-bridge")
Templates
Frida Hook Runner(Node.js + TypeScript)
1// index.ts — spawn 模式注入脚本,支持超时自动 detach
2import frida from "frida";
3import path from "path";
4
5const TARGET_PACKAGE = "com.example.target"; // 替换为目标包名
6const DEFAULT_TIMEOUT_MS = 15000;
7
8const hookScript = process.argv[2];
9if (!hookScript) {
10 console.error("Usage: node index.ts <hook-script.ts>");
11 process.exit(1);
12}
13const hookPath = path.isAbsolute(hookScript) ? hookScript : path.resolve(process.cwd(), hookScript);
14
15function getInjectTimeoutMs(): number {
16 const rawValue = process.env.INJECT_TIMEOUT_MS;
17 if (!rawValue) return DEFAULT_TIMEOUT_MS;
18
19 const timeoutMs = Number(rawValue);
20 if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return DEFAULT_TIMEOUT_MS;
21
22 return timeoutMs;
23}
24
25function onMessage(message: frida.Message, _data: Buffer | null): void {
26 if (message.type === "send") console.log("[*]", (message as frida.SendMessage).payload);
27 else if (message.type === "error") console.log("[!]", (message as frida.ErrorMessage).stack);
28}
29
30async function compileAgent(): Promise<string> {
31 const compiler = new frida.Compiler();
32 compiler.diagnostics.connect((diag) => console.log("[diag]", diag));
33 return compiler.build(hookPath);
34}
35
36async function main(): Promise<void> {
37 const bundle = await compileAgent();
38 const device = await frida.getUsbDevice();
39 const pid = await device.spawn([TARGET_PACKAGE]);
40 const session = await device.attach(pid);
41 const script = await session.createScript(bundle);
42 const timeoutMs = getInjectTimeoutMs();
43
44 let exitResolve: () => void;
45 const exitPromise = new Promise<void>((resolve) => { exitResolve = resolve; });
46
47 script.message.connect((message, data) => {
48 if (message.type === "send" && (message as frida.SendMessage).payload === "__exit__") {
49 console.log("[*] script requested exit, detaching...");
50 exitResolve();
51 return;
52 }
53 onMessage(message, data);
54 });
55
56 await script.load();
57 await device.resume(pid);
58 console.log(`[*] ${path.basename(hookPath)} loaded. Press Ctrl+C to detach.`);
59
60 await Promise.race([
61 exitPromise,
62 new Promise<void>((resolve) => { process.on("SIGINT", resolve); process.on("SIGTERM", resolve); }),
63 new Promise<void>((resolve) => setTimeout(() => {
64 const timeoutLabel = timeoutMs === DEFAULT_TIMEOUT_MS ? "15s" : `${timeoutMs}ms`;
65 console.log(`[*] timeout (${timeoutLabel}), force detaching...`);
66 resolve();
67 }, timeoutMs)),
68 ]);
69
70 await session.detach();
71 process.exit(0);
72}
73
74main().catch((e) => { console.error("[!]", e); process.exit(1); }).finally(() => {
75 console.log("[*] Force detached.");
76 process.exit(0);
77});
用法说明:
- 环境变量
INJECT_TIMEOUT_MS可覆盖默认 15s 超时 - Hook 脚本内
send("__exit__")可主动触发 detach - 支持 Ctrl+C / SIGTERM 优雅退出
Interceptor 基本模式 — onEnter/onLeave 上下文传值 & 寄存器读写
1Interceptor.attach(ptr(0x0), {
2 onEnter(args) {
3 this.anyValue = args[0]; // same context
4 (this.context as Arm64CpuContext).x1 = ptr(0x1234); // type safe read
5 },
6 onLeave(ret) {
7 if (this.anyValue) { console.log(ret) }
8 }
9})
Hooks
动态加载类全方法 Hook — ClassLoader 拦截 + 批量 hook
1// 拦截动态 dex 加载的类,hook 其所有方法并记录参数/返回值
2import Java from "frida-java-bridge";
3
4const TARGET_CLASS = "com.example.TargetClass"; // 替换为目标类名
5let installed = false;
6
7function hookAllMethods(loader: any): void {
8 if (installed) return;
9 installed = true;
10
11 const factory = Java.ClassFactory.get(loader);
12 const Clazz = factory.use(TARGET_CLASS);
13 const methods = Clazz.class.getDeclaredMethods();
14
15 for (let mi = 0; mi < methods.length; mi++) {
16 const name: string = methods[mi].getName();
17 const overloads = (Clazz as any)[name]?.overloads;
18 if (!overloads) continue;
19
20 for (const overload of overloads) {
21 const methodName = name;
22 overload.implementation = function (...args: any[]) {
23 console.log(`[CALL] ${methodName}(${args.map(a => a?.toString?.() ?? "null").join(", ")})`);
24 const ret = overload.call(this, ...args);
25 console.log(`[RET] ${methodName} => ${ret}`);
26 return ret;
27 };
28 }
29 }
30 console.log(`[+] ${TARGET_CLASS} all methods hooked`);
31}
32
33function tryFindAndHook(targetClass: string): void {
34 if (installed) return;
35 Java.enumerateClassLoaders({
36 onMatch(loader) {
37 if (installed) return;
38 try {
39 loader.loadClass(targetClass);
40 console.log(`[+] found ${targetClass} in loader: ${loader}`);
41 hookAllMethods(loader);
42 } catch (_) { /* not in this loader */ }
43 },
44 onComplete() {}
45 });
46}
47
48Java.perform(() => {
49 if (installed) return;
50
51 // 拦截 DexClassLoader 以捕获动态加载时机
52 try {
53 const DCL = Java.use("dalvik.system.DexClassLoader");
54 DCL.$init.implementation = function (dexPath: string, optimizedDirectory: string, librarySearchPath: string, parent: any) {
55 DCL.$init.call(this, dexPath, optimizedDirectory, librarySearchPath, parent);
56 if (!installed) setImmediate(() => Java.performNow(() => tryFindAndHook(TARGET_CLASS)));
57 };
58 console.log("[+] DexClassLoader.$init hooked");
59 } catch (e) {
60 console.log(`[i] DexClassLoader hook failed: ${e}`);
61 }
62
63 console.log(`[*] Waiting for ${TARGET_CLASS} to be loaded...`);
64});
Module Observer — 第一时间 Hook 目标 so
1// 监听 so 加载,目标模块映射后立即 hook
2import Java from "frida-java-bridge";
3
4Process.attachModuleObserver({
5 onAdded(module: Module) {
6 if (module.name !== "libtarget.so") return; // 替换为目标 so 名
7
8 const targetAddr = module.base.add(0x1000); // 替换为目标偏移
9 Interceptor.attach(targetAddr, {
10 onEnter(args) {
11 send(`JNI_OnLoad called! JavaVM=${args[0]} reserved=${args[1]}`);
12 },
13 onLeave(retval) {
14 send(`JNI_OnLoad returned: ${retval}`);
15 send("__exit__");
16 }
17 });
18 }
19});
异步 Hook — Promise 桥接回调,用于保证 hook 先后加载顺序、类的准确时机加载
1// 核心:用 Promise 把 hook 回调转成 await 接口,resolve 可传值给下游
2import Java from "frida-java-bridge";
3
4let resolveA: (v: string) => void;
5const classALoaded = new Promise<string>((r) => { resolveA = r; });
6
7// 第一步:hook ClassA,加载完成后 resolve 传值
8Java.perform(() => {
9 const ClassA = Java.use("com.example.ClassA");
10 ClassA["init"].implementation = function (...args: any[]) {
11 const result = this["init"](...args);
12 resolveA(result.toString()); // 传值给下游
13 return result;
14 };
15});
16
17// 第二步:ClassB 依赖 ClassA 先加载,必须 await 前者完成后才能 hook
18async function hookClassB() {
19 const valueFromA = await classALoaded; // 阻塞直到 ClassA.init 被调用
20 console.log(`ClassA loaded with: ${valueFromA}, now hooking ClassB...`);
21
22 // await 之后已在 Java 上下文中,大概率不需要再套 Java.perform
23 const ClassB = Java.use("com.example.ClassB");
24 ClassB["process"].implementation = function (...args: any[]) {
25 console.log(`ClassB.process called, depends on: ${valueFromA}`);
26 return this["process"](...args);
27 };
28}
29
30hookClassB();
Hook JNI RegisterNatives
1// Hook JNI RegisterNatives via JNIEnv 函数表(偏移 215,跨版本稳定)
2const env = Java.vm.getEnv();
3const RegisterNatives = env.handle.readPointer().add(215 * Process.pointerSize).readPointer();
4
5Interceptor.attach(RegisterNatives, {
6 onEnter(args) {
7 const env = Java.vm.tryGetEnv();
8 if (!env) return;
9
10 const className = env.getClassName(args[1]);
11 const methods = args[2];
12 const nMethods = args[3].toInt32();
13
14 for (let i = 0; i < nMethods; i++) {
15 const base = methods.add(i * Process.pointerSize * 3);
16 const name = base.readPointer().readCString();
17 const sig = base.add(Process.pointerSize).readPointer().readCString();
18 const fnPtr = base.add(Process.pointerSize * 2).readPointer();
19
20 const mod = Process.findModuleByAddress(fnPtr);
21 const modName = mod ? mod.name : "unknown";
22 const off = mod ? fnPtr.sub(mod.base) : ptr(0);
23
24 console.log(`[RegisterNatives] ${className}.${name}${sig} => ${fnPtr} (${modName}+0x${off.toString(16)})`);
25 }
26 }
27});