Keeping the wolves out of wolfSSL - The Trail of Bits Blog
Trail of Bits 公开披露了影响 wolfSSL 的四个漏洞:CVE-2022-38152、CVE-2022-38153、CVE-2022-39173 和 CVE-2022-42905。这四个问题的 CVSS 评分从中等到严重不等,都可能导致拒绝服务(DoS)。这些漏洞是使用新型协议模糊测试工具 tlspuffin 自动发现的。本文将探讨这些漏洞,然后深入概述该模糊测试工具。
tlspuffin 是一个受形式化协议验证启发的模糊测试工具。最初是在法国 INRIA 的 LORIA 实习期间开发的,特别针对 TLS 或 SSH 等密码学协议。
在 Trail of Bits 实习期间,我们通过支持新协议(SSH)、添加更多模糊测试目标以及(重新)发现漏洞,进一步推动了协议模糊测试。这项工作代表了首个 Dolev-Yao 模型引导的模糊测试工具开发的里程碑。通过支持额外协议,我们证明了我们的模糊测试方法对协议是不可知的。未来,我们旨在支持其他协议,如 QUIC、OpenVPN 和 WireGuard。
Targeting wolfSSL
在 Trail of Bits 实习期间,我们添加了多个版本的 wolfSSL 作为模糊测试目标。wolfSSL 库是一个理想的选择,因为它受到了 2022 年初发现的两个身份验证漏洞(CVE-2022-25640 和 CVE-2022-25638)的影响。这意味着我们可以通过使用 tlspuffin 重新发现已知漏洞来验证其有效性。
由于 tlspuffin 是用 Rust 编写的,我们首先必须为 wolfSSL 编写绑定。在实现绑定的过程中,在 OpenSSL 兼容层中发现了几个错误,这些错误也已报告给 wolfSSL 团队。绑定准备就绪后,我们让模糊测试工具开始工作:发现 wolfSSL 中的异常状态。
Discovered Vulnerabilities
在实习期间,我在 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 之前的版本受到影响。
“A few CVEs for wolfSSL, one giant leap for tlspuffin.”
这些漏洞标志着模糊测试工具的一个里程碑:它们是使用该工具发现的第一个具有深远影响的漏洞。我们还可以自信地说,使用传统的比特级模糊测试工具很难发现此漏洞。特别有趣的是,平均而言,模糊测试工具发现漏洞和崩溃所需的时间不到一小时。
在为 wolfSSL 准备模糊测试设置时,我们还发现了一个严重的内存泄漏,这是由于滥用 wolfSSL API 造成的。此问题已报告给 wolfSSL 团队,他们更改了文档以帮助用户避免泄漏。此外,其他几个代码质量问题也已报告给 wolfSSL,他们的团队在披露后一周内修复了所有发现。如果存在“最佳协调披露”奖,wolfSSL 团队肯定会赢得它。
以下部分将重点介绍其中两个漏洞,因为它们的影响更大且攻击痕迹更具表现力。
DOSC: Denial of service against clients
在 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 函数终止,因为传递的指针不是在堆上分配的。
请注意,票证长度本身并不是崩溃的原因。此漏洞与 Heartbleed(在 OpenSSL 中发现的缓冲区过度读取漏洞)完全不同。对于 wolfSSL,崩溃不是由缓冲区溢出引起的,而是由逻辑错误引起的。
Finding weird states
模糊测试工具在大约一小时内发现了该漏洞。模糊测试工具通过将实际票证替换为 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: Buffer overflow on servers
在 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 是如何开发的。
Next Generation Protocol Fuzzing
历史证明,密码学协议的实现容易出错。在将 RFC 或科学文章等规范转换为实际程序代码时,很容易引入逻辑缺陷。2017 年,研究人员发现著名的 WPA2 协议存在严重缺陷(KRACK)。像 FREAK 这样的漏洞,或像 2022 年初发现的 wolfSSL 错误(CVE-2022-25640 和 CVE-2022-25638)这样的身份验证漏洞,支持了这一观点。
模糊测试密码学协议的实现具有挑战性。与传统的文件格式模糊测试不同,密码学协议需要特定的密码和相互依赖的消息流才能到达深层协议状态。
此外,检测逻辑错误本身就是一个挑战。AddressSanitizer 使安全研究人员能够可靠地发现与内存相关的问题。对于像身份验证绕过或机密性丧失这样的逻辑错误,不存在自动检测器。
这些挑战是我和 Inria 着手设计 tlspuffin 的原因。该模糊测试工具由所谓的 Dolev-Yao 模型引导,该模型自 1980 年代以来一直用于形式化协议验证。
The Dolev-Yao Model
形式化方法已成为密码学协议安全分析的重要工具。现代工具如 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 Traces
Dolev-Yao 痕迹建立在 Dolev-Yao 模型之上,并使用项代数符号化表示消息。就像在 Dolev-Yao 模型中一样,密码原语被视为黑盒。这允许模糊测试工具专注于逻辑错误,而不是测试密码原语的正确性。
让我们从臭名昭著的 Needham-Schröder 协议的例子开始。如果你不熟悉,Needham-Schröder 是一个身份验证协议,允许两方通过可信服务器建立共享秘密;然而,其非对称版本因易受 MitM 攻击而臭名昭著。
该协议允许 Alice 和 Bob 通过可信第三方服务器创建共享秘密。该协议通过向服务器请求共享秘密来工作,该秘密一次为 Bob 加密,一次为 Alice 加密。Alice 可以向服务器请求新秘密,并将收到一个加密消息,其中包含共享秘密和另一个发给 Bob 的加密消息。Alice 将消息转发给 Bob。Bob 现在可以解密消息,并也可以访问共享秘密。
该协议中的缺陷允许冒名顶替者通过首先与 Alice 发起连接,然后将接收到的数据转发给 Bob 来冒充 Alice。(为了更深入地理解该协议,我们建议阅读其维基百科文章。)
在下面的 Dolev-Yao 痕迹 T 中,我们模拟了 Needham-Schröder 协议在名为 a 和 b 的两个代理之间的特定执行。每个代理都有一个底层实现。痕迹由以点分隔的步骤串联组成。有两种步骤:输入和输出。输出步骤由代理名称上方的条表示。
Dolev-Yao 对 Needham-Schröder 协议的攻击痕迹
现在让我们描述痕迹 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。
这样的痕迹允许我们模拟密码学协议的任意执行流。上面的痕迹模拟