Bypassing OpenSSL Certificate Pinning in iOS Apps

注:这是360安全播报翻译的版本,有些地方翻译的有问题,建议直接看原文:原文链接

移动应用程序普遍通过TLS / SSL(例如HTTPS)与API或Web服务进行通信。 TLS证书是为了验证该服务器的身份,并防止中间人攻击。 浏览器和移动操作系统都预置了信任的证书颁发机构(CA)的列表。 由于所有的证书授权中心都可以给任何一台主机/服务器颁发证书,有良好安全性的应用应该在应用中“钉扎”服务器证书,即不信任任何非可信CA颁发的证书。

从渗透测试的角度来看,这会对使用流量劫持来测试造成很大困难。 没有“pin”时,拦截流量通常需要给拦截代理(如Burp )加载一个TLS证书。 然而当应用程序使用“pin”时,一些问题经常被忽略。 在iOS上,当应用程序使用标准iOS API时, iOS SSL Kill Switch 可以被用作绕过“钉住”(由Matasano的姊妹公司开发iSEC Partners ),它可以用来强制应用程序接受任何服务器或代理颁发证书。 Kill Swith使用了Cydia Substrate 的钩子技术,这个钩子Hook了IOS的验证证书函数,使得他们接受任何证书。 当应用程序使用OpenSSL库时会变的更加复杂,因为它不受Kill Switch钩子影响。

绕过基于OpenSSL的证书“pin”在我们新发布的白皮书中 ,我们讨论两个主要方法:二进制补丁和内存钩子(使用cycript )。 在这篇博客文章中,我们专注于前一种方法,因为它更适合于教学。 二进制修补也是一种通用的技术,因为它适用于cycript和Cydia Substrate 工具不容易获得的平台。

场景:在Github模拟应用程序

为了让这个帖子更容易理解,我们创建了一个实体模型的iOS应用程序 ,它使用OpenSSL和“pin”证书的方式。 当然还有许多不同的实现方式,本文的实现方式和大部分应用使用的基本相同。

模拟的应用程序只是简单得连接到https://www.example.org并执行GET请求,路径为/ 。 然后将结果显示在一个简单的文本视图。为了方便调试,一些调试信息会通过NSLog输出。 相关的代码在ViewController.mm文件。

存储库中包含两个项目的XCode代码,其中一个版本的ARMv7和一个建立ARMv8可执行文件。 此外, binaries文件夹包括二进制文件。

注意,如果要尝试本篇blog中的步骤和修改,你必须有一个越狱的苹果设备。 主要的原因是,我们需要root权限来禁用应用签名检查。

在我们绕过“钉扎”之前,在接下来的章节中,我们首先介绍如何在应用程序与www.example.org之间的通讯过程 。

最容易的部分:应用重定向流量到Burp

当一个应用程序使用了OpenSSL而不是原生的iOS功能“pin”成为了一个挑战。 为了拦截应用流量,通常是设置iOS系统的代理为拦截代理。 但是,如果应用程序使用C(C++)代码通过OpenSSL来打开通TCP套接字时,你可以想到,它直接绕过了系统代理。

常见的方法是修改应用程序使用的主机名到IP地址的映射。 我们可以建立一个DNS服务器。 另一个简单的方法来达到同样目的,是修改设备的/etc/hosts的文件。 用我的工具IDB在“工具”选项卡下完成的:

t0189c10a1796923d88.png

在这里,我们把服务器www.example.org解析到环回地址127.0.0.1 :

  1  127.0.0.1   www.example.org

然后我们可以使用SSH转发(例如,在IDB)转发443端口上的流量到我们工作站的8080端口(Burp监听端口)

t01e3fdb244ffde831b.png

t012b3cdff1c42bdaf4.png

现在,应用程序试图连接到Burp,但因为证书“pin”而失败。 请注意,这可能会非常棘手弄清楚为什么连接失败。 检查Burp的“Alerts”选项卡,可能出现证书验证失败的日志。

t019917d82b0b777426.png

t01f0a738111ec518ab.png

困难的部分:绕过OpenSSL的证书

正如上面提到的,我们不能简单地用iSEC的SSL Kill Switch关闭SSL证书验证。 一种替代的方法是使用Cydia Substrate或者cycript来hook并覆盖证书验证函数,可以参考我们的白皮书 。 在这篇博客中,我们探讨如何修补的应用程序二进制用于绕过证书”pinning”(本博客文章的内容也是白皮书的一部分)。

分析Pinning机制

该应用程序使用pinning的主要机制是受限制的CA列表。 不依赖于第三方证书的收集,应用程序动态生成的OpenSSL的证书存储在内存中( X509_STORE * cert_store = X509_STORE_new(); ),并使用load_cert提供给单一可信证书链 :

t01431e15a266482824.png

可信CA证书硬编码在应用程序的源代码中(PEM格式)。 例如:

t01f290c63ee4dc102b.png

在一般情况下,通常把证书存储在文件系统中,同样也应该在应用程序中使用同样的机制。 这种方法的一个缺点是,在一个root后的手机上证书可以很容易地替换。 与此相反,二进制文件中的证书更难以替换。尽管如此也不能阻止一个逆向工程师来绕过这个机制。

这里并不需要去找到应用的源代码来替换证书。以上代码只是为了展示这项技术。 一旦相关的函数知道后,只需要二进制文件就能绕过pinning。

绕过Pinning

在这种情况下,我们可以替换二进制文件中的证书或者使用其他方法禁用证书认证。 交换的证书是具有挑战性的,因为不同的证书具有不同的长度,并且可能空间不足。 此外,大量编辑二进制文件极其容易出错。 因此我们先看一下启用证书验证的函数:

t01ecbffa407d243ba8.png

快速浏览一下文档的SSL_CTX_set_verify,我们需要设置SSL_VERIFY_PEER为SSL_VERIFY_NONE来禁用证书验证。 这两个值都在定义的常量ssl.h

t01fb05384e482377f7.png

这意味着我们需要找到二进制文件中调用SSL_CTX_set_verify的位置,改变第二个参数,从0x01到0x00 。 需要注意的是这些修改将破坏应用程序签名,所你必须使用一个越狱后的设备。

从设备(或者Github上可执行文件 )导出二进制文件,使用OS X上预装的otool。 (请注意,您可能需要解密应用程序商店的应用程序文件。IDB是能够做到这一,它使用了dumpdecrypted

t01b974b220bf2cf98a.png

请注意,如果你碰巧有HopperIDA可使这个过程更加容易。 所有现代的iOS设备(Apple A7)使用的具有完整的64位支持,所以我们会看到我们的二进制64位地址空间的ARMv8-A架构。 为了简单易懂,让我们先来看看ARMv7版本,并在之后看64位版本。

ARMv7的二进制

在汇编中搜索SSL_CTX_set_verify:

t01bb491e2e51c5a928.png

需要注意的是在(bl _SSL_CTX_set_verify) 参数设置之前。ARM通常会在寄存器中传递函数参数。SSL_CTX_set_verif接受3个参数,分别通过r0, r1, 和r2传递。这些寄存器使用16比特的指令来设置。因此,这些寄存器使用两条指令(movs和movt)来分别写入低字节和高字节寄存器。然而我们只关心修改低字节,我们可以忽略movt指令。

寄存器r0会指向OpenSSL ctx,并且r3是NULL。r1保存着SSL_CTX_set_verify第二个参数是#0x1(等于SSL_VERIFY_PEER)。只要使SSL_VERIFY_NONE等于0就可以禁用证书验证,因此修改在00009bf4的指令从2101改为2100(movs r1, #0x0)(见MOVS文档 )。

现在找到这个指令的位置是一个漫长而痛苦的过程,因为这个地址是内存的地址而不是文件偏移。我发现使用hex编辑器来搜索指令会很方便。

t01aab086e4ecc337a5.png

在二进制文件中向后移动来查看需要修改的地方:

t010cb58e460f763aae.png

修改之后可以使用otool再次查看是否修改成功:

t01f1bd54f6248452fa.png

重新上传二进制文件到设备的app目录,它会再次遇到证书认证失败的错误。结果这个app非常细致得检测了收到证书的方方面面(不只是依赖OpenSSL的功能)。

因此返回到修补相关的代码。在返回bool型的verify_certificate()函数位置,有4个看上去类似结构的顺序。

t01d54e44c305594732.png

这段代码把当前证书和信任的证书进行了验证。这是OpenSSL在peer认证开启的时候就启用的功能,造成了冗余验证。但是这个应用的开发者打算再次验证一次。在反汇编代码中搜索SSL_get_verify_result可以看到以下:

t016a6815a6d4e82042.png

需要注意的是ARM没有RET指令。返回的值通过r0传递,然后通过一个分支(b)指令跳转到返回地址(从寄存器或者堆栈),这个案例中是一个写死的地址0xa138。因此r0被设置为#0x0(false)。为了让返回值变为true并且接受这个证书,我们只需将ro改为#0x1,对应的指令为2001。我们再次使用笨拙的方法去查找,因此我们保留movt指令。(提示: 搜索 8d f8 68 00 39 e1):

t012f2e209c122cd63e.png

t017dc7575d646f8b3a.png

这允许我们提早返回,跳过其他验证,我们的应用就可以使用burp抓包了

t01f4dad128e19fda10.png

到此为止我们破解成功ARMv7,我们继续看一下ARMv8(ARM64)

ARMv8(ARM64)二进制

有趣的是,ARMv8汇编后看上去非常不同,给我们增加了一些挑战。我们再来看一下SSL_CTX_set_verify

t01f4863c83cca82723.png

接着最重要的步骤是寄存器的初始化。这些寄存器不是按照直接的方式来命名:例如x寄存器是默认的通用目的寄存器,w寄存器是32-bit子寄存器。

在这里,x0是第一个参数,w1是第二个参数,x2是第三个参数。x0是指向OpenSSL ctx的指针,w1是证书验证的值,x3应该指向NULL。

我们来把目光放在w1寄存器上:寄存器wze总是返回#0x0。接着orr进行一次字节级别#0x0和#0x1的OR,然后把返回结果存储在w1中。这就意味着w1总是被设置为#0x1(=SSL_VERIFY_PEER)

通过查看320003e1处的机器代码,我们不是很清楚怎么修改orr w1,wzr,#0x0 (在ARM参考手册中也说不可能,因此我们这么修改,亦或任意寄存器与它本身来产生#0x0 因此eor w1, w1, w1会把#0x0存入寄存器w1)。但是我们必须决定使用哪个操作码。多亏了OS X系统上的XCode

t01bf23f4addcb7f452.png

通过运行llvm-gcc -arch arm64 -c eor.s -o eor.out。这条命令给了我们期望的指令:

t01f5cfa66df0a3b2a8.png

我们现在就可以替换在0000000100004f84 (320003e1)的指令为4a010021。当我们搜索这条指令在二进制中的地址时,需要注意的是word的大小在这个架构上是不同的。这就意味着需要小字节序来编码9402620b(调用_SSL_CTX_set_verify的地方),编码后为0b620294。通过搜索我们找到了需要的地址:

t01d2d730e412898073.png

使用otool进行修改后的输出如下:

t014771ea503fbcccaf.png

通过运行输出的二进制文件,我们可以看到第二次证书验证失败了。我们继续修补这里。与之相关的_SSL_get_verify_result调用如下:

t01765cb6549380675d.png

这是一部分反汇编结果。把movz w9, #0 改为movz w9, #1就足够了,我们来看一下机器代码:

t015a9345874bcdbba7.png

通过修改得到最终的结果:

t014f04a6da8858701d.png

最后一步需要做的就是替换掉原始的二进制文件为打过补丁的版本,我们再一次绕过了pinning

结论

证书pinning是一个对抗恶意CA的有效措施,同时也可以防止企业代理截取TLS流量(请注意,即使你的应用可能会被破坏,但数据安全得到保障)。 从渗透测试的角度来看,它们带来挑战,但总是有方法可以绕过它!(通过iOS的SSL Kill Switch,手动cycript hook或如二进制补丁。)因此,依靠证书pinning来防止黑客洞察底层传输协议几乎就是’security by obscurity’.

以上提到的,在我们的白皮书中还提到另外一种基于cycript的方法。

转载自:http://bobao.360.cn/learning/detail/193.html