访问数组越界有多危险?

时间:2013-03-26 20:41:30

标签: c arrays memory

在其边界之外访问数组有多危险(在C中)?有时候我会从数组外部读取(我现在理解我然后访问我的程序的某些其他部分使用的内存,甚至超过它)或者我试图将值设置为数组之外的索引。该程序有时会崩溃,但有时只是运行,只会产生意想不到的结果。

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

我的问题:

  1. 从数组外部读取值会损坏任何内容 除了我的节目?我会想象只是看事情 不改变任何东西,或者它会改变'最后一次 打开了我碰巧到达的文件的属性?
  2. 在数组之外设置值可以破坏除了我之外的任何东西 程序?由此     Stack Overflow question我认为可以访问     任何记忆位置,没有安全保障。
  3. 我现在从XCode中运行我的小程序。那样做 为我的程序提供一些额外的保护,它不能 到达自己的记忆之外?它会伤害XCode吗?
  4. 有关如何安全地运行我的固有错误代码的任何建议吗?
  5. 我使用的是OSX 10.7,Xcode 4.6。

11 个答案:

答案 0 :(得分:116)

就ISO C标准(该语言的官方定义)而言,访问其边界之外的数组具有“未定义的行为”。字面含义是:

  

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

非规范性说明扩展了这一点:

  

可能的未定义行为包括忽略这种情况   完全具有不可预测的结果,在翻译过程中表现出色   或者以文件化的方式执行程序   环境(有或没有发出诊断信息),到   终止翻译或执行(发布a   诊断信息)。

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

在“最佳”情况下,您将访问某些内存,这些内存由您当前正在运行的程序拥有(可能导致您的程序行为异常),或者您当前拥有的运行程序(这可能会导致程序崩溃,例如分段错误)。或者您可能尝试写入程序拥有的内存,但标记为只读;这可能也会导致程序崩溃。

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

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

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

总是Skynet要担心。

最重要的是:如果你可以编写一个程序来做一些不好的事情故意,那么理论上至少可能一个错误的程序可以做同样的事情意外

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

答案 1 :(得分:25)

通常,今天的操作系统(无论如何都是流行的)使用虚拟内存管理器在受保护的内存区域中运行所有应用程序。事实证明,简单地读取或写入已经分配/分配给您的过程的区域之外的REAL空间中的位置是非常容易的(根据说)。

直接答案:

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

2)通过写入内存地址可访问的内容来实现损坏的唯一方法是,您写入的内存地址实际上是硬件寄存器(实际上不是用于数据存储但是用于控制某些硬件)而不是RAM位置。事实上,除非你正在写一些不可重写的可编程位置(或那种性质的东西),否则你通常不会损坏某些东西。

3)通常从调试器内运行在调试模式下运行代码。在调试模式下运行TEND(但并不总是)在你做了一些被认为没有实践或完全非法的事情时更快地停止你的代码。

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

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

答案 2 :(得分:9)

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

这意味着您的用户可以向您发送一个字符串,该字符串将使您的程序基本上调用exec("/bin/sh"),这会将其转换为shell,在系统上执行他想要的任何内容,包括收集所有数据并转动机器进入僵尸网络节点。

有关如何执行此操作的详细信息,请参阅Smashing The Stack For Fun And Profit

答案 3 :(得分:8)

你写道:

  

我读了很多'任何事情都可能发生','细分可能就是这样   最不好的问题','你的硬盘可能变成粉红色和独角兽可能   在你的窗下唱歌',这很好,但真的是什么   危险?

让我们这样说:装枪。将它指向窗外,没有任何特定的目标和火。有什么危险?

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

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

答案 4 :(得分:7)

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

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

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

因此直接(不以root身份运行并直接访问/ dev / mem等文件),程序不会干扰操作系统上运行的任何其他程序。

尽管如此 - 可能这就是你在危险方面所听到的 - 通过盲目地将随机数据随意写入随机存储位置你肯定会损坏任何你能够损坏的东西。

例如,您的程序可能希望删除存储在程序中某处的文件名给出的特定文件。如果偶然您只是覆盖存储文件名的位置,您可能会删除一个非常不同的文件。

答案 5 :(得分:4)

您可能希望在测试代码时尝试使用memcheck中的Valgrind工具 - 它不会捕获堆栈框架内的单个数组边界违规,但它应该捕获许多其他类型记忆问题,包括那些会在单一功能范围之外引起细微,更广泛问题的问题。

从手册:

  

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

     
      
  • 您不应该访问内存,例如overrunning和underrunning堆块,超越堆栈顶部,并在释放后访问内存。
  •   
  • 使用未定义的值,即尚未初始化的值,或从其他未定义的值派生的值。
  •   
  • 错误释放堆内存,例如双重释放堆块,或者malloc / new / new []与free / delete / delete []
  • 的使用不匹配   
  • 在memcpy及相关函数中重叠src和dst指针。
  •   
  • 内存泄漏。
  •   

ETA:虽然,正如Kaz的回答所说,它不是灵丹妙药,并不总能提供最有用的输出,特别是当您使用令人兴奋的访问时图案。

答案 6 :(得分:3)

Objective-C中的

NSArray被分配了一个特定的内存块。超出数组的范围意味着您将访问未分配给该数组的内存。这意味着:

  1. 此内存可以有任何值。根据您的数据类型,无法知道数据是否有效。
  2. 此内存可能包含敏感信息,如私钥或其他用户凭据。
  3. 内存地址可能无效或受到保护。
  4. 内存可以有一个不断变化的值,因为它正被另一个程序或线程访问。
  5. 其他内容使用内存地址空间,例如内存映射端口。
  6. 将数据写入未知内存地址可能会导致程序崩溃,覆盖操作系统内存空间,并导致太阳内爆。
  7. 从程序的角度来看,您总是想知道代码何时超出数组的范围。这可能导致返回未知值,从而导致应用程序崩溃或提供无效数据。

答案 7 :(得分:3)

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

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

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

最后,像MC68000这样的旧CPU可以锁定到只有硬件复位才能让它们再次运行的程度。几十年没有对它们进行过工作,但我相信它在尝试处理异常时遇到总线错误(不存在的内存)时,它会停止,直到硬件复位被声明为止。

我最大的推荐是产品的公然插件,但我对它没有个人兴趣而且我不以任何方式与它们有任何关系 - 但基于几十年的C编程和嵌入式系统,其中可靠性至关重要,Gimpel的PC Lint不仅可以检测到这些错误,而且还可以通过不断地向您提供关于坏习惯的方法,使您成为更好的C / C ++程序员。

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

Dunno关于你,但是关于第二次或第三次我从任何应用程序中获得coredump或hangup,我对任何公司产生的看法都下降了一半。第四次或第五次,无论包裹是什么,我都会通过包裹/圆盘的中心开一个木桩,以确保它永远不会回来困扰我。

答案 8 :(得分:2)

除了你自己的程序之外,我认为你不会破坏任何东西,在最坏的情况下,你会尝试读取或写入与内核没有分配给你的程序的页面对应的内存地址,生成正确的例外和被杀(我的意思是,你的过程)。

答案 9 :(得分:2)

我正在使用一个DSP芯片的编译器,它故意生成代码,用C代码访问一个超出数组末尾的代码,而不是!

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

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

更常见的是,没有巧妙地优化访问越界的程序。它只是马车。代码获取一些垃圾值,并且与上述编译器的优化循环不同,代码然后在后续计算中使用值,从而破坏了它。

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

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

  

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

然而,访问未初始化或超出界限的值有时是一种有效的优化技术,即使不是最大程度的可移植性。这就是为什么Valgrind工具在这些访问发生时不报告对未初始化数据的访问的原因,但仅当稍后以某种方式使用该值时可能会影响程序的结果。你得到的诊断就像“xxx中的条件分支:nnn取决于未初始化的值”,有时很难追踪它的起源。如果所有这些访问都被立即捕获,那么编译器优化代码以及正确的手动优化代码会产生大量误报。

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

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

答案 10 :(得分:0)

具有两个或更多维度的数组是其他答案中提到的那些之外的一个考虑因素。考虑以下函数:

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 似乎在涉及使用 [ ] 运算符对数组类型的值,而不是使用显式指针算法的运算符。