[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 和反检测啥的再议吧,摸了

posted @ 2024-08-20 18:26  iPlayForSG  阅读(110)  评论(0编辑  收藏  举报