frida-reverse-analysis
Frida逆向与协议分析
起因是第七期分享会,有师傅推荐这本书,正好自己在frida脚本上很欠缺,所以买来看看。重点是实操,和C++反汇编不一样,别懒!
0x00 安卓逆向环境搭建
htop:动态查看当前活跃的,占用高的进程。
jnettop:查看下载速度与对应的 IP。
解锁OEM时会出现waiting for any device
的情况,解决办法。刷机过程见P9。adb devices
只有进入系统时才能看到,fastboot devices
只有进入bootloader时才能看到。
Kali NetHunter刷机:针对移动设备的安卓渗透测试平台。它主要修改安卓内核的内容,这些修改对日常使用无影响。最后使用链接下载SuperSU-2.82
。对于用NetHunter刷完机无法被虚拟机发现的情况,看链接。
0x01 Frida Hook基础与快速定位
基础
frida基础
两种操作模式:(1)CLI模式:通过命令行将js脚本注入到进程中。(2)RPC模式:通过py脚本间接完成js脚本的注入。本章重点关注CLI模式。
两种操作App的方式:(1)spawn(调用)模式:将启动App的权利交给Frida来控制,即使目标App已启动,在使用Frida对程序进行注入时,还是会重新启动App并注入。frida -f
就会调用spawn模式。(2)attach(附加)模式:在目标App已启动的前提下,利用ptrace原理注入程序进而完成Hook操作,默认以attach模式注入。
注:ida attach调试程序时,无法以frida attach模式注入;但是当先进行frida attach模式注入后,可以使用ida attach调试(先后问题)。
对于在App启动时执行的方法(so库的.init_array
、.init_proc
),使用spawn方式hook。而对于频繁运行的函数,可以使用attach方式hook。
frida连接手机的两种模式:(1)usb数据线:使用adb链接,加入-U
参数即可。(2)网络模式:adb链接,frida-server在运行时使用-l
监听ip与端口,主机上的frida通过-H
参数指定手机的ip和端口。
frida hook基础
xposed使用java代码编写hook模块后,需要重启后才能使hook代码运行起来。而frida只需在手机上运行frida-server,然后在主机上将js脚本注入即可。frida注入成功后,即使脚本被修改,hook效果也能及时生效。
针对显示->主动显示
的钩取如下:
1 | // hello.js |
之后运行:frida -U com.android.settings -l hello.js
。
注:
(1)所有针对java层函数的hook脚本必须处于Java.perform
的包装中。
(2)Java.use
获取指定类的handle后,使用类似于调用类静态方法的方式获得对应函数(上述代码中的settings.getMetricsCategory
)。如果函数存在多个重载,还需要添加.overload(<signature>)
获取指定函数。
如下所示,subString函数有两个重载:String substring(int)
与String substring(int, int)
。我们只想要钩取substring(int)
,如下所示:
1 | function hookSubString(){ |
objection基础
frida提供了API供用户自定义使用,在此基础上可以实现很多具体功能。而objection则是将各种常用功能整合并可在命令行使用的利器。在安卓中使用objection,可以快速完成内存、类与模块搜索、方法hook、打印参数等。
以movetv.apk
为例,通过jadx查看AndroidManifest.xml,获取到包名为com.cz.babySister
。安装到手机上,并启动frida-server,之后运行:
1 | objection -g com.cz.babySister explore |
之后,就可以对进程进行操作:
(1)打印内存:可打印出内存中已加载类的相关信息,从而快速定位app中的关键类。
1 | android hooking list classes // 列出进程加载过的类 |
1 | android hooking search classes <pattern> // 列出加载的类名中包含<pattern>格式的类 |
1 | android hooking list class_methods <class_name> // 获取指定类中所有非构造函数的方法名 |
(2)Hook函数:
objection可以hook一个类中所有函数。
1 | android hooking watch class <class_name> // 钩取某个类中所有非构造函数 |
1 | // hook 指定函数 |
下图就默认hook了所有setForegroundColor重载函数:
如果要指定钩取参数为int数组
的setForegroundColor函数,那么就写为:
1 | android hooking watch class_method com.cz.babySister.view.Loading.setForegroundColor "[I" --dump-args --dump-return --dump-backtrace |
(3)jobs:hook任务管理,android heap:用于操作内存中类的实例。这些略。
Hook快速定位
逆向分析时遵循hook->主动调用->RPC
,这是什么?即,协议分析的流程为:(1)用hook的方式确定关键业务逻辑的位置;(2)通过主动调用实现关键业务逻辑的调用;(3)通过RPC远程过程调用(python+js)的方式进行关键业务逻辑的批量调用。frida将hook工作成功地从xposed模块每次编译都需要重启的循环中解放出来。
关键类定位:基于trace的枚举
DDMS(Dalvik Debug Monitor Server)是早期android studio中用于调试和监视 Android 设备和模拟器的工具。其中method trace功能可以获得一段时间内app执行的函数记录,但是,它有以下缺点:(1)要求app的debuggable为true。(2)会记录所有函数,包括系统函数。
而frida无需处于debuggable状态,且支持跟踪指定类中的函数,支持跟踪特定类中的所有函数。在此节中,不直接编写frida脚本来进行trace,而是使用frida封装的可用于进行trace定位的工具。
objection trace
以movetv.apk
为例,首先搜索包含com.cz.babySister
的类:
1 | android hooking search classes com.cz.babySister |
将输出保存为文件hook
,并在每一行开头加入android hooking watch class
,表示钩取所有与com.cz.babySister.xxx
类相关的函数,如下所示:
通过如下命令,使用objection对所有com.cz.babySister.xxx
类相关的函数进行钩取:
1 | objection -g com.cz.babySister explore -c hook.txt |
此时再触发我们所关心的业务逻辑,就能快速筛选出业务逻辑所在类的范围。
注1:search classes
是先获取所有已加载类,之后再筛选。因此,它可能无法涵盖应用中所有的类。因此,在hook之
前,要尽可能多的使用app,以求加载足够多的类。objection中trace的核心源码见P37。
注2:如果app加壳,那么最好选择attach模式进行trace/hook。否则在应用还未启动时,真实的类还没有被释放。此时会报ClassNotFoundException错误,这是因为app的ClassLoader在运行时的切换问题所导致的。
ZenTracer
具体见P39。
总结一下,frida相比于DDMS,能够依据使用者的想法对执行函数进行跟踪,同时无需样本处于debuggable状态。但是,由于frida本身不太稳定,因此被测试的程序会经常崩溃。
关键类定位:基于内存的枚举
当逆向者通过分析发现某些类可能是关键类时,可以通过对此关键类进行hook去验证分析的结果。相比于xposed,frida不仅不用重启,还支持对内存进程的漫游功能,即:能够通过Java.choose()
在目标进程的java堆中寻找和修改已存在的java对象实例。这使得我们不仅能够对未执行的函数设置hook,还能对已经创建的实例进行操作。
以OkHttp中的hook抓包进行介绍。OkHttp的核心是拦截器。一个完整的网络请求被拆分为多个步骤,每个步骤都由拦截器完成。拦截器由okhttp3.OkHttpClient类中的List成员_interceptors
数组管理,此数组包含Client中的所有拦截器。那么,如果写一个打印日志的LogInterceptor拦截器,并添加到_interceptors
数组中,就可以完成对所有数据包的抓取。
如何做?通过Java.choose从内存中找到okhttp3.OkHttpClient的实例对象,并修改_interceptors
数组内容,使得此数据包含我们刚才写的LogInterceptor拦截器,从而完成数据的抓包。
1 | // hookOkHttp3.js |
上述代码中,myok2curl.dex负责具体拦截器的实现,okhttplogging.dex负责具体拦截器的实现。这样做的目的是为了避免将Java翻译成js的复杂工作。Java.openClassFile("xx").load()
用于导入dex文件,在内存中注册一个类,当然也可以使用Java.registerClass
完成。
注:objection的插件WallBreaker,可以快速定位类中所包含的属性与函数,也可以直接通过对象的句柄获取所在类中所有属性的值
。其基本原理就是使用Java.choose对内存中对象进行搜索,然后通过对对象的句柄进行反射,从而获得相应成员与函数。
frida也可以在native层进行hook。以脱壳为例,hexdump工具的核心原理是在目标进程的内存空间中遍历搜索包含DEX文件特征的数据,匹配成功则将dex dump出来,它主要使用了frida的Memory.scanSync
函数。hexdump仅仅能够解决一代整体加固,对于二代抽取加固,有工具frida_fart。其通过hook app运行过程中的native函数,即LoadMethod函数(此函数处于加载与执行dex文件的ART虚拟机中),利用此函数的参数,可以获得加载的java函数所在的dex文件与函数代码。
0x02 Frida脚本开发之主动调用与RPC入门
之前的内容讲了如何定位到算法的关键函数,之后我们就需要反复调用此函数,以进行调试,了解此函数真正的程序逻辑。但是如果按部就班的进行调用,函数传入的参数往往是变化的。因此,就需要主动调用,即:目标函数的参数是可控的。远程过程调用(RPC)(就是python脚本的frida)也是与主动调用一起使用的一种技术。
之前说过Frida调试的3板斧:(1)Hook定位关键逻辑;(2)主动调用构造参数进行利用;(3)RPC导出结果进行规模化利用。Frida实际上是基于Python和JS的进程级Hook框架,其中JS承担Hook函数的工作,Python相当于提供给外界的绑定接口,使用者可通过Python将JS脚本注入到进程中。其中,官方提供了frida-python来进行远程过程调用。
python实现frida注入的基本知识
获取设备
1 | import frida |
获取到设备后,实现进程注入同样有spawn(重新启动app)与attach(附到已启动的app)。如下所示:
注入进程-两种方式
1 | import time |
成功注入进程后,之后要进行hook脚本的注入,实际上就是将js脚本作为字节流通过frida提供的api加载到相应进程的session中。
hook脚本注入
方式1:js字符串
1 | # 读取 hook 脚本内容 |
方式2:js文件
1 | with open("hook.js") as f: |
rpc学习
学习rpc实际上是学习js脚本与python进行交互的方式。rpc脚本示例如下,功能主要是打开settings
,并调用两个函数:
1 | # 导入 print 函数 |
调用结果如下:
注意js脚本中带大写字母的导出函数变成了小写加下划线的形式,例如failPlease
变为fail_please
。可以这么说,rpc介绍了在python中主动调用js中函数的方式。下面再来介绍js主动向python发送数据的方式:
1 | from __future__ import print_function |
上述两个脚本是frida-python给出的两个简单的例子,js还可以用过send函数向python脚本发送数据。frida-python还包括:(1)child_gating.py:子进程注入;(2)bytecode.py:将脚本编译为字节码然后再加载脚本;(3)inject_library文件夹:手动向进程中注入一个动态库。等等。
这对于之前的ZenTracer项目,其真实用于Hook的代码只有traceClass函数,剩下的部分都是js与python之间的数据传递,主要是:通过send将js中的数据传输到python用于接收数据的FridaReceive函数中。
注:frida中session.on、script.on、device.on的区别?
session.on
:这个方法用于在Frida会话(Session)对象上注册事件处理程序。Frida会话是与目标应用程序通信的主要接口。通过session.on
方法,可以注册多种类型的事件处理程序,如detached
(会话与目标应用程序断开连接时触发)和message
(接收到来自目标应用程序的消息时触发)等。script.on
:这个方法用于在Frida脚本(Script)对象上注册事件处理程序。Frida脚本是在目标应用程序中执行的用户定义代码。通过script.on
方法,可以注册多种类型的事件处理程序,如message
(接收到来自目标应用程序的消息时触发)和destroyed
(脚本被销毁时触发)等。device.on
:这个方法用于在Frida设备(Device)对象上注册事件处理程序。Frida设备是与目标设备或模拟器通信的接口。通过device.on
方法,可以注册多种类型的事件处理程序,如spawned
(目标应用程序被启动时触发)和output
(设备的输出消息时触发)等。
frida java层主动调用与rpc
以demoso1工程为例,此工程中存在两个native函数,在应用打开后被循环调用。其中method01是静态函数,对输入进行加密并返回,method02是成员函数,对密文解密并返回。其java层声明如下:
注:如果native函数是静态注册的,那么so层最终生成的函数是以Java_
开头的导出函数。然而此静态注册方式不安全,因此用RegisterNatives()函数对上述两个函数进行了动态注册,以加强安全性。注册过程如下:
运行日志如下:
首先使用上一章介绍的objection工具来注入目标进程,以确定method01与method02的签名与调用情况。
1 | // 使用 objection 注入到进程 |
函数签名如下:
调用情况如下:
可以看出,此程序主动调用了method01。之后,我们就要写hook脚本,从而对此函数进行主动调用。主动调用最重要的是参数的构造,主动调用其实就是构造和hook时使用的参数类型一致的参数。首先,编写invoke.js如下:
1 | function hook(){ |
当注入并调用Hook函数时,结果如下所示:
重写一下invoke.js,目的是调用method01函数,并对xcl
进行加密:
1 | function invokeMethod01(){ |
结果如下所示:
刚才的method01是静态函数,这类函数的主动调用只需要获得对应类的句柄。而对于一般的函数method02来说,这样调用就会出错,因为此函数需要通过实例进行调用,而不是一个类句柄。而之前学过,Java.choose()能够在内存中获取实例,因此对method02的主动调用脚本如下所示:
1 | function invokeMethod02(){ |
结果如下所示:
测试好主动调用后,下一步就是rpc(python脚本调用js中的函数)。此时需要将主动调用提供为外部接口,需要两步:
(1)将主动调用参数配置为js函数的参数并将主动调用的结果返回,以方便外部自定义参数进行主动调用,更改后的代码如下所示:
(2)将这两个主动调用的函数导出,最终export.js如下所示:
1 | // export.js |
到此,rpc中js部分完成,此时再写一个Python外部调用的脚本invoke.py,如下所示:
1 | # invoke.py |
运行结果如下所示:
再丰富一下上述代码,使其能够通过浏览器访问批量调用:
1 | # invoke_flask.py |
启动之后,运行:
1 | curl -X POST http://127.0.0.1:5000/encrypt -H "{Content-Type:application/json}" -d '{"data":"xcl"}' |
注:-X
指定http请求的方法,-H
指定http请求的头部,-d
指数据体。
运行结果如下:
如果进一步想要将rpc变成批量化集群调用,则可以部署到服务器上。但是需要考虑手机性能、frida版本的稳定性、网络状况等。使用siege压力测试工具进行测试:
1 | siege -c10 r100 "http://127.0.0.1:5000/encrypt POST < encrypt.json" |
1 | // encrypt.json |
注:-c10
代表并发数为10,-r100
代表重复次数为100。结果如下所示:
这个速率还可以。对于method02而言,由于其中有Java.choose(),且此操作非常耗时,因此会降低method02调用的速度。为什么此操作耗时?因为每次调用Java.choose都会在内存中重新搜索实例。
我们改进如下:将对象始终保存在程序的外部全局变量中,这样每次调用method02的时候不至于重新寻找。
但是这利用了app的特殊性,即app不会退出(一直循环调用method01与method02),且调用的函数处于MainActivity中,即只要app不退出,MainActivity就会一直存在于内存中。那么,如果在其他app中,对象被系统进行垃圾回收了怎么办?这就需要下一节知识:Native层的主动调用。
frida native层函数的主动调用
虽然method01与method02分别是static与成员函数,但是他们都是native层的。这样的话,我们可以直接在native层进行主动调用,而无需考虑对象是否被回收的问题。Ok,那我们要完成native层的主动调用。继续三板斧。
(1)找到java函数在native层对应的函数符号,并以此找到函数地址。此时分为两种情况:
(a)静态注册的jni函数,其函数符号为Java_<类名>_<函数名>
,例如stringFromJNI,此时hook脚本为:
1 | function hookmethod(){ |
如果不确定函数的符号,可以通过Objection来查看导出的函数列表:
- 查看此时运行的app名称:
- 使用 objection 注入到进程。
1 | objection -g com.example.demoso1 explore |
- 列出libnative-lib.so模块中的导出函数。
1 | // 列出so模块中的导出函数 |
(b)使用RegisterNatives来进行动态注册的函数,例如method01与method02,可以使用frida_hook_libart项目中的hook_RegisterNatives.js脚本获取动态注册后函数所在的地址(注意由于实验版本的frida版本低,所以将脚本中的let改为var)。
1 | frida -U -f com.example.demoso1 -l hook_RegisterNatives.js --no-pause |
可以发现method01的名称改为:_Z8method01P7_JNIEnvP7_jclassP8_jstring
。
(2)写Hook脚本,并改写为主动调用。可以写hook脚本如下:
1 | function hook_native_method(method_name){ |
结果如下:
实际上,书中使用frida_hook_libart项目时只给出了method01在libnative-lib.so中的偏移量offset,此时脚本改为:
1 | function hook_native_method(addr){ |
注:为什么要用Java.vm.getEnv().getStringUtfChars
?这是因为Java中的string参数到native层变成了Jstring对象。
上述脚本并没有涉及函数的主动调用,因为在脚本中并没有主动调用原来的函数。如果主动调用函数的话,应该是这样:
因此,将上述脚本修改如下,这样程序调用method01时,就会执行替换后的逻辑:
1 | function hook_native_method(method_name){ |
可改写为针对method01的主动调用,这样就可以主动调用native层的method01(最终形态):
1 | function hook_native_method(method_name, data){ |
注:(1)JNI函数(native层的函数),第一个参数一定是JNIEnv的指针;第二个参数为jclass或jobject,分别表示jni函数在Java层中是静态还是动态函数。由于method01中没有使用第二个参数,因此可以任意传递相同类型的数据,这里使用ptr(1)构造了一个指针。(2)由于函数主动调用时使用了Java.vm.getEnv这个API,因此需要包裹在java.perform()中。
执行结果如下:
(3)对native函数进行导出,并配置rpc,前面说过,因此不再赘述。
native函数调用的另类之法
以method01为例,此法脱离特定apk加载模块并调用native的固有模式。
(1)将apk解压,并将method01所在模块libnative-lib.so导出到/data/app目录下(必须要放到这个目录下),并赋予权限。
(2)编写主动调用脚本,如下所示:
1 | function invoke_func(addr,contents){ |
注:frida-server仅在python脚本下需要手动启动,在命令行下会自动启动(有时也需要手动),objection使用时也需要手动启动frida-server。
此时注入任何应用都会并调用invoke_method01,都会成功,如下所示(注入了com.android.settings):
注:真实的情况是,脱离app进行模块函数调用时会发生各种问题,例如签名校验,native层调用Java函数等,这个时候此方法就行不通了。
总结:此章的作用就是非常便捷的模拟app中的某些函数,且仅需要知道函数的参数与返回值。
留言
- 文章链接: https://wd-2711.tech/
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处!