终端应用安全之网络流量分析
前言
在日常对客户端应用进行安全审计或者漏洞挖掘的时候,或多或少都会涉及到网络协议的分析。而对于业务风控安全而言,APP 的网络请求往往也代表着终端安全防御水平的上限,因为客户端是掌控在攻击者手中的,服务端的业务逻辑才是安全的核心兜底保障。
本文是笔者在分析众多 Android 应用协议的过程中所尝试总结的一些经验,大部分情况下也可以适用于其他平台的终端应用,如 iOS、macOS、Windows 等,尽管各个操作系统中会存在一些特有的小技巧。
网络流量分析
在介绍具体的流量分析方法之前,我们需要先明确流量分析的目的。对于笔者而言,流量分析的目的通常是为了搞清楚应用某个操作背后实际的行为。比如对于 IM 聊天软件而言,需要了解发送文本消息、图片消息、多媒体消息等背后实际的请求是什么,以梳理其中潜在的攻击面;对于社交类应用,可能黑灰产最关心的是账号创建、登录以及点赞等操作背后的协议,以用于水军的构建;而对于病毒分析师,可能更关注样本与 CC 服务器之间命令协议,以尝试溯源木马背后的僵尸网络。……
其中木马病毒的场景相对特殊,本文更多是针对商业应用的网络流量分析。在分析流量的时候,我们显然需要能够观测到目标流量的明文。这看似是一个简单的需求,却并不容易实现。
首先考虑网络协议的类型,既然是商业应用,那么大部分情况下使用的也是较为通用的网络协议,即 HTTP。但由于面临运营商劫持等原因,目前以 HTTP 请求进行明文传输的仅是少数了,TLS 已经成为目前网络加密的主流。
其次在应用层,目标请求也有不少是经过自行加密的,可以是一个简化版的 ECDH,也可以是随机 AES 秘钥 + 内置公钥加密的方式。这对于第三方而言,并没有一个很好的方式进行通用解密,只能根据不同应用进行 case by case 的逆向分析并进行还原。
因此,对于应用层加密的情况姑且先不考虑,在面对常规 TLS 加密的流量时,我们最好能够有一种方法能够对其进行解密还原。在 TLS 之下并非只有 HTTP,也有部分使用了 MQTT、Websocket 等协议的请求,必要时也需要考虑在内。
因此,我们的应用网络流量分析目标,又可以分解成两个小目标:
1. 目标应用流量截取(捕获);
2. 目标应用流量解密(TLS 解密);
当然,实际的流量分析并不一定需要两个目标同时达成。在某些场景下,可以仅仅通过解密观察到目标的请求数据,并通过日志打印等方式展示出来。
流量抓取
首先看应用流量截取的方案,也即通常我们所说的“抓包”。
系统代理
这是最为简单也是最为常用的一种抓包方案。在 Android、iOS、macOS 这些操作系统中连接 WiFi 时可以指定我们自定义的 HTTP 代理地址。如果应用使用了是系统提供的网络库或者遵循这个代理配置,那么就会将 HTTP(S) 请求通过该代理进行发送。
因此,这种方式有个很明显的缺点: 应用可能不按规矩来。不管是设置的系统代理,还是通过 HTTP_PROXY
环境变量指定代理,都只是一种约定俗成的规则而已。应用完全可以在自身代码中通过 socket
、connect
、write
三连去直接发送网络请求,这样我们的代理就无法收到任何数据。即便是对于系统提供的 HTTP 网络库,一般也会有额外的配置选项,让调用者指定或者忽略任何系统代理。
设置系统代理进行抓包的方法也是有优点的。这种方式配置起来比较简单,而且对应的配套工具也相对成熟,Burpsuite、Fiddle 之类的工具可以很方便的对代理到的流量进行格式化和重放分析。当然,对于非 HTTP 的其他协议支持可能就不是很完善了。
除了直接设置系统代理,对于桌面应用其实还有一些其他方式去对目标应用进行代理,比如使用 proxychains
工具去加载动态库从而重定向目标网络请求。虽然原理不同,但其实际效果比较类似,因此也统一归类为系统代理方法。
路由抓包
既然系统代理模式抓包容易被应用绕过导致流量缺失,那么一个直观的想法就是回归流量分析的本身,直接在网络层进行抓包。其中比较常用的方式就是路由抓包,即在网关层进行抓包。
具体实现也不难,大家应该都知道在近源渗透中的 evilTwin AP, 即假热点的攻击方法。攻击者通过可以完全控制的机器创建一个假热点,待目标连入后进行网络劫持,从而获取敏感信息,或者替换关键流量实现代码执行。从这个角度上看,该方法也可以看作是一种 “网络临侦”。
对于我们的抓包方案来说,可以构建一个真热点,一般使用开源的 hostapd
可以实现;而热点网络所需要的 DHCP 服务器或者 DNS 服务器可以通过 dnsmasq
去实现。
路由抓包方案通常与透明代理进行结合使用,以实现 HTTP 请求分析、劫持与重放的功能。透明代理对于客户端而言是一个真实的 HTTP 服务器,通常通过 DNS 劫持或者路由劫持去实现。Nginx 中一个重要的功能就是透明代理,在同一个公网地址进行监听,并通过不同的域名分发到实际的后端服务器中。
前文提到的许多 HTTP 代理工具也支持透明代理,比如 BurpSuite 就支持 invisible proxy[1],mitmproxy 中称为 Transparent proxy[2] ,通过 -m transparent
指定。
路由抓包和系统代理相比具有很多优点,比如可以保证抓到所有的 HTTP/HTTPS 流量,并且对于目标应用透明。这里有个潜在的好处,由于对被抓包目标透明,因此路由抓包可以用于一些 IoT 设备的流量分析,比如车机设备、智能家居等等。
缺点也很明显,即路由抓包需要额外的自定义路由设备。如果是运行在 PC 端且驱动支持完善的话还好,可以在个人电脑上作为临时热点;如果电脑(系统)不支持可能还需要在虚拟机里跑个 Ubuntu 去运行,或者使用额外的硬件如树莓派去执行代码,相比于前面的方法复杂度上升不少。另外使用路由抓包方法捕获到的流量是整个系统的流量,而不仅限于目标应用,因此我们还需要对这些流量进行额外的过滤。
虚拟网卡
路由抓包方案的目标是为了完全捕捉到目标应用流量,确保没有遗漏,另外还可以使用透明代理去实现 HTTP 请求的解析。这种方法的本质是在目标应用的路由中间节点进行网络劫持,其实要实现这个目标可以直接在应用端实现。
实现原理就是在本地运行一个虚拟专用网络,构建一个虚拟网卡设备,并将路由规则修改为优先通过我们虚拟的网卡进行请求。比如,Android 上可以通过实现 VPNService 去构建自己的 VPN 服务,ProxyDroid
就是其中的一个实现;在 macOS 或者 iOS 中可以使用 Surge 等工具去实现类似的功能。
将路由抓包在客户端上实现的一大好处是可以通过系统信息将网络请求与实际的进程相关联,同时省去了对额外设备的依赖;缺点则是比较依赖代理工具的工程实现,据我所知实现效果较好的工具其价格也不菲。当然这种抓包方式笔者使用得不多,如果有其他好的类似工具也欢迎留言分享一下。
libpcap
那位说了,既然都在端上抓包了,为什么不更直接一点,使用 tcpdump 或者 Wireshark 这类工具进行抓包?别急,这正是这一节要介绍的最后一种抓包方法。tcpdump 这类工具本质上是基于 libpcap 的,相信大家都比较熟悉了。
其实在前文中路由(网关)抓包和虚拟网卡抓包的方式中,都可以使用 tcpdump 进行抓包,前者需要指定网卡为热点网卡(AP),后者指定为虚拟网卡(TUN)即可。这样抓包出来的结果是一个 pcap
文件,一般直接丢到 Wireshark 里面去进行分析。与前面的方法相比,这种方法对于 HTTP 的可视化解析可能相对简陋,但是对于协议类型的支持却非常广泛,基本上你能叫出名字的标准协议都可以进行解析。
因此,pcap 分析往往作为一种兜底的网络分析方式。而之所以很多情况下使用前面的方法而不是此法,是因为商业应用的大部分是 HTTP/HTTPS 流量,前面那些代理工具可以帮我们解决以很大部分的解密和可视化工作,即与下一节中提到的 TLS 解密方法相互权衡后所采用的决策。
这里分享一个 libpcap 的抓包技巧。笔者平时分析 Android 应用比较多,而在抓包时会发现有很多不相关的后台流量扰乱视听。实际上可以在 UID 为维度进行抓包,这样就可以只抓取对应应用的流量了。
这个功能称为 NFLOG[3],即 NetFilter Log。libpcap 在 1.2.1 版本之后支持抓取 nflog 的数据,而 iptables
又支持支持 uid 的过滤,二者相结合,比如要抓取 uid 为 1000 的应用数据,可以这样实现:
$ iptables -A OUTPUT -m owner --uid-owner 1000 -j CONNMARK --set-mark 1
$ iptables -A INPUT -m connmark --mark 1 -j NFLOG --nflog-group 30
$ iptables -A OUTPUT -m connmark --mark 1 -j NFLOG --nflog-group 30
$ tcpdump -i nflog:30 -w uid-1000.pcap
由于 uid 信息只关联在发送包上,因此只能添加到 OUTPUT
链上,但可以通过 connmark
去关联返回的数据,这样即可实现单个指定应用(uid)的的抓包了。
流量解密
上节中介绍了流量抓取的一些方案,对于常规 HTTP 流量而言没有太大争议,但是对于当今日趋普遍的 HTTPS 却有一些悬而未决的问题。如果我们抓到的所有 HTTPS 流量都是通过 TLS 进行加密的,那对于分析而言就几乎毫无价值了。
因此,为了能够对目标应用进行网络分析,应用层的私有加密姑且不论,至少要解决标准的 TLS 的解密问题。解密是为了中间人攻击,获取明文流量,那么就需要知道 TLS 中常规的中间人防御方案,一般来说,有下面几种:
1. 客户端验证服务端的证书链可信,基本操作;
2. 服务端验证客户端的证书可信(Certificate Request),又称为双向证书绑定;
3. 客户端验证服务端的证书 HASH 是否在白名单中,即 SSL Pinning;
4. ……
在介绍后文的具体解密方案中也会围绕这几点去进行分析。
根证书
添加自定义的根证书应该是 HTTPS 流量分析的标准答案了。前文中提到的 Burpsuite、mitmproxy 等工具的文档中肯定都有介绍如何添加自定义根证书,根证书的存放对于不同的操作系统甚至不同的应用都有不同路径。比如在 Android 中,根证书存放在 /system/etc/security/cacerts
目录之下;在 iOS/macOS 中,根证书存放在 Keychain
中;对于 Firefox
浏览器,其应用中打包了证书链,不使用系统的证书。……
至于如何添加根证书,上过学的话应该都能在搜索引擎中找到方法,这里就不再啰嗦了。虽然添加自定义根证书可以让我们很方便地使用代理工具进行 HTTPS 流量分析,但其实际上只解决了第一个问题,因此对于某些做了额外校验的应用而言 TLS 握手还是无法成功的。
如果目标应用服务器在 TLS 握手中校验了客户端证书,那么我们还需要在代理工具中添加对应私钥才能顺利完成握手。该证书一般以 p12
格式存放,包含了客户端的证书及其私钥,通常还有一个额外的密码。通过逆向分析目标应用的的加载代码往往不难发现客户端证书的踪迹,甚至有时可以直接在资源文件中找到。
如果目标应用使用了 SSL Pinning 加固,那么通常是将服务器的证书 HASH 保存在代码中,并在握手之后进行额外校验。由于相关数据(证书HASH)和逻辑都在代码中,因此这种情况下往往只能通过侵入式的方式去绕过 Pinning 校验,比如 Patch 代码或者使用 hook 等方法实现。由于这是一个较为常见的功能,因此网上有很多相关脚本可以实现常规的 SSL Pinning bypass,但需要注意的是这并不意味着可以 100% 绕过,对于一些特殊实现仍然需要进行特殊分析。
SSL keylog
除了在端侧添加自定义根证书,还有一种方式可以解密 SSL/TLS 的流量,即在握手过程中想办法获取到 TLS 会话的 Master Key
,根据协商的加密套件,就可以对整个 TLS stream 进行解密。关于 TLS 握手的原理介绍,可以参考笔者上一篇文章 —— 深入浅出 SSL/TLS 协议。
知名的抓包和网络协议分析工具 Wireshark
就支持通过添加 keylog
文件去辅助 TLS 流量的解密。这里的 keylog 就是 TLS 会话秘钥[4],文件格式为 NSS Key Log Format[5]。对于不同版本的 TLS 内容略有不同,在 TLS 1.2 中只需要一个会话的 MasterKey,使用 CLIENT_RANDOM
去区分不同会话;而在 TLS 1.3 中每个会话包含 5 个秘钥,分别用于解密握手、数据阶段的不同数据。
那么,这个 keylog 文件我们应该如何获取呢?对于大部分 SSL 库而言,比如 OpenSSL、BoringSSL、libreSSL 等,都可以通过 SSL_CTX_set_keylog_callback[6] 这个 API 去设置获取的回调,令 SSL 库在握手成功后调用对应的回调函数从而获取 keylog。因此我们就需要通过静态 patch 或者动态 hook 的方式去为 TLS 添加该回调。
这看似很简单,但实际操作起来会遇到一些问题。比如,很多大型商业应用都封装了自己的 SSL 库,甚至同一个应用中不同组件中又间接包含了有多个 SSL 库,为了每一个 TLS 会话都能成功解密,需要确保每个 SSL 库都要被正确 patch 或者 hook;
其次,对于某些组件而言,实际是通过静态编译的方式引入 SSL 库,比如 webview
、libflutter
、libffmpeg
等。在去除掉符号后,我们可能需要通过一些方法去搜索定位所需的符号地址。这个任务的难度可大可小,简单的可以通过 yara、Bindiff 去进行定位,复杂的话也可以通过一些深度学习算法去进行相似度分析,比如科恩的 BinaryAI
或者阿里云的 Finger
等。感兴趣的也可以去进一步阅读相关的综述文章:
USENIX Security 2022 - How Machine Learning Is Solving the Binary Function Similarity Problem[7]
另外还有一个问题,SSL_CTX_set_keylog_callback
这个 API 并不是最初就存在于 SSL 库中的。以 openssl 为例,keylog 文件的支持实际上是在 commit 4bf73e[8] 中才被引入。因此,如果遇到了某些应用中依赖于旧版本的 SSL 库,那么可能就不支持 keylog。我们要想强行支持就要进行二进制级别的 cherry-pick,这个工作量还是挺大的。
虽然有这些那些难点,但这种解密方法的一大优点是可以一次性解决本节开头所提及的三个问题,即服务端证书校验、客户端证书校验和 SSL Pinning。因为该方法并没有对流量进行网络层面的中间人,而是在应用的运行过程中泄露会话秘钥,因此不会影响上层的证书校验。
SSL read/write
既然设置 keylog 如此麻烦,为什么不找一些相对简单且通用的 API 去进行解密呢?一个直接的思路就是通过挂钩 SSL_read[9]、SSL_write 来获取 SSL 读写的明文数据。基于这个思路目前网上有许多工程化的实现,比如 eCapture[10] 是基于 eBPF/uprobes
的 TLS 抓包方案;r0capture[11] 则是基于 frida
注入的 TLS 抓包方案。
使用该方法进行解密的一大优点,或者说特点,是这种方式可以在解密的同时直接输出明文信息,因此可以完全略过流量抓取这一步。虽然许多开源工具是将结果保存为 pcap 文件进行进一步分析的,但实际上也可以直接在标准输出或者日志文件中打印出来进行分析。
由于这些抓包工具本质上都是在获取 SSL read/write 明文的基础上再以 pcap 格式进行转储,因此同样会面临 keylog 方案所面临的问题,即依赖 SSL 库的符号。但不同的是其所依赖的是较为通用的符号,因此不太会受到 SSL 库版本的限制。唯一需要考虑的难题是如何解析无符号的 SSL 库中相关函数的偏移地址,这在上节中有些简单介绍,展开的话又是另一篇论文了。
小结
上述介绍的每一种流量抓取方法都可以和任意一种流量解密方法相结合,组成一种网络流量分析方案。实践上使用较多的是下面几种组合:
• 系统 HTTP 代理 + 根证书
• 路由抓包/透明代理 + 根证书
• tcpdump + keylog
• SSL_read/SSL_write hook
每种方案都有其优点和缺点:
抓包方案 | 优点 | 缺点 |
系统代理 | 配置简单,工具成熟 | 可被忽略,流量不全,证书问题 |
路由抓包 | 流量完整,应用透明 | 配置复杂,协议受限,证书问题 |
tcpdump + keylog | 流量完整,无需证书 协议丰富,应用过滤 | TLS 解密需要 hook 应用 且依赖于 SSL 库的版本和符号 |
SSL read/write | 劫持简单,无需证书 | 需要(某些)符号,依赖 hook,流量不全 |
那么实际安全分析中要如何选择呢?正如那句老话所说: 网络安全没有银弹,实际情况也无法一概而论,通常是根据具体的目标去进行分析。
例如,对于操作系统比较封闭的 IoT 设备,通过路由抓包是唯一选择;对于移动应用或者桌面应用而言,可以先尝试传统的系统代理方式,添加对应根证书,如果不能抓到包,可以通过流量分析可能的问题:
• 流量日志中服务端有 Certificate Request 则表示进行了客户端证书校验;
• 流量日志中握手成功但很快断开,则客户端中可能使用了 SSL Pinning 加固;
• 流量日志中客户端握手失败,Alert 提示证书不可信,则说明客户端使用了自定义的 keystore 而不是系统的根证书;
• ……
此时如果发现客户端使用了多种方法防止 HTTP 代理进行中间人抓包,那么就可以尝试使用 keylog 或者 SSL read/write hook 的方式进行分析。总而言之,流量层的分析可以确保数据的完备性,即应用发送的请求都能够确保抓到;而 HTTP 代理和部分解密方法对于 Web 流量则更具针对性,在不同的场景中可以对不同分析方案进行灵活切换。
参考链接
• Intercepting traffic from Android Flutter applications[12]
• 自动定位webview中的SLL_read和SSL_write[13]
引用链接
[1]
invisible proxy: https://portswigger.net/burp/documentation/desktop/tools/proxy/options/invisible[2]
Transparent proxy: https://docs.mitmproxy.org/stable/howto-transparent/[3]
NFLOG: https://wiki.wireshark.org/CaptureSetup/NFLOG[4]
TLS 会话秘钥: https://wiki.wireshark.org/TLS[5]
NSS Key Log Format: https://firefox-source-docs.mozilla.org/security/nss/legacy/key_log_format/index.html[6]
SSL_CTX_set_keylog_callback: https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_keylog_callback.html[7]
USENIX Security 2022 - How Machine Learning Is Solving the Binary Function Similarity Problem: https://www.eurecom.fr/publication/6847/download/sec-publi-6847.pdf[8]
4bf73e: https://github.com/openssl/openssl/commit/4bf73e9f86804cfe98b03accfc2dd7cb98e018d6[9]
SSL_read: https://www.openssl.org/docs/man1.1.1/man3/SSL_read.html[10]
eCapture: https://github.com/ehids/ecapture[11]
r0capture: https://github.com/r0ysue/r0capture[12]
Intercepting traffic from Android Flutter applications: https://blog.nviso.eu/2019/08/13/intercepting-traffic-from-android-flutter-applications/[13]
自动定位webview中的SLL_read和SSL_write: https://mabin004.github.io/2020/07/24/%E8%87%AA%E5%8A%A8%E5%AE%9A%E4%BD%8Dwebview%E4%B8%AD%E7%9A%84SLL-read%E5%92%8CSSL-write/