超出范围访问数组有多危险?


221

在数组边界之外(在C中)访问数组有多危险?有时可能会发生以下情况:我从数组外部进行了读取(现在我了解到然后我会访问程序其他部分甚至更远的部分使用的内存),或者试图将值设置为数组外部的索引。该程序有时会崩溃,但有时只能运行,只会产生意外的结果。

现在我想知道的是,这到底有多危险?如果它破坏了我的程序,那还不错。另一方面,如果它由于我设法以某种方式设法访问了一些完全不相关的内存而破坏了程序外部的某些内容,那么我想这是非常糟糕的。我读到很多“可能发生的事情”,“分割可能是最不严重的问题”,“您的硬盘可能变粉并且独角兽可能正在您的窗户下面唱歌”这一切都很好,但是真正的危险是什么?

我的问题:

  1. 从程序外部读取值是否会破坏程序以外的任何内容?我可以想象只是看东西不会改变任何东西,还是会例如改变我碰巧到达的文件的“上次打开时间”属性?
  2. 设置值超出数组范围是否会损坏程序以外的任何内容?从这个 堆栈溢出问题可以看出,可以访问任何内存位置,并且没有安全保证。
  3. 现在,我从XCode中运行我的小程序。这样可以在程序无法到达其自身内存之外的程序周围提供一些额外的保护吗?会伤害XCode吗?
  4. 关于如何安全运行我的固有错误代码的任何建议?

我使用OSX 10.7,Xcode 4.6。


通常,操作系统会保护自己和其他进程免受恶意行为的侵害。不过,这并不是您一定要严重依赖的东西。
Hot Licks

7
同样,当访问和数组索引超出范围时(在您的内存中),您将永远不会“碰到”硬盘上的文件。
DrummerB

1
我相信你在问C数组,对不对?因此,这与ObjC无关,并且与任何IDE都不相关。
布赖恩·陈

17
这是我最喜欢的奇怪结果示例(它处理堆栈,但是我发现它确实很有启发性...)。
phipsgabler

Answers:


125

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

使用非便携式或错误程序构造或错误数据时的行为,对此国际标准不施加任何要求

一个非规范性注释扩展了这一点:

可能的不确定行为范围从完全忽略具有不可预知结果的情况到在翻译或程序执行过程中以环境特征记录的方式表现(带有或不带有诊断消息)到终止翻译或执行(带有发布)诊断消息)。

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

在“最佳”情况下,您将访问一些内存,这些内存要么是当前正在运行的程序所拥有的(这可能导致您的程序行为不当),要么不是当前正在运行的程序所拥有的(这可能会导致您的程序崩溃,例如细分错误)。或者,您可以尝试写入程序拥有的内存,但是将其标记为只读;这可能还会导致您的程序崩溃。

假设您的程序在一个试图保护同时运行的进程之间不相互影响的操作系统下运行。如果您的代码在“裸机”上运行,比如说它是OS内核或嵌入式系统的一部分,则没有这种保护;您行为不当的代码就是应该提供这种保护的内容。在这种情况下,损坏的可能性会更大,包括在某些情况下对硬件(或附近的东西或人)的物理损坏。

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

(一个历史示例:我听说过,在一些带有核心内存的旧系统中,在紧密循环中重复访问单个内存位置实际上可能导致那部分内存融化。其他可能性包括破坏CRT显示器和移动读取的内容。 /写入磁盘驱动器磁头,使其具有驱动器柜的谐波频率,从而使其跨过桌子滑落到地板上。)

而且始终有天网需要担心。

底线是:如果您可以编写一个程序来故意做一些不好的事情,那么从理论上讲,一个有缺陷的程序至少有可能偶然地做同样的事情。

在实践中,这是非常不可能的,你与MacOS X系统上运行错误的程序会做什么比暴跌更加严重。但是,不可能完全防止错误的代码做真正的坏事。


1
谢谢,我实际上完全理解这一点。但是,这立即引发了一个后续问题:新手程序员可以做什么,以保护他/她的计算机免受自己可能可怕的创作的侵害?在对程序进行彻底测试之后,我可以将其发布到全世界。但是第一次尝试运行肯定是一个错误的程序。你们如何使您的系统免受安全威胁?
ChrisD

6
@ChrisD:我们往往很幸运。8-)}认真的说,这些天操作系统级别的保护非常好。最坏的情况是,如果我写了一个意外的叉子炸弹,我可能必须重新启动才能恢复。但是,只要您的程序不试图在危险的边缘上做某事,对系统的实际损坏就不必担心。如果您真的很担心,那么在虚拟机上运行程序可能不是一个坏主意。
基思·汤普森

1
另一方面,我已经看到许多奇怪的事情在我使用的计算机上发生(损坏的文件,无法恢复的系统错误等),而且我不知道其中有多少可能是由于某些C程序的展示引起的可怕的不确定行为。(到目前为止,没有真正的恶魔从我的鼻子里飞出来。)
Keith Thompson

1
感谢您教我叉炸弹-尝试掌握递归时,我所做的工作很接近:)
ChrisD

2
scienceamerican.com/article/…因此,现代电子设备仍可能引起火灾。
Mooing Duck

25

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

直接答案:

1)读取几乎永远不会直接破坏另一个进程,但是,如果您碰巧读取用于加密,解密或验证程序/进程的KEY值,则读取可能间接破坏一个进程。如果您根据所读取的数据进行决策,则超出范围的读取可能会对您的代码产生一些不利/意外的影响

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

3)通常从调试器内部运行会在调试模式下运行代码。当您执行了一些被认为不可行或完全违法的操作时,以调试模式运行确实会使TEND(但并非总是)更快地停止代码。

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

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


4
始终采用安全的编码惯例。
Nik Bougalis

3
我建议不要对错误的代码使用try / catch,除非您捕获到非常具体的异常并知道如何从异常中恢复。Catch(...)是您可以添加到错误代码中的最糟糕的事情。
尤金

1
@NikBougalis-我完全同意,但是如果操作系统不包括内存保护/虚拟地址空间,或者缺少操作系统,则更为重要:-)
trumpetlicks

@Eugene-我从未注意到这对我来说是个问题,但我同意你的看法,我是否已将其删除:-)
trumpetlicks

1)您的意思是损害,因为我会透露本应保密的东西?2)我不确定我明白你的意思,但是我想我只是在尝试访问数组边界之外的位置而访问RAM?
ChrisD

9

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

这意味着您的用户可以向您发送一个字符串,该字符串将导致您的程序从根本上进行调用exec("/bin/sh"),这将使它变成外壳程序,并执行他在系统上执行的所有操作,包括收获所有数据并将您的计算机变成僵尸网络节点。

有关如何完成此操作的详细信息,请参见粉碎堆栈以获取乐趣和利润


我知道我不应该超出范围访问数组元素,这要感谢您的支持。但是问题是,除了对程序造成各种损害之外,我是否还会无意中超出程序的存储范围?我的意思是在OSX上。
克里斯迪(ChrisD)2013年

@ChrisD:OS X是现代操作系统,因此它将为您提供完整的内存保护。例如,您不应仅限于允许程序执行的操作。这不应该包括与其他进程的混乱(除非您以root特权运行)。
2013年

我宁愿说环0特权,而不是root特权。
罗斯兰

更有趣的是超现代的编译器可以决定,如果代码试图读foo[0]通过foo[len-1]之前曾使用的检查后,len对数组的长度既可以执行或跳过一段代码,编译器应该随意,甚至无条件地运行其他代码如果应用程序拥有数组之后的存储,并且读取该存储的效果将是良性的,但是调用其他代码的效果将不会如此。
超级猫

8

你写:

我读到很多“可能发生的事情”,“分割可能是最不严重的问题”,“您的硬盘可能变粉红色并且独角兽可能正在您的窗户下面唱歌”这一切都很好,但是真正的危险是什么?

这么说吧:装枪。将其指向窗户外,没有任何特定目标和火力。有什么危险?

问题是您不知道。如果您的代码覆盖了使您的程序崩溃的内容,那么您会好的,因为它将使程序停止进入已定义的状态。但是,如果它没有崩溃,则问题开始出现。哪些资源受程序控制,它可能对它们做什么?哪些资源可能会受到您程序的控制,它可能会对它们做什么?我知道至少有一个主要问题是由这种溢出引起的。问题出在一个看似毫无意义的统计功能中,该功能弄乱了生产数据库的一些无关表。结果是随后进行了一些非常昂贵的清理。实际上,如果此问题格式化了硬盘,它本来会便宜得多且容易处理……换句话说:粉红色的独角兽可能是您遇到的最少问题。

操作系统将保护您的想法是乐观的。如果可能,请尝试避免超出范围。


好的,这正是我所担心的。我将“尽力避免超出范围”,但是,鉴于最近几个月来我一直在做的事情,我肯定会继续做很多事情。如果没有安全的练习方法,你们是如何在编程方面如此出色的?
ChrisD

3
谁说过任何事都是安全的;)
Udo Klein

7

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

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

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

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

但是,通过危险将随机数据盲目地写入随机存储位置,您肯定会损坏您可能损坏的任何东西,这很可能就是您所听说的危险。

例如,您的程序可能想要删除由存储在程序中某个位置的文件名指定的特定文件。如果您偶然覆盖了文件名的存储位置,则可能会删除一个完全不同的文件。


1
如果您正在以root身份运行(或一些其他特权用户),不过,当心了。缓冲区和阵列溢出是常见的恶意软件利用。
约翰·博德

实际上,我用于所有日常计算的帐户不是管理员帐户(我使用OSX术语,因为这是我的系统)。您是要告诉我,尝试设置任何内存位置都无法破坏某些内容吗?这实际上是个好消息!
ChrisD

如前所述,您可能偶然造成的最严重伤害就是您作为用户可能造成的最严重伤害。如果您想100%确保不破坏您的任何数据,则可能要向计算机添加其他帐户并进行试验。
mikyra

1
@mikyra:只有系统的保护机制是100%有效的,这才是正确的。恶意软件的存在表明您不能始终依靠它。(我不想暗示这一定值得担心;程序有可能(但不太可能)意外地利用恶意软件利用的相同安全漏洞。)
Keith Thompson

1
此处的列表包括:从不受信任的来源运行代码。只需单击防火墙的任何弹出窗口上的“确定”按钮,甚至都没有阅读它的内容,或者在无法建立所需的网络连接时完全将其关闭。使用可疑来源中最新的黑客补丁二进制文件。如果拥有者自愿邀请任何一个带双臂并开着坚固的坚固防盗门的窃贼,这不是金库的错。
mikyra

4

NSArray在Objective-C中为分配了特定的内存块。超出数组的边界意味着您将访问未分配给数组的内存。这表示:

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

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


NSArrays超出范围。这个问题似乎与C数组有关。
DrummerB

我确实是说C数组。我知道这里有NSArray,但是现在我的大部分练习都在C中进行
ChrisD 2013年

4

您可能需要memcheck在测试代码时尝试在Valgrind中使用该工具-它不会捕获堆栈框架内单个数组范围冲突,但它可能会遇到许多其他类型的内存问题,包括会引起细微,更宽泛的内存问题单一功能范围以外的问题。

从手册中:

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

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

ETA:虽然,正如Kaz的回答所言,它不是万能药,也不总是提供最有用的输出,尤其是当您使用令人兴奋的访问模式时。


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

3

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

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

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

最后,像MC68000这样的旧CPU可以锁定到只有硬件复位才能使它们重新运行的程度。几十年来他们都没有为此工作,但我相信这是在尝试处理异常时遇到总线错误(不存在的内存)的时候,它只会暂停,直到断言硬件复位为止。

我最大的建议是为产品提供公然的插头,但我对此没有任何个人兴趣,并且我与它没有任何关系-但基于数十年的C编程和对可靠性至关重要的嵌入式系统,Gimpel的PC Lint不仅可以检测到这类错误,而且可以通过不断养成不良习惯来使您成为更好的C / C ++程序员。

如果您可以从某人那里窃取副本,我还建议您阅读MISRA C编码标准。我最近没有看过任何东西,但是在过去,他们很好地解释了为什么/不应该做他们报道的事情。

Dunno关于您,但是大约在第二或第三次我从任何应用程序中得到核心转储或挂断时,我对生产该产品的任何公司的看法下降了一半。第4次或第5次,无论包装是什么,都变成了架子餐具,我开车穿过包装/光盘的中心插入一个木桩,目的是确保它永远不会再困扰我。


视系统而定,超出范围的读取也可能会触发不可预测的行为,或者可能是良性的,尽管超出范围的负载的良性硬件行为并不意味着良性的编译器行为。
超级猫

2

我正在与用于DSP芯片的编译器一起工作,该编译器故意从C代码中生成可访问数组末尾的代码,而C代码则不会!

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

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

并非经常如此,无法巧妙地优化访问范围的程序。这简直是​​越野车。该代码获取一些垃圾值,并且与上述编译器的优化循环不同,该代码随后在后续计算中使用该值,从而破坏了主题。

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

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

顺便说一句,请注意,在C语言中,我们允许创建一个比数组末尾更远的​​指针。而且此指针必须比任何指向数组内部的指针都更大。这意味着C实现无法将数组直接放在内存的末尾,在内存末尾,一个加号地址将环绕并且看起来比数组中的其他地址小。

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

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

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


此外,只要存在至少一个程序,即使该程序名义上对标准中规定的所有实施限制进行了征税,该程序也能正确处理,该实施程序在馈入不受约束约束的任何其他程序时仍可以任意运行,并且仍然“合规”。因此,有99.999%的C程序(平台的“一个程序”以外的任何程序)都依赖于标准没有要求的行为。
超级猫

1

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


3
..什么?如何在您自己的进程中覆盖内存,该进程用于存储稍后使用的某些变量……现在它神秘地更改了它的值!我向您保证,这些错误很有趣,值得追踪。节段将是最好的结果。-1
Ed S.

2
我的意思是,除了他自己的程序外,他不会“破坏”其他进程;)
jbgs 2013年

我确实不在乎是否破坏自己的程序。我只是在学习,如果我访问数组范围之外的任何内容,该程序显然是错误的。我只是越来越担心在调试我的作品时破坏其他东西的风险
ChrisD

问题是:如果我尝试访问未分配给我的内存,可以确定我的进程将被杀死吗?(在OSX上发布)
ChrisD

3
多年前,我曾经是一个笨拙的C程序员。我无数次访问了数组。除了我的进程被操作系统杀死之外,什么都没有发生。
jbgs
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.