Frida Hook 脚本编写
基于 Frida 17.7.3,Android 逆向实战总结
学了快一周,但是本文还是由ai编写。
后面该学anti-frida了。
一、基本结构
所有脚本都必须包在 Java.perform 里,等 JVM 初始化完成后再执行:
1 2 3 4
| Java.perform(function() { console.log("[*] 脚本已加载"); });
|
二、找到目标类
1 2 3 4
| Java.perform(function() { var MyClass = Java.use("com.example.app.ClassName"); });
|
三、Hook 普通方法
3.1 无参方法
1 2 3 4 5 6 7 8 9 10
| Java.perform(function() { var LoginActivity = Java.use("com.*******.*******.activity.LoginActivity");
LoginActivity.e.overload().implementation = function() { var result = this.e(); console.log("[e()] 返回值 = " + result); return result; }; });
|
3.2 有参方法
1 2 3 4 5 6 7 8 9 10 11 12
| Java.perform(function() { var LoginActivity = Java.use("com.*******.*******.activity.LoginActivity");
LoginActivity.a.overload( 'java.lang.String', 'java.lang.String' ).implementation = function(phone, pwd) { console.log("[a()] phone=" + phone + " pwd=" + pwd); this.a(phone, pwd); }; });
|
3.3 void 方法
1 2 3 4
| 方法名.overload().implementation = function() { this.方法名(); };
|
四、处理重载方法
同一个方法名有多个参数版本时,必须用 .overload() 指定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| Java.perform(function() { var Utility = Java.use("com.common.utils.Utility");
Utility.getValue.overload('java.lang.String').implementation = function(str) { var result = this.getValue(str); console.log("[getValue(1)] in=" + str + " out=" + result); return result; };
Utility.getValue.overload( 'java.lang.String', 'java.lang.String' ).implementation = function(str1, str2) { var result = this.getValue(str1, str2); console.log("[getValue(2)] in1=" + str1 + " in2=" + str2 + " out=" + result); return result; }; });
|
常用参数类型对照:
| Java 类型 |
Frida 写法 |
| String |
'java.lang.String' |
| int |
'int' |
| long |
'long' |
| boolean |
'boolean' |
| byte[] |
'[B' |
| Object |
'java.lang.Object' |
五、Hook 构造函数
构造函数用 $init 表示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Java.perform(function() { var ApiClass = Java.use("com.*******.*******.retrofit.api.b");
ApiClass.$init.implementation = function() { console.log("[构造函数] api.b 被创建");
console.log(Java.use("android.util.Log") .getStackTraceString( Java.use("java.lang.Exception").$new() ) );
this.$init(); }; });
|
1 2 3 4 5 6 7 8 9 10 11
| Java.perform(function() { var TimerClass = Java.use( "com.*******.*******.personal.personinfo.utils.a" );
TimerClass.$init.overload('long', 'long').implementation = function(duration, interval) { console.log("[Timer] duration=" + duration + " interval=" + interval); this.$init(3000, interval); }; });
|
六、读写字段
6.1 静态字段
1 2 3 4 5 6 7 8 9
| Java.perform(function() { var LoginActivity = Java.use("com.*******.*******.activity.LoginActivity");
console.log(LoginActivity.isLoginPage.value);
LoginActivity.isLoginPage.value = true; });
|
6.2 实例字段(在 Hook 回调里)
1 2 3 4 5 6 7 8 9 10
| Java.perform(function() { var LoginActivity = Java.use("com.*******.*******.activity.LoginActivity");
LoginActivity.k.overload().implementation = function() { console.log("手机号 = " + this.l.value); this.l.value = "13900000000"; this.k(); }; });
|
6.3 不知道字段名时,枚举所有字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Java.perform(function() { Java.choose("com.*******.*******.activity.LoginActivity", { onMatch: function(instance) { var fields = instance.class.getDeclaredFields(); fields.forEach(function(field) { field.setAccessible(true); try { console.log(" " + field.getName() + " = " + field.get(instance)); } catch(e) {} }); }, onComplete: function() {} }); });
|
七、堆扫描:Java.choose
主动从堆内存里找到某个类的所有实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| Java.perform(function() { Java.choose("com.*******.*******.activity.LoginActivity", { onMatch: function(instance) { try { if (instance.isFinishing()) return; } catch(e) { return; }
console.log("[找到实例] " + instance);
Java.scheduleOnMainThread(function() { var editText = instance.mobile.value; var String = Java.use("java.lang.String");
editText.setText.overload( 'java.lang.CharSequence', 'android.widget.TextView$BufferType' ).call( editText, String.$new("13900000000"), Java.use("android.widget.TextView$BufferType").EDITABLE.value ); }); }, onComplete: function() { console.log("[扫描完成]"); } }); });
|
注意事项:
1 2 3 4 5 6 7 8
| 1. JADX 显示的字段名 ≠ 运行时真实字段名(混淆导致) → 用 getDeclaredFields() 枚举真实名字
2. 操作 UI 必须在主线程 → Java.scheduleOnMainThread()
3. 堆里可能有已销毁的旧实例 → 用 isFinishing() 过滤
|
八、Native Hook
8.1 Java Hook 和 Native Hook 的区别
1 2 3 4 5 6 7
| Java Hook Native Hook ──────────────────────────────────────────────────────── 操作 ART 方法表 直接修改机器码 需要知道类名和方法名 需要知道函数内存地址 参数是 Java 对象,直接可读 参数是原始指针,需要手动解析 只能 Hook Java 方法 可以 Hook 任意 Native 函数 Java.perform 里写 直接写在外层,不需要 Java.perform
|
8.2 找到 Native 函数地址
1 2 3 4 5 6 7 8 9 10
| var funcAddr = Process.findModuleByName("libfoo.so") .findExportByName("函数名"); console.log("[地址] " + funcAddr);
var barAddr = Process.findModuleByName("libfoo.so") .findExportByName( "Java_sg_vantagepoint_uncrackable2_CodeCheck_bar" );
|
8.3 Interceptor.attach:插桩
原函数还会执行,在前后插入观察点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| Interceptor.attach(barAddr, { onEnter: function(args) { console.log("[bar] onEnter"); console.log(" args[0] = " + args[0]); console.log(" args[1] = " + args[1]); console.log(" args[2] = " + args[2]);
this.byteArray = args[2]; }, onLeave: function(retval) { console.log("[bar] retval = " + retval);
retval.replace(1); } });
|
8.4 Interceptor.replace:完全替换
原函数完全不执行,用新函数取代:
1 2 3 4 5 6 7 8 9
| Interceptor.replace(barAddr, new NativeCallback( function(env, thiz, input) { console.log("[bar] replace,直接返回 true"); return 1; }, 'int', ['pointer', 'pointer', 'pointer'] ));
|
8.5 attach 和 replace 对比
1 2 3 4 5 6 7 8
| Interceptor.attach Interceptor.replace ────────────────────────────────────────────────────────── 原函数还会执行 原函数完全不执行 在原函数前后插入代码 完全替换原函数 有 onEnter 和 onLeave 两个时机 只有一个新函数 retval.replace(1) 篡改返回值 直接 return 1 不需要声明参数类型 必须用 NativeCallback 声明类型 适合:观察、监控、篡改返回值 适合:完全绕过某个函数
|
8.6 NativeCallback 类型表
1
| new NativeCallback(JS函数, 返回值类型, [参数类型列表])
|
| 类型 |
说明 |
'void' |
无返回值 |
'int' |
32位整数,bool 也用这个 |
'pointer' |
指针,JNIEnv* jobject jarray 都是指针 |
'long' |
64位整数 |
'float' |
浮点数 |
JNI 函数参数规律:
1 2 3 4 5 6
| 前两个参数固定: args[0] → JNIEnv* → 'pointer' args[1] → jobject → 'pointer' 后面的参数看具体函数签名: jbyteArray / jstring / jobject → 'pointer' jint / jboolean → 'int'
|
8.7 从 Native 层读取字节数组内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| Interceptor.attach(barAddr, { onEnter: function(args) { this.byteArray = args[2]; }, onLeave: function(retval) { var byteArray = this.byteArray;
Java.perform(function() { var env = Java.vm.getEnv(); var ptr = env.getByteArrayElements(byteArray, null); var len = env.getArrayLength(byteArray); var bytes = ptr.readByteArray(len); var result = ""; new Uint8Array(bytes).forEach(function(b) { result += String.fromCharCode(b); }); console.log("[native] 读取内容 = " + result); }); } });
|
8.8 Java 层和 Native 层篡改返回值的区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 方法名.implementation = function() { return true; }
onLeave: function(retval) { retval.replace(1); }
new NativeCallback(function() { return 1; }, 'int', [...])
|
九、自动追踪 RegisterNatives
9.1 为什么需要追踪 RegisterNatives
1 2 3 4 5 6 7 8
| 隐式注册(可见): 导出表直接有 Java_包名_类名_方法名 → findExportByName 直接找到
显式注册(不可见): env->RegisterNatives(clazz, methods, count) → 导出表里没有任何 Java_ 开头的函数 → 必须 Hook RegisterNatives 才能知道映射关系
|
9.2 Android 12+ 追踪方式(从 libart.so 内部符号)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| var libart = Process.findModuleByName("libart.so");
var matches = libart.enumerateExports().filter(function(exp) { return exp.name.indexOf("RegisterNative") !== -1 && exp.name.indexOf("Callback") === -1 && exp.name.indexOf("Free") === -1 && exp.name.indexOf("Allocation") === -1; }); matches.forEach(function(exp) { console.log("[候选] " + exp.name + " → " + exp.address); });
var registerNative = libart.findExportByName( "_ZN3art11ClassLinker14RegisterNativeEPNS_6ThreadEPNS_9ArtMethodEPKv" );
Interceptor.attach(registerNative, { onEnter: function(args) { try { var fnPtr = args[3]; var module = Process.findModuleByAddress(fnPtr); if (!module) return;
var skipList = [ "libart", "libc", "libandroid", "libmonochrome", "libhoudini", "libopenjdk", "libframework", "libwebview" ]; var skip = skipList.some(function(s) { return module.name.indexOf(s) !== -1; }); if (skip) return;
var offset = fnPtr.sub(module.base); console.log("[RegisterNative]" + " 模块=" + module.name + " 偏移=0x" + offset.toString(16) + " 绝对地址=" + fnPtr); } catch(e) {} } });
|
9.3 拿到偏移之后的标准操作
1 2 3 4 5 6 7 8 9
| Frida 输出:模块=libtarget.so 偏移=0x1234
后续步骤: 1. adb pull 把 so 拉到本地 adb pull /data/app/com.xxx-xxx/lib/arm64/libtarget.so
2. IDA 打开,按 G 输入 0x1234 跳转
3. 结合 Java 层方法名分析函数逻辑
|
9.4 模块偏移 vs 绝对地址
1 2
| 绝对地址:每次启动都不一样(ASLR)→ 不能复用 偏移: 相对 so 基址的固定距离 → IDA 里直接用
|
十、内存读写与扫描
10.1 Memory.scan 扫描特征码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| var module = Process.findModuleByName("libtarget.so");
function strToPattern(str) { return str.split('').map(function(c) { return ("0" + c.charCodeAt(0).toString(16)).slice(-2); }).join(' '); }
Memory.scan(module.base, module.size, strToPattern("target_string"), { onMatch: function(address, size) { console.log("[找到] 地址=" + address + " 偏移=0x" + address.sub(module.base).toString(16)); console.log(" 内容=" + address.readUtf8String()); }, onError: function(reason) {}, onComplete: function() { console.log("[扫描完成]"); } } );
Memory.scan(module.base, module.size, "63 7c 77 7b f2 6b 6f c5", { onMatch: function(address) { console.log("[AES S-Box] " + address); }, onError: function() {}, onComplete: function() {} } );
|
10.2 Memory.scan 能找到 vs 找不到
1 2 3 4 5 6 7 8 9
| 能找到: 明文字符串存在 .rodata 段 算法魔数(AES S-Box、MD5 初始值等) 固定的函数序言字节
找不到: 运行时 strcpy 到栈上的字符串 加密存储的字符串 编译器优化成立即数的常量
|
10.3 读取内存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| var bytes = new Uint8Array(ptr("地址").readByteArray(256));
var hex = "", ascii = "", result = ""; for (var i = 0; i < bytes.length; i++) { hex += ("0" + bytes[i].toString(16)).slice(-2) + " "; ascii += (bytes[i] >= 32 && bytes[i] < 127) ? String.fromCharCode(bytes[i]) : "."; if ((i + 1) % 16 === 0) { result += hex + "| " + ascii + "\n"; hex = ""; ascii = ""; } } console.log(result);
|
10.4 动态找某模块的内存段
1 2 3 4 5 6 7 8 9 10
|
var ranges = Process.enumerateRanges('r--').filter(function(r) { return r.file && r.file.path.indexOf("libtarget") !== -1; }); ranges.forEach(function(range) { console.log("基址=" + range.base + " 大小=" + range.size + " 权限=" + range.protection); });
|
10.5 Memory.alloc 申请内存
1 2 3 4 5 6 7 8 9 10 11 12 13
| var buf = Memory.alloc(256); buf.writeUtf8String("fake_data");
var codeBuf = Memory.alloc(64); Memory.protect(codeBuf, 64, 'rwx'); codeBuf.writeByteArray([0x90, 0x90, 0xC3]);
var fakeStr = Memory.alloc(64); fakeStr.writeUtf8String("fake_token"); retval.replace(fakeStr);
|
10.6 多线程上下文问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| Interceptor.attach(addr, { onEnter: function(args) { this.data = args[0]; }, onLeave: function(retval) { Java.perform(function() { console.log(this.data); }); } });
Interceptor.attach(addr, { onEnter: function(args) { this.data = args[0]; }, onLeave: function(retval) { var data = this.data; Java.perform(function() { console.log(data); }); } });
|
同样的规则适用于:
1 2 3 4
| Java.perform 内部的回调 Java.scheduleOnMainThread 的回调 setTimeout / setInterval 的回调 任何异步嵌套场景
|
十一、vtable Hook
11.1 vtable 是什么
1 2 3 4 5 6 7 8 9
| C++ 每个有虚函数的类,编译器自动生成一张函数指针表:
vtable for Animal: [0] → Animal::speak() 地址 [1] → Animal::move() 地址
每个对象的内存布局: [0~7] → vptr(指向 vtable) [8~n] → 成员变量
|
11.2 vtable Hook vs Interceptor.attach
1 2 3 4 5 6
| Interceptor.attach vtable Hook(正确姿势) ──────────────────────────────────────────────────── 修改函数入口机器码 给单个对象造一张假 vtable 影响所有对该函数的调用 只影响这一个对象实例 无法区分调用来源 天然隔离,其他实例不受影响 适合:监控所有调用 适合:精准控制特定对象
|
11.3 vtable Hook(造假 vtable)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
Java.choose("com.example.SomeClass", { onMatch: function(instance) { var objPtr = instance.handle;
var originalVptr = objPtr.readPointer();
var slotCount = 10; var tableSize = slotCount * Process.pointerSize; var fakeVtable = Memory.alloc(tableSize); Memory.protect(fakeVtable, tableSize, 'rwx');
for (var i = 0; i < slotCount; i++) { var originalSlot = originalVptr .add(i * Process.pointerSize).readPointer(); fakeVtable.add(i * Process.pointerSize) .writePointer(originalSlot); }
var originalFn = originalVptr.readPointer(); var hookFn = new NativeCallback(function(thiz) { console.log("[只有这个对象触发]"); var fn = new NativeFunction(originalFn, 'void', ['pointer']); fn(thiz); }, 'void', ['pointer']);
fakeVtable.writePointer(hookFn);
Memory.protect(objPtr, Process.pointerSize, 'rw-'); objPtr.writePointer(fakeVtable);
console.log("[vtable Hook 已安装] 只影响此对象"); }, onComplete: function() {} });
|
11.4 vtable Hook 使用场景
1 2 3 4 5 6
| 同一个类有多个实例,只想 Hook 其中一个: serverChecker → 假 vtable(Hook) localChecker → 原始 vtable(不动)
分析 C++ 框架内部的对象行为: 找到特定功能对象 → 只监控它的虚函数调用
|
十二、通用 Hook 模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| Java.perform(function() { var TargetClass = Java.use("完整包名.类名");
TargetClass.方法名.overload('参数类型').implementation = function(arg) { console.log("[方法名] 入参 = " + arg);
var result = this.方法名(arg);
console.log("[方法名] 返回值 = " + result);
return result; };
TargetClass.方法名.overload().implementation = function() { console.log("[方法名] 被调用"); this.方法名(); };
console.log("[*] Hook 已安装"); });
|
十三、Frida 启动命令
1 2 3 4 5 6 7 8
| frida-ps -Uai | findstr 关键词
frida -U -p <PID> -l 脚本.js
frida -U -f com.xxx.xxx -l 脚本.js
|
注意:
- 如果spawn 会导致 React Native 线程崩溃,则用attach模式
- attach 前确保 App 已完全启动
十四、常见错误处理
| 错误信息 |
原因 |
解决方法 |
has more than one overload |
方法有多个重载 |
加 .overload('参数类型') |
argument types do not match |
参数类型不匹配 |
枚举方法查看正确的重载 |
cannot read property of undefined |
字段名不对 |
用 getDeclaredFields() 枚举真实字段名 |
You cannot start a load for a destroyed activity |
扫描到已销毁的实例 |
加 isFinishing() 检查 |
Process crashed: Bad access |
spawn 模式 RN 线程冲突 |
改用 attach 模式 |