SSL终止交换机在iOS 12上的工作原理

2020-05-26 10:36:42

两周前,我发布了新版本的SSL Kill Switch,这是我的黑盒工具,用于禁用iOS应用程序中的SSL锁定,以增加对iOS 12的支持。

iOS11和iOS12之间的网络堆栈发生了重大变化,难怪iOS11版本的SSL Kill Switch不能在(越狱)iOS12设备上工作。这篇文章描述了为了支持iOS12,我必须对该工具所做的更改。

在移动应用程序中实现SSL固定需要在应用程序打开与此服务器的SSL连接时,自定义应用程序在服务器证书链上执行的验证逻辑。定制SSL验证几乎总是通过某种回调机制完成的,在回调机制中,应用程序代码在连接的初始TLS握手期间接收服务器的证书链,然后必须对该链做出决定(它是否“有效”)。例如,在iOS上:

用于打开HTTPS连接的最高级别接口是NSURLSession,它通过[NSURLSessionDelegateHTTPS委托方法]实现验证回调。

当使用低级Network.framework(IOS 12中新提供)时,可以使用sec_protocol_options_set_verify_block()将块设置为验证回调。

这篇来自Apple文档的旧文章介绍了如何使用其他iOS网络API自定义验证:HTTPS服务器信任评估。

因此,在应用程序中禁用SSL固定的高级策略是防止触发SSL验证回调,以便永远不会执行负责实现固定的应用程序代码。

在iOS上,相对容易阻止调用NSURLSessionDelegate验证方法(SSL Kill Switch的早期版本就是这样工作的),但是使用较低级别API(如Network.framework)的iOS应用程序怎么办?由于iOS上的每个网络API都构建在另一个之上,因此在最低级别禁用验证回调可能会禁用所有较高级别的网络API的验证,这将允许该工具针对更多应用程序工作。

从iOS 8开始,iOS上的网络堆栈总体上经历了很大的变化,而在iOS 12上,SSL/TLS堆栈构建在自定义的分支上(我想?)。BoringSSL的。例如,可以通过在运行打开连接的应用程序时在随机BoringSSL符号上设置断点来看到这一点:

如果您还记得这个策略:“防止SSL验证回调被触发”,很可能通过对准并修补iOS上最低级别的SSL/TLS API BoringSSL,iOS上所有更高级别的API(包括NSURLSession)也会关闭钉住验证。

使用BoringSSL时,自定义SSL验证的一种方法是通过SSL_CTX_SET_CUSTOM_VERIFY()函数配置验证回调函数:

//定义SSL/TLS握手过程中要触发的证书验证回调//定义在SSL/TLS handshakessl_ify_result_t ify_cert_chain_callback(ssl*ssl,uint8_t*out_alert){//检索服务器在握手时发送的证书链STACK_of(X509)*certificateChain=ssl_get_Peer_cert_chain(Ssl);//如果do_CUSTOM_VALIDATION(certificateChai。}ELSE{//否则关闭连接返回SSL_VERIFY_INVALID;}}//对所有使用SSL_ctxSSL_CTX_SET_CUSTOM_VERIFY(SSL_CTX,SSL_VERIFY_PEER,VERIFY_CERT_CHAIN_CALLBACK)实现的未来SSL/TLS连接启用我的回调;

使用为NSURLSession启用SSL固定的测试应用程序,我能够确认在打开连接时确实会调用SSL_CTX_SET_CUSTOM_Verify():

我们还可以看到APPLE/DEFAULT IOS验证回调函数作为第三个参数(在寄存器x2中)传递:boringssl_context_certificate_verify_callback().。该回调很可能包含(以及其他内容)逻辑,用于设置我的测试应用程序的NSURLSession回调/委托方法,以便最终使用服务器证书进行调用。

不出所料,我的测试应用程序用于固定验证代码的委托方法确实得到了执行:

我已经将我的测试应用程序设计为使其自定义/固定验证逻辑始终失败:

因此,如果我确实找到了绕过钉住的方法,则此连接应该会成功。

现在我们已经有了计划和正确的测试设置(带钉住的应用程序、越狱设备、Xcode等),让我们开始工作吧!

我尝试的第一件事是用一个完全不检查服务器证书链的空回调替换由IOS网络堆栈boringssl_context_certificate_verify_callback(),设置的默认BoringSSL回调:

//我的不检查任何内容的邪恶回调ssl_Verify_Result_t Verify_Callback_That_Does_Not_Validation(void*ssl,uint8_t*out_alert){return SSL_Verify_OK;}//我的邪恶&34;SSL_CTX_SET_CUSTOM_VERIFY()静态无效REPLACE_SSL_CTX_SET_CUSTOM_VERIFY(void*ctx,int mode,SSL_VERIFY_RESULT_t(*CALLBACK)(void*SSL,uint8_t*out_alert))的替换函数{//始终忽略传递的回调,改为设置我的";邪恶";回调Original_SSL_CTX_SET_CUSTOM_VERIFY(CTX,SSL_VERIFY_NONE VERIFY_CALLBACK_That_。}//最后,使用MobileSubstrate将SSL_CTX_SET_CUSTOM_VERIFY()替换为MY";EVISE";REPLEED_SSL_CTX_SET_CUSTOM_VERIFY()void*boringssl_Handle=dlopen(";/usr/lib/libboringssl.dylib";,RTLD_NOW);void*SSL_CTX_SET_CUSTOM_VERIFY=dlsym(boringssl_Handle。if(SSL_CTX_SET_CUSTOM_VERIFY){MSHookFunction((void*)SSL_CTX_SET_CUSTOM_VERIFY,(void*)REPLACE_SSL_CTX_SET_CUSTOM_VERIFY,NULL);}。

在将其实现为MobileSubstrate调整并将其注入到我的测试应用程序中之后,发生了一些有趣的事情:我的测试应用程序的NSURLSession委托方法不再被调用(意味着它被“绕过”),但是该应用程序完成的第一个连接将失败,并出现一个新的/未知错误“Peer is not Authenticated”,如日志中所示:

TrustKitDemo-Objc[3320:160146]=SSL终止开关2:replaced_SSL_CTX_set_custom_verifyTrustKitDemo-ObjC[3320:160146]无法克隆信任错误域=NSOSStatusErrorDomain Code=-50";NULL信任输入";用户信息={NSDescription=NULL信任输入}[-50]TrustKitDemo-Objc[3320:160146][BoringSSL]boringssl_SESSION_FINISH_HANSHARK(306)[c1.1:2][0x10。正在断开连接.TrustKitDemo-Objc[3320:160146]NSURLSession/NSURLConnection HTTP加载失败(kCFStreamErrorDomainSSL,-9810)TrustKitDemo-Objc[3320:160146]任务<;15E1F3B0-0B73-468A-9132-3E19048DDAE3>;.<;1>;已完成,错误代码:-1200.。

然后在应用程序本身中,第一个连接将失败,并出现与以前不同的错误:

但是,后续连接到同一服务器将会成功,而不会触发钉住验证回调:

因此,除了第一个连接之外,我对所有连接都绕过了钉住。快到了,…。

我需要更多的上下文来理解“Peer is not Authenticated”错误是什么,所以我最终从我的iOS12设备上拉出了共享缓存(苹果的所有库和框架都在这里,包括BoringSSL),如本指南中所述。

在将libboringssl.dylib加载到Hopper中之后,我能够在一个名为boringssl_session_Finish_handshake()的函数中找到“Peer is not Authenticated”错误的字符串(在屏幕截图中标记为“1”):

为了更好地理解错误本身,我试图理解该函数在做什么,但是由于我几乎不了解arm64(或任何)程序集,所以我无法理解它。我尝试了其他一些方法(例如修补boringssl_context_certificate_verify_callback()本身),但没有找到任何有效的方法。

当我的周末时间不多了,我可以让自己花在这个上面的时候,我选择了一个更绝望的方法。如果您再次查看反编译的boringssl_session_finish_handshake()函数,您可以看到两个“主”代码路径,这两个路径有条件地由if/Else语句触发,“Peer WAS Not Authenticated”错误出现在“if”代码路径中,而不是“Else”路径中。

一种天真的尝试是阻止具有此错误的代码路径运行,即。“如果”路径。如截图所示,触发“if”分支的一个条件是(_SSL_GET_PSK_Identity()==0x0)(在截图中标记为“2”)。如果我们将此函数修补为不返回0,以强制执行“Else”代码路径(这不会触发“Peer was not Authenticated”错误),会怎么样?

//使用MobileSubstrate将ssl_GET_PSK_IDENTITY()替换为该函数,该函数从不返回0:CHAR*REPLACE_SSL_GET_PSK_IDENTITY(VOID*ssl){Return";notarealPSKidentity";;}MSHookFunction((void*)SSL_GET_PSK_IDENTITY,(VOID*)REPLEED_SSL_GET_PSK_IDENTITY,(void**)NULL);

在将这个运行时补丁注入我的测试应用程序后,它起作用了!甚至第一次连接都成功了,我的应用程序的验证回调从未被触发。我通过修补BoringSSL绕过了应用程序的SSL固定验证代码。

这显然不是一个非常干净的运行时补丁,虽然在应用它之后似乎一切正常(这令人惊讶),但它会触发错误,每当应用程序打开连接时,都会在日志中看到这些错误:

TrustKitDemo-Objc[3417:166749]无法克隆信任错误域=NSOSStatusErrorDomain Code=-50";空信任输入";用户信息={NSDescription=空信任输入}[-50]。

它可能会弄乱与TLS-PSK密码套件相关的代码,也就是实际使用ssl_get_psk_entity()函数时。然而,这些密码套件很少使用,特别是在移动应用程序中。

永远不会调用IOS网络堆栈boringssl_context_certificate_verify_callback(),中的默认BoringSSL回调。这意味着IOS网络堆栈中的某些状态可能未正确设置,这将导致错误。

最后,还有几件额外的事情我没有时间去做:

仔细检查我的BoringSSL运行时补丁是否禁用了低级IOS网络API(如Network.framework或CFNetwork)的固定。

添加对MacOS的支持。我非常确定补丁本身应该可以正常工作,但是我还没有找到在MacOS上挂接BoringSSL(或共享缓存中的任何C函数)的方法。我之前使用的工具Facebook的鱼钩似乎不再起作用了。

就这样!。转到项目的repo查看代码并下载调整。