Download PDF version of this article PDF

缓冲区溢出狂潮

Rodney Bates,威奇托州立大学

为什么优秀的程序员会遵循不良实践?

2003年1月,据报道,冲击波蠕虫是有史以来传播速度最快的蠕虫。冲击波通过利用缓冲区溢出获得访问权限。如果您细阅 CERT(计算机紧急事件响应小组)的公告或安全升级发布,您会发现大多数计算机安全漏洞都是缓冲区溢出。如果不是因为世界对弱类型编程语言 C 及其衍生语言 C++ 的沉迷,这些只会是小麻烦。

缓冲区溢出是一种数组越界错误。关于它实际如何发生有很多种变体,但这是一个典型的场景。函数 F 调用函数 G,G 返回一个字符串。F 分配一个缓冲区来保存结果,并将指向其第零个字符的指针传递给 G。G 从某处(可能是网络连接)获取字符串,并将其复制到 F 提供的缓冲区中。

G 知道它应该返回的实际字符串的长度,但不知道所提供缓冲区的大小。F 不知道字符串会有多长,所以它只是分配一个它希望足够大的缓冲区来应对最坏的情况。如果字符串太长,G 会天真地将字节复制到缓冲区之外的任何内存中。

如果字符串来自网络连接,系统破解者可以使用精心构造的、比缓冲区长的消息来利用此漏洞。溢出的字节会覆盖附近的一些变量,通常是 F 或 G 的激活记录的一部分。一种可能性是它覆盖了返回地址,即 F 或 G 将返回到的地址,并用一个指向消息较早部分的指针来代替。在这里,破解者放置了机器代码。当函数返回时,它会执行破解者的代码,但具有 F 和 G 所属的网络或服务器代码的权限。

破解者必须了解很多关于 F、G 和朋友们的已编译机器代码的信息,包括它们的激活记录通常在堆栈上占据的实际地址。使用目标软件的副本,这些信息不难获取。还有其他方法,例如用一个值覆盖变量,使 F 或 G 认为已提供了正确的密码。

最初的漏洞可能以多种方式发生。C 语言的弱类型系统创造了一种文化,程序员根本不屑于将缓冲区大小提供给 G。如果 G 在一个库中,来自某个其他开发组织,而 F 的编写者无法控制它,则尤其成问题。为了应对日益增多的此类漏洞利用,程序员正在添加缓冲区大小作为附加参数,由 F 提供,并在 G 中显式检查。有时缓冲区大小会被传递,但检查却被遗忘。

早在 1958 年,在大多数其他编程语言中,缓冲区溢出都是由语言本身通过编译器生成的数组边界检查来捕获的。这不会阻止此类错误首先发生,但尝试利用它会导致目标机器的部分或全部软件立即崩溃。

这可能会导致单点拒绝服务攻击,但目标无法被接管作为进一步攻击的启动点,从而排除像冲击波这样的快速传播的蠕虫。攻击者也无法损坏、破坏、窃取或伪造数据。此外,第一次成功的攻击将立即指出错误的存在和位置。如果攻击者试图保持不被注意而不是制造引人注目的场面,通常的缓冲区溢出漏洞利用可能会在很长一段时间内未被发现。

当然,即使当语言支持边界检查时,大多数编译器也允许禁用它们的生成。一种非常常见的做法是在打开检查的情况下进行调试,然后在生产使用时重新编译而不进行检查。我记得至少早在 1970 年代初就有一个故事,说数百万美元的业务是在一个程序的建议下进行的,后来发现该程序存在未检测到的数组边界错误。它的输出一直都是错误的,但并不明显,所以没有人怀疑,直到一个不相关的软件更改揭露了它。

使用数组边界检查重新编译相对容易。当然,它需要分发和安装更新后的版本,但不需要重新编码。C 和 C++ 则不然。虽然它们的原始类型系统确实有数组类型,但该语言几乎总是坚持忽略它们。假设 A 被声明为数组变量。除了一种例外,每次您引用 A 时,引用都会立即转换为指向 A 的第零个元素的指针。与所有指针类型一样,这随后被视为指向数组中间某个元素的指针,该数组在两个方向上都延伸很远,仅受机器上有限的地址范围限制。边界已被类型系统遗忘,因此即使需要,也无法生成运行时检查。

聪明的语言设计者或许能够重新设计类型系统,使其在保持某些源代码序列合法的同时,允许边界检查。但还有另一个问题,部分是文化上的。由于数组很少被视为真正的数组,因此大多数 C/C++ 程序员都养成了首先使用指针类型声明真正数组的习惯。

这是糟糕的语言设计如何助长不良编程习惯的最佳例证之一。弱类型系统的支持者经常辩称,优秀的程序员会遵循良好的实践。对于少数人来说,这确实是正确的。但现实情况是,绝大多数程序员的做法恰恰相反。

如果数组是局部变量或全局变量,则必须在某处有一个带有数组类型的声明,以为其分配空间。然而,在那之后,程序员通常会使用指针类型将其到处传递。如果是堆分配的,它可能会立即转换为指针类型,并且永远不会被赋予数组类型。

这里面还有更多的浑水。即使程序员使用它们,C 和 C++ 数组类型也只能携带静态边界。程序中的许多数组需要具有动态大小:最常见的是数组形式参数。在这里,实际参数通常是静态大小的,但形式参数需要是动态大小的,以接受不同的实际参数。对于这些情况,C/C++ 的类型系统仅提供未知大小的数组类型。因此,即使程序员声明使用数组类型,边界也会丢失。

在语言忽略数组类型、缺乏携带其边界的动态大小数组类型以及程序员不使用数组类型的习惯之间,如果没有对几乎所有内容进行重大重新编码,就无法重新设计类型系统以支持数组边界检查。

您可能会想象一个自动代码转换程序来配合修复后的类型系统。它必须非常复杂才能区分用于访问数组的指针、按引用传递参数以及作为指向链接数据结构的合法指针。它必须进行整个程序的分析,而这在 C 和 C++ 中尤其困难,因为它们缺乏对单独编译的语言识别,使得即使找到所有相关的源代码也非常困难。

在具有良好设计的类型系统的语言中,必须使用数组类型来访问数组。如果边界是静态的,则这些类型将其边界作为类型的一部分携带;如果不是,则作为值的一部分携带。然后,他们可以在需要时生成数组元素访问的运行时边界检查。程序员仍然可以创建缓冲区溢出错误,但后果远没有那么严重。

代码本身可以更改以防止缓冲区溢出,即使在弱类型语言中也是如此。这正是正在发生的事情,逐个修复漏洞以关闭安全漏洞。但是,源源不断报告的缓冲区溢出漏洞表明漏洞的数量是巨大的。有多少比例可以直接归因于糟糕的类型系统,有多少比例归因于由此产生的不良程序员习惯,这只能由任何人猜测。到目前为止,新发现此类漏洞的速度没有显示出减缓的迹象。

为什么这么多人坚持使用比编程语言艺术状态落后 45 年的语言?作为一名编程语言专家,我与从业者和学者就此问题进行了数十年的讨论。大多数原因都归结为“这是其他人都在做的事情。”有一些例外——有效的观点,但很容易回答。当软件开发人员根据其优点选择语言时,许多棘手的问题将变成简单的问题。但不要担心这可能会使程序员变得多余。无法被任何已知语言技术解决的问题空间将始终大于可以解决的问题空间,并且远远大于我们的集体能力。

喜欢它,讨厌它?请告诉我们

[email protected]www.acmqueue.com/forums

RODNEY BATES ([email protected]) 从未从事过他所受训的电气工程的付费工作。相反,他已经在计算机软件领域工作了 33 年,三分之二在工业界,其余在学术界。他主要参与操作系统、编译器以及作为常驻编程语言律师。他曾为流行的计算机杂志、行业和研究会议以及学术期刊撰稿。他目前的大型项目是 Modula-3 和其他语言的语义编辑器。他是堪萨斯州威奇托州立大学计算机科学系的助理教授和研究生协调员。

© 2004 1542-7730/05/0500 $5.00

acmqueue

最初发表于 Queue 第 2 卷,第 3 期—
数字图书馆 中评论本文





更多相关文章

Catherine Hayes,David Malone - 质疑评估非加密哈希函数的标准
虽然加密和非加密哈希函数无处不在,但在它们的设计方式上似乎存在差距。出于各种安全需求,加密哈希存在许多标准,但在非加密方面,存在一定程度的民间传说,尽管哈希函数历史悠久,但尚未得到充分探索。虽然针对真实世界数据集的均匀分布非常有意义,但在面对具有特定模式的数据集时,这可能是一个挑战。


Nicole Forsgren,Eirini Kalliamvakou,Abi Noda,Michaela Greiler,Brian Houck,Margaret-Anne Storey - DevEx 在行动
DevEx(开发者体验)在许多软件组织中越来越受到关注,因为领导者寻求在财政紧缩和人工智能等变革性技术的背景下优化软件交付。技术领导者凭直觉接受良好的开发者体验可以实现更有效的软件交付和开发者幸福感。然而,在许多组织中,旨在改进 DevEx 的拟议倡议和投资难以获得支持,因为业务利益相关者质疑改进的价值主张。


João Varajão,António Trigo,Miguel Almeida - 低代码开发生产力
本文旨在通过展示使用基于代码、低代码和极端低代码技术进行的实验室实验结果来研究生产力差异,从而为该主题提供新的见解。低代码技术已清晰地显示出更高的生产力水平,为低代码在短期/中期内主导软件开发主流提供了强有力的论据。本文报告了程序和协议、结果、局限性和未来研究的机会。


Ivar Jacobson,Alistair Cockburn - 用例至关重要
虽然软件行业是一个快节奏且令人兴奋的世界,其中不断开发新的工具、技术和技术来为商业和社会服务,但它也很健忘。在其快速前进的过程中,它容易受到时尚潮流的影响,并且可能会忘记或忽略针对它面临的一些永恒问题的成熟解决方案。用例于 1986 年首次引入并在后来普及,是这些成熟的解决方案之一。





© 保留所有权利。

© . All rights reserved.