访问数组越界有多危险?

241

访问C语言数组时是否超出其边界有多危险?有时我会从数组外部读取(现在我知道我实际上是在访问程序的一些其他部分甚至是超出它们的内存),或者我试图将值设置为数组之外的索引。程序有时会崩溃,但有时仅提供意外结果。

现在我想知道的是,这到底有多危险?如果它只损坏了我的程序,那么就不算太糟糕。另一方面,如果因为我某种方式成功地访问了一些完全无关的内存而破坏了我的程序之外的东西,那么情况就很糟糕了,我想象。

我看到很多“任何事情都可能发生”,“分段可能是最不好的问题”, “您的硬盘可能变成粉色,独角兽可能在您的窗户下唱歌”。这都很好,但真正的危险是什么?

我的问题:

  1. 从数组外部读取值会除了我的程序之外有任何损害吗?我想仅仅查看东西不会改变任何东西,或者例如,我到达的文件的“上次打开”属性是否会更改?
  2. 在数组范围之外设置值会除了我的程序之外有任何损害吗?从这个Stack Overflow question中,我得出结论,可以访问任何内存位置,没有安全保障。
  3. 我现在从XCode中运行我的小程序。这会为我的程序提供一些额外的保护,使其无法超越自己的内存范围吗?它会损害XCode吗?
  4. 如何安全地运行我的本质上有缺陷的代码的任何建议?

我使用OSX 10.7,Xcode 4.6。


7
此外,当访问数组索引越界(在您的RAM中)时,您永远不会“偶然地访问”硬盘上的文件。 - DrummerB
1
我相信你在询问C数组,对吧?那么这与ObjC无关,也不涉及任何IDE。 - Bryan Chen
xlc:我在我的问题中提到了我的IDE XCode,因为我怀疑如果我在XCode中运行它们,它可能会保护我的系统免受错误程序的影响,并希望得到你们对这个想法的意见。抱歉没有表达清楚。 - ChrisD
18
这是我最喜欢的例子之一,讲的是栈的问题,但我认为它非常有启发性... - phipsgabler
11
http://xkcd.com/371/ - Dan Is Fiddling By Firelight
显示剩余4条评论
12个回答

135
就ISO C标准(语言的官方定义)而言,访问数组超出其边界具有“未定义行为”。这个术语的字面含义是:
行为,在使用不可移植或错误的程序结构或错误数据时,对于此国际标准没有强制要求。
一个非规范性的注释进一步解释了这一点:
可能的未定义行为范围从完全忽略情况并产生不可预测的结果,到在环境特征下进行翻译或程序执行的记录方式(带或不带诊断消息),到终止翻译或执行(带诊断消息)。
那么这就是理论。现实情况如何?
在“最好”的情况下,您将访问当前正在运行的程序拥有的某些内存片段(这可能会导致您的程序失灵),或者访问当前正在运行的程序不拥有的内存片段(这可能会导致您的程序崩溃,例如分段错误)。或者您可能尝试写入您的程序所拥有但被标记为只读的内存;这也可能导致您的程序崩溃。
这是假设您的程序在试图保护同时运行的进程之间的操作系统下运行。如果您的代码在“裸金属”上运行,例如如果它是操作系统内核或嵌入式系统的一部分,则没有这种保护;您的错误代码就是应该提供该保护的代码。在这种情况下,损坏的可能性要大得多,包括在某些情况下对硬件(或附近的物品或人)造成物理损坏。
即使在受保护的操作系统环境中,保护也不总是100%。例如,有些操作系统漏洞允许非特权程序获得根(管理)访问权限。即使具有普通用户权限,故障程序也可能消耗过多的资源(CPU,内存,磁盘),可能会导致整个系统崩溃。很多恶意软件(病毒等)利用缓冲区溢出来获取未经授权的系统访问权限。
(一个历史性的例子:我听说在一些旧的带有芯片内存的系统上,在紧密循环中反复访问单个内存位置可以导致该内存块熔化。其他可能性包括破坏CRT显示器,并使用驱动器柜的谐波频率移动磁头,导致其走过桌子并掉落到地上。)
还有总是要担心的Skynet
底线是:如果您可以编写一个程序故意做坏事,那么有缺陷的程序可能会意外地做同样的事情,这至少在理论上是可能的。

实际上,在MacOS X系统上运行的有缺陷的程序很可能只会崩溃,但无法完全防止有缺陷的代码造成严重后果。


1
谢谢,我完全理解这个。但是它立刻引发了一个后续问题:初学者程序员应该怎么做才能保护自己的计算机不受他/她可能创建的可怕程序的影响?在我彻底测试过一个程序之后,我才会将其释放到世界上。但第一次试运行肯定是错误的程序。你们如何保护自己的系统不受自己的影响? - ChrisD
7
@ChrisD:我们往往很幸运。8-)说真的,现在的操作系统层面保护措施相当不错。最糟糕的情况是,如果我意外地写了一个分叉炸弹,我可能需要重新启动才能恢复。但是真正对系统造成的损坏可能不值得担心,只要你的程序不试图做一些边缘危险的事情。如果你真的很担心,那么在虚拟机上运行程序可能不是一个坏主意。 - Keith Thompson
2
另一方面,我曾经在我使用的计算机上看到过很多奇怪的事情发生(文件损坏、无法恢复的系统错误等),而我不知道其中有多少是由某个C程序表现出可怕的未定义行为所引起的。(到目前为止,还没有实际的恶魔从我的鼻子里飞出来。) - Keith Thompson
1
谢谢你教我如何使用分叉炸弹 - 当我试图理解递归时,我也做了类似的事情 :) - ChrisD
2
现代电子设备仍然可能发生火灾,例如打印机可以被黑客攻击并引起火灾。 - Mooing Duck
显示剩余13条评论

26

一般来说,当今的操作系统(至少是受欢迎的操作系统)使用虚拟内存管理器在受保护的内存区域中运行所有应用程序。事实证明,如果要直接读取或写入存在于已分配/分配给您的进程的区域之外的实际空间中的位置,这并不是非常容易的(本质上)。

直接回答:

  1. 通常情况下,读取几乎永远不会直接损坏另一个进程,但是如果您偶然读取到用于加密、解密或验证程序/进程的关键值,则可能会间接地损坏进程。如果您基于正在读取的数据做出决策,则越界读取可能对代码产生相当不利/意外的影响。

  2. 通过写入可由内存地址访问的位置,真正“损坏”某些东西的唯一方法是如果您要写入的内存地址实际上是硬件寄存器(一个实际上不是用于数据存储而是用于控制某个硬件部件的位置),而不是 RAM 位置。实际上,除非您正在编写某个不可重写的“一次性可编程”位置(或类似物),否则通常不会损坏任何东西。

  3. 通常在调试器内运行代码会以调试模式运行。在调试模式下运行倾向于(但并不总是)在您执行某些被认为是非最佳实践或绝对不合法的操作时更快地停止您的代码。

  4. 永远不要使用宏,使用已经内置了数组索引边界检查的数据结构等……

额外信息我应该补充说明上述信息实际上仅适用于具有内存保护窗口的操作系统。如果编写嵌入式系统或甚至使用不具有内存保护窗口(或虚拟寻址窗口)的操作系统(实时或其他)的系统,则应更加谨慎地阅读和写入内存。此外,在这些情况下,应始终采用安全且可靠的编码实践以避免安全问题。


4
始终采用安全可靠的编码实践。 - Nik Bougalis
3
除非你能捕获特定异常并知道如何从中恢复,否则我建议不要在有缺陷的代码中使用try/catch。在有缺陷的代码中添加catch(...)是最糟糕的事情。 - Eugene
1
@NikBougalis - 我完全同意,但如果操作系统不包括内存保护/虚拟地址空间,或者缺乏操作系统,这就更加重要了 :-) - trumpetlicks
@Eugene - 我从未注意到这对我是个问题,但我同意你的看法,我已经将其编辑删除了 :-) - trumpetlicks
没有人问也没有人在意,但是正确的说法是“per se”,而不是“per say”:它来自拉丁语,意思是“本身”。 - josaphatv
显示剩余2条评论

11

没有检查边界可能会导致丑陋的副作用,包括安全漏洞。其中一个丑陋的后果是任意代码执行。在一个典型的例子中:如果你有一个固定大小的数组,并使用strcpy()将用户提供的字符串放在那里,用户可以提供一个溢出缓冲区并覆盖其他内存位置的字符串,包括CPU在函数完成时应返回的代码地址。

这意味着您的用户可以向您发送一个字符串,导致您的程序实际上调用exec("/bin/sh"),将其转换为shell,在您的系统上执行任何他想要的东西,包括收集所有数据并将您的机器变成僵尸节点。

请参见Smashing The Stack For Fun And Profit了解如何执行此操作的详细信息。


我知道不应该访问超出数组边界的元素,谢谢你强调这一点。但问题是,除了对程序造成各种伤害之外,我是否会无意中超出程序的内存范围?我的意思是在OSX上。 - ChrisD
@ChrisD:OS X是一种现代操作系统,因此它将为您提供完整的内存保护。例如,您不应该受到程序允许执行的限制。这不应包括干扰其他进程(除非您正在以root权限运行)。 - che
我更倾向于说在ring 0特权下,而不是root权限下。 - Ruslan
更有趣的是,超现代编译器可能会决定,如果代码尝试在之前使用len检查数组长度后通过foo [0]foo [len-1]读取数据,并且无论应用程序是否拥有数组之外的存储空间以及读取它的效果是否良好,编译器都可以自由地无条件运行其他代码,但调用其他代码的效果则不一定如此。 - supercat

9
你写道:
我读了很多“任何事情都可能发生”、“分段可能是最小的问题”、“你的硬盘可能会变成粉色,独角兽可能会在你的窗口下唱歌”,这些都很好,但真正的危险是什么?
来说得更明白一点:装上子弹。没有特定的目标指向窗外开枪。有什么危险吗?
问题在于你不知道。如果你的代码覆盖了某些东西,导致你的程序崩溃,那么你是安全的,因为它会将其停止到一个定义好的状态。但是,如果它没有崩溃,问题就开始出现了。哪些资源受到你的程序控制,它可能对它们做些什么?我知道至少有一个由这种溢出引起的重大问题。这个问题出现在一个看似毫无意义的统计函数中,该函数混淆了一些与生产数据库无关的转换表。结果是后面进行了一些非常昂贵的清理工作。实际上,如果能够格式化硬盘,处理这个问题将会更便宜、更容易……换句话说:粉色独角兽可能是你最不用担心的问题。
认为操作系统会保护你的想法是乐观的。如果可能的话,尽量避免越界写入。

好的,这正是我担心的。我会“试图避免越界写入”,但是,看到我过去几个月所做的事情,我仍然会经常这样做。你们如何在没有安全的练习方式的情况下变得如此擅长编程呢? - ChrisD
4
谁说过所有事情都是安全的呢 ;) - Udo Klein

8
不以root或其他特权用户身份运行程序不会对系统造成任何伤害,因此通常这是个好主意。
将数据写入随机内存位置并不会直接“损坏”计算机上运行的其他程序,因为每个进程都在自己的内存空间中运行。
如果尝试访问未分配给进程的任何内存,操作系统将使用段错误停止程序执行。
因此,在没有以root身份运行并直接访问像/dev/mem这样的文件的情况下,您的程序不会干扰操作系统上运行的任何其他程序。
然而,也许这就是你听说的危险所在——如果无意中盲目地向随机内存位置写入随机数据,那么你肯定会破坏任何你能够破坏的东西。
例如,您的程序可能想要删除一个由程序某处存储的文件名指定的特定文件。如果您无意中覆盖了存储文件名的位置,那么您可能会删除一个非常不同的文件。

1
如果您正在以root(或其他特权用户)身份运行,请小心。缓冲区和数组溢出是常见的恶意软件利用方式。 - John Bode
实际上,我用于日常计算的帐户不是管理员帐户(我使用OSX术语,因为那是我的系统)。你是想告诉我,我尝试设置任何内存位置都不可能损坏任何东西吗?这实际上是个好消息! - ChrisD
正如之前提到的,您在意外情况下可能造成的最严重的伤害就是作为用户可能造成的最严重的伤害。如果您想百分之百地确保不会破坏任何数据,您可能希望向计算机添加不同的帐户并进行实验。 - mikyra
1
@mikyra:只有当系统的保护机制百分之百有效时,这才是真的。恶意软件的存在表明您不能总是依赖它。 (我不想暗示这一定值得担心;虽然可能性很小,但某个程序可能会意外地利用恶意软件利用的相同安全漏洞。) - Keith Thompson
1
这里列出的清单包括:从不受信任的来源运行代码。只需点击防火墙弹出窗口上的“确定”按钮,而不必阅读其内容或完全关闭它,如果无法建立所需的网络连接。使用来自可疑来源的最新黑客补丁二进制文件。如果所有者自愿邀请任何两只手和额外强化门都敞开的盗贼,那么这并不是保险库的错。 - mikyra
显示剩余3条评论

4

Objective-C 中的 NSArray 被分配了一个特定的内存块。超出数组的边界意味着你将访问未被分配给数组的内存。

  1. 这个内存可能包含任何值。根据你的数据类型,无法确定数据是否有效。
  2. 此内存可能包含敏感信息,例如私钥或其他用户凭据。
  3. 内存地址可能无效或受保护。
  4. 由于正在被其他程序或线程访问,因此该内存可以具有可变值。
  5. 其他内容也使用内存地址空间,例如内存映射端口。
  6. 向未知内存地址写入数据可能会导致程序崩溃,覆盖操作系统内存空间并且通常会导致灾难性后果。

从程序的角度来看,您始终希望知道代码是否超出了数组的边界。这可能导致返回未知值,从而导致应用程序崩溃或提供无效数据。


NSArrays 会出现越界异常。而这个问题似乎是关于 C 数组的。 - DrummerB
我确实是指C数组。我知道有NSArray,但目前我的大部分练习都是用C语言。 - ChrisD

4
你测试代码时可以尝试使用Valgrind中的memcheck工具--它不能捕获堆栈帧内的单个数组边界违规,但它应该能够捕获许多其他类型的内存问题,包括会导致单个函数范围之外的微妙、更广泛问题的问题。
来自手册:
Memcheck是一个内存错误探测器。它可以检测C和C++程序中常见的以下问题。 - 访问不应访问的内存,例如越过和低于堆块、越过堆栈的顶部以及在释放后访问内存。 - 使用未定义的值,即未初始化的值或从其他未定义的值派生的值。 - 不正确地释放堆内存,例如双重释放堆块或malloc/new/new[]与free/delete/delete[]的不匹配使用。 - 在memcpy和相关函数中重叠的src和dst指针。 - 内存泄漏。 估计时间(ETA):正如Kaz的回答所说,这不是万能药,而且在使用激动人心的访问模式时往往不能提供最有用的输出。

我会怀疑XCode的分析器是否能找到大部分这些问题?我的问题不是如何找到这些错误,而是如果执行仍然存在这些错误的程序是否对未分配给我的程序的内存有危险。我必须执行程序才能看到这些错误发生。 - ChrisD

3
如果你从事系统级编程或嵌入式系统编程,如果随意写入内存位置,就会发生非常严重的问题。老旧系统和许多微控制器使用内存映射IO,因此写入映射到外围寄存器的内存位置可能会造成严重后果,特别是在异步情况下进行操作。
一个例子就是编程闪存。通过将特定序列的值写入芯片地址范围内的特定位置,可以启用内存芯片上的编程模式。如果另一个进程在这个过程中写入芯片中任何其他位置,那么它将导致编程周期失败。
在某些情况下,硬件将对地址进行包装(忽略地址的最高有效位/字节),因此写入超出物理地址空间末尾的地址实际上会导致数据被写入到中间的位置。
最后,像MC68000这样的老CPU可能会锁定到只有硬件复位才能重新启动的地步。虽然我已经几十年没接触过了,但我认为当它在处理异常时遇到总线错误(不存在地址)时,它将停止运行,直到硬件复位被断言。
我的最大建议是向产品做出公然推销,但我对此没有个人利益,并且我与他们没有任何联系。但是,基于几十年的C编程和嵌入式系统,其中可靠性至关重要,Gimpel的PC Lint不仅可以检测到这些错误,而且通过不断对你的坏习惯进行反复提醒,它将使您成为更好的C/C++程序员。
我还建议阅读MISRA C编码标准,如果可以从某人那里获取副本的话。我没有看到最近的版本,但在古老的日子里,它们提供了一个很好的解释,说明为什么应该/不应该做涵盖的事情。
不知道你怎么想,但是每当我从任何应用程序中获得核心转储或挂起的第二或第三次时,我对生产它的公司的意见会降低一半。第四或第五次,无论包装是什么,都会成为货架上的商品,我会在其中央驱动一根木桩,以确保它永远不会回来困扰我。

根据系统的不同,超出范围的读取操作可能会触发不可预测的行为,或者它们可能是良性的。然而,在超出范围的加载操作中良性的硬件行为并不意味着编译器的行为也是良性的。 - supercat

2
我正在使用一个为DSP芯片编译的编译器,该编译器会故意生成访问超出C代码数组末尾的代码,而这些代码实际上是不需要的!这是因为循环结构使得迭代结束时预取下一次迭代的某些数据。因此,在最后一次迭代结束时预取的数据实际上永远不会被使用。
像这样编写C代码会引发未定义行为,但这只是标准文档中关于最大可移植性的形式主义问题。
更多情况下,访问越界的程序并没有被巧妙地优化,它只是有缺陷的。代码会获取一些垃圾值,并且与前面提到的编译器优化循环不同,代码接下来会使用该值进行后续计算,从而破坏它们。
捕获此类错误非常值得,因此即使仅出于这个原因,使行为未定义也是值得的:这样运行时就可以生成诊断消息,例如“main.c的第42行数组溢出”。
在具有虚拟内存的系统上,可能会分配一个数组,使得其后面的地址位于虚拟内存的未映射区域。然后访问将导致程序崩溃。
顺便说一下,在C中,我们可以创建一个指向数组末尾之后的指针。这个指针必须比数组内部的任何指针更大。
这意味着C实现不能将数组放在内存的末尾,其中一个加地址会绕回并看起来比数组中的其他地址小。
尽管如此,访问未初始化或越界值有时是一种有效的优化技术,即使不是最大可移植性。例如,这就是Valgrind工具在访问未初始化数据时不会报告错误的原因,而只有当该值稍后被某种方式使用可能会影响程序结果时才会报告错误。你会得到像“xxx:nnn中的条件分支取决于未初始化的值”这样的诊断消息,有时很难追踪其源头。如果立即捕获所有这样的访问,那么会产生很多误报,包括编译器优化代码以及正确手动优化的代码。
说到这里,我正在使用来自供应商的某些编解码器,当它在Linux上运行并在Valgrind下运行时会出现这些错误。但是供应商说,实际上只有几个的值来自未初始化的内存,并且这些位被逻辑谨慎地避免了。只有值的好位被使用,而Valgrind无法跟踪到单个位。未初始化的材料来自读取编码数据位流之外的一个字,但是代码知道位流中有多少位,并且不会使用比实际位数更多的位。由于超出位流数组的访问不会在DSP架构上造成任何损害(数组后面没有虚拟内存,也没有映射到内存的端口,地址也不会绕回),因此这是一种有效的优化技术。
"未定义行为"并不是很具体,因为根据ISO C标准,仅仅包含一个C标准中未定义的头文件或调用程序本身或C标准中未定义的函数,都是未定义行为的例子。未定义行为并不意味着"地球上没有人定义过",只是"ISO C标准未定义"。但当然,有时候未定义行为真的是绝对没有被任何人定义过的。请保留HTML标签。

此外,只要存在至少一个程序,特定实现能够正确处理即使它名义上征收了标准中规定的所有实现限制,那么当输入任何其他没有违反约束的程序时,该实现可能会表现得任意。因此,99.999%的C程序(除了平台的“一个程序”之外的任何东西)都依赖于标准没有强制要求的行为。 - supercat
似乎不是值得信任的供应商。通常情况下,memcheck 是比特精确的,包括位域和二进制逻辑运算符。并非所有整数算术运算符都被跟踪到比特精度,您需要使用 --expensive-definedness-checks=yes 选项进行跟踪。浮点数没有被跟踪到定义性。 - Paul Floyd

1

除了您自己的程序,我认为您不会破坏任何东西,最坏的情况是您尝试读取或写入与内核未分配给您的进程对应的页面的内存地址,从而生成适当的异常并被杀死(我的意思是,您的进程)。


3
什么?如何覆盖自己进程中用于存储稍后使用的某些变量的内存,而这些变量的值现在已经神秘地改变了! 追踪这些错误非常有趣,我向你保证。段错误将是最好的结果。-1 - Ed S.
2
我是指他不会“打断”其他进程,除了他自己的程序 ;) - jbgs
我确实不在乎是否破坏自己的程序。我只是在学习,如果我访问数组越界,那么这个程序显然是错误的。我只是越来越担心在调试我的创作时破坏其他东西的风险。 - ChrisD
问题是:如果我尝试访问未分配给我的内存,我能确定我的进程是否会被终止吗? (运行在OSX上) - ChrisD
3
多年以前,我曾是一个笨拙的C程序员。我数百次地访问了数组范围之外的位置。除了我的进程被操作系统终止外,没有发生过任何事情。 - jbgs
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接