安卓抓包初探

安卓抓包初探

最近做实验需要对安卓应用的流量进行抓包作为 Ground Truth,而部分安卓应用的流量是加密的,并且没有走系统的证书链验证,所以信任自定义的 CA 证书并不能成功解密流量。经过调研,无法让应用信任用户证书链中的证书可以通过绕过证书校验相关函数/修改系统内置证书链来实现,下面对遇到的问题和解决方案进行简单记录。

Frida 动态注入(需要 Root 权限)

Frida 环境注入方案的最佳实践是 r0yuse 的 r0capture工具,该工具在 description 里宣称自己是“安卓应用层抓包通杀脚本”,当然,在新版项目 Readme 最后也提到了局限:

局限:部分开发实力过强的大厂或框架,采用的是自身的SSL框架,比如WebView、小程序或Flutter,这部分目前暂未支持。部分融合App本质上已经不属于安卓App,没有使用安卓系统的框架,无法支持。当然这部分App也是少数。暂不支持HTTP/2、或HTTP/3,该部分API在安卓系统上暂未普及或布署,为App自带,无法进行通用hook。各种模拟器架构、实现、环境较为复杂,建议珍爱生命、使用真机。暂未添加多进程支持,比如:service或:push等子进程,可以使用Frida的Child-gating来支持一下。支持多进程之后要考虑pcap文件的写入锁问题,可以用frida-tool的Reactor线程锁来支持一下。

环境配置

首先手机端需要 root 并安装 Frida Server,我的 Pixel 3A 实验机因为之前就 root 过了,并且同时为了用 MT 管理器根据某博客推荐装了 Magisk 的 magisk_cxf 模块,里面自带了若干 android 集成环境(lsposed Shamiko、Frida、android-server、MT 管理器、HttpCanary、sslping、webview、proxydroid、cacerts-added),因此此步骤跳过(这也是为啥我不按照 r0capture 同作者的环境配置教程的顺序先在 PC 上装好 Frida 本体再把 Frida Server 推到安卓上)。

接着使用 adb 连接手机,在 adb shell 中找到并查看 Frida Server 二进制的位置及版本:

sargo:/ $ su -
sargo:/ # which fs
/system/bin/fs
sargo:/ # /system/bin/fs --version
15.1.20

然后在 PC 上安装对应版本的 Frida,我根据官方文档直接用 pip install frida-tools==15.1.20 安装时 pip 却提示该版本,甚至可选的最高版本也才 14.x,为此翻了翻 Release,发现 frida-tools 的版本号和 Frida 版本号是分离的,翻了 14 页终于翻到了 Frida 15.1.20 的 Release,其对应的 frida-tools 版本号为 10.6.1,遂成功安装。

接着下载 r0capture,补全对应 pip 包就可以畅快(并非)抓包了~

无法连接 Frida Server

安装好版本对应的 Frida 工具后,使用 frida-ps -U 测试连接 Frida Server 时,遇到错误:

Failed to enumerate processes: cannot read properties of undefined (reading 'getRunningAppProcesses')

经过搜索,发现 GitHub Issue 中有人反馈了相同的问题,原因在于 Google Play 系统更新对 Android 运行时环境(ART)进行了变更,导致 Frida Server 执行注入脚本时报错甚至在高版本安卓系统中无法正常启动,因此不升级 Frida Server(因为后续有人反馈新版本虽然回应已修复但仍可能存在问题)的权宜之计便是把 ART 的更新包卸载,故在 adb Shell 中输入:

pm uninstall com.google.android.art
reboot

接着等待手机系统重启(时间会比启动久一些)即可解决问题。

libssl重复匹配

由于我测试的 App 是 Python QT 程序套壳打包成安卓,因此没有使用安卓系统默认的 libssl,而是自带了 libssl 的 so 库,因此使用 r0capture 会扫描到两个 libssl 动态链接库(系统的 /system/lib/libssl.so 和 APP 自带的 <apk>/lib/arm64/libssl1.1.so),在项目的 GitHub Issue 中也有人反馈了相同的问题,作者对此回复比较简单,只有一句 You can fix it by amend source code,那么我们就来看看怎么 amend source code。

可以看到相关报错是在 script.js 中的 initializeGlobals 函数中抛出,原因是 matches.length != 1,因此直接把匹配关键字修改成不会匹配到多个结果的就可以了(这里修改成了 *libssl1.1*

App 未调用 SSL_read

到这 r0capture 终于成功运行,但实际上并没有 Hook 到任意函数调用,根据对该应用的先验知识(师兄之前在 Windows 平台的 Hook 脚本),应用并没有读取证书,而是只调用了 SSL_CTX_set_verify 等函数校验证书,因此增加 Hook 逻辑:

...
function initializeGlobals() {
  ...
  var exps = [
    [Process.platform == "darwin" ? "*libboringssl*" : "*libssl1.1*", ["SSL_CTX_set_verify"]], // for ios and Android
    ...
  ];
  ...
}
...
Interceptor.attach(addresses["SSL_CTX_set_verify"], {
  onEnter: function (args) {
    // SSL_CTX_set_verify(SSL_CTX *ctx, int mode, int (*verify_callback)(... ))
    // args[0] = ctx
    // args[1] = mode (we want to change this to 0 = SSL_VERIFY_NONE)
    // args[2] = callback

    var original_mode = args[1].toInt32();
    console.log("[! ] SSL_CTX_set_verify:");
    console.log("    Original mode: " + original_mode);
    console.log("    (1=SSL_VERIFY_PEER, 4=SSL_VERIFY_NONE, etc.)");

    args[1] = ptr(0); // SSL_VERIFY_NONE (0)
    console.log("    [MODIFIED] Changed mode to 0 (SSL_VERIFY_NONE)");
  }
});

之后便可以成功使用 mitmproxy 抓到 TLS 流量了~

APK 解包修改(无需 Root 权限)

每次抓流量都要连接 adb 调试再运行 Frida Hook 脚本未免过于麻烦,并且还要 Root 条件过于苛刻,考虑到应用没有使用系统的 libssl 库而是自带了 libssl 库,因此直接解包对库做修改再打包便可以一劳永逸地解决证书验证问题。

工具准备

apktool:用于 APK 的解包与重打包

uber-apk-signer:用于给重打包的 APK 签名,否则无法安装

r2:用于修改 .so 库内容(直接使用 apt install adare2 安装即可)

修改流程

首先解包 APK:

apktool d ./foo.apk

接着使用 r2 修改 libssl 动态链接库:

$ r2 -w foo/lib/arm64-v8a/libssl1.1.so
[0x0003534c]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for objc references
[x] Check for vtables
[x] Finding xrefs in noncode section with anal.in=io.maps
[x] Analyze value pointers (aav)
[x] Value from 0x0003534c to 0x00098e00 (aav)
[x] 0x0003534c-0x00098e00 in 0x3534c-0x98e00 (aav)
[x] Emulate code to find computed references (aae)
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x0003534c]> s sym.SSL_CTX_set_verify
[0x0005b3a8]> wa ret
Written 4 byte(s) (ret) = wx c0035fd6
[0x0005b3a8]> q

上面首先搜索了 SSL_CTX_set_verify,然后在函数符号后面追加写了 ret,表示直接返回,从而跳过校验。

然后再使用 apktool 重新打包应用:

apktool b foo -o foo.apk

最后使用 uber-apk-signer 给 APK 重新签名:

java -jar ./uber-apk-signer-1.3.0.jar --apks ./foo.apk

然后卸载掉手机上原来安装的 APK,重新安装修改过的 APK,便可以成功使用 mitmproxy 抓到 TLS 流量了~

系统内置 CA 修改(需要 Root 权限)

安卓官方文档 中提到,Android 7.0 之后默认不信任用户添加到系统的 CA证书:

Android 中收录了一组预载的系统证书授权机构 (CA),这些授权机构在整个系统范围内受到信任。在 Android 7.0 之前的版本中,设备制造商可以修改其设备上搭载的 CA 组。不过,搭载 7.0 及更高版本的设备将具有一组统一的系统 CA,并且不再允许设备制造商对其进行修改。

因此,目标 SDK>=24 的安卓 APP 在不做特别设置的情况下,不会信任用户 CA 下安装的证书(即通过操作系统设置页面信任的证书)。

因此需要将证书放置到系统证书目录 /system/etc/security/cacerts/ 下,其中,每个证书的命名规则均为 <Hash>.<No>,其中 <Hash> 为证书的 8 位哈希值,<No> 为相同哈希值证书的编号,0-index。

但默认访问 mitm.it 下载的证书名字是 mitmproxy-ca-cert.cer.crt,根据此博客,可以使用 openssl 来计算哈希值:

openssl x509 -subject_hash_old -in mitmproxy-ca-cert.cer.crt

接着重命名好证书并复制到 /system/etc/security/cacerts/ 目录下即可,接着在设置中的系统 CA 页面便可以看到 mitmproxy 的根证书了。

其他未实践参考链接

[原创] Frida解决证书绑定问题-Android安全-看雪安全社区|专业技术交流与安全研究论坛

实用FRIDA进阶:内存漫游、hook anywhere、抓包-安全KER - 安全资讯平台