R0capture源码解读
R0capture介绍
r0capture是一个基于firda实现的优秀的开源的抓包工具。它能完成对TCP协议层及上层所有协议的报文捕获,且不用考虑证书校验等问题。
它的实现基于frida_ssl_logger,两项目侧重不同。
项目地址:(github) https://github.com/r0ysue/r0capture
安卓应用层抓包通杀脚本
- 仅限安卓平台,测试安卓7、8、9、10、11、12、13、14 可用 ;
- 无视所有证书校验或绑定,不用考虑任何证书的事情;
- 通杀TCP/IP四层模型中的应用层中的全部协议;
- 通杀协议包括:Http,WebSocket,Ftp,Xmpp,Imap,Smtp,Protobuf等等、以及它们的SSL版本;
- 通杀所有应用层框架,包括HttpUrlConnection、Okhttp1/3/4、Retrofit/Volley等等;
- 无视加固,不管是整体壳还是二代壳或VMP,不用考虑加固的事情;
原理介绍
用一句话概括r0capture的核心原理即为:通过frida hook libssl.so中的 SSL_read和SSL_write等函数以实现在ssl通道加密之前拿到明文内容并储存。
不同于我们一般对于ssl报文捕获的思路,我们通常对于https的报文都是通过中间人(MITM)原理,借助HTTP代理完成中间人建立,从而捕获明文请求。但是r0capture则是直达底层,通过hook安卓基础ssl收发包的so,完成对所有报文的捕获。这两种方案均有优劣,下面具体介绍:
优劣势
相较于传统抓包方案,r0capture解决了安卓端证书校验(ssl pinning)、代理检测、自建协议等抓不到包的问题。正是因为它hook到tcp层报文收发函数,它支持了tcp层及上层所有报文捕获,但是也正因如此,它失去了一些便捷性,如依赖frida、依赖wireshark解决报文展示、对安卓版本有限制等。
所以,当我们初次尝试抓包时,可以先使用Charles或者burp,但是当我们遇到上述问题抓不到包时,可以尝试使用R0capture这个工具。
源码解读
整个项目的代码分为两个文件r0capture.py和script.js。
入口文件为r0capture.py。
r0capture.py
if __name__ == "__main__": |
1、调用 show_banner()
函数,这个函数主要是展示当前项目的一些基本信息
2、接下来是 ArgParser
类的定义和使用,它继承自 argparse.ArgumentParser
,用于解析命令行参数:
class ArgParser(argparse.ArgumentParser): |
- 创建一个新的类
ArgParser
,它扩展了argparse.ArgumentParser
。 - 重写
error
方法,当解析参数时发生错误时这个方法会被调用。在这里,当出现错误时,它会打印出工具的版本、作者信息,以及错误信息和帮助信息。最后调用self.exit(0)
以结束程序。
3、接下来是参数解析的代码,详细描述了工具的功能,并通过示例用法帮助用户了解如何使用该工具。
4、后面是对应参数解析,参数含义如下:
参数 | 含义 | 必需性 |
---|---|---|
-p | 抓到的报文保存成pcap的文件名 | 非必需,不指定则不保存 |
-H | 自定义指定frida的host和port | 非必需 |
-v | 显示更详细的日志输出 | 非必需,默认是 |
-ssl | 指定ssl库名称 | 非必需,默认libssl.so |
-U | 连接usb设备的 | 非必需,默认否 |
-f | 以spawn形式启动 | 非必需,默认否 |
-w | 等待几秒后执行抓取 | 非必需,默认0 |
processname | 进程名或进程id | 必须 |
5、接下来进入ssl_log函数,内部定义了log_pcap和on_message,分别是保存pcap具体函数和frida的消息回调函数。
- log_pcap函数:要功能是将捕获到的 SSL 数据包写入指定的 pcap 文件中,以便进行后续的分析和处理
- 定义on_message函数:是
ssl_log
中的重要组成部分,负责接收并处理来自 Frida 的消息。它不仅管理错误情况,还记录捕获的数据,并将其输出到控制台或文件中。关键代码说明如下:
(1)首先连接设备获取到devices和session
if isUsb: |
(2)如果命令参数有 pcap,就创建 pcap 文件,将hook到的数据储存进去。这里只是先写了一个文件头。pcap 的格式可看:
https://blog.csdn.net/in7deforever/article/details/6460595
if pcap: |
(3)加载script.js脚本,调用定义的RPC方法
with open(Path(__file__).resolve().parent.joinpath("./script.js"), encoding="utf-8") as f: |
(4)监听中断信号(Ctrl+C)
print("Press Ctrl+C to stop logging.") |
script.js
首先,折叠一下代码,可以看到定义了如下函数:
rpc.exports
rpc.exports = { |
刚进来看到的导出的setssllib函数,用于从外部指定ssl库名称,主要用于部分非原始ssl.so文件名的场景。
uuid
接下来的是uuid生成算法,主要功能是生成一个随机的唯一标识符。在本脚本中是用于生成文件名的一部分。特别是,在保存证书为 p12 格式文件时,使用了这个随机字符串,以确保文件名的唯一性,避免在同一目录中产生重复文件。
return_zero
没啥作用,就是返回了一个 0
initializeGlobals
1、这里使用了一个 frida 的 api,ApiResolver。
var resolver = new ApiResolver("module"); |
枚举当前进程中已经加载的动态链接库的导入导出函数名称。
还兼容了 ios。ApiResolver 会自动搜索符合匹配规则的符号地址,如果是安卓则从libssl和libc中找到相关函数,如果是ios则……。可以看到这里关注的函数分别有:libssl.so里面的”SSL_read”, “SSL_write”, “SSL_get_fd”, “SSL_get_session”, “SSL_SESSION_get_id”和libc.so里面的”getpeername”, “getsockname”, “ntohs”, “ntohl”。
2、接下来通过enumerateMatches函数找到这两个lib库的这些导出函数,并通过函数名保存为名称-地址的映射。
3、最后,经过上述拿到了函数的地址,再通过NativeFunction生成可调用的函数。就是通过函数地址和参数类型生成NativeFunction,后续使用时即可直接调用。
接下来执行了initializeGlobals函数。
ipToNumber
主要作用是用于将四段式的ip串转换为整数数字形式。如下所示,192.168.0.1 转换后为 16820416
getPortsAndAddresses
主要功能是根据socket拿到本地地址和远端地址,这里主要依赖的是两个接口Socket.localAddress和Socket.peerAddress。这两个地址就是wireshark显示的src和dest地址了,根据这俩地址也就能清楚地知道是本地哪个端口和外部哪个ip的哪个端口通信了。
getSslSessionId
接下来是getSslSessionId函数,该函数主要用于根据ssl对象生成唯一了hex字符串以代表该ssl对象。为什么要有这么个东西呢?想象一下,如果app中建立了多个信道进行ssl通信,然后同时hook到了大量的报文,那如何将这些报文区分开来?则是通过该id区分不同ssl通信信道。也就是我们使用wireshark时,使用follow tcp(http) stream时,一个stream下的所有收发报文都是用一个session_id。
Interceptor.attach(addresses[“SSL_read”]
通过hook http://aospxref.com/android-8.1.0_r81/xref/external/conscrypt/common/src/main/java/org/conscrypt/NativeCrypto.java
的 native 方法 SSL_read SSL_write, 使流量进入SSL层时对数据进行dump
接下来则是真正实现hook的代码。上述代码实现了对SSL_read的hook,该函数原型如下:
一旦该函数被调用,触发onEnter,程序会将ssl对象及相关session_id和src,dest地址及参数2中的buf指针保存起来。在函数执行完成后触发onLeave,这时app已经完成了消息接收,buf中已经拿到明文信息,程序根据返回值得到接收的包体大小,并将之前保存的相关信息一并,通过send发回到on_message回调函数,on_message回根据发回的消息进行处理和保存。这样就完成了收消息的捕获和保存。
Interceptor.attach(addresses[“SSL_write”]
SSL_write函数原型:
发消息的hook更简单,不用等到函数执行完,在SSL_write函数被调用时,触发onEnter,程序开始保存ssl相关信息及发送的buf信息,一并发回到on_message处理。
客户端证书导出
hook了java.security.KeyStore$PrivateKeyEntry的getPrivateKey函数和getCertificateChain函数,在他们被调用时通过执行getPrivateKey和getCertificate函数拿到私钥和证书对象,实现将客户端证书导出保存的功能。默认保存的路径在/sdcard/Download/+包名+随机字符串.p12的位置。
SSLpinning helper
接下来是辅助分析ssl pinning位置的代码,如果一旦程序加载证书文件则会被该段代码捕获,并主动查询堆栈是否存在关键词checkServerTrusted,如果存在则很可能是证书绑定的相关代码,此时发送堆栈信息到外部,便于定位。
考虑到App在验证证书时会打开证书文件判断是否是App自身所信任的,因此一定会使用File类的构造函数打开证书文件获得文件的句柄,所以我们在测试时可以Hook上所有File类的构造函数,即对File.init函数进行hook。
Http请求&响应捕获
接下来实现的两个hook则是在java层完成了对socketWrite0和socketRead0的hook,这俩函数是安卓端非ssl加密的正常socket通信的收发函数。至此也完成了对非加密的报文的捕获。
堆栈打印
最后这一段,是为了支持对ssl_read和ssl_write的堆栈信息打印单独写的hook。因为在so层已经拿不到java层的堆栈信息了,所以此处hook了java层的write和read函数(这其实也就是libssl.so导出函数在java层的调用函数),虽然此处一部分在外部一部分在内部略显不优雅,但是在辅助我们分析发包位置或报文构建时还是很有裨益的。
参考链接:
https://mp.weixin.qq.com/s/lMV1UZYOaSRJjMa8PNFkow