越界访问数组有多危险?

访问超出其边界的数组(在C语言中)有多危险?有时会发生这样的情况:我从数组外部读取数据(我现在知道我访问了程序的其他部分使用的内存,甚至超出了这个范围),或者我试图将一个值设置为数组外部的索引。程序有时会崩溃,但有时只是运行,只会给出意想不到的结果。

现在我想知道的是,这到底有多危险?如果它损坏了我的程序,那也不算太坏。另一方面,如果它破坏了我程序之外的东西,因为我设法访问了一些完全不相关的内存,那么我想这是非常糟糕的。 我读过很多“任何事情都可能发生”,“市场细分可能是最不坏的问题”,“你的硬盘可能会变成粉红色,独角兽可能会在你的窗户下唱歌”,这些都很好,但真正的危险是什么?< / p >

我的问题:

  1. 从数组外部读取值会损坏任何东西吗 除了我的程序?我想仅仅是观察事物就能做到 没有改变任何东西,或者它会改变'last time 打开'属性的文件,我碰巧达到?李< / >
  2. 在数组外设置值是否会损坏除my之外的任何东西 项目?从这个 堆栈溢出问题我收集到它是可能访问 任何内存位置,都没有安全保证 我现在在XCode中运行我的小程序。这 为我的程序提供一些额外的保护 超越自己的记忆?它会伤害XCode吗?李< / >
  3. 关于如何安全地运行我固有的bug代码,有什么建议吗?

我使用OSX 10.7, Xcode 4.6。

56155 次浏览

除了你自己的程序,我不认为你会破坏任何东西,在最坏的情况下,你会尝试从一个内存地址读取或写入一个页面,内核没有分配给你的进程,产生适当的异常并被杀死(我的意思是,你的进程)。

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

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

如果你试图访问任何没有分配给你的进程的内存,操作系统将停止你的程序执行分割错误。

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

尽管如此——这可能是你听说过的危险——盲目地将随机数据写入随机的内存位置,你肯定会损坏任何你能损坏的东西。

例如,您的程序可能希望删除存储在程序某处的文件名所给出的特定文件。如果你不小心覆盖了文件名所在的位置,你可能会删除一个完全不同的文件。

你写的:

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

这么说吧,给枪上膛。瞄准窗外,不要瞄准,然后开火。危险在哪里?

问题是你不知道。如果你的代码覆盖了一些使你的程序崩溃的东西,你没有问题,因为它会将程序停止到一个定义的状态。然而,如果它没有崩溃,那么问题就开始出现了。哪些资源在您的程序的控制之下,它可能对它们做什么?我知道至少有一个主要问题是由这种溢出引起的。问题在于一个看似毫无意义的统计函数,它混淆了生产数据库的一些不相关的转换表。结果是一些< >强非常< / >强昂贵的清理之后。实际上,如果这个问题格式化了硬盘,它会更便宜,更容易处理……换句话说:粉色独角兽可能是你最不担心的问题。

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

Objective-C中的__abc0被分配一个特定的内存块。超过数组的边界意味着您将访问没有分配给数组的内存。这意味着:

  1. 这个内存可以有任何值。无法根据数据类型知道数据是否有效。
  2. 该内存可能包含敏感信息,如私钥或其他用户凭证。
  3. 内存地址可能无效或受保护。
  4. 内存可以有一个变化的值,因为它正在被另一个程序或线程访问。
  5. 其他东西使用内存地址空间,比如内存映射端口。
  6. 将数据写入未知的内存地址会使程序崩溃,覆盖操作系统内存空间,通常会导致太阳内爆。

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

一般来说,现在的操作系统(流行的操作系统)使用虚拟内存管理器在受保护的内存区域中运行所有应用程序。事实证明,简单地读取或写入存在于已分配给进程的区域之外的REAL空间中的位置(就其本身而言)并不容易。

直接回答:

  1. 读取几乎永远不会直接损害另一个进程,但是如果碰巧读取用于加密、解密或验证程序/进程的KEY值,则可能间接损害进程。如果基于所读取的数据进行决策,那么越界读取可能会对代码产生一些不利/意外的影响

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

  3. 通常从调试器内部运行,以调试模式运行代码。在调试模式下运行确实倾向于(但不总是)更快地停止您的代码,当您做了一些被认为是不实践或完全非法的事情时。

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

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

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

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

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

可能的未定义行为包括忽略情况 翻译过程中的行为完全无法预测结果 或以文件化的方式执行程序的特点 环境(有或没有发出诊断消息),到 终止翻译或执行(通过发出 诊断消息)。< / p >

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

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

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

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

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

而且总是有天网要担心。

底线是:如果你可以编写一个程序来做一些糟糕的故意,那么至少在理论上,一个有bug的程序可能会做同样的事情不小心

实际上,在MacOS X系统上运行的有bug的程序不太可能比崩溃更严重,这是非常。但是完全不可能阻止有bug的代码做真正糟糕的事情。

我正在使用一个用于DSP芯片的编译器,该编译器故意生成代码,从C代码中访问一个数组的末尾,而C代码没有!

这是因为循环是结构化的,因此迭代结束时将为下一次迭代预取一些数据。因此,在最后一次迭代结束时预取的数据实际上从未被使用。

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

更常见的情况是,访问越界的程序没有被巧妙地优化。它只是有bug。该代码获取一些垃圾值,并且与前面提到的编译器的优化循环不同,该代码随后在后续计算中使用该值,从而破坏了它们。

捕获这样的错误是值得的,因此即使仅仅为了这个原因,也值得使行为未定义:这样运行时就可以产生类似“main.c第42行数组溢出”这样的诊断消息。

在具有虚拟内存的系统上,分配数组时,后面的地址可能位于虚拟内存的未映射区域。访问将轰炸程序。

说句题外话,请注意,在C语言中,我们允许创建一个指针,该指针位于数组的末尾之后。这个指针必须比任何指向数组内部的指针都要大。 这意味着C实现不能将数组放在内存的末尾,在那里,1 +地址会被环绕,看起来比数组中的其他地址更小

然而,访问未初始化或越界的值有时是一种有效的优化技术,即使不能最大限度地移植。例如,这就是为什么Valgrind工具在未初始化数据被访问时不报告这些访问,而只在稍后以某种方式使用该值可能影响程序结果时才报告这些访问。你会得到类似于“xxx:nnn中的条件分支依赖于未初始化的值”这样的诊断,有时很难追踪到它的起源。如果所有这样的访问都立即被捕获,那么编译器优化的代码和正确手工优化的代码都会产生很多误报。

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

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

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

摘自手册:

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

  • 访问你不应该访问的内存,例如,溢出或低于堆块,溢出堆栈顶部,以及在释放内存后访问内存。
  • 使用未定义值,即尚未初始化的值,或已从其他未定义值派生的值。
  • 不正确的释放堆内存,例如双重释放堆块,或者错误地使用malloc/new/new[]与free/delete/delete[]
  • 在memcpy和相关函数中重叠src和dst指针。
  • 内存泄漏。

埃塔:虽然,正如卡兹的回答所说,它不是万能的,并不总是提供最有帮助的输出,特别是当你使用令人兴奋的访问模式时。

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

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

有关如何做到这一点的详细信息,请参阅为了乐趣和利益而砸碎堆栈

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

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

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

最后,像MC68000这样的老cpu可能会锁定到只有硬件重置才能让它们重新工作的地步。我已经几十年没有使用它们了,但我相信当它在试图处理异常时遇到总线错误(不存在内存)时,它会简单地停止,直到断言硬件重置。

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

我还建议你阅读MISRA C编码标准,如果你能从别人那里得到一份的话。我没有看到最近的任何一个,但在过去的日子里,他们给了一个很好的解释,为什么你应该/不应该做他们覆盖的事情。

我不知道你的情况,但当我第二次或第三次从任何应用程序中得到一个coredump或挂起时,我对任何公司的看法都会下降一半。第四次或第五次,不管是什么包装都变成了架子,我用一根木桩穿过包装/光盘的中心,只是为了确保它永远不会回来缠着我。

二维或多维数组的考虑超出了其他答案中提到的那些。考虑以下函数:

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似乎在涉及对数组类型的值使用[]操作符和使用显式指针算术的情况下,对数组边界和指针衰减的概念进行了不同的解释。