作者 | John Viega
译者 | 王强
策划 | Tina
本文作者 John Viega 拥有 25 年安全和开发经验,喜欢研究编译器,是 AES-GCM 加密算法的联合作者,曾是 McAfee 首席安全架构师、SaaS 业务部门的 CTO。并在纽约大学担任兼职教授至今。撰写了多本关于安全的书籍,包括《安全神话》、《构建安全软件》和《使用 OpenSSL 进行网络安全》等。是 IEEE 安全与隐私杂志的前主编。
他的这篇文章围绕行业长期存在的,关于 C/C++ 安全性不足及其应该被 Rust 等语言替代的话题展开了讨论,并发表了一些独特而有价值的观点。原文内容较长,译文删减了一部分不重要的内容以方便阅读。
前不久我又看到了一篇非常轻视内存安全性,觉得这方面没什么必要做改变的文章,然后我又看到一些安全专家对这篇文章回应说,想要保障安全、负起责任就得尽快放弃 C 和 C++ 才行。这篇文章就是我对这个话题的分析,我会尽可能覆盖所有方面,尽量让行业内的读者都能理解我的意思。
太长不看版本
我曾看到一位安全人员和业务部门争吵不休,于是我问他:
如果你认为安全是首要问题,那你为什么还要使用电脑?
人们每天都愿意承担风险。我们知道,只要我们外出,就有可能感染某种病毒。我们知道,只要我们上车,就有可能发生事故并丧命。
但众所周知,我们倾向于大大高估或低估自己的风险水平。
一般来说,安全行业可能认为普通人大大低估了风险。过去这个观点可能是正确的,但现在的世界已经不同了。曾几何时,大多数行业都大大低估了风险,网络连接可能被轻易篡改、代码可能被轻易攻破,没有例行补丁,也没有广泛的隔离或其他有效的权限机制。
但由于整个安全行业的辛勤工作,其他科技界从业人员最终承认他们错了。安全行业产生了巨大的影响力,从硬件架构到网络协议,再到编程语言设计都开始重视安全性。这条路走下来并不轻松,因为我们常常无法真正理解来自其他领域的专业观点。
如果我们能牢记这一点,行业就能取得更快的进步,获得更好的信誉。我们需要这样做,因为正如我们所看到的,要做的事情还有很多,而且还有很多重要的变化根本无法快速实现。
内存安全问题有多严重?
内存安全往往被认为是最严重的漏洞类别,因为这个层面的漏洞经常可以获得最高权限。漏洞经常可以被远程利用,有时甚至不需要任何身份验证。
但有观点认为内存安全漏洞数量众多,老练的攻击者很容易找到并利用它们,这种说法是错误的。
世纪初的时候情况的确是那样,但现在不一样了。从安全角度来看,内存不安全代码的影响肯定还是非常大,但还没有高到你要顶着经济压力为它换成内存安全语言的程度。
风险状况已经变了
我坚决承认相比 C/C++,其他语言本质上更安全;我只是在质疑“它们的安全性到底强多少?尤其考虑到我们已有的很多安全措施时。”
世界发生了许多变化,直接影响了风险水平(双向),包括:
考虑上述情况:
为什么对比不同语言的 CVE 数量容易误导人呢?举个例子——Linux 内核最近正式获得了为自己的代码库发布 CVE 的能力。但在他们看来,任何错误都可能带来他们不了解的安全隐患,因此 Linux 内核中发现的每一个错误现在都有自己的 CVE,尽管它们大多不是可利用的内存问题。
漏洞的利用可能性降低了
一方面,找到好的漏洞变得越来越难,这一事实并不重要,因为如果你不再用 C,这类问题就不再是问题了。
另一方面,漏洞研究人员比从前更努力,找到的漏洞却少很多,这表明实际的风险比以前更低了(尤其是在有良好的补偿控制措施的情况下)。如今,我倾向于认为许多 C 程序中存在问题的几率很高,但如果你有正确的设计,并花钱请合适的人来审查你的代码,那么找到下一个错误的经济成本就足够高了。
我刚开始进入这个领域时,工作往往很简单。如果你能找到一个数组形式的局部变量,那么你很可能可以诱使程序在数组之外写入。而且内存布局很容易预测,因此那时候我们很容易利用这种情况。
具体来说,局部变量经常保存在程序栈中。当你进入一个函数时,数据会被推送到堆栈上,你退出时,数据会从堆栈中弹出。这与函数调用后仍存在的长期内存分配(堆存储)是不一样的。
例如,过去我们经常看到这样的代码:
对外行来说上述代码可能看起来没什么问题。它创建一个数组,该数组初始化为零,其大小为操作系统支持的路径的最大大小。然后,它将某个 base_path 名复制到该数组中,最后将文件名附加到该数组的末尾。但即使在今天的系统上,如果攻击者可以控制这个函数或 base_path 的输入,也很容易让程序崩溃。
部分原因是 C 不跟踪内容的长度。在 C 中,strcpy 逐字节复制,直到遇到值为 0 的字节(所谓的 NULLbyte)。类似地,strcat 的工作方式是向前扫描到 full_path 中的第一个空字节,并从 filename 中复制,直到找到 NULL。在任何情况下,这两个函数都不会根据 fullpath 的长度来检查它们正在做什么。因此,如果你可以传入超过 PATH_MAX 减去 len(base_path) 个字符,你就能写入超出缓冲区末尾的数据。
过去,程序栈将自己的运行时数据与用户的数据混合在一起,这就是传统的栈溢出如此容易实现的原因所在。