访问数组越界有多危险?
访问数组之外的数组有多危险? 有时候可能会发生,我从数组外读取(我现在明白,然后访问我的程序的一些其他部分使用的内存,甚至超出),或者我试图设置一个值的数组以外的索引。 该程序有时会崩溃,但有时只是运行,只会给出意想不到的结果。
现在我想知道的是,这真的有多危险? 如果它损害了我的程序,那还不错。 另一方面,如果它破坏了我的程序之外的某些东西,因为我设法访问了一些完全不相关的内存,那么我想它是非常糟糕的。 我读了很多“什么都可能发生”, “细分可能是最不好的问题” ,“你的硬盘可能会变成粉红色,独angular兽可能会在你的窗下唱歌”,这是很好的,但真正的危险是什么?
我的问题:
- 从arrays外读取值可以损害除我的程序之外的任何东西吗? 我会想象只是看着事情不会改变什么,或者它会例如改变我碰巧到达的文件的“上次打开”属性?
- 除了我的程序之外,是否可以将数值设置在数组之外? 从这个stackoverflow问题我收集,有可能访问任何内存位置,没有安全保证。
- 我现在从XCode中运行我的小程序。 这是否为我的程序提供了额外的保护,使其无法到达自己的内存之外? 它会伤害XCode吗?
- 任何关于如何安全地运行我自己的错误代码的build议?
我使用OSX 10.7,Xcode 4.6
这是我的第一个Stackoverflow问题。 我花了很多时间阅读这个主题,但是我可能错过了很多资源。 如果你觉得我没有做足够的研究和/或你看到这个问题的其他问题,请告诉我。
就ISO C标准(语言的官方定义)而言,访问超出范围的数组具有“ 未定义的行为 ”。 这个字面意思是:
行为,在使用不可移植或错误的程序结构或错误的数据时,本国际标准对此没有要求
一个不规范的说明扩展了这一点:
可能的未定义的行为范围包括从完全忽略情况和不可预测的结果,到在翻译或程序执行过程中以环境特征(有或没有发出诊断消息),终止翻译或执行(在发行的诊断消息)。
这就是理论。 现实是什么?
在“最好”的情况下,你将访问一些由你当前正在运行的程序所拥有的内存(这可能会导致你的程序行为不端),或者这些内存不属于你正在运行的程序(这可能会导致你的程序崩溃像分段故障的东西)。 或者你可能试图写入你的程序拥有的内存,但是这标记为只读; 这可能也会导致你的程序崩溃。
这是假设你的程序运行在一个操作系统下,试图保护彼此同时运行的进程。 如果你的代码运行在“裸机”上,那么说明它是OS内核还是embedded式系统的一部分,那么就没有这种保护。 你的行为不当的代码是应该提供这种保护。 在这种情况下,损坏的可能性要大得多,包括在某些情况下对硬件(或附近的人或事)的物理损坏。
即使在受保护的操作系统环境中,保护并不总是100%。 例如,操作系统错误允许非特权程序获得根(pipe理)访问权限。 即使使用普通的用户权限,出现故障的程序也会占用过多的资源(CPU,内存,磁盘),有可能导致整个系统的运行。 许多恶意软件(病毒等)利用缓冲区溢出来获取对系统的未授权访问。
(一个历史的例子:我听说在一些有核心内存的旧系统上,反复地在一个紧密的循环中访问单个内存位置可能会导致大量的内存消失,其他的可能性包括破坏CRT显示器和移动读取/写驱动器的谐波频率的磁盘驱动器的头,导致它走过一个桌子,并落在地板上。)
而且总有Skynet担心。
底线是这样的:如果你可以写一个程序来故意做坏事,那至less在理论上可能是一个错误的程序可能意外地做同样的事情。
实际上,在MacOS X系统上运行的bug程序不太可能会比崩溃更严重。 但是不可能完全防止错误的代码做非常糟糕的事情。
一般来说,今天的操作系统(无论如何都是stream行的)使用虚拟内存pipe理器来运行受保护内存区域中的所有应用程序。 事实certificate,简单地读或写一个存在于已经分配/分配给你的进程的区域之外的REAL空间中的位置并不是非常简单的(比方说)。
直接回答:
1)读取几乎不会直接损害另一个进程,但是如果碰巧读取用于encryption,解密或validation程序/进程的KEY值,它可能会间接损害进程。 如果您正在根据您正在阅读的数据做出决定,那么阅读越界可能会对您的代码产生一定的不利影响
2)通过写入内存地址可访问的地址,可能真的损坏某些东西的唯一方法是,如果您正在写入的内存地址实际上是硬件寄存器(实际上不是用于数据存储,而是用于控制某个内存的地址的硬件)不是RAM的位置。 事实上,除非你正在编写一些不可重写(或某种性质)的一次性可编程位置,否则你通常不会损坏某些东西。
3)通常从debugging器中运行代码以debugging模式运行。 在debugging模式下运行时,TEND(但不是总是)当你做了一些事情被认为是练习或彻底违法的时候,会更快地停止你的代码。
4)不要使用macros,使用已经有内置数组索引边界检查的数据结构等。
其他我应该补充一点,上述信息实际上只适用于使用带有内存保护窗口的操作系统的系统。 如果要为embedded式系统编写代码,甚至使用不具有内存保护窗口(或虚拟寻址窗口)的操作系统(实时或其他)的系统,则应该在读写内存时小心谨慎。 同样在这些情况下,总是应该使用SAFE和SECURE编码实践来避免安全问题。
不检查范围可能导致丑陋的副作用,包括安全漏洞。 其中一个丑陋的是执行任意代码 。 在经典的例子中:如果你有一个固定大小的数组,并使用strcpy()
来放置一个用户提供的string,那么用户可以给你一个string来溢出缓冲区并覆盖其他的内存位置,包括CPU应该返回的地址当你的function完成。
这意味着你的用户可以向你发送一个string,这个string会导致你的程序本质上调用exec("/bin/sh")
,这将把它变成shell,执行你想要的任何东西,包括收集所有的数据,机器进入僵尸networking节点。
有关如何完成此操作的详细信息,请参阅“为乐趣和利润而粉碎堆栈” 。
你写:
我读了很多“什么都可能发生”,“细分可能是最不好的问题”,“你的硬盘可能会变成粉红色,独angular兽可能会在你的窗下唱歌”,这是很好的,但真正的危险是什么?
让我们这样说:加载一把枪。 把它指向窗外,没有特别的目的和火。 有什么危险?
问题是你不知道。 如果你的代码覆盖了你的程序崩溃的事情,那么你会很好,因为它会使它停止在一个已定义的状态。 但是,如果它不崩溃,那么问题就会出现。 哪些资源在您的程序的控制之下,它们对它们有何影响? 哪些资源可能会受到您的程序的控制,以及它们可能对他们有何影响? 我知道至less有一个主要问题是由这种溢出引起的。 问题出在一个看起来毫无意义的统计函数中,这个函数搞砸了生产数据库的一些不相关的转换表。 其后的结果是一些非常昂贵的清理。 事实上,如果这个问题已经格式化了硬盘,处理起来会更便宜,更容易…换句话说:粉红色的独angular兽可能是您最不容易的问题。
你的操作系统会保护你的想法是乐观的。 如果可能,尽量避免写出界限。
不以root或其他特权用户身份运行你的程序不会损害你的任何系统,所以通常这可能是一个好主意。
通过将数据写入一些随机内存位置,当每个进程运行在自己的内存空间时,不会直接“损害”计算机上运行的其他程序。
如果您尝试访问未分配给您的进程的任何内存,操作系统将停止您的程序执行分段错误。
因此,直接(无需以root身份运行并直接访问/ dev / mem之类的文件),程序不会干扰在您的操作系统上运行的任何其他程序。
尽pipe如此,也许这就是你所听到的有关危险的方法 – 盲目地随意地将随机数据写到随机存储器的位置,你肯定会损坏任何你能够损坏的东西。
例如你的程序可能要删除程序中某处存储的文件名给出的特定文件。 如果偶然地覆盖文件名的存储位置,你可能会删除一个非常不同的文件。
除了你自己的程序之外,我不认为你会破坏任何东西,在最坏的情况下,你将尝试从与内核没有分配给你的进程的页面对应的内存地址读取或写入,产生适当的exception并被杀害(我的意思是,你的过程)。
Objective-C中的NSArray
被分配了一个特定的内存块。 超出数组的范围意味着您将访问未分配给数组的内存。 意即:
- 这个内存可以有任何价值。 根据您的数据types,无法知道数据是否有效。
- 此内存可能包含敏感信息,如私钥或其他用户凭据。
- 内存地址可能无效或受保护。
- 内存可以有一个变化的值,因为它被另一个程序或线程访问。
- 其他东西使用内存地址空间,如内存映射端口。
- 将数据写入未知的内存地址可能会导致程序崩溃,覆盖OS内存空间,并且通常会导致太阳内爆。
从程序的angular度来看,你总是想知道你的代码何时超出了数组的范围。 这可能会导致返回未知的值,导致您的应用程序崩溃或提供无效的数据。
当你testing你的代码时,你可能想尝试使用Valgrind中的memcheck
工具 – 它不会捕获栈帧中的单个数组边界违例,但是它应该捕获许多其他种类的内存问题,包括会导致细微的,单一职能范围以外的更广泛的问题。
从手册:
Memcheck是一个内存错误检测器。 它可以检测到C和C ++程序中常见的以下问题。
- 访问内存你不应该,例如溢出和溢出堆块,溢出堆栈顶部,并释放内存后访问。
- 使用未定义的值,即未被初始化的值或从其他未定义的值派生的值。
- 释放堆内存错误,例如释放双重堆块,或者使用malloc / new / new []与free / delete / delete []不匹配
- 在memcpy和相关函数中重叠src和dst指针。
- 内存泄漏。
ETA:尽pipe如Kaz的回答所言,这并不是万能的,并不总是给出最有用的输出,特别是当你使用令人兴奋的访问模式时。
我正在使用一个DSP芯片的编译器,它故意生成的代码可以从C代码中访问一个数组的末尾,而不是!
这是因为循环的结构是这样的,迭代结束时会为下一次迭代预取一些数据。 所以在最后一次迭代结束时预取的数据永远不会被实际使用。
编写像这样的C代码调用未定义的行为,但这只是一个标准文档的forms,它涉及到最大的可移植性。
更常见的是,一个访问出界的程序并没有被巧妙地优化。 这只是越野车。 代码获取一些垃圾值,而不像上述编译器的优化循环,代码然后在随后的计算中使用该值,从而破坏了它们。
值得捕捉这样的错误,因此,即使仅仅是这个原因,也是值得使用的行为是未定义的:这样运行时可以产生一个诊断消息,如“main.c的第42行中的数组溢出”。
在具有虚拟内存的系统上,可能会恰好分配一个数组,使得后面的地址位于虚拟内存的未映射区域中。 访问将然后炸毁程序。
另外,请注意,在C中我们可以创build一个超过数组末尾的指针。 而且这个指针必须比任何指向数组内部的指针都要大。 这意味着一个C实现不能将一个数组放在内存的最后,其中一个加地址将环绕并看起来小于数组中的其他地址。
尽pipe如此,访问未初始化的值或超出边界值有时是一种有效的优化技术,即使不是最大限度的便携式。 这就是为什么当这些访问发生时Valgrind工具不会报告对未初始化数据的访问,而只是在稍后以某种方式使用该值时才会影响程序的结果。 你得到一个像“xxx条件分支:nnn取决于未初始化的值”的诊断,并且有时很难追踪它起源的地方。 如果所有这些访问都被立即捕获,那么编译器优化的代码会产生大量的误报以及正确的手工优化代码。
说到这一点,我正在和一个供应商的编解码器合作,这个供应商在移植到Linux并在Valgrind下运行时发布了这些错误。 但供应商确信,只有几位被使用的值实际上来自未初始化的内存,并且这些位被逻辑小心地避免了。只有值的好位被使用,Valgrind没有能力追踪到个人位。 未初始化的材料来自读取经过编码数据的比特stream的末尾的单词,但是代码知道stream中有多less比特,并且将不使用比实际更多的比特。 由于比特streamarrays末尾的访问不会对DSP体系结构造成任何伤害(arrays后没有虚拟内存,没有内存映射端口,地址也不会换行),这是一种有效的优化技术。
“未定义的行为”并没有什么意义,因为根据ISO C,只包括一个未在C标准中定义的头文件,或者调用一个未在程序本身或C标准中定义的函数,都是未定义的行为。 未定义的行为并不意味着“没有被地球上的任何人定义”,而是“没有被ISO C标准定义”。 但是,当然,有时候未定义的行为实际上是绝对没有被任何人定义的。
如果您曾经进行过系统级编程或embedded式系统编程,那么如果您写入随机内存位置,则会发生非常糟糕的情况。 旧系统和许多微控制器使用内存映射IO,因此写入映射到外设寄存器的内存位置会造成严重破坏,尤其是在asynchronous完成时。
一个例子是编程闪存。 存储器芯片上的编程模式通过将特定的值序列写入芯片地址范围内的特定位置来启用。 如果另一个进程在芯片上写入其他任何位置,则会导致编程周期失败。
在某些情况下,硬件会绕过地址(忽略最高有效位/字节的地址),所以写入超出物理地址空间末尾的地址实际上会导致数据正好写在中间。
最后,像MC68000这样的老式CPU可以locking到只有硬件复位才能让它们重新启动的地步。 几十年来一直没有对它们进行处理,但是我相信它是在处理exception时遇到总线错误(不存在的内存)的时候,它会暂停,直到硬件复位被声明为止。
我最大的build议是一个明显的产品插件,但我没有个人兴趣,我也不以任何方式隶属于他们,但基于几十年的C编程和embedded式系统的可靠性至关重要,Gimpel的PC Lint不仅会检测出这些错误,还会不断地在你身上引起一些坏习惯,从而成为更好的C / C ++程序员。
我也build议你阅读MISRA C编码标准,如果你可以从别人那里获取一份。 我还没有看到最近的一些,但是在那些日子里,他们给出了一个很好的解释,说明为什么你应该/不应该去做这些事情。
不知道关于你,但是关于第二次或第三次,我从任何应用程序得到一个coredump或hangup,我对任何公司生产的意见下降了一半。 第四或第五次,无论包装是什么变成了货架,我通过包装/光盘的中心驱动一个木桩,只是为了确保它永远不会回来困扰我。