安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )

 

From:Hook 神器家族的 Frida 工具使用详解:https://blog.csdn.net/FlyPigYe/article/details/90258758

详解 Hook 框架 frida ( 信抢红包 ):https://www.freebuf.com/company-information/180480.html

神奇的 Hook技术,Frida 框架的简单使用:https://www.toutiao.com/i6847039494655312398

APP逆向神器之Frida【Android初级篇】:https://www.jianshu.com/p/2d755beb1c54

frida 官网文档:https://frida.re/docs/home/

《FRIDA操作手册》:https://python.ctolib.com/hookmaster-frida-all-in-one.html

FridaApp_Python的Hook脚本.zip:链接: https://pan.baidu.com/s/196-f9xggQ6QNNavmwiP8PA 提取码: vzuh

 

利用Frida绕过Android App(途牛apk)的SSL Pinning:https://blog.csdn.net/weixin_44677409/article/details/106650473

Frida从入门到入门—安卓逆向菜鸟的 frida 使用说明:https://bbs.pediy.com/thread-226846.htm

哔哩哔哩视频教程:
        frida java 层 hook:https://www.bilibili.com/video/BV1UE411A7rW?p=78
        frida native 层 hook:https://www.bilibili.com/video/BV1UE411A7rW?p=79

关键字:frida hook    fridaapp.apk    frida工具使用详解

 

 

 

一、前言

 

        说到逆向APP,很多人首先想到的都是反编译,但是单看反编译出来的代码很难得知某个函数在被调用时所传入的参数和它返回的值,极大地增加了逆向时的复杂度,有没有什么办法可以方便地知道被传入的参数和返回值呢?答案是有的,这个方法就是Hook,Hook的原理简单地说就是用一个新的函数替代掉原来的函数,在这个新的函数中你想做什么都可以,为所欲为。

        在逆向过程中有一个 Hook 神器是必不可少的工具,之前已经介绍了 Xposed 和 Substrate 了,不了解的可以看这两篇文章:Android中Hook神器Xposed工具介绍 和 Android中Hook神器SubstrateCydia工具介绍 这两篇文章非常重要一个是 Hook Java 层的时候最常用的 Xposed 和 Hook Native 层的 SubstrateCydia,可以看我之前的文章比如写微信插件等都采用了Xposed工具,因为个人觉得Xposed用起来比较爽,写代码比较方便。而对于SubstrateCydia工具可以 Hook Native 层。

        那么有了这两个神器为啥还要介绍 Frida工具呢?而且这个工具网上已经有介绍了,为什么还有介绍了,因为这个Frida工具对于逆向者操作破解来说非常方便,所谓方便是他的安装环境和配置要求都非常简单兼容性也非常好,因为最近在弄一个协议解密,无奈手机上安装 Cydia 之后不兼容导致死机所以就转向用了这个工具实现了 hook,所以觉得这个工具非常好用就单独介绍一下。

        Frid a就是一个很常用的 Hook 工具,只需要编写一段 Javascript 代码就能轻松地对指定的函数进行 Hook,而且它基本上可以算是全平台的(主流平台全覆盖),除了 Android 以外,iOS 和 PC 端的APP也可以用它来进行Hook,非常方便。

 

1、Hook是个什么鬼?

  Hook 翻译过来就是 "钩子" 的意思,钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。Hook 技术无论对安全软件还是恶意软件都是十分关键的一项技术,其本质就是 劫持函数调用

 

2、Frida框架的那些事

  frida 是一款基于 python 和 java 的 hook 框架,是一种动态插桩工具,可以插入代码到原生App的内存空间中,动态的监视和修改其行行为,可运行在Android、iOS、Linux和windows等多个平台。

插桩技术 是指将额外的代码注入程序中以收集运行时的信息,可分为两种:

  • 1. 源代码插桩【Source Code Instrumentation(SCI)】:额外代码注入到程序源代码中。
  • 2. 二进制插桩【Binary Instrumentation】:额外代码注入到二进制可执行文件中,其又可分为两种:
        ● 静态二进制插桩  【 Static Binary Instrumentation(SBI) 】:
                    在程序执行前插入额外的代码和数据,生成一个永久改变的可执行文件。
        ● 动态二进制插桩  【 Dynamic Binary Instrumentation(DBI) 】:
                    在程序运行时实时地插入额外代码和数据,对可执行文件没有任何永久改变。

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第1张图片

使用 Frida 框架到底能做什么呢 ?

  • (1)访问进程的内存,提取我们感兴趣的信息或敏感信息
  • (2)在应用程序运行时覆盖一些功能,改变其程序运行逻辑
  • (3)从导入的类中调用函数
  • (4)在堆上查找对象实例并使用这些对象实例
  • (5)Hook,动态跟踪、拦截变量和函数 等等。

 

 

二、环境安装配置

 

如何让 Frida 奔跑起来 ?

用到的 frida 框架分为两部分:

  • 一部分是 "客户端", 即:用于连接远程设备,提交要注入的 JS 代码到服务端,并接受服务端发来的消息; 
  • 另一部分是 "服务器端", 即:注入JS代码到目标进程,操作内存数据,并将相关信息发送至给客户端。

官网安装说明

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第2张图片

 

frida

        环境要求
          系统环境  – Windows、macOS、Linux
          Python     – 最新的3.x版本,
          Adb环境   – 请自行下载adb工具或安装 android studio 工具          
            环境准备好了,让我们来安装 frida CLI 吧!
            安装 frida CLI 有很多种方法,这里只介绍两种,即:pip 和 npm
        
        pip 安装 frida      
              执行 pip install frida
              执行 pip install frida-tools
              执行 frida -version
        
        npm 安装 frida,首先安装 NodeJS
              执行 npm install frida
              执行 npm install frida-tools
              执行 frida --version

执行一波 pip install frida-tools,安装完毕以后,测试是否安装正确,因为官网安装文档的下半部分是用于测试刚装好的库是否可用,但是比较麻烦,这里可以直接使用 frida-ps 命令来测试(显示本机的进程)

如果加上参数 U 表示的是 usb 连接的设备。( 模拟器也是 -U 参数)

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第3张图片

frida-ps -aU

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第4张图片

看起来是没问题了,然后我们怎么Hook Android手机上的 APP 呢?别急,还需要在手机上做一些操作你才能这么做。

我们需要有一台已经Root了的Android手机,因为不同型号的手机Root方法存在差异,本文中就不指导如何对手机进行Root操作了,请自行通过搜索引擎查找方法。实在没有可以Root的Android手机的话可以选择使用模拟器,推荐使用Genymotion之类系统较为原生的模拟器,并将Android版本选择在6.0以上,否则可能会出现一些奇奇怪怪的问题

手机准备好了之后,找到 Frida 文档中 Tutorials 栏里的Android页,开始进行Frida的手机端准备工作。

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第5张图片

文档中能看到,Frida官方最近的大部分测试都是在运行着Android 9的Pixel 3上进行的,所以理论上来讲如果你在使用中遇到任何奇怪的问题,都可能是你手机的系统导致,所以这里再次建议,使用较为原生和偏高版本的系统(建议至少6.0)进行操作

 

Frida 环境

1. 手机运行服务端
2. 电脑端运行客户端,电脑端再进行端口转发
      adb forward tcp:27042 tcp:27042
      adb forward tcp:27043 tcp:27043

 

frida-server

环境要求
          手机一部,本文主要讲述 Android系统。手机必须被 Root,具体 Root 方法请自行查询        
          下载 frida-server ,地址:https://github.com/frida/frida/releases

 

安装方法( 先要看构架:adb shell getprop ro.product.cpu.abi

打开 GitHub 之后你会发现,这里有很多个不同的版本,应该下载哪一个呢?

可以看到这一排的文件中,末尾处都有个系统和CPU架构的标识,我们直接看Android的。这里标了Android的一共有四个,因为 X86不是手机使用的CPU架构,可以直接 pass 掉,剩下的就是 arm 和 arm64 两种了,那么我们需要怎么判断自己的手机/模拟器属于哪一种CPU架构的呢?

查CPU架构的方法很多,这里介绍一个比较方便快捷的——使用一个名叫 Device Info HW的APP。

                下载的文件名的格式是: frida-server-(version)-(platform)-(cpu).xz 
                例如:手机是 nexus6p, cpu 为 arm64
                需要下载的是: frida-server-11.0.13-android-x86_64.xz;
                注意, frida-server 的版本一定要跟 frida CLI 的版本一致。

          执行以下命令
                adb devices
                adb push path/to/frida-server /data/local/tmp
                adb shell
                su
                cd /data/local/tmp
                chmod a+x ./frida-server         或者   chmod 777 ./frida-server
                ./frida-server                            或者   ./frida-server &

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第6张图片

 

frida tools

Frida 提供的工具,frida-trace,frida-ps,frida,frida-discover,这些工具都位于 python 的 Scripts 路径下,

frida 的工具共有 6 个:

(1)frida 命令是一个交互式解释器(REPL),用于实现 Hook 代码的动态注入,他的交互形式跟 IPython 很类似。         

          命令帮助:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第7张图片

示例:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第8张图片

将一个脚本注入到 Android 目标进程:frida -U -l myhook.js com.xxx.xxxx

参数解释:

  • -U 指定对 USB 设备操作
  • -l   指定加载一个 Javascript 脚本
  • 最后指定一个进程名,如果想指定进程 pid,用 -p 选项。正在运行的进程可以用 frida-ps -U 命令查看

frida 运行过程中,执行 %resume 重新注入,执行 %reload 来重新加载脚本;执行 exit 结束脚本注入

 

(2) frida-ps: 用于列出进程的一个命令行工具,当我们需要跟远程系统进行交互的时候,这个是非常有用的。

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第9张图片

(备注:这里的 com.android.chrome 就是通过 frida-ps 命令查询到的远程客户端进程。)

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第10张图片

另外还有四个分别是: frida-trace,frida-discover, frida-ls-devices, frida-kill

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第11张图片

由于不是经常用到,这边就不一一详细介绍了, 感兴趣的可以去 frida 的官网查看他们的详细介绍和用法。

 

常用 Java API

官网也有文档说明:https://www.frida.re/docs/javascript-api,

  由于 frida 的注入脚本是 Java,因此在开始 frida 初探之前,有必要对 Java 注入相关的 API 做一个简单介绍, 后面我们都将通过 js 脚本来操作设备上的 Java 代码。

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第12张图片

  当我们使用 Java.use() 获取 Java类之后,我们就可以通过 class.method.implementations = function() {} 的形式 hook 某类的某方法,不管是实例方法还是静态方法都可以。

  另外,由于 js 代码注入时可能会出现超时的错误, 为了防止这个问题,我们通常还需要在最外面包装一层setImmediate(function(){}) 的代码,如下所示:

 

 

初探 Frida:Hello World 程序

 

apk 下载地址:https://github.com/OWASP/owasp-mstg/tree/master/Crackmes/Android

github 下载网速有点感人。链接: https://pan.baidu.com/s/1zYd4PmMQNlGqhzEUtXee6w 提取码: 6y8e

CTF之安卓逆向 uncrackme level1https://blog.csdn.net/nini_boom/article/details/104565943/

Frida Hook app简单教程:https://blog.csdn.net/PLA12147111/article/details/100525588

  通过上面的介绍,现在终于可以来真格的了。而这里所说的 Hello World 程序,和我们日常看编程书籍所说的有所不同,我们上网找了一个 OWASP Uncrackable Crackme Level 1 的 APP,并对其 "防Root检测" 功能进行了Hook。

将 UnCrackable-Level1.apk 安装到安卓设备( 可以直接拖进模拟器安装 ):adb install UnCrackable-Level1.apk

打开运行,存在 root 检测,由于我的模拟器是已经 root,所以点击 OK,程序它就自己退出了。

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第13张图片

 

 

方法 1( 使用 jadx-gui 或者 jeb 打开并自动反编译 apk ):

 

使用 jadx-gui  或者 jeb 打开 apk ,自动反编译,然后查看 java 源码。这里使用 jadx-gui 打开 apk 并自动反编译。。。

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第14张图片

关键函数:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第15张图片

在 MainActivity 类中可以看到,函数 a 打开了一个对话框,并设置了一个 onClickListener监听,当按 ok 按钮时,onClick 事件函数就会被触发。onClick事件函数只是简单的使用 system.exit ( 0 ) 来退出 app 。

因此只需要不调用 system.exit ( 0 ) 阻止 App退出即可。所以需要 hook 住 onClick 方法。。。。。

frida_hook_java.py 代码:

        hook 匿名内部类:
        https://stackoverflow.com/questions/60236107/how-to-hook-into-onclick-method-inside-a-method-using-frida

import sys
import frida


js_code = '''
Java.perform(function(){
    console.log("[*] START...")
    var mClass = Java.use("sg.vantagepoint.uncrackable1.MainActivity$1")
    mClass.onClick.implementation=function() {
        console.log("[*] Clicked ")
    }
});
'''


def message(msg, data):
    if msg['type'] == 'send':
        print(f'[*] {msg["payload"]}')
    else:
        print(msg)


if __name__ == '__main__':
    # get_remote_device 获取远程设备 (get_usb_device)  attach 附加进程
    process = frida.get_remote_device().attach('owasp.mstg.uncrackable1')
    script = process.create_script(js_code)
    script.on('message', message)  # 绑定一个事件
    script.load()
    sys.stdin.read()
    pass


打开两个 cmd 窗口,一个执行 frida-server 对应的版本,一个执行 端口转发,如图:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第16张图片

然后在模拟器中打开 apk,再执行上面的 python 代码,可以看到,点击 OK 后 apk 不会退出,hook 成功

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第17张图片

 

 

方法 2( dex2jar classes.dex -o classes.jar ):

 

  将 APK 文件扩展名修改为.zip,然后找到 classes.dex 文件将其编译为 classes.jar 文件。

dex2jar classes.dex -o classes.jar

编译完后,使用 JD-GUI 打开 classes.jar 文件,查找关键字,这里的关键字是 “Root detected”。

a 方法中关键代码,可以看到 new 了一个 b ,然后把 b 类的实例转换成 OnClickListener。 

js 代码

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第18张图片

现在让我们运行此 App 程序,它依然弹出了 Root检测的对话框。

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第19张图片

现在让我们来运行 frida 注入程序,然后点击 OK 按钮,神奇的事情发生了,App 并没有退出,我们正常的进入了App界面。

frida注入界面:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第20张图片

frida 注入后的 App 界面

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第21张图片

  至此,我们以一个实例展示了frida框架的强大和神奇之处,这个Hook功能虽然简单,但是同样体现了整个的Hook过程,所谓麻雀虽小,但五脏俱全。

 

 

 

三、Hook java层 和 so层

 

 

Java 层 Hook 操作案例分析

 

下面主要从以下几个方面来介绍:

  • 第一、如何修改Java层的函数参数和返回值
  • 第二、如何打印Java层的方法堆栈信息
  • 第三、如何拦截native层的函数参数和返回值

对于 Java 层会注重介绍,因为我们用过 Xposed 工具之后都知道,比如参数是自定义类型怎么Hook等。

因为 Frida 大致原理是手机端安装一个 server 程序,然后把手机端的端口转到 PC端,PC端写 python 脚本进行通信,而 python 脚本中需要 hook 的代码采用 javascript 语言。

所以这么看来我们首先需要安装 PC端的 python 环境,安装 frida 直接运行命令:pip install frida 

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第22张图片

安装完成之后,我们再去官网下载对应版本的手机端程序 frida-server:https://github.com/frida/frida/releases 

注意:frida-server 版本 和 PC端安装的 frida 版本必须一致,不然运行报错的。其实这里看到真的实现hook功能的是手机端的frida-server,这个也是开源的大家可以研究他的原理。我们也看到这个工具和 IDA 是不是很类似,也是把手机端的端口转发到PC端进行通信而已。

有了 frida-server 之后就好办了,直接 push 到手机目录下,然后修改一下文件的属性即可:

adb push C:\frida-server /data/local/tmp

chmod 777 /data/local/tmp/frida-server

# 然后直接运行这个程序:
/data/local/tmp# ./frida-server

# 然后 端口转发
adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043

接下来就开始 在PC端开始编写 hook 程序进行操作了:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第23张图片

这里代码也非常简单,因为安装好了 frida 模块,直接导入模块,然后调用 api 获取设备的 session 然后 hook 程序包名,接着就可以执行 js 脚本代码进行 hook 操作,然后打印消息:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第24张图片

这里用了 python 的 print 函数打印,其实如果想要打印可以在上面的 js 脚本中使用 console.log 也是可以的,看自己的习惯了。

可以看到脚本的大致流程就是:最外面用 python 引用 frida 库进行和设备通信,然后编写 js 脚本执行 hook 操作。

所以这里最主要的还是 js 脚本也就是需要理解 js 语法了。

下面就开始分部拆解操作,看看如何涵盖我们平常使用的 hook 案例。

Utils 类:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第25张图片

CoinMoney 类:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第26张图片

 

 

第一个案例:hook类的构造方法

 

        我们有时候想 hook 一个类的构造方法,在 Xposed 中直接用 findConstructor 方法就可以了,因为构造方法可能有多种重载形式,所以需要用参数作为区分。这里 hook 案例的 CoinMoney 类 的 构造方法:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第27张图片

        首先脚本中使用 Java.use 方法通过类名获取类类型,构造方法是固定写法:$init;这个要记住,然后因为需要重载所以用overload(......)形式即可,参数和参数之间用逗号隔开即可。后面就是拦截之后的操作了,这里方法参数可以自定义变量名,因为js是弱语言,不对类型做强检查,当然这里还有其他获取参数的方法后面会介绍。

        这里使用 send 来发送打印消息即可,当然也可以用 console.log 形式打印日志,代码编写完了,下面就开始运行看效果,运行也很简单,直接 python frida.py:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第28张图片

在这之前一定要先打开 hook 的应用,不然会报错提示找不到这个程序进程:

这时候在运行看到了就成功了,我们把构造方法的参数打印出来了,那么这里 hook 就成功了。所以可以看到这个操作是不是比Xposed 工具更方便呢。但是他也有弊端后面会总结的。

 

 

第二、hook类的普通方法

 

这里的普通方法包括了静态方法,私有方法和公开方法等,这个操作和上面的构造方法其实很类似,代码如下:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第29张图片

这个就是把构造方法的固定写法 $init 改成了需要 hook  的方法名即可。如果方法有重载形式还是用 overload 进行区分即可,比如这里我们 hook 了 Uitls.getPwd(String pwd) 方法:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第30张图片

然后这里我们看到可以用一个隐含的变量 arguments 获取参数,这个是保存了方法的参数信息是系统自带的。所以我们有两种方式获取方法的参数信息。运行看一下效果:

看到打印消息,hook成功了。所以这里就把 hook 方法获取参数的案例都介绍完了。

总结一下很简单:

  • 构造方法使用固定写法 $init。
  • 其他方法全部用 方法名 即可。
  • 如果方法有重载形式需要用 overload 形式操作参数用逗号分隔。
  • 获取参数可以自定义参数名或者用系统隐含的 arguments 变量获取。
  • 当然,上面的所有操作之前,都需要用 Java.use 通过类名获取类型。

 

 

第三、修改方法的参数和返回值

 

我们在使用 Xposed 进行 hook 的时候最常用的可能就是修改参数和返回值来实现插件和外挂功能了,在Frida中其实也可以做到但是和 Xposed不一样,我们从上面的代码可以看到,没有像 Xposed 的 before 方法和 after 方法,而 Frida 直接是你可以在function 中调用原来的方法这样来进行参数修改,比如这里我要修改上面的方法参数和返回值:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第31张图片

因为 Frida 中没有 before 和 after 方法,但是可以直接调用原来的方法其实 Xposed 中也可以可以直接调用原来的方法的,但是不怎么常用,只要可以调用原来的方法,那么参数和返回值就可以随意修改了,这里我们把参数改成 jiangwei212,返回值后面追加 yyyy ,看打印的日志:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第32张图片

其实这么做比 before 和 after 形式更为方便,而且可以在原始方法调用前做一些事情和后面做一些事情。

 

 

第四、构造和修改自定义类型对象和属性

 

我们在 Xposed 写外挂的时候也会遇到这种比较常见的问题,就是方法的参数不是基本类型是自定义类型,然后也想修改他的属性值或者调用他的一个方法我们会使用反射来进行操作,而在返回值的时候,想构造一个自定义类型的对象也是直接用反射实例化一个对象进行操作的。其实在这里因为js中也是支持反射操作的,所以就很简单了:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第33张图片

这里构造一个对象其实很简单直接固定写法 $new 即可,然后有了对象也可以直接调用其对应的方法即可,然后就是如何修改一个对象类型的字段值呢?这个就要用反射了:

这里我们拦截了 getCoinMoney 方法,参数是 CoinMoney 类型,我们想修改他的money字段值,这时候我们直接调用他的方法没什么问题,但是如果直接调用字段值或者修改就会出现失败了,所以只能通过反射去修改字段值,不过要先获取这个对象对应的class类型,用Java.cast接口就可以,然后获取反射字段直接修改即可,这里要注意不管字段是private还是public的写法都是一样的,都是这段代码大家要注意把这段代码记住即可。我们看看hook之后的结果:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第34张图片

如果没有用反射去操作直接获取字段值打印就是object了。

 

 

第五、打印方法的堆栈信息

 

我们在破解过程中有时候通过抛出异常来打印堆栈信息跟踪代码效率会更高,Xposed 中操作很方便直接 Java 代码用 Log.xxx 方法打印堆栈信息即可,但是在 Frida 中有点麻烦了,因为他是js代码不好操作,第一次想到的办法就是自己写一个打印堆栈信息的类然后弄成一个dex之后,把这个dex注入到程序中,因为Frida支持把一个dex文件注入到原始程序中运行的,注入之后在需要打印堆栈信息的方法中调用这个dex中的那个方法就可以了。具体怎么注入本文不多介绍了。当时觉得这种方案太麻烦了,那么还有其他方案吗?其实还是有的,因为我们既然可以构造一个对象那么为什么不直接构造一个 Exception 对象呢?其实操作很简单,首先我们用 Java.use 方法获取类型变量:var Exception = Java.use("java.lang.Exception"); 然后是js中支持 throw 语法的,直接在需要打印堆栈信息的方法中调用即可:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第35张图片

不过这个是真得抛出异常了,没有捕获住,所以程序崩溃,我们在开发 Android 应用的时候如果程序崩溃了最快的查看异常信息的方法就是用日志过滤方式:adb logcat -s AndroidRuntime

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第36张图片

这样我们就把堆栈信息打印出来了,其实这里可以看到这个是真的一个崩溃异常了,因为没有 catch 所以直接用系统崩溃日志就可以查看了。这种方式最简单粗暴了。对于跟踪代码非常有用的。

 

到这里我们就把所有可能遇到的情形Java层hook操作都介绍完了,主要包括以下几种常见情形:

  • 第一、Hook类的构造方法和普通方法,注意构造方法是固定写法$init即可,获取参数可以通过自定义参数名也可以直接用系统隐含的arguments变量获取即可。
  • 第二、修改方法的参数和返回值,直接调用原始方法传入需要修改的参数值和直接修改返回值即可。
  • 第三、构造对象使用固定写法$new即可。
  • 第四、如果需要修改对象的字段值需要用反射去进行操作。
  • 第五、堆栈信息打印直接调用Java的Exception类即可,通过adb logcat -s AndroidRuntime来过滤日志信息查看崩溃堆栈信息。

总结:记得用 Java.use 方法获取类的类型,如果遇到重载的方法用 overload 实现即可。

 

 

四、Native层 Hook 操作案例分析

 

        下面继续来看 Frida 更强大的地方就是 hook native 代码,说的强大不是因为功能,而是便捷程度,我们之前 hook native 可能用Cydia 比较多,但是都知道 Cydia 和 Xposed 一样都有兼容问题,环境安装配置太麻烦了,而 Frida 还是只需要几行 js 代码即可搞定,

        这里 hook native 还是用两个案例介绍:一个是hook导出的函数,一个是hook未导出的函数,通过获取参数和修改返回值来演示,这里我们不自己写 native 代码了,直接用之前破解快手的数据请求的 so文件,他有一个函数在底层获取字符串信息,还有一个是最近正在研究的资讯类app的加密算法so,我们修改他的函数返回值。

 

 

第一、hook未导出函数功能

 

未导出的函数我们需要手动的计算出函数地址,然后将其转化成一个 NativePointer 的对象然后进行 hook 操作,那么如何计算一个函数地址呢?这个很简单只要得到 so 的内存基地址加上函数的相对地址就可以了。基地址获取直接查看程序对应的 maps 文件即可:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第37张图片

相对地址直接用 IDA 打开 so 文件就可以查看,比如这里我们通过静态分析之后想 hook 这个 sub_5070 函数:

然后我们 F5 查看函数对应的 C 语言代码查看参数信息:

这里看到是三个参数,那么计算了后的实际地址就是 0x7816A000+5070=0x7816F070,不过这个地址不是最后的地址,因为thumb 和 arm 指令的区分,地址最后一位的奇偶性来进行标志,所以这里还需加1也就是最终的0x7816F071,这一点很重要不管使用 Cydia 还是 Frida 都要注意最后计算的绝对地址要 +1,不然会报错的:

这里 hook 之后有两个回调方法一个是进入函数之前,一个是执行完之后,这个和 Xposed 非常类似了,我们打印参数,不过这个和之前Hook Java层就不一样了,因为在C中大部分都是和地址指针相关,特别是常见的字符串信息,我们如果要正确的打印字符串值就需要借助 Memory系统类来通过指针获取字符串信息了,这个类非常重要,在后面修改返回值也是用它写内存值的。我们先看看这个函数原始返回值是什么:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第38张图片

这个是加密之后的值了,然后我们获取到参数了,而通过 IDA 分析之后发现这个函数最终的结果不是通过 return 来返回的,而是通过第三个指针参数返回的,因为 C中有一个参数传值功能,就是直接操作指针就可以传回结果,这个在C中经常用到,因为一个函数返回值只有一处要是一个函数有多个返回值就没办法了,所以可以通过参数指针来传递。所以如果我们想修改函数的最终结果,需要修改参数指针的内存段数据,我们先把那个内存段数据获取到打印出来,这里因为通过静态分析知道最终的结果是16个字节数据,所以这里不能在用读取内存字符串方法了,而是读取纯的字节数据:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第39张图片

然后在把返回值修改了,返回值修改也很简单,直接重写那段内存值就可以了,比如这里修改成1111:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第40张图片

所以看到了C语言中很多地方都在直接操作内存也就是地址,特别需要借助 Memory类,他有很多方法,包括内存拷贝等。具体用到的可以去官网查询:https://www.frida.re/docs/javascript-api/#memory;然后我们看hook结果:

我们hook到了他的参数信息,第一个参数是需要加密的字符串信息我们是通过Memory方法获取字符串的,因为本身这个参数是一个字符串指针,第二个参数应该是字符串长度,第三个参数是操作结果值的指针,然后看到我们获取到的结果值就是原始加密的信息。说明我们获取成功了,然后再看看我们修改之后的1111值,通过日志查看:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第41张图片

看到了在 Java 成通过 native 访问得到的签名信息已经被修改成了1111了,说明我们成功了。到这里我们就成功的,在hook native的时候一定要注意函数的绝对地址要计算对,最后一定要记住+1,函数的返回值有可能不是通过return而是参数指针传递的,操作内存的时候用 Memory类即可。

 

 

在雷电模拟器上 hook native层   

 

apk 和 源码地址:链接: https://pan.baidu.com/s/1pcd-sHa9tgGqwCxjpVMrAQ 提取码: 6ztm

模拟器运行 apk 截图:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第42张图片

注意:雷电模拟器是 x86 架构,所以需要使用 IDA Pro 打开 x86 的 so 文件,找到 函数偏移地址,然后再通过  " 基址 + 函数偏移地址 " 得到函数加载到内存后的真实地址,hook 这个函数的真实地址。。。

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第43张图片

函数偏移地址:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第44张图片

frida_hook_native.py 代码:

import sys
import frida


js_code = '''
setImmediate(function(){
    send("start");
    // 遍历模块,找基址
    Process.enumerateModules({
        onMatch:function(exp){
            if(exp.name == "libnative-lib.so"){
                send("enumerateModules find");
                send(exp.name + "|" + exp.base + "|" + exp.size + "|" + exp.path);
                send(exp);
                return "stop";
            }            
        },
        onComplete:function(){
            send("enumerateModules stop");
        }
    });
    
    // hook 导出函数
    var exports = Module.enumerateExportsSync("libnative-lib.so");
    for (var i = 0; i < exports.length; i++){
        send("name:" + exports[i].name + "    address:" + exports[i].address);           
    }
    
    // 通过模块名直接查找基址
    var baseAddress = Module.findBaseAddress("libnative-lib.so");
    Interceptor.attach(baseAddress.add(0x000091D0), {
        onEnter:function(args){
            send(args[2]);
            send(args[3]);
            send(args[4]);
            //send(Memory.readCString(args[2]));
            //send(Memory.readCString(args[3]));
            //send(Memory.readCString(args[4]));
        },
        onLeave:function(ret_val){
            console.log(ret_val);
            send(ret_val)
        }
    });        
})
'''


def message(msg, data):
    if msg['type'] == 'send':
        print(f'[*] {msg["payload"]}')
    else:
        print(msg)


if __name__ == '__main__':
    # get_remote_device 获取远程设备 (get_usb_device)  attach 附加进程
    process = frida.get_remote_device().attach('com.example.frida_native_demo')
    script = process.create_script(js_code)
    script.on('message', message)  # 绑定一个事件
    script.load()
    sys.stdin.read()
    pass

打开两个 cmd 窗口,一个执行 frida-server 对应的版本,一个执行 端口转发,如图:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第45张图片

模拟器中打开 apk,再执行上面的 python 代码:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第46张图片

 

 

第二、hook导出函数功能

 

这部分内容很简单了,比上面的简单是因为不需要手动的计算函数地址,因为是导出的,所以直接可以得到导出的函数名即可,因为C语言中没有重载的形式,而C++中有,所以有时候发现导出的函数名和正常的函数名前面加上了一串数据作为区分那应该是 C++ 代码写的。有了so文件和导出的函数名就不需要构造 NativePoniter 了:

这个看到比上面自己手动找函数地址方便多了吧,打印参数都一样的代码了。这里通过函数名可以知道就是一个native函数了,那么他第一个参数肯定是JNIEnv指针,第二个参数是jclass类型,这个是标准的如果是静态方法第二个参数没啥用,后面的参数就是真的传递到native层的值了,比如这里Java层的方法:

那么按照上面的说明native层的函数就是4个参数了:

的确是这样的,后面两个参数才是我们想要的值,我们通过IDA查看这个函数:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第47张图片

然后我们用F5查看伪代码他的返回值:

用 env 指针调用了 NewStringUTF 返回一个 jstring 对象了,好了到这里我们先不说返回值修改的问题,先看看hook参数信息:

但是我们看到我们打印的返回值是个空也就是空指针,而如果这里我们想hook他的返回值怎么办呢?如果是一个正常的返回字符串信息,我们可以直接用Memory的方法构造出来Memory.allocUtf8String("XXXXX")一个内存字符串信息,然后直接返回一个指针地址即可,但是现在这里是返回一个jstring对象,其实这个我们通过查看jni.h文件可以知道jstring是C++中定义的对象:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第48张图片

而基本类型就是基本数据类型:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第49张图片

这个修改没有任何问题的,那么现在问题是修改非基本类型,比如这里的如何返回jstring对象呢?这里我能想到的一个办法就是通过获取NewStringUTF函数指针,通过NativeFunction方法获取函数,然后调用

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第50张图片

这里看到代码逻辑没什么问题,现在缺的就是 NewStringUTF 的函数地址了,这个因为在 so 中没法查看,所以怎么办呢?不着急我们在看看 JNIEnv 的定义:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第51张图片

他是一个结构体,再看看那个函数地址:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第52张图片

我们已经有了 JNIEnv 结构体指针了,每个函数指针都是 int 类型也就是 四个字节,所以从 JNIEnv 指针开始依次计算就可以得到NewStringUTF 函数对应的地址了。不过都说了找不到方法的时候就去官网找,JNIEnv 变量其实有对应的方法,这里构造 jstring 方法其实很简单: 

这个比找 函数指针 方便多了,其实 env 有很多方法在这里都有对应的 api。

所以到这里我们发现了 Frida 在 Hook 底层函数返回 jni 中的类型的时候有点麻烦了,但是 Cydia 就不会了,因为他是 Android 工程,可以引用 jni.h 头文件的,比如我们用 Cydia 来修改这个函数的返回值:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第53张图片

看到了吧,这样就很方便了因为是 Android 工程,所以可以直接应用 jni.h 头文件,然后直接调用 NewStringUTF 方法返回了,看看 hook 的结果:

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第54张图片

也修改成功了。所以这里看到Frida也不是万能的,要看什么问题怎么去分析了。

 

 

 

五、技术总结

 

到这里我们就把Frida常用的功能和hook常见的用法都说明完了,下面就来总结一下:

 

第一、Java层代码Hook操作

1、hook方法包括构造方法和对象方法,构造方法固定写法是$init,普通方法直接是方法名,参数可以自己定义也可以使用系统隐含的变量arguments获取。

2、修改方法的参数和返回值,直接调用原始方法通过传入想要修改的参数来做到修改参数的目的,以及修改返回值即可。

3、构造对象和修改对象的属性值,直接用反射进行操作,构造对象用固定写法的$new即可。

4、直接用Java的Exception对象打印堆栈信息,然后通过adb logcat -s AndroidRuntime来查看异常信息跟踪代码。

总结:获取对象的类类型是Java.use方法,方法有重载的话用overload(.......)解决。

 

第二、Native层代码Hook操作

1、hook导出的函数直接用so文件名和函数名即可。

2、hook未导出的函数需要计算出函数在内存中的绝对地址,通过查看maps文件获取so的基地址+函数的相对地址即可,最后不要忘了+1操作。

总结:Native中最常用的就是内存地址指针了,所以如果要正确的获取值一定要用Memory类作为辅助,特别是字符串信息。

 

 

frida 简单使用

From:https://www.cnblogs.com/tjp40922/p/11357660.html

示例代码:

# get_front_app.py.py
# 得到android手机当前最前端Activity所在的进程

import frida
rdev = frida.get_remote_device()
front_app = rdev.get_frontmost_application()
print (front_app)



# enum_process.py内容如下:
# 枚举android手机所有的进程

import frida
rdev = frida.get_remote_device()
processes = rdev.enumerate_processes()
for process in processes:
    print (process)



# 枚举某个进程加载的所有模块以及模块中的导出函数
import frida
rdev = frida.get_remote_device()
session = rdev.attach("com.tencent.mm")  #如果存在两个一样的进程名可以采用rdev.attach(pid)的方式
modules = session.enumerate_modules()
for module in modules:
    print (module)
    export_funcs = module.enumerate_exports()
    print ("\tfunc_name\tRVA")
    for export_func in export_funcs:
        print ("\t%s\t%s"%(export_func.name,hex(export_func.relative_address)))


# 枚举某个进程加载的所有模块以及模块中的导出函数
from __future__ import print_function
import frida, sys

def on_message(message, data):
    print(message)

jscode = """
    Process.enumerateModules({
          onMatch:function(exp){
        send(exp.name);
      },
          onComplete:function(){
        send("stop");
      }
})
"""

process = frida.get_usb_device().attach('com.yinghuan.aiyou')
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
sys.stdin.read()


# hook android的native函数
import frida
import sys
rdev = frida.get_remote_device()
session = rdev.attach("com.tencent.mm")
scr = """
Interceptor.attach(Module.findExportByName("libc.so" , "open"), {
    onEnter: function(args) {
        send("open("+Memory.readCString(args[0])+","+args[1]+")");
    },
    onLeave:function(retval){
    
    }
});
"""
script = session.create_script(scr)
def on_message(message ,data):
    print (message)
script.on("message" , on_message)
script.load()
sys.stdin.read()


# hook android的java层函数
# 如下代码为hook微信(测试版本为6.3.13,不同版本由于混淆名字
# 的随机生成的原因或者代码改动导致类名不一样)com.tencent.mm.sdk.platformtools.ay类
# 的随机数生成函数,让微信猜拳随机(type=2),而摇色子总是为6点(type=5)

import frida
import sys
rdev = frida.get_remote_device()
session = rdev.attach("com.tencent.mm")

scr = """
Java.perform(function () {
var ay = Java.use("com.tencent.mm.sdk.platformtools.ay");
ay.pu.implementation = function(){
    var type = arguments[0];
    send("type="+type);
    if (type == 2)
    {
    return this.pu(type);
    }
    else
    {
    return 5;
    }
};

});
"""

script = session.create_script(scr)
def on_message(message ,data):
    print message
script.on("message" , on_message)
script.load()
sys.stdin.read()



# 通过frida向android进程注入 dex
import frida, sys, optparse, re
def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)

jscode = """
Java.perform(function () {
    var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
    var context = currentApplication.getApplicationContext();
    var pkgName = context.getPackageName();
    var dexPath = "%s";
    var entryClass = "%s";
    Java.openClassFile(dexPath).load();
    console.log("inject " + dexPath +" to " + pkgName + " successfully!")
    Java.use(entryClass).%s("%s");
    console.log("call entry successfully!")
});
"""

def checkRequiredArguments(opts, parser):
    missing_options = []
    for option in parser.option_list:
        if re.match(r'^\[REQUIRED\]', option.help) and eval('opts.' + option.dest) == None:
            missing_options.extend(option._long_opts)
    if len(missing_options) > 0:
        parser.error('Missing REQUIRED parameters: ' + str(missing_options))

if __name__ == "__main__":
    usage = "usage: python %prog [options] arg\n\n" \
            "example: python %prog -p com.android.launcher " \
            "-f /data/local/tmp/test.apk " \
            "-e com.parker.test.DexMain/main " \
            "\"hello fridex!\""
    parser = optparse.OptionParser(usage)
    parser.add_option("-p", "--package", dest="pkg", type="string",
                      help="[REQUIRED]package name of the app to be injected.")
    parser.add_option("-f", "--file", dest="dexPath", type="string",
                      help="[REQUIRED]path of the dex")
    parser.add_option("-e", "--entry", dest="entry", type="string",
                      help="[REQUIRED]the entry function Name.")

    (options, args) = parser.parse_args()
    checkRequiredArguments(options, parser)
    if len(args) == 0:
        arg = ""
    else:
        arg = args[0]

    pkgName = options.pkg
    dexPath = options.dexPath
    entry = options.entry.split("/")
    if len(entry) > 1:
        entryClass = entry[0]
        entryFunction = entry[1]
    else:
        entryClass = entry[0]
        entryFunction = "main"

    process = frida.get_usb_device(1).attach(pkgName)
    jscode = jscode%(dexPath, entryClass, entryFunction, arg)
    script = process.create_script(jscode)
    script.on('message', on_message)
    print('[*] Running fridex')
    script.load()
    sys.stdin.read()


# 通过注入抛出异常代码实现跟踪程序调用栈
    在<>这本书中第八章有介绍通过重打包
    写入异常代码进行栈跟踪,但是这样比较麻烦,使用frida注入更方便。

 

 

六、Hook 家族神器的对比

 

下面继续来看看 Frida,Xposed,SubstrateCydia 这三个 Hook 神器的区别和优缺点:

 

第一、Xposed的优缺点

优点:在编写Java层hook插件的时候非常好用,这一点完全优越于Frida和SubstrateCydia,因为他也是Android项目,可以直接编写Java代码调用各类api进行操作。而且可以安装到手机上直接使用。

缺点:配置安装环境繁琐,兼容性差,在Hook底层的时候就很无助了。

 

第二、Frida的优缺点

优点:在上面我们可以看到他的优点在于配置环境很简单,操作也很便捷,对于破解者开发阶段非常好用。支持Java层和Native层hook操作,在Native层hook如果是非基本类型的话操作有点麻烦。

缺点:因为他只适用于破解者在开发阶段,也就是他没法像Xposed用于实践生产中,比如我写一个微信外挂用Frida写肯定不行的,因为他无法在手机端运行。也就是破解者用的比较多。

 

第三、SubstrateCydia的优缺点

优点:可以运行在手机端,和Xposed类似可以用于实践生产中。支持Java层和Native层的hook操作,但是Java层hook不怎么常用,用的比较多的是 Native 层 hook 操作,因为他也是Android工程可以引用系统api,操作更为方便。

缺点:和Xposed一样安装配置环境繁琐,兼容性差。

以上这三个工具可以说是现在用的最多的 hook工具了,总结一句话就是写Java层Hook还是Xposed方便,写 Native 层 Hook 还是Cydia 了,而对于破解者开发那还是 Frida 最靠谱了。但是不管怎么样,写外挂最难的也是最重要的不是写代码而是寻找 hook点,也就是逆向分析 app 找到那个地方,然后写 hook 代码实现插件功能。

 

 

《Android应用安全防护和逆向分析》

安卓逆向_24 --- Hook 框架 frida( Hook Java层 和 so层) )_第55张图片

 

 

 

 

 

 

 

 

你可能感兴趣的:(Android,逆向)