[Android 逆向整理笔记] Frida
妈的终于考过科目三了👼
Frida 好大一坨,而且在写 Native 层 Hook 的时候突然发现好像 so 层的玩意也得整理一下...
以及在查资料的时候才发现,原来新版本的 jadx-gui 已经可以一键复制出对应函数的 frida 和 xposed 片段了,于是火速更新了 jadx-gui
简介
Frida 是一个用于在运行时分析、修改和控制应用程序的开源插桩工具,可以用于 Android、Linux、Windows、IOS 等平台上,Frida 分为两部分,服务端运行在目标机上,通过注入进程的方式来实现劫持应用函数,另一部分运行在自己操作的主机上。
Xposed 是基于 zygote 的,frida 则是基于 ptrace。frida 用 ptrace 跟踪目标进程,然后在目标进程中找到存放 frida-agent 的空间,剩下的操作就由 frida-agent 来实现了。
常用 Hook 方法 (Java 层)
记一下,frida-server 默认端口是 27042,adb 抓日志用 logcat |grep "D.filter",D 是级别,filter 是标签
基本 API
API名称 | 描述 |
---|---|
Java.use(className) |
获取指定的 Java 类并使其在 JavaScript 代码中可用。 |
Java.perform(callback) |
确保回调函数在 Java 的主线程上执行。 |
Java.choose(className, callbacks) |
枚举指定类的所有实例。 |
Java.cast(obj, cls) |
将一个 Java 对象转换成另一个 Java 类的实例。 |
Java.enumerateLoadedClasses(callbacks) |
枚举进程中已经加载的所有 Java 类。 |
Java.enumerateClassLoaders(callbacks) |
枚举进程中存在的所有 Java 类加载器。 |
Java.enumerateMethods(targetClassMethod) |
枚举指定类的所有方法。 |
这里的用法感觉和 Xposed 差不多
板子
function main() {
Java.perform(function() {
Hook(); // 自己实现即可
});
}
setImmediate(main);
基本的 Hook
套之前那个板子改就行
function Hook() {
var utils = Java.use("类名");
utils.方法名.implementation = function (a, b) { // 这个方法有几个参就写几个
// 修改参数值为 123 和 456
a = 123;
b = 456;
var ret = this.方法名(a, b); // 接收返回值
console.log(a, b, ret); // 打印参数的值和返回值
return ret; // 想改返回值把这 return 的东西改了即可
}
}
function main() {
Java.perform(function () {
Hook();
});
}
setImmediate(main);
然后先 frida-ps -U 找目标进程名,再 frida -U [进程名] -l hook.js 即可,记得先开个 cmd 把 frida-server 启动着
这里启动脚本有两种方法
Spawn 方法:frida -U -f [进程名] -l hook.js
Attach 方法:frida -U [进程名] -l hook.js
Spawn 方法会让 Frida 决定进程的启动,就算目标进程启动了也会重启
Attach 方法就是目标进程已经启动的时候再用 ptrace 注入从而实现 Hook
重载参数
如果有多个同名方法时使用,例如我们目标的方法重名了,但是其参数类型是独特的,只有一个 String 类型的参数,就可以像下面这样写
function Hook() {
var utils = Java.use("类名");
utils.方法名.overload("java.lang.String").implementation = function (var1) {
var1 = "Hooked";
var ret = this.方法名(var1);
console.log(var1, ret);
// return ret;
return "Hacked";
}
}
如果是一个复杂的自定义参数类型,我们可以去 smail 里面找这个参数的类型,就先搜方法名然后把参数类型抄上去完事,如下
utils.方法名.overload("com.example.demo$myargs", "java.lang.String").implementation = function (var1, var2)
其实这个 java.lang.String 如果记不住也是可以去 smail 里面抄下来的
当然如果你懒的话,也不用去 smail 找什么参数类型,直接写个 java.lang.String 在里面, Frida 报错的时候会把参数名给你写出来的,复制粘贴即可(
Hook 构造函数
和前面一个意思,多个 $init 表示这个 Hook 的是构造函数,记得重载,因为可能同时有有参和无参构造函数存在
还是假设就一个 String 参数
function Hook() {
var utils = Java.use("类名");
utils.$init.overload("java.lang.String").implementation = function (var1) {
console.log(var1);
var1 = "Hooked";
console.log(var1);
this.$init(var1);
}
}
Hook 变量
分为静态和非静态两种情况
如果是静态的话
function Hook() {
var utils = Java.use("类名");
utils.变量名.value = "Hooked";
console.log(utils.变量名.value);
}
如果是非静态的话,得枚举一下
function Hook() {
Java.choose("类名", {
onMatch: function (obj) {
// obj._变量名.value = "23333"; // 如果要改的这个变量的名字和某函数名相同,就得在名字前面加下划线进行区分
obj.变量名.value = 66666;
console.log(obj.变量名.value);
},
onComplete: function () {
}
});
}
但是可能会有奇怪的问题,写的时候最好写成反射的,不然可能改不上
Hook 内部类
整理的时候发现 Frida 的 hook 很多语法都和 Xposed 差不多,比如这个也是加个 $内部类名 完事
function Hook() {
var utils = Java.use("类名$内部类名");
console.log(utils);
var Log = Java.use("android.util.Log"); // frida 的 Log 日志输出
utils.$init.implementation = function () { // 内部类的构造函数 Hook,逻辑和之前提到的基本逻辑一致
Log.d("frida_filter", "Hooked");
console.log("Hooked");
}
}
主动调用方法
静态方法
function Hook() {
var utils = Java.use("类名");
utils.方法名(参数)
}
非静态和上面变量的方法差不多
function Hook() {
Java.choose("类名", {
onMatch: function (obj) {
ret = obj.方法名("Hooked"); // 自己写参数
},
onComplete: function () {
console.log("Hook Result is: " + ret);
}
});
}
常用 Hook 方法 (Native 层)
so 层有些东西其实还是能用 java 层的方法来进行 Hook,比如说 JNI 的静态注册之类的,不一定非要用传统的 so 层的 API
Hook 导出函数
function Hook() {
var addr = Module.findExportByName("libxxx.so", ".so中的函数名");
console.log(addr);
if (addr != null) {
// Interceptor.attach 是 Frida 里的一个拦截器
Interceptor.attach(addr, {
// onEnter 里可以打印和修改参数
onEnter: function (args) { // args 传入参数
console.log(args[0]); // 打印第一个参数的值
args[0] = ptr(2333); // 修改第一个参数,先转化为指针再去赋值
console.log(args[0]);
console.log(this.context.x1); // 打印寄存器内容
console.log(args[1].toInt32()); // toInt32() 转十进制
console.log(args[2].readCString()); // 读取字符串 char 类型
console.log(hexdump(args[2])); // 内存 dump,和 ida 的 hex view 差不多
},
// onLeave 里可以打印和修改返回值
onLeave: function (ret) {
console.log(ret);
ret.replace(23333);
console.log("ret", ret.toInt32());
}
})
}
}
根据导出函数名来找到地址,然后根据地址来进行操作
额外举个栗子,如果我们遇到一个 JNI 函数 TestFunc,他的参数是 TestFunc(JNIEnv *a1, jclass a2, jstring a3),这个参数类型就很特殊,那我们想要 Hook 就这样写
function Hook() {
var addr = Module.findExportByName("lib2333.so", "TestFunc");
if (addr != null) {
Interceptor.attach(addr, {
onEnter: function (args) {
var JNIEnv = Java.vm.getEnv();
var orig_str_ptr = JNIEnv.getStringUtfChars(args[2], null).readCString();
console.log("orig args: ", orig_str_ptr);
var hook_val = "Now We Hooked Args!";
var new_JString = JNIEnv.newStringUtf(hook_val);
args[2] = new_JString;
console.log("hooked args: ", JNIEnv.getStringUtfChars(args[2], null).readCString());
},
onLeave: function (ret) {
var orig_ret = Java.cast(ret, Java.use('java.lang.String'));
console.log("orig ret val: ", orig_ret.toString());
var JNIEnv = Java.vm.getEnv();
var hook_val = "Now We Hooked Return Value!";
var new_JString = JNIEnv.newStringUtf(hook_val);
ret.replace(new_JString);
console.log("hooked ret val: ", JNIEnv.getStringUtfChars(ret, null).readCString());
}
})
}
}
遍历导入导出函数
其实没啥 b 用,ida 全能看见,但万一用上了呢
function Hook() {
console.log("\n\n======================Enumerating imports======================\n\n");
var imports = Module.enumerateImports("lib2333.so");
for (var i = 0; i < imports.length; ++i) {
console.log(JSON.stringify(imports[i]));
}
console.log("\n\n======================Enumerating exports======================\n\n");
var exports = Module.enumerateExports("lib2333.so");
for (var i = 0; i < exports.length; ++i) {
console.log(JSON.stringify(exports[i]));
}
}
Hook 未导出函数
首先得知道咋获取 .so 的基址
function Hook() {
var addr1 = Process.findModuleByName("lib2333.so").base;
var addr2 = Process.getModuleByName("lib2333.so").base;
var addr3 = Module.findBaseAddress("lib2333.so");
console.log("addr1: ", addr1);
console.log("addr2: ", addr2);
console.log("addr3: ", addr3);
}
其实你会发现三种方法都能获取到 .so 的基址,都是一个值
function Hook() {
var so_addr = Module.findBaseAddress("lib233.so");
console.log(so_addr);
var func_addr = so_addr.add(0x23333); // 加上函数偏移地址
console.log(func_addr);
if (func_addr != null) {
Interceptor.attach(func_addr, {
onEnter: function (args) {
},
onLeave: function (ret) {
console.log(ret.toInt32());
}
})
}
}
一般 32 位安卓是 thumb 指令,64 位是 arm,thumb 的话还得在偏移那额外 +1
Hook dlopen
dlopen 主要是加载库文件用的,有的 app 在 dlopen 里面藏东西,其加载的时间非常早,而且只加载一次,可能稍不注意就给漏掉了
源码如下
void* dlopen(const char* filename, int flag) {
const void* caller_addr = __builtin_return_address(0);
return __loader_dlopen(filename, flag, caller_addr);
}
这时候我们要对 dlopen 进行 hook,一旦检测到目标的 .so 文件被加载就,就立马跳转到我们对 .so 的 hook 中去
function hook_dlopen() {
var dlopen = Module.findExportByName(null, "dlopen"); // 高版本 Android 使用 android_dlopen_ext,把 dlopen 换成 android_dlopen_ext 即可
Interceptor.attach(dlopen, {
onEnter: function (args) {
var so_name = args[0].readCString(); // 根据源码,先获取 so 文件的名称,再过滤
if (so_name.indexOf("lib2333.so") >= 0) this.call_hook = true;
}, onLeave: function (ret) { // 设置监听,捕捉 so 文件加载的时机,然后跳转到 Hook so 层的逻辑
if (this.call_hook) Hook();
}
});
}
常用的就这些,这次主要是整理了一下怎么去 Hook,至于 rpc 和反检测啥的再议吧,摸了