将狼挡在wolfSSL之外 - Trail of Bits博客
Trail of Bits公开披露了影响wolfSSL的四个漏洞:CVE-2022-38152、CVE-2022-38153、CVE-2022-39173和CVE-2022-42905。这四个问题的CVSS评分从中等到严重不等,都可能导致拒绝服务(DoS)。这些漏洞是使用新型协议模糊测试工具tlspuffin自动发现的。本文将探讨这些漏洞,然后深入概述该模糊测试工具。
tlspuffin是一个受形式化协议验证启发的模糊测试工具。最初作为我在法国洛林大学计算机科学及应用实验室(LORIA, INRIA)实习期间开发的项目,它特别针对TLS或SSH等密码学协议。
在Trail of Bits实习期间,我们通过支持新协议(SSH)、添加更多模糊测试目标以及(重新)发现漏洞,进一步推动了协议模糊测试的发展。这项工作代表了首个Dolev-Yao模型引导的模糊测试工具开发的重要里程碑。通过支持额外协议,我们证明了我们的模糊测试方法对协议是不可知的。未来,我们旨在支持其他协议,如QUIC、OpenVPN和WireGuard。
针对wolfSSL
在Trail of Bits实习期间,我们添加了多个版本的wolfSSL作为模糊测试目标。wolfSSL库是一个理想的选择,因为它受到了2022年初发现的两个身份验证漏洞(CVE-2022-25640和CVE-2022-25638)的影响。这意味着我们可以通过使用tlspuffin重新发现已知漏洞来验证其有效性。
由于tlspuffin是用Rust编写的,我们首先必须为wolfSSL编写绑定。在实现绑定的过程中,在OpenSSL兼容层发现了几个错误,这些错误也已报告给wolfSSL团队。绑定准备就绪后,我们让模糊测试工具开始工作:发现wolfSSL中的异常状态。
发现的漏洞
在实习期间,我在wolfSSL中发现了几个可能导致拒绝服务(DoS)的漏洞。
DOSC:CVE-2022-38153允许中间人(MitM)攻击者或恶意服务器通过拦截和修改TLS数据包对TLS 1.2客户端执行DoS攻击。此漏洞影响wolfSSL 5.3.0。
DOSS:CVE-2022-38152是针对使用wolfSSL_clear函数而不是wolfSSL_free; wolfSSL_new序列的wolfSSL服务器的DoS漏洞。恢复会话会导致服务器因空指针解引用而崩溃。此漏洞影响wolfSSL 5.3.0至5.4.0。
BUF:CVE-2022-39173由缓冲区溢出引起,导致wolfSSL服务器DoS。它通过假装恢复会话并在Client Hello中发送重复的密码套件来触发。攻击者可能在某些架构或目标上获得远程代码执行(RCE);然而,这一点尚未得到确认。wolfSSL 5.5.1之前的版本受到影响。
HEAP:CVE-2022-42905由解析TLS记录头时的缓冲区过度读取引起。wolfSSL 5.5.2之前的版本受到影响。
“wolfSSL的几个CVE,tlspuffin的一大步。”
这些漏洞标志着模糊测试工具的一个重要里程碑:它们是使用该工具发现的第一个具有深远影响的漏洞。我们还可以自信地说,使用传统的比特级模糊测试工具很难发现此漏洞。特别有趣的是,平均而言,模糊测试工具发现漏洞和崩溃所需的时间不到一小时。
在准备wolfSSL的模糊测试设置时,我们还发现了一个严重的内存泄漏,这是由于误用wolfSSL API引起的。此问题已报告给wolfSSL团队,他们更改了文档以帮助用户避免泄漏。此外,其他几个代码质量问题也已报告给wolfSSL,他们的团队在披露后一周内修复了所有发现。如果存在“最佳协调披露”奖,wolfSSL团队肯定会赢得它。
以下部分将重点关注其中两个漏洞,因为它们具有更高的影响和表达性攻击痕迹。
DOSC:针对客户端的拒绝服务
在wolfSSL 5.3.0中,MitM攻击者或恶意服务器可以使TLS客户端崩溃。该错误存在于AddSessionToCache函数中,该函数在客户端从服务器接收到新会话票证时调用。
假设wolfSSL的会话缓存的每个桶至少包含一个条目。一旦新的会话票证到达,客户端将重用先前存储的缓存条目,尝试将其缓存到会话缓存中。此外,由于新的会话票证较大,为700字节,它将使用XMALLOC在堆上分配。
在以下示例中,SESSION_TICKET_LEN为256:
|
|
此分配导致cacheTicBuff的初始化,因为ticBuff已初始化,cacheSession->ticketLenAlloc为0,且ticLen为700:
|
|
cacheTicBuff设置为先前会话的票证cacheSession->ticket。cacheTicBuff指向的内存未在堆上分配;实际上,cacheTicBuff指向cacheSession->_staticTicket。这是有问题的,因为如果cacheTicBuff不为空,它稍后会被释放。
|
|
进程通过执行XFREE函数终止,因为传递的指针未在堆上分配。
请注意,票证长度本身不是崩溃的原因。此漏洞与OpenSSL中发现的缓冲区过度读取漏洞Heartbleed非常不同。在wolfSSL中,崩溃不是由缓冲区溢出引起的,而是由逻辑错误引起的。
发现异常状态
模糊测试工具在大约一小时内发现了该漏洞。模糊测试工具通过将实际票证替换为700字节的大数组(large_bytes_vec)来修改NewSessionTicket(new_message_ticket)消息。这种对原本正常的痕迹的变异导致对非分配值调用XFREE。这最终导致接收如此大票证的客户端崩溃。
DOSC(CVE-2022-38153)的可视化利用。每个框代表一个TLS消息。每条消息由不同的字段组成,如协议版本或密码套件向量。可视化是使用tlspuffin模糊测试工具生成的,并反映了DY攻击痕迹的结构,这将在下一节中介绍。
执行上述痕迹的单个实例不足以到达易受攻击的代码。由于错误存在于wolfSSL的会话缓存中,我们需要让客户端缓存填满以触发崩溃。根据经验,我们发现大约需要30个先前的连接才能可靠地使其崩溃。随机行为的原因是缓存由多行或多桶组成;wolfSSL的默认编译配置包含11个桶。基于TLS会话ID的哈希,会话存储在这些桶之一中。仅当当前桶已包含先前的会话时,才会触发DoS。
重现此漏洞很困难,因为需要准备状态才能达到该行为。通常,全局状态(如wolfSSL缓存)使模糊测试更难以应用。理想情况下,人们可能假设程序的每次执行在给定相同输入时产生相同的输出。如果此假设被违反,因为程序使用全局状态,则重现和调试变得更具挑战性;这在模糊测试未知目标时代表一般挑战。
幸运的是,tlspuffin允许研究人员重新创建与模糊测试工具观察到崩溃时存在的程序状态相似的状态。我们能够重新执行模糊测试工具评为有趣的所有痕迹,这使我们能够在更受控的环境中观察wolfSSL的崩溃,并使用GDB调试wolfSSL。在分析导致无效释放的调用堆栈后,很明显该错误与会话缓存有关。
DOSC的根本原因在于使用共享的全局状态。发现wolfSSL在库的多次调用之间共享状态非常令人惊讶。从概念上讲,会话缓存的生命周期应绑定到TLS上下文,该上下文已经作为TLS会话的容器。每个SSL会话与TLS上下文共享状态。维护全局可变状态的增加增加了整个代码库的复杂性。因此,应仅在绝对必要时使用。
BUF:服务器上的缓冲区溢出
在wolfSSL 5.5.1之前的版本中,恶意客户端可以在恢复的TLS 1.3握手期间导致缓冲区溢出。如果攻击者通过发送恶意制作的Client Hello,然后是另一个恶意制作的Client Hello来恢复或假装恢复先前的TLS会话,则可能发生缓冲区溢出。必须至少发送两个Client Hello:一个假装恢复先前的会话,另一个作为对Hello Retry Request消息的响应。
恶意Client Hello包含支持的密码套件列表,其中至少包含⌊sqrt(150)⌋ + 1 = 13个重复项,且总共少于150个密码。缓冲区溢出发生在握手期间第二次调用RefineSuites函数时。
|
|
RefineSuites函数期望一个包含可接受密码套件列表的struct WOLFSSL,位于ssl->suites,以及一个对等密码套件数组。两个输入都由WOLFSSL_MAX_SUITE_SZ限制,该值等于150个密码套件或300字节。
假设ssl->suites由单个密码套件(如TLS_AES_256_GCM_SHA384)组成,并且用户可控的peerSuites列表包含重复13次的相同密码。RefineSuites函数将为ssl->suites中的每个套件迭代peerSuites,并在匹配时将套件附加到suites数组。suites数组的最大长度为WOLFSSL_MAX_SUITE_SZ套件。
使用上述输入,suites的长度现在等于13。suites数组现在复制到struct WOLFSSL,如上面列表的最后一行所示。因此,ssl->suites数组现在包含13个TLS_AES_256_GCM_SHA384密码套件。
在假定恢复的TLS握手期间,如果客户端触发了Hello Retry Request,则再次调用RefineSuites函数。struct WOLFSSL在之间未重置,并保持先前的13个密码套件。因为TLS对等方控制peerSuites数组,我们假设它再次包含13个重复的密码套件。
RefineSuites函数将为ssl->suites中的每个元素迭代peerSuites,并在匹配时将套件附加到suites。因为ssl->suites数组已经包含13个TLS_AES_256_GCM_SHA384密码套件,总共13 x 13 = 169个密码套件被写入suites。169个密码套件超过了分配的最大允许WOLFSSL_MAX_SUITE_SZ密码套件。suites缓冲区在堆栈上溢出。
到目前为止,我们无法利用此错误,例如获得远程代码执行,因为可以溢出suites缓冲区的字节集很小。只有有效的密码套件值可以溢出缓冲区。
由于空间限制,我们未提供将正常痕迹变异为攻击痕迹所需的突变的详细审查,如我们对DOSC所做的那样。
为了理解我们如何发现这些漏洞,值得研究tlspuffin是如何开发的。
下一代协议模糊测试
历史证明,密码学协议的实现容易出错。在将RFC或科学文章等规范转换为实际程序代码时,很容易引入逻辑缺陷。2017年,研究人员发现著名的WPA2协议存在严重缺陷(KRACK)。像FREAK这样的漏洞,或像2022年初发现的wolfSSL错误(CVE-2022-25640和CVE-2022-25638)这样的身份验证漏洞,支持了这一观点。
模糊测试密码学协议的实现具有挑战性。与传统的文件格式模糊测试不同,密码学协议需要特定的密码和相互依赖的消息流才能到达深层协议状态。
此外,检测逻辑错误本身就是一个挑战。AddressSanitizer使安全研究人员能够可靠地发现与内存相关的问题。对于逻辑错误,如身份验证绕过或机密性丢失,没有自动检测器存在。
这些挑战是我和Inria设计tlspuffin的原因。该模糊测试工具由所谓的Dolev-Yao模型引导,该模型自1980年代以来一直用于形式化协议验证。
Dolev-Yao模型
形式化方法已成为密码学协议安全分析的重要工具。现代工具如ProVerif或Tamarin具有完全自动化的框架来建模和验证安全协议。ProVerif手册和DEEPSEC论文提供了协议验证的良好介绍。这些工具的基础理论使用符号模型——Dolev-Yao模型——源自Dolev和Yao的工作。
使用Dolev-Yao模型,攻击者完全控制通信网络中发送的消息。消息使用项代数符号化建模,该代数由一组函数符号和变量组成。这意味着消息可以通过对变量和其他函数应用函数来表示。
对手可以窃听、注入或操纵消息;Dolev-Yao模型旨在模拟对这些协议的真实世界攻击,如中间人(MitM)式攻击。密码原语通过抽象语义建模,因为Dolev-Yao模型专注于查找逻辑协议缺陷,而不关心密码原语的正确性。因为原语通过抽象语义描述,所以在Dolev-Yao模型中没有定义真正的实现,例如RSA或AES。
已经可以使用此模型在密码学协议中发现攻击。TLS规范已经在2006年和2017年经历了这些工具的各种分析,这导致了对RFC草案的修复。但是为了模糊测试协议的实现,而不是验证其规范,我们需要稍微不同地做事。我们选择用更具体的语义替换抽象语义,其中包括原语的实现。
tlspuffin模糊测试工具基于Dolev-Yao模型设计,并由符号形式模型引导,这意味着它可以执行Dolev-Yao模型中可表示的任何协议流。它还可以生成以前未见过的协议执行。以下部分解释了Dolev-Yao痕迹的概念,该概念松散地基于Dolev-Yao模型。
Dolev-Yao痕迹
Dolev-Yao痕迹建立在Dolev-Yao模型之上,并使用项代数符号化表示消息。就像在Dolev-Yao模型中一样,密码原语被视为黑盒。这允许模糊测试工具专注于逻辑错误,而不是测试密码原语的正确性。
让我们从臭名昭著的Needham-Schröder协议的例子开始。如果您不熟悉,Needham-Schröder是一个身份验证协议,允许两方通过可信服务器建立共享秘密;然而,其非对称版本因易受MitM攻击而臭名昭著。
该协议允许Alice和Bob通过可信第三方服务器创建共享秘密。该协议通过向服务器请求共享秘密来工作,该秘密一次为Bob加密,一次为Alice加密。Alice可以向服务器请求新秘密,并将收到包含共享秘密的加密消息和进一步加密的消息 addressed to Bob。Alice将消息转发给Bob。Bob现在可以解密消息,并也可以访问共享秘密。
协议中的缺陷允许冒名顶替者通过首先与Alice发起连接,然后将接收到的数据转发给Bob来冒充Alice。(为了更深入理解该协议,我们建议阅读其维基百科文章。)
在下面的Dolev-Yao痕迹T中,我们模拟了Needham-Schröder协议在名为a和b的两个代理之间的特定执行。每个代理都有一个底层实现。痕迹由以点分隔的步骤串联组成。有两种步骤:输入和输出。输出步骤由代理名称上方的条表示。
Dolev-Yao攻击痕迹 for the Needham-Schröder protocol
现在让我们描述痕迹T的语义。(不需要深入理解此协议的步骤。此示例应仅让您感受Dolev-Yao模型的表达力以及什么是Dolev-Yao痕迹。)
在第一步中,我们将项pk(sk_E)发送给代理a。代理a将序列化该项并将其提供给其底层Needham-Schröder实现。
接下来,我们让代理a输出一个位串并将其绑定到h_1。通过遵循Dolev-Yao痕迹中的步骤,我们可以观察到我们现在将项aenc(adec(h_1, sk_E), pk(sk_B))发送给代理b。
接下来,我们让代理b的底层实现输出一个位串并将其绑定到h_2。接下来的两个步骤将消息h_2转发给代理a并将其新输出绑定到h_3。最后,我们为不同的输入(即h_3)重复第三和第四步,并将项h_3发送给代理a。
这样的痕迹允许我们模拟密码学协议的任意执行流。上面的痕迹模拟了由Gavin Lowe最初发现的MitM攻击。协议的固定版本称为Needham-Schroeder-Lowe协议。
TLS 1.3握手协议
在提供现代密码学协议的例子之前,我快速解释TLS握手的不同阶段。
TLS握手阶段概述
密钥交换:建立共享密钥并选择密码方法和参数。此阶段的两条消息未加密。
服务器参数:交换不再以明文发送的进一步参数。
服务器身份验证:通过确认密钥和握手完整性来验证服务器。
客户端身份验证:可选地,通过确认密钥和握手完整性来验证客户端。
就像在Needham-Schröder例子中一样,TLS握手的每条消息都可以由符号项表示