SSL Pining问题的解决方案

在第 12 章中,我们了解了 App 抓包的相关内容,但并不是每时每刻都能顺利地抓到包。在某些情况下,我们可能会抓包失败,一个比较典型的现象是包能抓到,响应状态码是 200,但就是获取不到最终的结果,报错信息一般跟 SSL Pining(证书锁定)有关系。

本节我们就具体了解一下什么是 SSL Pining 以及怎么解决这个问题。本节的解决方案和 Xposed、Frida 有关系,正好我们刚学习了这两个工具,因此也可以加深对它们的理解。

实战案例

为了更好地复现 SSL Pining 场景,我们对一个 App( https://app4.scrape.center/ )进行抓包,这个 App 里包含 SSL Pining 的相关设置,如果我们将手机的代理设置为抓包软件提供的代理服务,那么这个 App 在请求数据的时候会检测出证书并不是受信任的证书,从而直接断开连接,不继续请求数据,相应的数据便会加载失败。

首先,在手机上安装这个 App,此时的手机没有设置任何代理,可以发现数据是能正常加载的,如图 13-76 所示。

接下来就要抓包了,我们还是以 Charles 为例,当然用其他抓包软件(如 Fiddler)也可以。在电脑上启动 Charles 之后,确保手机和电脑连在同一个局域网下,然后在手机上设置 Charles 的代理,具体的配置方法见 12.1 节。

然后重启手机,重新打开 App,会出现 “证书验证失败” 的提示信息,而且不会加载出任何数据,如图 13-77 所示。

图13-77 “证书验证失败” 的提示信息

与此同时,Charles 的抓包结果如图 13-78 所示。

图13-78 Charles 的抓包结果

可以看到这里报了一个错误的原因(Failure):Client closed the connection before a request was made. Possibly the ssL certificate was rejected.。

此时如果取消 Charles 的代理,然后重新打开 App,就又能成功加载数据了。

以上展示的就是 SSL Pining 导致的抓包失败现象,为什么会这样呢?下面我们具体了解一下其中的原理。

SSL Pining 技术的原理

SSL Pining 是一种防止中间人攻击的技术,只针对 HTTPS 协议。在遵循 HTTPS 协议的数据通信过程中,客户端和服务端在握手建立信任时,有一步是客户端收到服务器返回的证书,然后对该证书进行校验,如果这个证书不是自己信任的证书,就直接断开连接,不再进行后续的数据传输,这就会导致整个 HTTPS 请求失败。

为了更好地理解其中的原理,我们在电脑上做一个小实验,打开百度首页,在览器左上角看一下证书的信息,如图 13-79 所示。

图13-79 百度首页的证书

点击 “证书”,可以看到证书详情,如图 13-80 所示。

可以看到证书的签发者是 GlobalSign Organization Validation CA。GlobalSign Organization 成立于 1996 年,是一家声誉卓著,备受信赖的 CA 中心和 SSL 数字证书提供商,鉴于其权威性,我们认为其颁发的证书是可信的。

接下来,我们将电脑的全局代理设置为 Charles,一般在 Charles 的菜单中可以设置,打开 Proxy一→macOS Proxy/Windows Proxy,将此选项勾选上即可。

在设置全局代理之前,请先在电脑上设置信任 Charles Proxy CA 这个根证书颁发机构(这也是一种证书),具体的设置方法可以参考 https://setup.scrape.center/charles

现在刷新一下百度首页,再次查看证书详情,如图 13-81 所示。

图13-80 百度首页的证书详情 图13-81 设置 Charles 代理后的证书详情

可以看到,当前的证书签发者变成了 Charles Proxy CA。那此时的电脑要不要信任 Charles Proxy CA 颁发的证书呢?答案是要,因为我们已经设置了信任 Charles Proxy CA,如果没设置,那现在访问百度页面就会出现 SSL 安全提示。

于是我们可以初步得出一个结论:在电脑上设置了信任 Charles Proxy CA 证书后,如果PC 使用 Charles 的代理来访问 HTTPS 网站,所使用的证书就会变成 Charles Proxy CA 颁发的。

电脑上是这样,手机上自然也是。在抓包之前,我们先在手机上设置信任 Charles 的证书(也就是信任 Charles Proxy CA),之后在手机上使用 Charles 代理访问 HTTPS 网站的时候,所有的网站证书就会是 Charles Proxy CA 颁发的,因为手机信任 Charles Proxy CA,所以自然就能正常访问对应的 HTTPS 网站了。

那么关键点来了。

我们在开头提到客户端(这里就是指 App)在获取证书信息之后,是可以对证书做校验的,如果不做校验,那么不会有任何问题,但一旦校验,并发现指纹不匹配,就会直接中断连接,请求自然就失败了?

那这个校验过程怎么实现呢?校验证书的指纹即可。因为使用代理和不使用代理的证书颁发机构不是一个,所以两个证书的指纹也不一样,只要证书的指纹跟指定的指纹不一样,就算校验失败。例如当前证书的指纹,见图 13-82 中框出来的内容。

图13-82 当前证书的指纹

在开发阶段,如果知道服务器返回的证书指纹,是可以提前把指纹写死在客户端这边的。客户端获取证书后,对比证书的指纹跟写死的指纹是否一致,如果一致就通过校验,否则不通过,中断后续的数据传输。

这个过程具体怎么实现呢?通常有两种方式。

  • 对于 7.0 及以上版本的 Android 系统,SDK 提供了原生的支持。在 App 开发阶段,会直接将指纹写死在一个 xml 文件里,然后在 AndroidManifest.xml 文件中添加一个 android:networkSecurityConfig 配置,具体的配置可以参考 Android 官方文档 https://developer.android.com/training/articles/security-config 。不过要注意 Android 系统的版本。

  • 直接将指纹和校验流程写在 Android 代码里,现在 Android 的很多 HTTP 请求库是基于 OkHttp 库开发的,OkHttp 的 SDK 就提供了对 SSL Pinning 的支持,一般可以在初始化 OkHttpclient 对象的时候添加 certificatePinner 这个选项,将信任的证书指纹写死。当然除了 OkHttp,其他库也提供类似的支持。

第二种方式的适用性更广,不局限于特定的 Android 版本,本节也将基于第二种方式实现。

至此,SSL Pining 技术的原理就解释清楚了。简单点讲,就是客户端和服务端在握手过程中,客户端对服务端返回的证书进行校验,如果证书不是自已信任的,就拒绝后续的数据传输过程,这样抓包工具自然抓不到有效的信息。

绕过

明白了原理,那怎样才能绕过这个技术,解除它的限制呢?有以下几个解决思路。

  • 某些 App 是使用第一种方式实现的 SSL Pinning,这种方式对 Android 版本有要求。所以,直接使用 7.0 以下的 Android 系统,即可解除限制。

  • 既然客户端会校验证书,那我们可以直接 Hook 某些用于校验证书的 API,不管证书是否可信都直接返回 true,从而绕过校验证书的过程。我们已经学习了 Xposed、Frida 等工具,可以基于它们实现 Hook 操作。

  • 通过反编译的方式还原 App 代码,修改 AndroidManifest.xml 文件或者代码中用于校验证书的逻辑,修改完后重新打包签名。不过由于 App 代码不好完全还原,该方法的可行度并不高。

其中第二个的可行度最高,所以下面介绍三种基于第二个思路的解决方案。

Xposed+JustTrustMe

JustTrustMe 是一个 Xposed 模块( https://github.com/Fuzion24/JustTrustMe ),其基本原理就是通过 Hook 证书校验相关的 API,绕过证书校验的过程。

请先确保手机已经 ROOT 并安装好了 Xposed 模块,具体配置流程可以参考 https://setup.scrape.center/xposed。

首先下载 apk 文件( https://github.com/Fuzion24/JustTrustMe/releases/ ),并把 JustTrustMeApp 安装到手机上,然后在 Xposed 的模块设置里开启 JustTrustMe,如图 13-83 所示。

之后重启手机,使刚才的操作生效。此时重新打开示例 App,会发现数据成功加载出来了,如图 13-84 所示。

图13-83 开启 JustTrustMe

图13-84 数据成功加载出来

Charles 中也能正常抓取数据包了,如图 13-85 所示。

图13-85 Charles 抓取的数据包

VirtualXposed+JustTrustMe

Xposed+JustTrustMe 的方案有一个限制,就是手机需要 ROOT,解锁 bootloader 等。对于一些系统版本比较高的手机,ROOT 操作是比较困难的,所以提供了另一种解决方案,用 VirtualXposed 代替 Xposed( https://github.com/android-hacker/VirtualXposed )。

VirtualXposed 是基于 VirtualApp 和 epic,在非 ROOT 环境下运行 Xposed 模块实现(支持 Android 5.0~Android 10.0)的,是一款运行在 Android 系统中的沙盒产品,可以将其理解为轻量级的 Android 虚拟机。

要实现 Hook,我们需要将 App 安装到 VirtualXposed 对应的沙盒里,再配以一些 Xposed 模块(如 JustTrustMe ),即可绕过 SSL Pining 技术。

首先,安装 VirtualXposed,其运行页面如图 13-86 所示。

然后点击界面下方的菜单按钮,进入设置页面,如图 13-87 所示。

图13-86 VirtualXposed 的运行页面 图13-87 VirtualXposed 的设置页面

点击 “添加应用”,将手机中的 App 安装到 VirtualXposed 的沙盒环境中,如图 13-88 所示,勾选对应的两个 App(这里需要提前在手机上安装好示例 App 和 JustTrustMe App),再点击 “安装” 即可。

图13-88 安装App

在安装过程中,可能会提示 “是安装到 VirtualXposed 还是 TaiChi”,这里我们直接选择 VirtualXposed。

补充 TaiChi(太极)也是一个类似 Xposed 的模块,同样不需要 ROOT 就能使用,大家可以了解一下。

接下来返回设置页面,点击 “模块管理”,如图 13-89 所示。

这里会自动检测到刚安装的 JustTrustMe 模块,勾选即可,如图 13-90 所示。

图13-90 勾选 JustTrustMe 模块

之后需要重新后动 VirtualXposed。

最后,我们在 VirtualXposed 里打开安装好的示例 App,即可发现数据能成功加载出来了,Charles 也可以成功抓取数据包了。

Frida+DroidSSLUnpinning

既然 Xposed+JustTrustMe 的原理是 Hook 证书校验的逻辑,这个逻辑是通过 Xposed 模块实现的,那能不能基于同样的原理利用 Frida 实现 Hook 呢?能。

如果想基于 Frida 实现 Hook,那么可以结合 DroidSSLUnpinning 这个开源库,其 GitHub 地址是 https://github.com/WooyunDota/DroidSSLUnpinning

首先下载对应的 GitHub 仓库:

git clone https://github.com/WooyunDota/DroidSSLUnpinning

该仓库中有一个 Hook 脚本,我们可以直接使用 frida 命令启动:

cd DroidsSLUnpinning/ObjectionUnpinningPlus
frida -U -f com.goldze.mvvmhabit -l hooks.js --no-pause

这里要给 frida 命令的 -f 选项传入要处理的 App 包名,给 -l 传入要 Hook 的脚本,命令的运行结果如图 13-91 所示。

图13-91 启动命令的运行结果

之后,Hook 脚本便会生效,而且我们可以发现,示例 App 可以成功加载数据了!Charles 也可以成功抓取数据包,和使用 Xposed/VirtualXposed+JustTrustMe 的效果是一样的。

总结

本节我们介绍了 SSL Pinning 技术的原理和解决办法,随着移动互联网的发展,使用 SSL Pinning 的现象会越来越普遍,因此绕过它成为了移动爬虫开发者的必备技能之一,需要好好掌握。