按照笔者推崇的Frida三板斧理论:先Hook定位关键逻辑,然后主动调用构造参数进行利用,最后通过RPC导出结果进行规模化调用。本节本应该先介绍主动调用的理论与方法,但考虑到后续知识的连贯性,因此这里先对RPC的一些使用方法进行介绍。
读者还记得在安装Frida时使用的是哪种方式进行安装的吗?没错,就是使用pip这一Python的包安装程序对Frida环境进行配置的。事实上,Frida是一款基于Python和JavaScript的进程级Hook框架,其中JavaScript语言承担了Hook函数的主要工作,而Python语言的角色则相当于一个提供给外界的绑定接口,使用者可以通过Python语言将JavaScript脚本注入进程中,只是相比于命令行注入的方式,Python通过代码注入的方式更加优雅。另外,官方也提供了通过Python远程外部调用JavaScript中的函数的方式,并在相应的项目仓库(项目地址:https://github.com/frida/frida-python)中提供了一些例子。本节将通过其中的一部分代码来介绍一些基础的frida-python远程过程调用的方式。
在正式介绍官方仓库的例子前,首先介绍一些通过Python实现Frida注入的基础知识:相对于命令行直接指定参数注入通过USB连接的手机进程,通过Python注入Android进程的方式步骤更加分明。
通过Frida获取特定设备。代码清单3-1分别展示了连接USB设备和网络设备的方式。
代码清单3-1 获取设备
import frida # 获取USB连接设备 device = frida.get_usb_device() # 获取网络设备 device = frida.get_device_manager().add_remote_device('192.168.50.96:6666')
在获取到设备device后,与Frida通过命令行实现进程注入的两种方式——spawn和attach对应,使用Python完成进程注入的方式同样也有两种。以注入“设置”应用为例,其代码如代码清单3-2所示。
代码清单3-2 注入进程
# -*- coding: utf-8 -*- import time import frida # spawn 方式注入进程 pid = device.spawn(["com.android.settings"]) # 注意这里spawn的参数是一个list类型的参数 device.resume(pid) # 唤起进程,也可以在通过attach函数注入进程后再调用 time.sleep(1) # 这里休眠是为了等待进程被完全唤起 session = device.attach(pid) # attach模式注入进程 session = device.attach("com.android.settings") # 直接通过指定包名进行进程注入
成功注入进程后,还需要最后一步:Hook脚本的注入。这一步实际上就是将JavaScript脚本作为字符串或者字节流通过Frida提供的API加载进相应的进程session中。简单的JavaScript hook脚本通过Python注入的方式如代码清单3-3所示。
代码清单3-3 注入脚本
import frida script = session.create_script(""" setImmediate(Java.perform(function(){ console.log("hello python frida"); })) """) # 读入Hook脚本内容 script.load() # 将脚本加载进进程空间中
在代码清单3-3中,创建脚本的方式是通过读入一段代表Hook脚本的字符串。在真实地完成脚本的注入时,推荐将JavaScript脚本和Python代码分离,通过读文件的方式将脚本加载进进程中,这样在编写脚本时有智能提示,而且可以单独使用命令行测试脚本的正确性,笔者通常使用如代码清单3-4所示的方式注入脚本。
代码清单3-4 文件方式注入进程
with open("hook.js") as f: script = session.create_script(f.read()) script.load() # 将脚本加载进进程空间中
在介绍了通过Python注入脚本的基础知识后,让我们来正式了解一下Frida官方提供的一些例子。
笔者认为学习RPC实际上就是学习一些关于JavaScript脚本和Python进行交互的方式。
以frida-python仓库的example目录下的rpc.py脚本文件为例,这里将代码修改为适合Android应用的形式,具体内容如代码清单3-5所示。
代码清单3-5 rpc.py
# -*- coding: utf-8 -*- from __future__ import print_function from frida.core import Session import frida import time device = frida.get_usb_device() # 通过USB连接设备 # frida.get_device_manager().add_remote_device('192.168.50.96:6666') pid = device.spawn(["com.android.settings"]) # spawn方式注入进程 device.resume(pid) time.sleep(1) session = device.attach(pid) script = session.create_script(""" rpc.exports = { hello: function () { return 'Hello'; }, failPlease: function () { return 'oops'; } }; """) script.load() # 加载脚本 api = script.exports # 获取rpc导出函数 print("api.hello() =>", api.hello()) # 执行导出函数 print("api.fail_please() =>", api.fail_please()) # 执行导出函数
在确保手机使用USB数据线连接上计算机并且测试机上相应版本的frida-server正在运行后,直接通过python命令运行rpc.py脚本,其结果如图3-1所示。
图3-1 rpc.py执行结果
在代码清单3-5这个例子中,会发现如果想要在Python中调用JavaScript中的函数,首先需要在JavaScript中将相应函数写到rpc.exports这个字典中,在编写完成后,如果想要在Python中进行调用,只需先通过script.exports获取相应的导出函数字典,再通过相应的字典键值进行调用即可。
另外,细心的读者会发现,JavaScript脚本中的failPlease键值在Python脚本中调用时,从最初的驼峰命名法(第一个单词以小写字母开始,从第二个单词开始以后的每个单词的首字母都采用大写字母)变成了下画线命名法(每个单词用下划线隔开并且单词都是小写)。简单来说,就是所有的JavaScript脚本中带大写字母的导出函数键值被替换为“_”加上相应小写字母的方式,对应代码清单3-5中JavaScript中的failPlease导出函数变成了Python中的fail_please。
如果说rpc.py介绍的是在Python中远程主动调用JavaScript中的函数的方式,那么接下来要介绍的就是JavaScript主动向Python发送数据的方式。
以examples目录下的detached.py文件为例,其代码在修改为适配于Android应用后,具体内容如代码清单3-6所示。在运行脚本后,手动通过adb命令断开USB连接,运行结果如图3-2所示。
代码清单3-6 detached.py
# -*- coding: utf-8 -*- from __future__ import print_function import sys import frida import time def on_detached(): print("on_detached") def on_detached_with_reason(reason): print("on_detached_with_reason:", reason) def on_detached_with_varargs(*args): print("on_detached_with_varargs:", args) device = frida.get_usb_device() # frida.get_device_manager().add_remote_device('192.168.50.96:6666') pid = device.spawn(["com.android.settings"]) device.resume(pid) time.sleep(1) session = device.attach(pid) print("attached") session.on('detached', on_detached) # 注入分离响应函数 session.on('detached', on_detached_with_reason) # 注入分离响应函数 session.on('detached', on_detached_with_varargs) # 注入分离响应函数 sys.stdin.read()
图3-2 运行效果
观察图3-2会发现,打印出来的信息是frida-server进程终止导致的注入分离,而这个实现正是通过session.on()这个API指定deatached行为对应的处理函数打印出来的日志。
同样,读者如果研究crash_report.py文件代码,会发现相对于detached.py,crash_report.py只是多了一个针对进程崩溃的响应函数而已。但要注意的是,针对进程崩溃的响应函数是通过设备device添加的,而不是进程的session(device.on()函数),其中crash_report.py中的主要代码如代码清单3-7所示。
代码清单3-7 crash_report.py
def on_process_crashed(crash): print("on_process_crashed") print("\tcrash:", crash) ... device = frida.get_usb_device() # frida.get_device_manager().add_remote_device('192.168.50.96:6666') pid = device.spawn(["com.android.settings"]) ... device.on('process-crashed', on_process_crashed) # 进程崩溃响应函数 session = device.attach(pid) # session = device.attach("Hello") session.on('detached', on_detached) # 注入分离响应函数 ...
到这里,frida-python中与RPC远程调用相关的代码差不多就介绍完毕了,当然frida-python的examples中远不止前面介绍的这些代码,比如脚本child_gating.py介绍了子进程的注入方式,bytecode.py介绍了将脚本编译为字节码后再加载脚本的方式,inject_library文件夹中的代码则介绍了手动向进程中注入一个动态库文件的方式,等等。因为这部分代码与本章RPC内容的相关性不大,故这里不再赘述,读者如果感兴趣,可以自行研究。
另外,在上一章中曾介绍过ZenTracer在Trace快速定位类方面的应用,但并未深究其代码细节,事实上在获取到ZenTracer全部代码后会发现,整个项目除去部分UI相关的代码,真实用于Hook的代码只有一个traceClass()函数,剩下的部分都是用于在JavaScript和Python界面之间的数据传递,数据的传递方式是通过send函数将JavaScript中的数据传输到Python用于接收信息的FridaReceive函数中。其具体代码如代码清单3-8和3-9所示。
代码清单3-8 JavaScript中的数据传输到Python
代码清单3-9 在Python中接收消息
与代码清单3-7中使用session.on('detached', on_detached)函数指定进程崩溃的响应函数相比,在代码清单3-9中则通过script.on("message", FridaReceive)函数注册用于接收message信息的函数,做到了当JavaScript中调用send函数时,消息序列被发送到指定的FridaReceive函数中。另外,这里值得一提的是,ZenTracer中利用JavaScript脚本以字符串的方式读入这一特点,直接通过特定字符串匹配后,替换的方式最终做到了对指定函数进行Hook的效果。当然其实这里不大推荐这种方式,笔者更推荐通过RPC调用的方式传递参数完成函数的Hook,但不排除编写代码时存有一些特殊考虑,笔者限于水平并未发现。
ZenTracer作者的另一个项目——FRIDA-DexDump同样利用了很多RPC相关的知识,与ZenTracer相比,作者在这个项目中频繁地调用JavaScript中的一些导出函数,比如扫描内存中符合条件的DEX文件的函数scandex()等。这里不再一一介绍,如果读者感兴趣,可以自行研究。
在RPC调用中还存在着一些与send()相对应的函数,比如wait()和recv()这两个函数分别用于在JavaScript中阻塞线程和接收从Python中通过script.post()函数传回的数据。由于这几个API笔者平时使用的频率并不是很高,这里不再展开介绍。