前言

一些链接:

准备工作

Frida是一个全平台注入框架,使用JavaScript编写注入代码,可以很方便的实现动态Hook。这里用Frida-Demo这个程序来演示。

  1. 一台Root过的手机或者带有完整Root权限的模拟器。

  2. 电脑端环境配置

    • 安装Frida

      pip install frida frida-tools

    • 测试Frida

      frida --version

      输出版本号就说明安装成功了

    • 配置好ADB,与真机或者模拟器连接,常用的连接方式如下:

      # USB连接
      adb usb
      # 网络连接
      adb connect 192.168.x.x
  3. 手机端环境配置

    • 获取Frida-Server: 下载链接

      平台选择

      注意要下载frida-server开头的文件,末尾的armx86代表适用的平台,真机就选择arm,模拟器选择x86

    至于选择32位的还是64位的frida-server,主要取决于目标APP是32位还是64位的,根据需要选择。

    • 设置Frida-Server

      首先把Frida-Server传到手机上:

      # 请将【 】中的内容修改为文件所在路径
      adb push 【frida-server-12.10.4-android-arm】 /data/tmp/frida-server
      adb push 【frida-server-12.10.4-android-arm64】 /data/tmp/frida-server64

      然后打开一个Shell

      adb shell

      运行以下命令:

      su
      cd /data/tmp
      chmod 700 frida-server*
      ./frida-server -l 0.0.0.0 &

      最后一行加了个&,代表把frida-server作为后台任务启动,这样即使我们Shell连接,frida-server也不会被终止。

      测试Frida-Server

      我们打开另一个命令行,输入以下命令:

      # 如果使用USB连接
      frida-ps -U
      # 如果使用网络连接
      frida-ps -R

    手机进程列表

    如果能看到手机进程列表说明设置成功了。

  4. 安装Frida-Demo练习程序

    下载地址:GitHub

    安装过程就不赘述了。

  5. 准备开发环境

    Frida使用JavaScript编写注入代码,我们选择Python作为加载器。

    我使用的是VSCode+Python3.8作为开发环境,配置方法参见另一篇博文:

    也可以选择PyCharm,看个人喜好了。

    首先新建一个run.py作为加载器,内容如下:

    import frida
    # PS: 现在的Frida好像是根据进程名称而不是包名来识别
    # 如果提示attach失败就改成进程名,也就是Frida Demo再试试
    process_name = 'infosecadventures.fridademo'
    # 发送信息回调函数
    def on_message(message, data):
     if message['type'] == 'send':
         print(f"[*] {message['payload']}")
     else:
         print(message)
    if __name__ == '__main__':
     try:
         device = frida.get_usb_device()
     except:
         device = frida.get_remote_device()
     if not device:
         print("* 连接到Frida Server失败")
     else:
         process = device.attach(process_name)
         # 加载JS脚本
         js = open('hook.js', encoding='utf-8').read()
         script = process.create_script(js)
         script.on('message', on_message)
         script.load()
         # 输入任意值结束
         input()
         script.unload()

    然后在相同的目录下新建一个hook.js来编写注入代码

    console.log("脚本载入成功");
    Java.perform(function() {
     //在这里编写Hook函数
    });

注入实战

我们打开Frida-Demo

Frida-Demo界面

如图,Frida Demo一共有3个模块,从左到右依次是【PIN密码爆破】【绕过root检测】【加密算法破解】。

我们先用工具分析一下代码,这个演示程序的代码没有经过混淆,而且去除了不必要的代码,很容易看。

我使用的是Jadx,把frida-demo.apk拖进去即可。

Jadx界面

左侧展示了APK中所有模块和资源文件,黄色图标的是“包”(package),绿色图标的是“类”(Class)。

我们只需要关注infosecadventures.fridademo包,其他3个包是一些通用系统组件。

数字密码爆破

代码分析

我们定位到infosecadventures.fridademo.utils.PinUtil类,代码非常简单,密码是一个Base64字符串。

接下来我们尝试Hook这个函数,示例代码:

Java.perform(function() {
    //获取指定类
    var cls = Java.use('infosecadventures.fridademo.utils.PinUtil');
    //HookCheckPin函数
    cls.checkPin.overload("java.lang.String").implementation = function(arg1) {
        //打印入参
        console.log('checkPin-in:', arg1);
        //调用原函数
        var result = this.checkPin(arg1);
        //打印出参
        console.log('checkPin-out:', result);
        //返回给原函数的调用
        return result;
    }
});

看起来挺复杂的,其实非常好理解,我们先获取了infosecadventures.fridademo.utils.PinUtil类,然后使用overload方法获取了checkPin的一个重载方法,最后使用implementation方法创建一个Hook,我们在Hook函数中使用this.checkPin()即可调用原函数。

执行结果

运行加载器,然后随意输几个数值测试,可以看到已经Hook成功了。

我们稍微修改一下Hook函数,让它把任何密码都当做正确的密码:

Java.perform(function() {
    var cls = Java.use('infosecadventures.fridademo.utils.PinUtil');
    //HookCheckPin函数
    cls.checkPin.overload("java.lang.String").implementation = function(arg1) {
        //打印入参
        console.log('checkPin-in', arg1);
        //直接返回true
        return true;
    }
});

这次我们不调用原函数,我们直接返回true,相当于屏蔽了原函数。

执行结果

随意输入的密码都被当成了正确密码。

在实战中这么做肯定是不行的,所以接下来我们改造一下,让它暴力破解密码。示例代码:

Java.perform(function() {
    var cls = Java.use('infosecadventures.fridademo.utils.PinUtil');
    //HookCheckPin函数
    cls.checkPin.overload("java.lang.String").implementation = function(arg1) {
        //函数进入
        console.log('开始破解密码');
        //暴力破解
        for (var i = 0;; i++) {
            var result = this.checkPin(i.toString());
            if (result) {
                console.log('破解完成,密码是', i)
                break;
            } else {
                console.log('密码错误', i)
            }
        }
        //返回给原函数的调用
        return result;
    }
});

运行加载器以后,点击PIN CHECK开始破解密码。

执行结果

稍等片刻即可看到破解结果。爆出的密码是4863,经过Base64编码以后就是NDg2Mw==,与代码中的值是一样的。

绕过Root检测

代码分析

我们定位到infosecadventures.fridademo.utils.RootUtil类,可以看到实现原理是检测存不存在特征文件。

接下来我们尝试Hook这个函数,示例代码:

Java.perform(function() {
    //获取指定类
    var cls = Java.use('infosecadventures.fridademo.utils.RootUtil');
    //Hook指定函数
    cls.isRootAvailable.overload().implementation = function() {
        //进入函数
        console.log('isRootAvailable-in');
        //调用原函数
        var result = this.isRootAvailable();
        //打印出参
        console.log('isRootAvailable-out', result);
        //返回给原函数的调用
        return result;
    }
});

执行结果

点一下ROOT CHECK,可以看到已经Hook成功了

接下来我们尝试绕过检测,有两种方法,可以直接屏蔽原函数,直接返回false,也可以Hookexists方法,让它在检测特征文件的时候,即使文件存在也返回false

第一种方法跟上面很像,不再赘述了,我们实现第二种方法,示例代码:

Java.perform(function() {
    //获取指定类
    var cls = Java.use('infosecadventures.fridademo.utils.RootUtil');
    //Hook指定函数
    cls.isRootAvailable.overload().implementation = function() {
        //进入函数
        console.log('isRootAvailable -in');
        //调用原函数
        var result = this.isRootAvailable();
        //打印出参
        console.log('isRootAvailable -out', result);
        //返回给原函数的调用
        return result;
    }
    var cls2 = Java.use('java.io.File');
    //Hook指定函数
    cls2.exists.overload().implementation = function() {
        //进入函数
        console.log('exists-in');
        //获取自身的path
        var mypath = this.getPath();
        //root特征文件列表
        var paths = new Array("/system/app/Superuser.apk", "/sbin/su",
            "/system/bin/su", "/system/xbin/su",
            "/data/local/xbin/su", "/data/local/bin/su",
            "/system/sd/xbin/su", "/system/bin/failsafe/su",
            "/data/local/su", "/su/bin/su");
        //判断路径是否匹配root特征
        var flag = false;
        paths.forEach(function(path) {
            if (mypath == path) {
                console.log('检测到root特征文件', path);
                flag = true;
                return;
            }
        });
        if (!flag) {
            //调用原函数,避免影响正常功能
            var result = this.exists();
        } else {
            //返回false,绕过root检测
            result = false;
        }
        console.log('exists-out', result)
        return result;
    }
});

执行结果

再点一下ROOT CHECK,可以看到我们已经成功Hook了exists方法,并且成功通过了Root检测。

加密函数破解

代码分析

我们定位到infosecadventures.fridademo.utils.EncryptionUtil类,可以看到他是一个AES加密算法,AES是一种对称加密算法,拿到密钥就可以解密了。

老样子,我们先HOOK加密函数,示例代码:

Java.perform(function() {
    //获取指定类
    var cls = Java.use('infosecadventures.fridademo.utils.EncryptionUtil');
    //Hook指定函数
    cls.encrypt.overload('java.lang.String', java.lang.String').implementation = function(arg1, arg2) {
        //打印入参
        console.log('encrypt-in', arg1, arg2);
        //调用原函数
        var result = this.encrypt(arg1, arg2);
        //打印出参
        console.log('encrypt-out', result);
        //返回给原函数的调用
        return result;
    }
});

执行结果

可以看到已经拿到了加密密钥infosecadventure,通过它就可以对加密后的字符串解密了。

最后修改:2021 年 08 月 27 日
Null