越界访问数组有多危险?

Posted

技术标签:

【中文标题】越界访问数组有多危险?【英文标题】:How dangerous is it to access an array out of bounds? 【发布时间】:2013-03-16 19:59:30 【问题描述】:

访问超出其边界的数组(在 C 中)有多危险?有时可能会发生我从数组外部读取的情况(我现在明白我然后访问了我的程序的某些其他部分甚至超出该部分使用的内存),或者我试图为数组外部的索引设置一个值。该程序有时会崩溃,但有时只是运行,只会给出意想不到的结果。

现在我想知道的是,这到底有多危险?如果它损坏了我的程序,那还不错。另一方面,如果它破坏了我的程序之外的某些东西,因为我以某种方式设法访问了一些完全不相关的内存,那么我想这是非常糟糕的。 我读了很多“任何事情都可能发生”,'segmentation might be the least bad problem',“你的硬盘可能会变成粉红色,独角兽可能会在你的窗下唱歌”,这些都很好,但真正的危险是什么?

我的问题:

    从数组外部读取值会损坏任何东西 除了我的程序?我会想象只是看东西 不改变任何东西,或者它会例如改变'最后一次 我碰巧到达的文件的opened'属性? 在数组之外设置值是否会损坏除我之外的任何东西 程序?由此 Stack Overflow question我认为可以访问 任何内存位置,都没有安全保证。 我现在从 XCode 中运行我的小程序。这样做 在我的程序无法提供的地方提供一些额外的保护 到达自己的记忆之外?它会损害 XCode 吗? 关于如何安全地运行我固有的错误代码有什么建议吗?

我使用 OSX 10.7、Xcode 4.6。

【问题讨论】:

一般来说,操作系统会保护自己和其他进程免受您的不当行为。不过,您不一定要严重依赖它。 此外,当访问和数组索引超出范围(在您的内存中)时,您永远不会“碰巧到达”硬盘上的文件。 我相信你问的是 C 数组,对吧?所以这与 ObjC 无关,也与任何 IDE 无关。 这是我的favorite example 奇怪的结果(它处理堆栈,但我发现它真的很有启发性......)。 xkcd.com/371 【参考方案1】:

你写:

我读了很多“任何事情都可能发生”,“细分可能是 最不坏的问题','你的硬盘可能会变成粉红色,而独角兽可能会 在你的窗下唱歌”,这一切都很好,但真正的 危险吗?

这么说吧:装枪。将它指向窗外,没有任何特别的目标和射击。有什么危险?

问题是你不知道。如果您的代码覆盖了导致程序崩溃的内容,那您很好,因为它会将其停止到定义的状态。但是,如果它没有崩溃,那么问题就会开始出现。哪些资源在您的程序的控制之下,它可能对它们做什么?我知道至少有一个主要问题是由这种溢出引起的。问题出在一个看似毫无意义的统计函数中,它弄乱了生产数据库的一些不相关的转换表。结果是之后进行了一些非常昂贵的清理工作。实际上,如果这个问题会格式化硬盘,它会更便宜,更容易处理......换句话说:粉红色的独角兽可能是你最小的问题。

操作系统会保护您的想法是乐观的。如果可能,尽量避免写出越界。

【讨论】:

好吧,这正是我害怕的。我会“尽量避免写得越界”,但是,看看我过去几个月一直在做的事情,我肯定还会做很多事情。在没有安全的练习方式的情况下,你们是如何在编程方面变得如此出色的? 谁说任何东西都是安全的;)【参考方案2】:

具有两个或更多维度的数组提出了超出其他答案中提到的考虑因素。考虑以下函数:

char arr1[2][8];
char arr2[4];
int test1(int n)

  arr1[1][0] = 1;
  for (int i=0; i<n; i++) arr1[0][i] = arr2[i];      
  return arr1[1][0];

int test2(int ofs, int n)

  arr1[1][0] = 1;
  for (int i=0; i<n; i++) *(arr1[0]+i) = arr2[i];      
  return arr1[1][0];

gcc 处理第一个函数的方式不允许尝试写入 arr[0][i] 可能会影响 arr[1][0] 的值,并且生成的代码无法返回除了硬编码值 1 之外的任何值。尽管标准将 array[index] 的含义定义为精确等同于 (*((array)+(index))),但在涉及对值使用 [] 运算符的情况下,gcc 似乎以不同的方式解释数组边界和指针衰减的概念数组类型的,而不是那些使用显式指针算法的。

【讨论】:

【参考方案3】:

一般来说,当今的操作系统(无论如何都是流行的)使用虚拟内存管理器在受保护的内存区域中运行所有应用程序。事实证明,简单地读取或写入存在于已分配/分配给您的进程的区域之外的真实空间中的位置并不是非常容易(本身)。

直接回答:

    读取几乎不会直接损坏另一个进程,但是如果您碰巧读取了用于加密、解密或验证程序/进程的 KEY 值,它可能会间接损坏一个进程。如果您根据正在阅读的数据做出决定,那么越界读取可能会对您的代码产生一些不利/意外的影响

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

    通常在调试器中运行以调试模式运行代码。当您做了一些被认为不合常规或完全非法的事情时,在调试模式下运行会倾向于(但并非总是)更快地停止您的代码。

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

附加 我应该补充一点,上述信息实际上仅适用于使用带有内存保护窗口的操作系统的系统。如果为嵌入式系统或什至使用没有内存保护窗口(或虚拟寻址窗口)的操作系统(实时或其他)的系统编写代码,则在读取和写入内存时应更加谨慎。同样在这些情况下,应始终采用 SAFE 和 SECURE 编码实践来避免安全问题。

【讨论】:

应该始终采用安全可靠的编码实践。 我建议不要对错误代码使用 try/catch,除非您捕获到非常具体的异常并知道如何从中恢复。 Catch(...) 是您可以添加到错误代码中的最糟糕的事情。 @NikBougalis - 我完全同意,但如果操作系统不包括内存保护/虚拟地址空间,或者缺少操作系统,那就更重要了:-) @Eugene - 我从来没有注意到这对我来说是个问题,但我同意你的观点,我是否已经将其编辑掉了 :-) 1) 你的意思是损害,因为我会透露一些应该保密的东西? 2) 我不确定我明白你的意思,但我想我只是在尝试访问数组边界之外的位置来访问 RAM?【参考方案4】:

就 ISO C 标准(语言的官方定义)而言,访问超出其边界的数组具有“未定义的行为”。这个字面意思是:

行为,在使用不可移植或错误的程序结构或 错误数据,本国际标准对此没有规定 要求

非规范性注释对此进行了扩展:

可能的未定义行为包括忽略情况 完全具有不可预测的结果,在翻译过程中表现得很好 或以文件化方式执行程序的特征 环境(无论是否发布诊断消息),以 终止翻译或执行(发出 诊断信息)。

这就是理论。现实是什么?

在“最佳”情况下,您将访问当前运行的程序所拥有的一些内存(这可能会导致您的程序行为不端),或者您当前所拥有的正在运行的程序(这可能会导致您的程序因分段错误而崩溃)。或者您可能会尝试写入您的程序拥有的内存,但它被标记为只读;这也可能会导致您的程序崩溃。

这是假设您的程序在操作系统下运行,该操作系统试图保护同时运行的进程彼此之间。如果您的代码在“裸机”上运行,比如它是操作系统内核或嵌入式系统的一部分,那么就没有这种保护;你的行为不端的代码应该提供这种保护。在这种情况下,损坏的可能性要大得多,在某些情况下,包括对硬件(或附近的事物或人)的物理损坏。

即使在受保护的操作系统环境中,保护也并非总是 100%。例如,存在允许非特权程序获得 root(管理)访问权限的操作系统错误。即使拥有普通用户权限,故障程序也会消耗过多的资源(CPU、内存、磁盘),可能会导致整个系统瘫痪。许多恶意软件(病毒等)利用缓冲区溢出来获得对系统的未经授权的访问。

(一个历史例子:我听说在一些带有core memory 的旧系统上,在紧密循环中重复访问单个内存位置可能会导致该内存块融化。其他可能性包括破坏 CRT 显示器,并以驱动器柜的谐波频率移动磁盘驱动器的读/写磁头,使其穿过桌子并掉到地板上。)

而且总是有Skynet 需要担心。

底线是这样的:如果你可以编写一个程序来故意做一些坏事,那么至少从理论上讲,一个有缺陷的程序可能会意外地做同样的事情.

在实践中,非常在 MacOS X 系统上运行的错误程序不太可能发生比崩溃更严重的事情。但是不可能完全防止有缺陷的代码做坏事。

【讨论】:

谢谢,我其实完全理解这一点。但这立即引发了一个后续问题:初学者可以做些什么来保护他/她的计算机免受他/她自己可能可怕的创造?在我彻底测试了一个程序之后,我可以在世界上释放它。但是第一次试运行肯定是一个错误的程序。你们如何保护自己的系统安全? @ChrisD:我们往往很幸运。 8-) 说真的,这些天操作系统级别的保护非常好。最坏的情况,如果我不小心写了一个fork bomb,我可能需要重新启动才能恢复。但是,只要您的程序没有试图在危险边缘做某事,对系统的真正损坏可能并不值得担心。如果你真的很担心,在虚拟机上运行程序可能不是一个坏主意。 另一方面,我看到我用过的电脑上发生了很多奇怪的事情(文件损坏、无法恢复的系统错误等),我不知道其中有多少可能是由某些 C 程序表现出可怕的未定义行为引起的。 (到目前为止,还没有真正的恶魔从我的鼻子里飞出来。) 感谢你教我使用叉子炸弹——我在尝试掌握递归时已经做过类似的事情了 :) scientificamerican.com/article/… 所以现代电子产品仍然可能着火。【参考方案5】:

如果您曾经进行过系统级编程或嵌入式系统编程,那么如果您写入随机内存位置,可能会发生非常糟糕的事情。较旧的系统和许多微控制器使用内存映射 IO,因此写入映射到外围寄存器的内存位置可能会造成严重破坏,尤其是在异步完成的情况下。

一个例子是对闪存进行编程。通过将特定的值序列写入芯片地址范围内的特定位置,可以启用存储芯片上的编程模式。如果另一个进程在此过程中写入芯片中的任何其他位置,则会导致编程周期失败。

在某些情况下,硬件会环绕地址(地址的最高有效位/字节被忽略),因此写入超出物理地址空间末尾的地址实际上会导致数据被写入事物的中间。

最后,像 MC68000 这样的旧 CPU 可以锁定到只有硬件重置才能让它们再次运行的程度。几十年来一直没有处理它们,但我相信它是在尝试处理异常时遇到总线错误(不存在的内存)时,它会简单地停止,直到硬件复位被断言。

我最大的建议是一个产品的公然插件,但我对它没有个人兴趣,我与他们没有任何关系 - 但基于几十年的 C 编程和嵌入式系统,可靠性至关重要, Gimpel 的 PC Lint 不仅会检测出这类错误,而且会通过不断对你的坏习惯喋喋不休,让你成为一个更好的 C/C++ 程序员

如果您可以从某人那里获得一份副本,我还建议您阅读 MISRA C 编码标准。我没有看到任何最近的,但在过去的日子里,他们很好地解释了为什么你应该/不应该做他们所涵盖的事情。

不知道你的情况,但在我第二次或第三次收到任何应用程序的核心转储或挂断时,我对生产它的任何公司的看法下降了一半。第 4 次或第 5 次,无论包裹是什么,都变成了货架,我用木桩穿过包裹/光盘的中心,以确保它永远不会回来困扰我。

【讨论】:

取决于系统,超出范围的读取也可能触发不可预知的行为,或者它们可能是良性的,尽管超出范围负载的良性硬件行为确实如此不暗示良性编译器行为。【参考方案6】:

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

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

请参阅 Smashing The Stack For Fun And Profit 了解如何完成此操作的详细信息。

【讨论】:

我知道我不应该越界访问数组元素,感谢您强调这一点。但问题是,除了对我的程序造成各种伤害外,我是否会在不经意间超出程序的内存范围?我的意思是在 OSX 上。 @ChrisD:OS X 是一个现代操作系统,因此它会为您提供全面的内存保护。例如。你不应该被限制在你的程序被允许做什么。这不应该包括弄乱其他进程(除非你在 root 权限下运行)。 我宁愿说在 ring 0 权限下,而不是 root 权限下。 更有趣的是,超现代编译器可能会决定,如果代码在之前使用了 len 对数组长度的检查以执行或跳过之后尝试读取 foo[0]foo[len-1]一段代码,编译器应该可以无条件地运行其他代码,即使应用程序拥有数组之外的存储并且读取它的效果是良性的,但调用其他代码的效果不会。 【参考方案7】:

您可能想在测试代码时尝试使用Valgrind 中的memcheck 工具——它不会捕获堆栈帧中的单个数组边界违规,但它应该会捕获许多其他类型的内存问题,包括那些会导致超出单个函数范围的微妙、更广泛问题的问题。

来自手册:

Memcheck 是一个内存错误检测器。它可以检测出以下 C 和 C++ 程序中常见的问题。

访问您不应该访问的内存,例如堆块溢出和运行不足、栈顶溢出以及在内存被释放后访问内存。 使用未定义的值,即未初始化的值,或从其他未定义值派生的值。 堆内存释放不正确,例如两次释放堆块,或 malloc/new/new[] 与 free/delete/delete[] 的不匹配使用 memcpy 和相关函数中的 src 和 dst 指针重叠。 内存泄漏。

ETA:尽管正如 Kaz 的回答所说,它不是万能的,而且并不总是提供最有用的输出,尤其是当您使用 令人兴奋的 访问权限时模式。

【讨论】:

我怀疑 XCode 的分析器会找到大部分内容?我的问题不是如何找到这些错误,而是如果执行仍然有这些错误的程序对未分配给我的程序的内存是危险的。我必须执行程序才能看到发生的错误【参考方案8】:

我正在使用 DSP 芯片的编译器,该芯片故意生成代码,该代码从 C 代码中访问超出数组末尾的代码!

这是因为循环是结构化的,因此迭代结束时会为下一次迭代预取一些数据。所以在最后一次迭代结束时预取的数据永远不会被实际使用。

编写这样的 C 代码会调用未定义的行为,但这只是标准文档中的一种形式,它涉及最大的可移植性。

通常情况下,越界访问的程序没有得到巧妙的优化。这简直是​​越野车。代码会获取一些垃圾值,并且与上述编译器的优化循环不同,代码会在后续计算中使用该值,从而破坏它们。

捕获这样的错误是值得的,因此即使仅仅因为这个原因,也值得将行为设为未定义:以便运行时可以生成诊断消息,例如“main.c 的第 42 行中的数组溢出” .

在具有虚拟内存的系统上,可能会碰巧分配一个数组,使得后面的地址位于虚拟内存的未映射区域中。然后访问将轰炸程序。

顺便说一句,请注意,在 C 语言中,我们可以创建一个指针,该指针位于数组末尾之后。并且这个指针必须比任何指向数组内部的指针都要大。 这意味着 C 实现不能将数组放在内存的末尾,因为加号地址会环绕并且看起来比数组中的其他地址要小。

尽管如此,访问未初始化或越界值有时是一种有效的优化技术,即使不是最大程度地可移植。这就是为什么 Valgrind 工具在访问发生时不报告对未初始化数据的访问,但仅在以后以某种可能影响程序结果的方式使用该值时才报告。您会得到类似“xxx:nnn 中的条件分支取决于未初始化的值”之类的诊断信息,并且有时很难找到它的来源。如果所有此类访问都立即被捕获,那么编译器优化代码以及正确手动优化的代码都会产生大量误报。

说到这一点,我正在使用来自供应商的一些编解码器,当它移植到 Linux 并在 Valgrind 下运行时会发出这些错误。但是供应商说服我,实际上只有几个 正在使用的值来自未初始化的内存,并且这些位被逻辑小心地避免了。只有值的好位被使用和 Valgrind没有能力追踪到单个位。未初始化的材料来自读取编码数据位流末尾的单词,但代码知道流中有多少位,并且不会使用比实际更多的位。由于超出位流数组末尾的访问不会对 DSP 架构造成任何损害(数组后没有虚拟内存,没有内存映射端口,并且地址不换行),因此它是一种有效的优化技术。

“未定义的行为”实际上并没有太大的意义,因为根据 ISO C,简单地包含 C 标准中未定义的标头,或调用程序本身或 C 标准中未定义的函数,都是未定义行为的示例。未定义的行为并不意味着“地球上的任何人都没有定义”,只是“ISO C 标准没有定义”。但当然,有时未定义的行为真的绝对不是任何人定义的。

【讨论】:

此外,如果存在至少一个特定实现可以正确处理的程序,即使它名义上对标准中给出的所有实现限制征税,该实现在馈送任何其他程序时可以任意行为没有违反约束并且仍然是“合规的”。因此,99.999% 的 C 程序(除了平台的“一个程序”之外的任何程序)都依赖于标准未强加要求的行为。【参考方案9】:

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

【讨论】:

..什么?在您自己的进程中覆盖用于存储稍后使用的变量的内存怎么样......现在它已经神秘地改变了它的值!我向你保证,追踪这些错误很有趣。段错误将是 best 结果。 -1 我的意思是他不会“破坏”其他进程,除了他自己的程序;) 我确实不在乎我是否破坏了自己的程序。我只是在学习,如果我访问超出数组范围的任何内容,程序显然是错误的。我越来越担心在调试我的作品时破坏其他东西的风险 问题是:如果我尝试访问未分配给我的内存,我能否确定我的进程会被杀死? (在 OSX 上) 多年前,我曾经是一个笨拙的 C 程序员。我数百次访问超出其范围的数组。除了我的进程被操作系统杀死之外,什么也没发生。【参考方案10】:

不以 root 或任何其他特权用户身份运行您的程序不会损害您的任何系统,因此通常这可能是个好主意。

通过将数据写入某个随机内存位置,您不会直接“损坏”计算机上运行的任何其他程序,因为每个进程都在自己的内存空间中运行。

如果您尝试访问任何未分配给您的进程的内存,操作系统将阻止您的程序执行并出现分段错误。

因此直接(无需以 root 身份运行并直接访问 /dev/mem 之类的文件)不会有您的程序会干扰操作系统上运行的任何其他程序的危险。

尽管如此 - 就危险而言,这可能就是您所听说的 - 通过偶然盲目地将随机数据写入随机内存位置,您肯定会损坏您能够损坏的任何东西。

例如,您的程序可能想要删除由存储在程序中某处的文件名给出的特定文件。如果您不小心覆盖了存储文件名的位置,您可能会删除一个非常不同的文件。

【讨论】:

如果您正在以root(或其他特权用户)身份运行,请注意。缓冲区和数组溢出是一种常见的恶意软件利用。 实际上我用于所有日常计算的帐户不是管理员帐户(我使用 OSX 术语,因为那是我的系统)。你的意思是告诉我我不可能通过尝试设置任何内存位置来损坏某些东西吗?这实际上是个好消息! 如前所述,意外造成的最严重伤害是作为用户可以造成的最严重伤害。如果您想 100% 确定不破坏您的任何数据,您可能需要在您的计算机上添加不同的帐户并进行试验。 @mikyra:只有当系统的保护机制 100% 有效时才会如此。恶意软件的存在表明您不能总是依赖它。 (我不想暗示这一定值得担心;程序有可能(但不太可能)意外利用恶意软件所利用的相同安全漏洞。) 这里的列表包括:从不受信任的来源运行代码。如果无法建立所需的网络连接,只需单击防火墙任何弹出窗口上的确定按钮,甚至无需阅读它的内容或将其完全关闭。使用来自可疑来源的最新黑客修补二进制文件。如果主人自愿邀请任何双臂和超强坚固门敞开的窃贼,这不是金库的错。【参考方案11】:

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

    此内存可以有任何值。根据您的数据类型,无法知道数据是否有效。 此内存可能包含敏感信息,例如私钥或其他用户凭据。 内存地址可能无效或受保护。 内存的值可能会发生变化,因为它正在被另一个程序或线程访问。 其他事物使用内存地址空间,例如内存映射端口。 将数据写入未知内存地址可能会导致程序崩溃、覆盖操作系统内存空间,并且通常会导致太阳内爆。

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

【讨论】:

NSArrays 有超出范围的异常。而这个问题似乎是关于 C 数组的。 我确实是指 C 数组。我知道有 NSArray,但现在我的大部分练习都是在 C 中的

以上是关于越界访问数组有多危险?的主要内容,如果未能解决你的问题,请参考以下文章

VS调试数组越界问题

数组访问越界

数组访问越界

数组访问越界

数组访问越界

c内存读写越界