微软70的漏洞仍然是内存安全问题的例子?|译文
2021-08-14
以下是翻译:
之前,我们讨论了主动解决内存安全问题的必要性。显然,仅靠工具和指导无法防止此类漏洞。十多年来,内存安全问题与 CVE(常见漏洞披露)的比率非常接近。我们相信使用内存安全语言可以通过工具和培训无法实现的方式缓解这种情况。
在本文中系统编程语言,我们将探讨一些可以通过使用内存安全语言来防止的 产品漏洞(经过测试和静态分析)的真实示例。
内存安全
内存安全是编程语言的一个特性。在具有内存安全性的编程语言中,所有内存访问都是明确定义的。今天使用的大多数编程语言都是通过某种形式的垃圾收集来实现内存安全的。但是,无法承受垃圾收集器繁重的运行时间的系统级语言(即用于构建其他软件所依赖的底层系统的语言,例如OS内核、网络堆栈等)。通常不是内存安全的。
微软已经修复并指定了 CVE 安全漏洞,大约 70% 的根本原因是内存安全问题。虽然我们采取了缓解措施,包括严格的代码审查、培训、静态分析等。
微软 70% 的漏洞仍然是内存安全问题
尽管很多有经验的程序员都可以编写正确的系统级代码,但很明显,无论采取何种缓解措施,使用传统的系统级编程语言编写内存安全代码几乎是不可能的。
接下来,让我们看一些现实生活中使用没有内存安全保证的语言导致的安全漏洞的例子。
空间内存安全
空间内存安全是指确保所有内存访问都在访问类型的边界内。为此,需要代码来跟踪这些大小并根据这些大小正确检查所有内存操作。
在控制流的极端情况下,可能会因为没有考虑整数符号、整数提升或整数溢出的复杂性,可能会错过检查,或者可能会错误地执行检查。下面我们来看看Edge的这个例子,由(CVE-2018-8301):
[0] 处的检查是正确的。但是,[1] 可以修改字符串的大小,使获取的偏移量无效。这会导致在 [2] 处调用复制函数时产生与预期不同的偏移量,从而导致越界写入。
此漏洞的修复方法很简单:将“偏移检查”移到更接近使用时间的位置。问题是这个错误很容易出现在复杂的代码库中,简单的重构代码也可能再次导致这个漏洞。现代 C++ 提供了跨度来强制执行数组访问的边界检查。但是,不幸的是,这不是默认值,因此完全取决于开发人员使用 span。因此,在实践中很难强制使用这种结构。
如果编程语言可以自动跟踪和验证大小,那么程序员就不再需要担心正确执行这些检查,我们也可以确定这些问题不存在于我们的代码中。
时间记忆安全
时间内存安全是指确保指针在解除引用时仍指向有效内存。
一个常见的模式是发布后使用。触发此漏洞的方法是先引用一个内部访问并保存到本地指针系统编程语言,然后进行一系列复杂的操作,可能会释放或移动内存,导致本地指针中的引用失效,而然后在引用无效后取消引用。比如找到Edge的源码示例(CVE-2017-8596):
这个错误的原因是太多复杂的API相互交互,程序员无法在整个代码中强制对内存的所有权。在 [0] 处,程序获得指向该对象拥有的对象的指针。然后在[1]处,由于语言的复杂性,代码需要执行更多的代码来获取另一个变量。在[2]中,它将使用缓冲区和宽度,并使用指针的内容创建一个新对象。
问题是:
该程序同时使用垃圾收集和手动内存管理。垃圾收集器会跟踪对象,但不知道是否有指向对象内部的指针。由于可重入,JS程序可以修改状态并清除在[1]处创建别名的指针的所有权。该漏洞类似于迭代器失效漏洞。当状态被修改时,所有指向内部状态的指针都可能变成无效指针。但是在像浏览器这样的复杂程序中,几乎不可能使用静态方法来确保不出现错误。问题的根源在于为指向可修改状态的指针添加别名。 C 和 C++ 没有相应的工具来防止这种错误。但是,我们建议始终使用“智能指针”来跟踪内存所有权。
数据竞争条件
当同一个进程中的两个或多个线程同时访问同一个内存地址,并且至少有一次访问是写操作,并且线程不使用任何显式的锁操作来控制对内存的访问,则会发生数据竞争。在多线程访问共享数据的情况下,保持空间和时间内存安全变得更加困难且更容易出错。即使未同步的内存只共享了很短的一段时间,也有可能被其他线程修改数据,而被修改的数据就是引用其他内存地址的数据。这就是检查时间/使用时间()漏洞的原因之一,会导致空间和时间内存安全漏洞。
2018 年披露的漏洞表明了数据竞争可能带来的影响。当虚拟机向主机发送特定消息时调用此代码。这意味着可以并行调用它来处理其他控制消息和数据包。这是有问题的,因为控制消息的处理函数使用的信息被修改,没有任何锁定操作[0]。
以下代码被多个控制消息处理函数使用,从中我们可以看到更新的信息是如何使用的:
由于访问不同步,新缓冲区可能会被旧的 -> [1] 值使用,导致越界写操作 [3]。
防止此类漏洞需要对多个线程访问的数据结构进行锁定操作,直到数据处理完成。但是,C++ 中没有简单的静态检查方法来强制执行此操作。
我们该怎么办
需要几个不同的指标来解决本文中提出的问题。 C++ 中的“现代”结构(例如 span)至少可以防止某些类型的内存安全问题,而其他现代 C++ 功能(例如智能指针)应尽可能使用。但是,现代 C++ 仍然不是一种完全内存安全且完全没有数据竞争的语言。更糟糕的是,这些功能的使用完全依赖于程序员“做正确的事情”,这几乎不可能在大型、晦涩的代码库中强制执行。 C++ 也没有工具可以用安全的抽象来包装不安全的代码,这意味着虽然可以在本地强制执行正确的编程习惯,但在 C 或 C++ 中构建安全组件将极其困难。
此外,软件也应尽可能转为完全内存安全的语言,例如C#或F#,它们使用运行时检查和垃圾收集来确保内存安全。毕竟,除非必要,否则您不应该参与复杂的内存管理。
如果您出于合理的原因(例如速度、控制和可预测性)使用 C++,则可以考虑转向内存安全系统编程语言。在下一篇文章中,我们将介绍为什么我们认为 Rust 是目前最合适的编程语言,因为它可以以内存安全的方式编写系统级程序。
原文: