如果 OpenSSL 是用 Go/D/Vala 编写的,Heartbleed 错误会被阻止吗?

信息安全 openssl 缓冲区溢出 编程 心血来潮
2021-08-30 17:16:44

IIUC Heartbleed漏洞是由于 OpenSSL 的 C 源代码中的一个错误,通过memcpy()从太短的缓冲区执行的。我想知道在其他具有比 C 或 C++ 更高级别的内存管理系统的语言中是否会自动阻止该错误。

特别是,我的理解是GoDVala各自编译为本机代码,不需要 VM 来运行,并且应该允许编写提供 C 兼容二进制接口的本机库。

那么,这些语言是否可以用于实现具有相同接口的 OpenSSL 库,并且它们是否可以针对诸如 Heartbleed 漏洞之类的错误提供保护?

4个回答

实际上,这些语言都不会阻止这个错误,但它们会减轻后果。

从抽象机器的角度来看,OpenSSL 的代码正在做一些荒谬的事情:它从缓冲区中读取的字节数比缓冲区中实际的字节数多。使用 C,读取仍然“有效”并返回缓冲区后滞留的任何字节。使用更严格的语言,越界内存访问将被捕获,并触发异常:有问题的代码不会读取和发送字节,而是崩溃,导致(在 Web 服务器的上下文中)终止当前线程并可能关闭连接,而不改变服务器的其余部分。

所以,仍然是一个错误,但不再是一个真正的漏洞。

这与自动内存管理只有间接关系。这里真正的安全网是访问的系统数组边界检查。然后,严格类型间接支持该系统检查(这防止使用“字节数组”以外的任何东西作为“字节数组”)。自动内存管理 (GC) 间接支持严格类型本身,因为它可以防止悬空指针以及违反严格类型的释放后使用条件。

最近的“心血”在本质上并不是什么新鲜事;多年来,OpenSSL 已经有很多与缓冲区溢出相关的错误(其中一些来自 ASN.1 处理代码,用于证书解析)。这只是列表中的另一个。

如果您认为该错误是读取当前结构的范围之外,那么这可能会在其他语言中被阻止,因为一个人没有对内存的无限访问,并且需要以不同的方式实现这些东西。

但我宁愿将此错误归类为缺少对用户输入的验证,例如它认为在数据包中发送的大小实际上是有效负载的大小。这些错误不是简单地通过使用另一种语言来修复的。

不幸的是,该错误无法避免,因为OpenSSL 使用自己的内存分配器,而不是系统提供的内存分配器。

读取臭名昭著的心跳数据的缓冲区由ssl/s3_both.cfreelist_extract中调用的函数分配默认情况下,此函数管理 OpenSSL 自己的已使用/未使用内存列表,并且不执行任何现代安全检查。

即使它是用另一种语言编写的,假设 OpenSSL 仍然保持自己的缓冲区分配器,那么这个 bug 也会同样发生。通过重用以前的缓冲区结构,无论编程语言如何,memcpy“缓冲区复制”功能等效项都可以完成相同的操作而不会引发任何错误。

在现代编程语言中,这将类似于:

request = last_used_buffer;
/* I'm sure it doesn't actually read bytes like this, but you get the idea */
while (byte = read(connection)) {
    request[i++] = byte;
}

/* ... some time later, in the heartbeat processing function */

output = new Buffer();
output.write(header);
output.write(request, start, len); /* dutifully copies from the request buffer,
                                      but since end was not checked, it can copy
                                      bytes from last_used_buffer */

相反,如果 OpenSSL 直接使用系统 (libc)mallocfree不是它自己的分配器,那么这个 bug 可能在几年前就被发现了。许多 libc 实现对分配/释放的内存提供了更好的边界检查,并且像 valgrind 这样的工具可以很容易地发现这个错误。

Ted Unangst 在http://www.tedunangst.com/flak/post/heartbleed-vs-mallocconf中提到了 OpenSSL 内存分配器在处理 heartbleed 错误时的这种后果

我想我可以针对用 Go 编写的加密库的具体情况回答这个问题——这很简单,而且根本不是假设的,因为在 Go 中已经有一个独立的TLS 包,crypto/tls它不依赖于任何外部库.

虽然就典型的缓冲区溢出而言,惯用的 Go 比传统的 C 安全得多,但 Go 为敏锐的开发人员提供了很多回避它的选项——例如通过 Go 的unsafe.pointer. 有人想知道是否可以同意不在关键软件中使用脆弱代码。

当然,密码学正是使用这种脆弱代码的那种软件,这是有充分理由的。毕竟,在 Go 包中实现的恒定时间比较crypto.subtle确实需要并且具有与来自unsafe. 真正剩下的唯一问题是,是否有任何虫子仍然可以在这样的环境中生存。

据我所知,Go 确实正确地实现了哈希值的恒定时间比较。我什至没有费心去查看是否以恒定时间方式计算涉及 S-box 的复杂哈希——没有人会费心将它们放入一个带有诸如subtle破坏事物多么容易的名称和警告的包中,所以我我其实很怀疑。

我所做的检查是椭圆曲线密码学在 Go 中没有以恒定时间的方式实现。似乎没有人考虑过至少尝试一下——该实现调用了许多甚至不是为加密使用而设计的任意长度的整数函数,并且确实使用了非常量时间算法。当这种定时侧信道最近被证明也发生在OpenSSL一个架构中时,它足以用于证明私钥妥协的论文

在所有架构上,Go 中都存在相同的持续情况。而且,好吧,似乎没有人真正关心像实际工作加密这样的细节。重点只在于可读性和速度(好吧,我猜,以临时用户和一些单元测试被它愚弄的方式工作)。毕竟,既然语言已经让你免受大多数引入缓冲区溢出的方式的影响,而且选择正确的算法有可能毁掉谷歌关于 TLS 在计算上变得便宜的断言,你为什么还要费心选择合适的算法呢?撇开讽刺不谈,让语言负责捕捉我们的错误只会意味着语言无法为我们捕捉到的所有错误仍然存​​在。

最后,类似库OpenSSL的优点是及时修正错误是一种合理的可能性。虽然原则上 Go 包也是如此,但一旦某些东西成为 Go 核心的一部分,比如 TLS 包,它就会受到发布时间表的影响。显然,这会干扰错误修复的及时部署。我想今年夏天到期的 Go-1.3 肯定不会解决 ECC 计时问题,即使它被认为是关键问题,考虑到功能冻结。