为什么用 C 和 C++ 编写的程序如此容易受到溢出攻击?

信息安全 缓冲区溢出 C C++
2021-08-24 00:33:01

当我查看过去几年与实现相关的漏洞时,我发现其中很多来自 C 或 C++,其中很多是溢出攻击。

  • Heartbleed 是 OpenSSL 中的缓冲区溢出;
  • 最近发现glibc的一个bug,导致DNS解析时缓冲区溢出;

这只是我现在能想到的那些,但我怀疑这些是唯一的 A) 用于用 C 或 C++ 编写的软件,B) 基于缓冲区溢出。

特别是关于 glibc 错误,我阅读了一条评论,指出如果这发生在 JavaScript 而不是 C 中,就不会有问题。即使代码只是编译为 Javascript,也不会成为问题。

为什么 C 和 C++ 如此容易受到溢出攻击?

4个回答

与大多数其他语言相反,C 和 C++ 传统上不检查溢出。如果源代码要求将 120 字节放入一个 85 字节的缓冲区中,CPU 会很乐意这样做。这与 C 和 C++ 具有数组概念的事实有关,但该概念仅适用于编译时。在执行时,只有指针,因此没有运行时方法来检查关于数组概念长度的数组访问。

相比之下,大多数其他语言都有一个在运行时存在的数组概念,因此运行时系统可以系统地检查所有数组访问。这并不能消除溢出:如果源代码要求在长度为 85 的数组中写入 120 个字节这样荒谬的事情,它仍然没有意义。但是,这会自动触发一个内部错误条件(通常是“异常”,例如ArrayIndexOutOfBoundExceptionJava 中的一个),它会中断正常执行并且不会让代码继续执行。这会中断执行,并且通常意味着整个处理的停止(线程死亡),但它通常可以防止除了简单的拒绝服务之外的利用。

基本上,缓冲区溢出漏洞利用需要代码进行溢出(读取或写入超出访问缓冲区的边界)继续执行超出溢出的操作。大多数现代语言,与 C 和 C++(以及一些其他语言,如 Forth 或 Assembly)相反,不允许溢出真正发生,而是射击违规者。从安全的角度来看,这要好得多。

请注意,其中涉及一定数量的循环推理:安全问题经常与 C 和 C++ 相关联。但其中有多少是由于这些语言的固有弱点,又有多少是因为这些只是大多数计算机基础设施所的语言?


C 旨在“比汇编程序更上一层楼”。除了您自己实现的之外,没有任何边界检查可以将最后一个时钟周期挤出您的系统。

C++ 确实提供了对 C 的各种改进,与安全性最相关的是它的容器类(例如<vector><string>),并且从 C++11 开始,智能指针允许您处理数据而无需手动处理内存。但是,由于它是 C 的演变,而不是一门全新的语言,它仍然提供了 C 的手动内存管理机制,所以如果你坚持自取其辱,C++ 不会阻止你。


那么为什么 SSL、bind 或 OS 内核之类的东西仍然用这些语言编写呢?

因为这些语言可以直接修改内存,这使得它们特别适合某种类型的高性能、低级应用程序(例如加密、DNS 表查找、硬件驱动程序......或 Java VMs ;-)) .

因此,如果与安全相关的软件遭到破坏,它以 C 或 C++ 编写的可能性很高,这仅仅是因为大多数与安全相关的软件都是用 C 或 C++ 编写的,通常是出于历史和/或性能原因。如果它是用 C/C++ 编写的,主要的攻击向量是缓冲区溢出。

如果它是一种不同的语言,那将是一个不同的攻击媒介,但我相信也会有安全漏洞。


利用 C/C++ 软件比利用 Java 软件更容易。与利用 Windows 系统比利用 Linux 系统更容易的方式相同:前者无处不在,很好理解(即众所周知的攻击向量,如何找到以及如何利用它们),并且很多人都在寻找漏洞利用奖励/努力比率高的地方。

这并不意味着后者本质上是安全的(也许更安全,但不安全)。这意味着——作为更难的目标和更低的收益——坏小子们并没有在这上面浪费太多时间。

实际上,“heartbleed”并不是真正的缓冲区溢出。为了让事情更“高效”,他们将许多较小的缓冲区放入一个大缓冲区中。大缓冲区包含来自不同客户端的数据。该错误读取了它不应该读取的字节,但实际上并没有读取那个大缓冲区之外的数据。检查缓冲区溢出的语言不会阻止这种情况,因为有人不顾一切或阻止任何此类检查发现问题。

首先,正如其他人所提到的,C/C++ 有时被描述为一种美化的宏汇编器:它意味着“接近铁”,作为一种用于系统级编程的语言。

例如,该语言允许我将一个长度为零的数组声明为占位符,而实际上,它可能表示数据包中的可变长度部分或内存中可变长度区域的开头,用于与一个硬件通信。

不幸的是,这也意味着 C/C++ 在坏人手中是危险的。如果程序员声明了一个由 10 个元素组成的数组,然后写入元素 101,编译器将愉快地编译它,代码将愉快地执行,丢弃发生在该内存位置的任何内容(代码、数据、堆栈,谁知道呢。)

其次,C/C++ 是特殊的。一个很好的例子是字符串,它基本上是字符数组。但是每个字符串常量都带有一个额外的、不可见的终止字符。这是导致无数错误的原因,因为(尤其是但不完全是)新手程序员经常无法分配终止 null 所需的额外字节。

第三,C/C++ 实际上已经很老了。该语言是在对软件系统的外部攻击基本上不存在的时候产生的。用户应该被信任和合作,而不是敌对,因为他们的目标是让程序运行,而不是让它崩溃。

这就是标准 C/C++ 库包含许多本质上不安全的函数的原因。以 strcpy() 为例。它会愉快地复制任何内容,直到一个终止的空字符。如果它没有找到终止的空字符,它将继续复制,直到地狱冻结,或者更有可能,直到它覆盖了一些重要的东西并且程序崩溃了。在过去的好日子里,这不是问题,当用户不希望进入一个为邮政编码保留的字段时,16000 个垃圾字符后跟一组特殊构造的要执行的字节在堆栈被丢弃并且处理器在错误的地址处恢复执行之后。

可以肯定的是,C/C++ 并不是唯一的特殊语言。其他系统有不同的特殊行为,但也可能同样糟糕。以 PHP 等后端编程语言为例,以及编写允许 SQL 注入的代码是多么容易。

最后,如果我们为程序员提供完成工作所需的强大工具,但如果没有足够的培训和对安全环境的认识,那么无论使用哪种编程语言都会发生坏事。