Frida初试,手把手教你学习 Frida

3,141 阅读6分钟

frida 是啥

frida是啥:frida是平台原生app的Greasemonkey,说的专业一点,就是一种动态插桩工具,可以插入一些代码到原生app的内存空间去,(动态地监视和修改其行为),这些原生平台可以是Win、Mac、Linux、Android或者iOS。而且frida还是开源的。

Greasemonkey可能大家不明白,它其实就是firefox的一套插件体系,使用它编写的脚本可以直接改变firefox对网页的编排方式,实现想要的任何功能。而且这套插件还是外挂的,非常灵活机动。

frida 为啥这么火

动静态修改内存实现作弊一直是刚需,使用frida可以“看到”平时看不到的东西。出于编译型语言的特性,机器码在CPU和内存上执行的过程中,其内部数据的交互和跳转,对用户来讲是看不见的。当然如果手上有源码,甚至哪怕有带调试符号的可执行文件包,也可以使用gdb、lldb等调试器连上去看。

那如果没有呢?如果是纯黑盒呢?又要对app进行逆向和动态调试、甚至自动化分析以及规模化收集信息的话,我们需要的是细粒度的流程控制和代码级的可定制体系,以及不断对调试进行动态纠正和可编程调试的框架,这就是frida。

frida使用的是python、JavaScript等“胶水语言”也是它火爆的一个原因,可以迅速将逆向过程自动化,以及整合到现有的架构和体系中去,为你们发布“威胁情报”、“数据平台”甚至“AI风控”等产品打好基础。

环境搭建

mac+Android(已经 root)

mac 上

  1. 安装frida
pip3 install frida
  1. 安装frida-tools
pip3 install frida-tools
  1. 安装objection
pip3 install objection

Android 手机上

root 后的手机,安装frida-server之前需要知道Android手机的cpu架构,命令如下

adb shell getprop ro.product.cpu.abi 

查询结果是 arm64,还要知道电脑安装的frida的版本,frida-server的版本要与电脑端的frida版本相同,查看电脑端的frida版本的命令如下:

frida --version

知道了Android手机的cpu架构和frida的版本,到github下载相应版本的frida-server,github地址点击这里

我这里是 16.0.19,手机上也下载对应版本+对应架构的 frida-server :frida-server-16.0.19-android-arm64.xz

测试是否安装成功

启动frida-server
  1. 将下载的frida-server压缩包解压
  2. 解压后的文件push到手机
  3. 启动frida-server服务

上面步骤的对应命令如下

// 这个命令是 adb shell 请求 root 权限的,报错的话可以先执行后面的
$ adb root 
// frida-server 的名字改成自己下载的文件名字
$ adb push  frida-server-16.0.8-android-arm /data/local/tmp
$ adb shell
$ su
$ cd /data/local/tmp
$ chmod 777 /data/local/tmp/frida-server-16.0.8-android-arm
$ ./frida-server-16.0.8-android-arm
端口映射

在电脑上另开一个命令行窗口,启动frida-server之后还要进行端口映射,否则电脑无法连接到手机

adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043
查看进程

上面的步骤都完成后,在电脑上另开一个命令行窗口 就可以执行下面的命令,获取手机当前的进程信息

frida-ps -U

image.png

如果能看到你手机的进程信息,如图,则说明你的frida和frida-server安装配置成功。

实战

目标

📎UnCrackable-Level1.apk.zip

APP:UnCrackable-Level1.apk 去掉上面的 zip 就可以用了

APP 检测了 root,如果手机 root 了,会强制退出 APP,过了 root 检测后,还需要输入一个字符串进行校验。

当前效果:

image.png

逆向分析

使用 jadx 分析该 app,先安装好 jadx,然后打开命令行输入 jadx-gui ,把目标 apk 拖进去搜索 Root detected!

image.png

可以看到图中有三个检测方法 c.a()、c.b()、c.c(),其中一个返回为真,则弹出 Root detected!,然后弹框还有一个 onClick 方法,如果点击 OK 按钮,则触发 System.exit(0);,即退出 APP,先点进三个检测方法看看:

a() 方法通过检测 Android 系统环境变量中是否有 su 文件来判断是否被 root;

b() 方法通过检测 Build.TAGS 中是否包含字符串 test-keys 来判断是否被 root;

c() 方法通过检测指定路径下是否包含指定的文件来判断是否被 root。

image.png 所以我们这里就有多种过掉检测的方法:

ps:下面👇🏻先理解看懂,后面再说具体操作的事情

方法一:Hook 三个检测方法,让它们都返回 false,不再执行后续的 a 方法,就不会退出 APP 了

Java.perform(
    function(){
        console.log("[*] Hook begin")
        var vantagePoint = Java.use("sg.vantagepoint.a.c")
        vantagePoint.a.implementation = function(){
            console.log("[*] Hook vantagepoint.a.c.a")
            this.a();
            return false;
        }
        vantagePoint.b.implementation = function(){
            console.log("[*] Hook vantagepoint.a.c.b")
            this.b();
            return false;
        }
        vantagePoint.c.implementation = function(){
            console.log("[*] Hook vantagepoint.a.c.c")
            this.c();
            return false;
        }
    }
)

方法二:Hook a() 方法,置空,什么都不做,不弹出对话框,也不退出 APP:

Java.perform(
    function(){
        console.log("[*] Hook begin")
        var mainActivity = Java.use("sg.vantagepoint.uncrackable1.MainActivity");
        mainActivity.a.implementation = function(){
            console.log("[*] Hook mainActivity.a")
        }
    }
)

方法三:Hook onClick() 方法,点击 OK 后不让其退出 APP,注意这里是内部类的 Hook 写法:

Java.perform(
    function(){
        console.log("[*] Hook begin")
        var mainActivity$1 = Java.use("sg.vantagepoint.uncrackable1.MainActivity$1");
        mainActivity$1.onClick.implementation = function(){
            console.log("[*] Hook mainActivity$1.onClick")
        }
    }
)

方法四:Hook System.exit() 方法,点击 OK 后不让其退出 APP:

Java.perform(
    function(){
        console.log("[*] Hook begin")
        var javaSystem = Java.use("java.lang.System");
        javaSystem.exit.implementation = function(){
            console.log("[*] Hook system.exit")
        }
    }
)

root 检测过掉之后,APP 还要输入一个字符串,输入错误会提示 That's not it. Try again.,如下图所示:

image.png 分析 Java 代码,有一个 if-else 判断,obj 为输入的字符串,a.a(obj) 判断为真,就表示输入正确。

image.png

跟到 a.a() 方法,可以看到 bArr 是内置的字符串,通过 equals() 方法比较输入的 str 是否和 bArr 相等:

image.png

image.png bArr 的值,主要经过 sg.vantagepoint.a.a.a() 方法处理后得到,继续跟进去可以发现是 AES 加密算法:

image.png

这里就可以直接 Hook sg.vantagepoint.a.a.a(),直接拿到加密后的值,也就是我们要的正确字符串,由于这里返回的是 ASCII 码,所以我们还需要在 JavaScript 代码中使用 String.fromCharCode() 将其转换成正常字符,Hook 代码如下:

Java.perform(
    function(){
        var cryptoAES = Java.use("sg.vantagepoint.a.a");
        cryptoAES.a.implementation = function(bArr, bArr2){
            console.log("[*] Hook cryptoAES")
            var secret = "";
            var decryptValue = this.a(bArr, bArr2);
            console.log("[*] DecryptValue:", decryptValue)
            for (var i=0; i < decryptValue.length; i++){
              secret += String.fromCharCode(decryptValue[i]);
            }
            console.log("[*] Secret:", secret)
            return decryptValue;
        }
    }
)

上面就是整体逆向hook的思路,下面进入实战环节

实操

运行 Hook 脚本有两种方式,一是结合 Python 使用,二是直接通过 frida 命令使用脚本,注入 Hook 代码也有个时机问题,有时候需要在 APP 启动就开始 Hook,有时候可以等 APP 启动加载完毕了再 Hook。

本例中,过 root 检测的时候,如果采用第一、二种方法,即 Hook 三个检测方法或者 a 方法,那就需要在 APP 启动的时候就 Hook,如果采用第三、四种方法,即 Hook onClick() 或者 System.exit() 方法,那么等 APP 启动了再 Hook 也可以。

结合 python 使用
代码准备

首先来看一下结合 Python 怎么使用,JavaScript 代码如下(frida-hook.js):

/* ==================================
# @Time    : 2022-08-29
# @Author  : 微信公众号:K哥爬虫
# @FileName: frida-hook.js
# @Software: PyCharm
# ================================== */


Java.perform(
  function(){
    console.log("[*] Hook begin")

    // 方法一:Hook 三个检测方法,让它们都返回 false,不再执行后续的 a 方法,就不会退出 APP 了
    // var vantagePoint = Java.use("sg.vantagepoint.a.c")
    // vantagePoint.a.implementation = function(){
    //     console.log("[*] Hook vantagepoint.a.c.a")
    //     this.a();
    //     return false;
    // }
    // vantagePoint.b.implementation = function(){
    //     console.log("[*] Hook vantagepoint.a.c.b")
    //     this.b();
    //     return false;
    // }
    // vantagePoint.c.implementation = function(){
    //     console.log("[*] Hook vantagepoint.a.c.c")
    //     this.c();
    //     return false;
    // }

    // 方法二:Hook a() 方法,置空,什么都不做,不弹出对话框,也不退出 APP
    // var mainActivity = Java.use("sg.vantagepoint.uncrackable1.MainActivity");
    // mainActivity.a.implementation = function(){
    //    console.log("[*] Hook mainActivity.a")
    // }

    // 方法三:Hook onClick() 方法,点击 OK 后不让其退出 APP
    // var mainActivity$1 = Java.use("sg.vantagepoint.uncrackable1.MainActivity$1");
    // mainActivity$1.onClick.implementation = function(){
    //     console.log("[*] Hook mainActivity$1.onClick")
    // }

    // 方法四:Hook System.exit 方法,点击 OK 后不让其退出 APP
    var javaSystem = Java.use("java.lang.System");
    javaSystem.exit.implementation = function(){
      console.log("[*] Hook system.exit")
    }

    var cryptoAES = Java.use("sg.vantagepoint.a.a");
    cryptoAES.a.implementation = function(bArr, bArr2){
      console.log("[*] Hook cryptoAES")
      var secret = "";
      var decryptValue = this.a(bArr, bArr2);
      console.log("[*] DecryptValue:", decryptValue)
      for (var i=0; i < decryptValue.length; i++){
        secret += String.fromCharCode(decryptValue[i]);
      }
      console.log("[*] Secret:", secret)
      return decryptValue;
    }
  }
)

Python 代码如下(frida-hook.py):

# ==================================
# --*-- coding: utf-8 --*--
# @Time    : 2022-08-29
# @Author  : 微信公众号:K哥爬虫
# @FileName: frida-hook.py
# @Software: PyCharm
# ==================================


import sys
import frida


def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)


with open("./frida-hook.js", "r", encoding="utf-8") as fp:
    hook_string = fp.read()

# 方式一:attach 模式,已经启动的 APP
process = frida.get_usb_device(-1).attach("Uncrackable1")
script = process.create_script(hook_string)
script.on("message", on_message)
script.load()
sys.stdin.read()

# 方式二,spawn 模式,重启 APP
# device = frida.get_usb_device(-1)
# pid = device.spawn(["owasp.mstg.uncrackable1"])
# process = device.attach(pid)
# script = process.create_script(hook_string)
# script.on("message", on_message)
# script.load()
# device.resume(pid)
# sys.stdin.read()

Python 代码中,attach 模式 Hook 已经存在的进程,spawn 模式会重启 APP,启动一个新的进程并挂起,在启动的同时注入 frida 代码,适用于在进程启动前的一些 Hook,attach 模式传入的是 APP 名称,spawn 模式传入的是 APP 包名,查看 APP 名称和包名的方法有很多,这里介绍两个 frida 命令,frida-ps -Uai:列出安装的程序,frida-ps -Ua:列出正在运行中的程序,如下图所示,本例中 Uncrackable1 就是 APP 名称,owasp.mstg.uncrackable1 就是包名。

具体操作
  1. 在 pycharm 中新建一个 python 项目,创建 frida-hook.py 和 frida-hook.js 两个文件,复制上面的内容粘贴进去

image.png

  1. attach 模式需要先启动 APP,在手机上打开目标 APP,此处可以使用 frida-ps -Ua 来查看手机上启动的程序

image.png

  1. 运行 Python 代码之前,手机端也要启动 frida-server

image.png

注意看我标红的部分,容易遗漏,启动的时候如果不要 root 权限的话,后面启动会失败

  1. 在 pycharm 中运行 frida-hook.py

image.png 运行 python 代码之后会先看到 hook begin,手机上点击弹框按钮,然后输出 Hook system.exit;

这里我们可能会遇到几个报错,拉到最后面看看有没有遇到。

frida 命令

不使用 Python,也可以直接使用 frida 命令来实现,和前面 Python 一样也有两种模式,同样的一个是 APP 名一个是包名:

frida -U Uncrackable1 -l .\frida-hook.js:attach 模式,APP 启动后注入 frida 代码;

frida -U -f owasp.mstg.uncrackable1 -l .\frida-hook.js --no-pause:spawn 模式,重启 APP,启动的同时注入 frida 代码。

attch模式操作:

  1. 先启动
  2. 执行命令,这里通过上面的 方法三:Hook onClick() 方法,点击 OK 后不让其退出 APP 来 hook
  3. 下图 红色、黄色、蓝色 就是三个要点

image.png

spawn 模式操作:

方法二:Hook a() 方法,置空,什么都不做,不弹出对话框,也不退出 APP

frida -U -f owasp.mstg.uncrackable1 -l /Users/tuionf/PycharmProjects/fenci/frida-hook.js

加 --no-pause 会报错,可能是版本问题吧,所以去掉了

image.png

可能的报错

报错一:unable to connect to remote frida-server: closed

Traceback (most recent call last):
  File "/Users/tuionf/PycharmProjects/fenci/frida-hook.py", line 25, in <module>
    process = frida.get_usb_device(-1).attach("Uncrackable1")
  File "/Users/tuionf/PycharmProjects/fenci/venv/lib/python3.9/site-packages/frida/core.py", line 86, in wrapper
    return f(*args, **kwargs)
  File "/Users/tuionf/PycharmProjects/fenci/venv/lib/python3.9/site-packages/frida/core.py", line 1010, in attach
    return Session(self._impl.attach(self._pid_of(target), **kwargs))  # type: ignore
frida.ServerNotRunningError: unable to connect to remote frida-server: closed

报错很清晰了,手机上的 frida-server 的没启动,参考

image.png

报错二:Unable to save SELinux policy to the kernel: Permission denied

手机上启动 frida-server 的时候报错,这个主要是缺少了上图 su 这个步骤,adb shell 没有 root 权限

报错三:frida.ProcessNotFoundError: unable to find process with name 'system_server'

运行 python 的时候报错这个,原因和报错二一样,手机上的 frida-server 的没启动成功,参考报错一、二重新启动一下就好了

Traceback (most recent call last):
  File "/Users/tuionf/PycharmProjects/fenci/frida-hook.py", line 25, in <module>
    process = frida.get_usb_device(-1).attach("Uncrackable1")
  File "/Users/tuionf/PycharmProjects/fenci/venv/lib/python3.9/site-packages/frida/core.py", line 86, in wrapper
    return f(*args, **kwargs)
  File "/Users/tuionf/PycharmProjects/fenci/venv/lib/python3.9/site-packages/frida/core.py", line 1010, in attach
    return Session(self._impl.attach(self._pid_of(target), **kwargs))  # type: ignore
  File "/Users/tuionf/PycharmProjects/fenci/venv/lib/python3.9/site-packages/frida/core.py", line 1142, in _pid_of
    return self.get_process(target).pid
  File "/Users/tuionf/PycharmProjects/fenci/venv/lib/python3.9/site-packages/frida/core.py", line 86, in wrapper
    return f(*args, **kwargs)
  File "/Users/tuionf/PycharmProjects/fenci/venv/lib/python3.9/site-packages/frida/core.py", line 899, in get_process
    for process in self._impl.enumerate_processes()
frida.ProcessNotFoundError: unable to find process with name 'system_server'

常用命令

frida --version 查看版本号

frida-ps -U 可查看手机进程

frida-ps -Uai 列出安装的程序

frida-ps -Ua 列出正在运行中的程序

参考文档

【APP 逆向百例】Frida 初体验,root 检测与加密字符串定位 - 掘金

Hook神器—Frida安装 - 掘金