Frida是一款基于JS代码编写的Hook框架,易用且可跨平台。本文不赘述其背景,直入实战——聚焦Android平台下Frida在Java层、Native层的深度Hook技巧,以及对动态加载Dex等加固/插件化场景的应对策略。
我们就直接步入正题。环境配置好后,需通过启动命令注入hook脚本。这里补充一个常被忽略但极其实用的命令:
frida -U -F -l hook.js
当你觉得每次都要手动查进程名麻烦时,-F 参数可替代进程名。它代表“附加到前台应用”,只需将目标APP置于最前端,Frida便会自动附加到当前活跃的进程,省去手动确认包名的步骤。
一、Java层Hook核心模式
1. 获取类与执行沙箱
要Hook Java方法,必须先获取目标类的引用,并确保操作在正确的类加载上下文中进行:
Java.perform(function() {
const Map = Java.use("java.util.Map");
Map.put.implementation = function(key, value) {
console.log("[+] put called with key:", key, "value:", value);
return this.put(key, value);
};
});
⚠️ 注意:Java.use("类名") 必须包裹在 Java.perform() 回调中。这是Frida操作Java环境的安全沙箱,确保使用APP自身的类加载器,避免因类加载器不一致导致ClassNotFoundException或Hook失效。
2. 处理重载方法
Java方法重载要求明确指定参数类型。例如Hook MMKV的decodeString:
MMKV.decodeString.overload('long', 'java.lang.String', 'java.lang.String').implementation = function(handle, key, defaultValue) {
console.log(" decodeString: key =", key);
return this.decodeString(handle, key, defaultValue);
};
若需Hook所有重载版本,可遍历overloads数组:
const ClassName = Java.use("类名");
for (let i = 0; i < ClassName.MethodName.overloads.length; i++) {
ClassName.MethodName.overloads[i].implementation = function() {
console.log(" Method called with", arguments.length, "args");
// 使用 arguments 判断实际参数类型与个数
};
}
3. 构造函数与对象实例化
构造函数通过$init标识:
Java.perform(function() {
const TargetClass = Java.use("com.example.Target");
TargetClass.$init.implementation = function() {
console.log("[+] TargetClass instantiated");
return this.$init.apply(this, arguments);
};
});
实例化新对象则使用$new():
const TargetClass = Java.use("com.example.Target");
const instance = TargetClass.$new("param1", 123);
const result = instance.someMethod();
4. 主动调用静态/非静态方法
-
静态方法:直接通过类对象调用
const Utils = Java.use("com.example.Utils");
Utils.encrypt("data"); // 无需实例
-
非静态方法:需先获取运行时实例(两种方式)
方式一:从Java堆中查找现有实例
Java.choose("com.example.Manager", {
onMatch: function(instance) {
console.log("- Found instance:", instance);
const ret = instance.doWork("input");
},
onComplete: function() {}
});
方式二:主动创建新实例
const Manager = Java.use("com.example.Manager");
const instance = Manager.$new();
instance.doWork("input");
5. 字段(Field)Hook
-
静态字段:直接赋值
const Config = Java.use("com.example.Config");
Config.API_URL.value = "https://test.example.com";
-
非静态字段:需结合Java.choose
Java.choose("com.example.User", {
onMatch: function(obj) {
console.log("old name:", obj.name.value);
obj.name.value = "Hacker";
// 字段名与方法名冲突时加下划线前缀
obj._token.value = "fake_token_123";
},
onComplete: function() {}
});
6. 内部类与匿名类
内部类名用$连接;匿名类由编译器生成(如Test$1):
// Hook 内部类
const InnerClass = Java.use("com.example.Outer$Inner");
InnerClass.method.implementation = function() { /* ... */ };
// Hook 匿名类(需反编译确认类名)
const Anonymous = Java.use("com.example.Test$1");
Anonymous.run.implementation = function() { /* ... */ };
7. 枚举类与方法(反射增强)
同步枚举所有已加载类(阻塞式):
Java.perform(function() {
const classes = Java.enumerateLoadedClassesSync();
for (let i = 0; i < classes.length; i++) {
if (classes[i].indexOf("com.yourapp") !== -1) {
const clazz = Java.use(classes[i]);
const methods = clazz.class.getDeclaredMethods();
methods.forEach(m => console.log("[METHOD]", m.toString()));
}
}
});
异步枚举(推荐用于大型APP,避免卡顿):
Java.enumerateLoadedClasses({
onMatch: function(name, handle) {
if (name.indexOf("com.yourapp") !== -1) {
const clazz = Java.use(name);
console.log("[CLASS]", name);
}
},
onComplete: function() {}
});
8. 动态加载Dex的Hook
动态Dex(如RePlugin、Tinker、ClassLoader加载)中的类不会出现在初始enumerateLoadedClasses中。需枚举所有类加载器并尝试加载:
function hookDex() {
Java.perform(function() {
Java.enumerateClassLoaders({
onMatch: function(loader) {
try {
if (loader.loadClass("com.plugin.DynamicClass")) {
Java.classFactory.loader = loader;
const clazz = Java.use("com.plugin.DynamicClass");
clazz.doSomething.implementation = function() {
console.log("[+] DynamicClass hooked!");
};
}
} catch (e) {
// ignore
}
},
onComplete: function() {}
});
});
}
💡 提示:该方法依赖loader.loadClass()触发类加载。若目标类已被加载但未被Java.enumerateLoadedClasses捕获,此方式仍有效。
9. 特殊类型参数处理(Map、Context等)
当方法参数为Map、Context等复杂类型时,可直接调用其Java方法:
TargetClass.onWakeMap.implementation = function(map) {
console.log("[MAP KEYS]");
const entrySet = map.entrySet();
const iterator = entrySet.iterator();
while (iterator.hasNext()) {
const entry = iterator.next();
console.log(" Key:", entry.getKey(), "Value:", entry.getValue());
}
// 修改参数
const Integer = Java.use("java.lang.Integer");
map.put("2", Integer.$new(999));
// 调用原方法
this.onWakeMap(map);
};
10. 获取调用堆栈
-
Java层堆栈(推荐):
function printStackTrace() {
const stackTrace = Java.use("android.util.Log").getStackTraceString(
Java.use("java.lang.Throwable").$new()
);
console.log("Java Stack:\n" + stackTrace);
}
-
Native层堆栈(需Interceptor.attach):
Interceptor.attach(funcAddr, {
onEnter: function(args) {
console.log("[STACK TRACE]");
const bt = Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0, 10);
bt.forEach((addr, i) => {
const symbol = DebugSymbol.fromAddress(addr) || "unknown";
console.log(` [${i}] ${addr} -> ${symbol}`);
});
}
});
11. Hook系统API对抗混淆
当目标方法名被混淆时,可转而Hook其调用的系统API,例如:
java.util.HashMap.put → 监控加密参数写入
javax.crypto.Cipher.doFinal → 拦截明文/密文
okhttp3.Request.Builder.build → 抓取请求体
常见高价值系统API包括:
- 集合操作:
Map.put, ArrayList.add, ConcurrentHashMap.put
- 字符串/JSON:
StringBuilder.append, JSONObject.$init, URLEncoder.encode
- 加密:
MessageDigest.getInstance, Cipher.doFinal
- 网络:
HttpURLConnection.connect, OkHttpClient.newCall
- 反射:
Class.forName, Method.invoke
- Native加载:
System.loadLibrary, DexClassLoader.loadClass
二、Native层(so)Hook进阶
1. 函数地址定位
-
基址+偏移(需确认指令集):
const libBase = Module.findBaseAddress("libxxx.so");
const funcAddr = libBase.add(0x166C); // ARM无需+1;Thumb需+1
-
导出函数名查找(更稳定):
const funcAddr = Module.findExportByName("libxxx.so", "target_func");
2. Interceptor.attach基础用法
Interceptor.attach(funcAddr, {
onEnter: function(args) {
console.log("[+] args[0] =", args[0].toString());
// this.context.x0, x1... 可读写寄存器
this.inputPtr = args[0]; // 保存供onLeave使用
},
onLeave: function(retval) {
console.log("[-] return =", retval.toString());
// 修改返回值(如jstring)
const env = Java.vm.tryGetEnv();
if (env) {
const newStr = env.newStringUtf("modified");
retval.replace(newStr);
}
}
});
3. 参数/返回值解析技巧
4. JNI关键函数Hook:RegisterNatives
RegisterNatives是JNI函数注册入口,Hook它可发现所有Native方法绑定关系:
function find_RegisterNatives() {
const symbols = Module.enumerateSymbolsSync("libart.so");
for (let i = 0; i < symbols.length; i++) {
const sym = symbols[i];
if (sym.name.includes("art") &&
sym.name.includes("JNI") &&
sym.name.includes("RegisterNatives") &&
!sym.name.includes("CheckJNI")) {
console.log(" Found RegisterNatives at:", sym.address, sym.name);
hook_RegisterNatives(sym.address);
}
}
}
function hook_RegisterNatives(addr) {
Interceptor.attach(addr, {
onEnter: function(args) {
const jclass = args[1];
const class_name = Java.vm.tryGetEnv().getClassName(jclass);
const methods_ptr = ptr(args[2]);
const method_count = parseInt(args[3]);
for (let i = 0; i < method_count; i++) {
const name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
const sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
const fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));
const name = Memory.readCString(name_ptr);
const sig = Memory.readCString(sig_ptr);
const symbol = DebugSymbol.fromAddress(fnPtr_ptr);
console.log("[JNI] class:", class_name, "method:", name, "sig:", sig, "addr:", fnPtr_ptr, "symbol:", symbol);
}
}
});
}
🔍 提示:libart.so路径因Android版本而异:
- Android 12+:
/apex/com.android.art/lib64/libart.so
- Android 10+(64位):
/apex/com.android.runtime/lib64/libart.so
- Android <10:
/system/lib64/libart.so
5. Native主动调用
const libBase = Module.findBaseAddress("libcrypto.so");
const funcAddr = libBase.add(0x906c0);
// 声明函数指针:返回pointer,参数为两个pointer
const aesFunc = new NativeFunction(funcAddr, 'pointer', ['pointer', 'pointer']);
const input = Memory.allocUtf8String("plaintext");
const key = Memory.allocUtf8String("secretkey");
const result = aesFunc(input, key).readCString();
console.log("[AES RESULT]:", result);
6. 文件写入(两种方式)
-
Frida File API(简洁):
const file = new File("/sdcard/frida_log.txt", "w");
file.write("Hook triggered at " + new Date().toISOString() + "\n");
file.flush();
file.close();
-
libc系统调用(灵活):
const fopen = new NativeFunction(Module.findExportByName("libc.so", "fopen"), 'pointer', ['pointer', 'pointer']);
const fputs = new NativeFunction(Module.findExportByName("libc.so", "fputs"), 'int', ['pointer', 'pointer']);
const fclose = new NativeFunction(Module.findExportByName("libc.so", "fclose"), 'int', ['pointer']);
const filename = Memory.allocUtf8String("/sdcard/frida_libc.txt");
const mode = Memory.allocUtf8String("w");
const fp = fopen(filename, mode);
const content = Memory.allocUtf8String("Hello from libc!\n");
fputs(content, fp);
fclose(fp);
7. 寄存器级Hook与修改
Hook BLR X23指令,动态追踪跳转目标:
Interceptor.attach(targetAddr, {
onEnter: function(args) {
const targetAddr = this.context.x23;
console.log("[X23] target address: 0x" + targetAddr.toString(16));
const module = Process.findModuleByAddress(targetAddr);
if (module) {
console.log("→ Module:", module.name, "base:", module.base, "offset: 0x" + ptr(targetAddr).sub(module.base).toString(16));
}
}
});
修改寄存器值(如替换字符串指针):
Interceptor.attach(funcAddr, {
onEnter: function(args) {
// 替换x1为自定义字符串
const newStr = Memory.allocUtf8String("Hacked by Frida!");
this.context.x1 = newStr;
}
});
三、实战建议与避坑指南
- 动态Dex场景:优先使用
Java.enumerateClassLoaders + loader.loadClass,而非等待类自动加载。
- 混淆对抗:系统API Hook比业务方法Hook更稳定,尤其适用于加密、网络、存储模块。
- so层分析:善用
Module.enumerateExports、enumerateImports、enumerateSymbols三者互补(见下表):

- 寄存器Hook:ARM64中X0-X7传参,X0同时为返回值;修改
this.context.x0可劫持返回值。
- 内存操作:
Memory.writeByteArray写入字节,Memory.allocUtf8String分配C字符串,ptr()转换地址类型。
- 调试技巧:
console.log()输出有限,关键数据建议写入文件或用hexdump可视化。
如需深入学习Android逆向技术栈,可参考云栈社区提供的 安全/渗透/逆向 专题资源库,涵盖反编译、调试、漏洞利用、加固脱壳等完整知识链路。