当我查看过去几年与实现相关的漏洞时,我发现它们中有很多来自C或C ++,并且其中很多是溢出攻击。


Heartbleed是OpenSSL中的缓冲区溢出;
最近,发现glibc中的一个错误在DNS解析过程中允许缓冲区溢出;

这就是我现在可以考虑的问题,但我对此表示怀疑这些是A)唯一的用C或C ++编写的软件,而B)则是基于缓冲区溢出。

特别是关于glibc错误,我读到一条评论,指出如果发生在JavaScript而不是C中,不会有问题。即使代码只是编译为Javascript,也不是问题。

为什么C和C ++如此容易受到溢出攻击?

评论

强大的力量伴随着巨大的责任

这个答案和这个答案可能是有趣的读物。它基本上取决于语言的设计以及实现的级别。

@RoraZ有一些将C编译为javascript的工具,例如emscripten。 dankaminsky.com/2016/02/20/skeleton,我指的是底部附近。

您的问题有点像“为什么只有Windows计算机会感染Windows病毒?”。因为Windows病毒仅在Windows计算机上才可能。 C和C ++可以通过执行未经检查的指针算术来获得缓冲区溢出漏洞。其他大多数语言都没有此功能,因此不会有缓冲区溢出。您的问题还没有考虑这些语言的流行性。 (也许其他语言有更多问题,但使用不多,因此它们的总漏洞较少。)

评论不作进一步讨论;此对话已移至聊天。

#1 楼

与大多数其他语言相反,C和C ++传统上不检查溢出。如果源代码说要在85字节的缓冲区中放入120字节,CPU会很乐意这样做。这与以下事实有关:尽管C和C ++具有数组概念,但该概念仅在编译时。在执行时,只有指针,因此没有运行时方法来检查关于该数组概念长度的数组访问。

相反,大多数其他语言都具有数组概念它可以在运行时保留下来,因此运行时系统可以系统地检查所有数组访问。这不能消除溢出:如果源代码在将120个字节写入长度为85的数组中时要求无意义的内容,则仍然没有意义。但是,这会自动触发内部错误条件(通常是“异常”,例如Java中的ArrayIndexOutOfBoundException),该条件会中断正常执行并且不让代码继续执行。这会中断执行,并通常意味着停止整个处理(线程死亡),但通常可以防止超出简单的拒绝服务的利用。

基本上,缓冲区溢出利用要求代码执行以下操作:使溢出(读取或写入访问的缓冲区的边界之外)并继续执行超出该溢出的范围。与C和C ++(以及其他一些语言,例如Forth或Assembly)相反,大多数现代语言都不允许真正的溢出发生,而要冒犯者。从安全角度来看,这要好得多。

评论


“从安全角度来看,这要好得多。”尽管这确实是事实,但它也使某些类型的编程(尤其是操作系统编程)变得更加困难。请记住,C的传统可以追溯到一种旨在以可移植方式实现Unix的编程语言。出于充分的原因,C有时被称为便携式汇编程序。

–用户
16-2-23在15:32



评论不作进一步讨论;此对话已移至聊天。

–Rory Alsop♦
16-2-27在15:11

@MichaelKjörling是的,但是话又说回来,有很多Microsoft Research OS都基于完全托管(并且以这种方式,完全安全)的代码(包括静态验证)构建。微软花了很多钱来系统地解决这个问题,而不是等待人们明智地解决。与往常一样:D性能总是很棘手,但是再说一次,与托管程序相比,托管和可反射代码为您提供了很多优化的机会-对于许多服务器软件,由于以下原因,它们甚至还获得了可观的性能提升:那。

–罗安
16-2-28在11:29

@Luaan我怀疑主要区别是从“汇编”变为“托管代码”。如果有的话,似乎更有可能是由于从非JIT代码转换为JIT代码。通过优化编译时间,您必须选择一个愿意支持的最低基准。使用JITed代码,您可以针对正在运行的特定计算机进行优化。原则上,您可能可以用C编写JIT代码;不过,我不确定是否有人尝试过...

–用户
16-2-28在16:40



@MichaelKjörling实际上,其中许多不是JIT编译的。不过,它们仍针对特定的硬件配置进行编译。但是,要想使JITted编译有效,就需要大量的额外信息和很多限制-C太自由了,以至于即使从源代码开始,也可以进行很多有意义的优化,而编译后的代码要少得多。即使像边界检查一样简单,C编译器也无法为您进行边界检查,因为就编译器所知,您只是在操纵一些随机指针。安全地省略它们也是如此。

–罗安
16年2月28日在18:09

#2 楼

请注意,涉及一些循环推理:安全问题经常与C和C ++关联。但是其中有多少是由于这些语言的固有弱点,又有多少是由于这些语言只是大多数计算机基础结构所使用的语言?


成为“从汇编程序迈出的第一步”。除了您自己实现的功能之外,没有其他限制可用于将系统的最后一个时钟周期挤出系统。

C ++确实对C进行了各种改进,与安全性最相关的是其容器类(例如,容器类)。 <vector><string>)以及自C ++ 11起的智能指针,它们使您可以处理数据,而不必手动处理内存。但是,由于是C语言的演进而不是全新的语言,它仍然提供C语言的手动内存管理机制,因此,如果您坚持不懈地学习,C ++并不会阻止您。 />

那为什么还用这些语言编写SSL,绑定或OS内核之类的东西呢?

因为这些语言可以直接修改内存,这使得它们特别适合某种类型的高性能,低级应用程序(例如,加密,DNS表查找,硬件驱动程序...或Java VM ;-))。

相关软件遭到破坏,用C或C ++编写的可能性很高,这仅仅是因为大多数与安全相关的软件都是用C或C ++编写的,通常是出于历史和/或性能方面的原因。在C / C ++中,主要的攻击媒介是缓冲区溢出。

如果是不同的语言,它将是不同的攻击媒介,但是我相信也会存在安全漏洞。


开发C / C ++软件比开发Java软件要容易得多。利用Windows系统比利用Linux系统更容易:前者无处不在,广为人知(即众所周知的攻击媒介,如何查找和利用它们),并且许多人正在寻找漏洞奖励/努力比率高的地方。

并不意味着后者天生就安全(更安全,也许但不安全)。这意味着-作为较难获得较低收益的目标-坏男孩并没有浪费太多时间。

#3 楼

实际上,“令人沮丧”实际上并不是缓冲区溢出。为了使事情“更有效率”,他们将许多较小的缓冲区放入一个大缓冲区中。大缓冲区包含来自各种客户端的数据。该错误读取了本不应该读取的字节,但实际上并没有读取该大缓冲区之外的数据。一种检查缓冲区溢出的语言并不能阻止这种情况,因为有人不愿意或阻止任何此类检查发现问题。

评论


IIRC,BSD内存分配本可以防止该错误被忽视,但是实现者由于认为它“太慢”而积极地规避了该系统。在某种程度上,这就是C / C ++的全部选择,只是这一次的决定确实很糟糕。 ;-)

–DevSolar
16年2月23日在16:43

确实。如果您使用C#进行此操作,则可以轻松引入等效攻击。

–约书亚
16年2月24日在19:11

他们在做出此设计决定之前是否已对代码进行概要分析?我发现很难相信他们在malloc(3)上遇到瓶颈。

–凯文
16 Feb 25'2:58

@Kevin内存分配是相对较慢的操作,特别是与一次分配缓冲区并重新使用它相比。如果您正在编写快速代码(并且人们经常抱怨Web服务器的内容必须很快),那么可以,在消除所有其他瓶颈之后,很容易成为瓶颈!如果您分配了许多小缓冲区,这是非常正确的。

– gbjbaanb
16-2-25在14:20

@Kevin:使用malloc()响应从不受信任来源收到的数据将使代码易于受到攻击者的攻击,该攻击者触发分配/释放模式会导致碎片。使用和回收内存池的代码可以通过使用malloc()的代码无法防范的方式来防止此类问题。

–超级猫
16年2月25日在16:36

#4 楼

首先,正如其他人所提到的那样,C / C ++有时被描述为美化的宏汇编器:它被称为“贴近铁”,作为系统级编程的语言。

例如,该语言允许我将零长度的数组声明为占位符,而实际上它可能表示数据包中的可变长度部分或用于与之通信的内存中可变长度区域的开始

不幸的是,这也意味着C / C ++在不当之手很危险。如果程序员声明了一个由10个元素组成的数组,然后写入元素101,则编译器将愉快地对其进行编译,代码将愉快地执行,从而破坏了该内存位置处的所有内容(代码,数据,堆栈等)。 br />
其次,C / C ++是特质的。一个很好的例子是字符串,它基本上是字符数组。但是每个字符串常量都带有一个额外的,不可见的终止符。这是造成无数错误的原因,因为(特别是但非排他性的)新手程序员经常无法分配终止null所需的额外字节。

第三,C / C ++实际上已经很老了。该语言是在基本上不存在对软件系统的外部攻击的时候出现的。人们希望用户信任并合作,而不是敌对,因为他们的目标是使程序正常运行,而不是使其崩溃。

这就是为什么标准C / C ++库包含许多本质上不安全的功能的原因。以strcpy()为例。它将很高兴地复制任何内容,直到终止为空字符为止。如果找不到终止的空字符,它将继续复制,直到死机,甚至更有可能,直到它覆盖了至关重要的内容并且程序崩溃为止。在过去的好日子里,这不是问题,当时,不希望用户进入一个保留用于邮政编码的字段,例如16000个垃圾字符,然后是要执行的一组特殊构造的字节在堆栈被废弃并处理器在错误的地址处恢复执行之后。

可以肯定的是,C / C ++并不是唯一的特殊语言。其他系统具有不同的特质行为,但可能同样糟糕。以PHP之类的后端编程语言为例,编写允许进行SQL注入的代码是多么容易。

最后,如果我们为程序员提供了完成工作所需的强大工具,但是没有足够的培训和对安全环境的意识,无论使用哪种编程语言,都会发生坏事。

评论


需要有效编程的强大工具。通常,不需要直接内存访问。几乎可以看到其他任何高级语言。

–user253751
16 Feb 24'2:33

“最后,如果我们为程序员提供完成工作所需的强大工具,但是如果没有足够的培训和对安全环境的意识,则无论使用哪种编程语言,都会发生Bad Things。”使用任何编程语言都可能发生不好的事情。但是,由于您对C&C ++的描述之类的原因,当使用某些与其他相比时,它们也倾向于(倾向于)更容易,更频繁地发生。

–大多数情况下
16-2-24在5:34



更糟糕的是,C标准实际上并未让程序员“接近金属”编写代码。如果编译器可以确定某种输入组合会导致标准不施加任何要求的情况,则即使对于未定义行为几乎没有其他可能的结果,标准也可能会省略本来会处理此类输入的代码。根据对可能的输入的推论,可能会与遗漏代码一样糟糕。

–超级猫
16年2月24日在9:06

没有“ C / C ++”之类的东西。您在这里谈论的大部分都是针对C的。

–leftaround关于
16-2-24在12:49



@MSalters它并不特定于C,因为它也适用于C ++。我无法理解您如何认为C ++代码不会有缓冲区溢出。甚至std :: vector :: operator []也没有边界检查。

–user253751
16-2-25在23:44



#5 楼

我可能会谈谈某些其他答案已经说明的问题。..但是,我发现问题本身是错误的并且是“脆弱的”。

被问到,这个问题在很大程度上是假设的不了解基本问题。 C / C ++不比其他语言“更容易受到攻击”。相反,它们将计算设备的大部分功能以及使用该功能的责任直接交给程序员。因此,实际情况是,许多程序员编写的代码容易受到利用,并且由于C / C ++不能像某些语言一样竭尽全力保护程序员免受自身攻击,因此他们的代码更容易受到攻击。这不是C / C ++问题,例如,用汇编语言编写的程序也会遇到同样的问题。

之所以如此低级的编程如此容易受到攻击,是因为像数组这样的事情/ buffer边界检查可能会在计算上变得昂贵,并且在进行防御性编程时通常是不必要的。例如,想象一下,您正在为一些主要的搜索引擎编写代码,该引擎必须在眨眼之间处理数万亿条数据库记录,因此最终用户在“页面加载...”时不会感到无聊或沮丧。被陈列。您不希望代码在循环中每次都检查数组/缓冲区边界。虽然进行这种检查可能需要几纳秒的时间,但是如果仅处理十条记录,这是微不足道的,但是当您循环浏览数十亿或数万亿条记录时,这可能会花费多达几秒钟或几分钟的时间。

因此,您可以“信任”数据源(例如,扫描网站并将数据放入数据库的“ Web bot”)已经检查了数据。这不应该是不合理的假设。对于典型的程序,您想在输入时检查数据,因此处理数据的代码可以最大速度运行。许多代码库也采用这种方法。甚至有一些文档,他们希望程序员在调用库函数对数据进行操作之前已经检查过数据。

但是,不幸的是,许多程序员并没有进行防御性的编程,只是假设数据是正确的必须有效且在安全范围/参数之内。这就是攻击者利用的东西。

某些编程语言经过精心设计,旨在通过自动将其他检查插入到生成的程序中来尝试保护程序员免受不良编程习惯的侵害。没有明确地写入他们的代码。同样,当您仅循环遍历几百次或更短的代码时,这很好。但是,当您进行数十亿或数万亿次迭代时,它会加长数据处理的长时间延迟,这可能变得无法接受。因此,在选择用于特定代码段的语言以及检查数据中潜在的危险/可利用条件的频率和位置时,这是一个折衷方案。

评论


tl; dr:可能不必要的安全检查和速度之间需要权衡。

–通配符
16-2-27在0:15

“但是,当您进行数十亿或数万亿次迭代时,它加起来就是”-当您遍历数组时,它在循环之前总计起来只有一次检查,因为现代编译器非常聪明。您唯一需要支付边界检查的时间是编译器是否无法确定它是否安全,这通常意味着这是随机访问。在这种情况下,您要多支付大约1个周期,这在某些情况下是可以累加的(例如矩阵运算),但是对于所有代码的99.9%而言,这是完全可以忽略的。

– Voo
16-2-27在16:15



不一定是真的。是的,现代编译器确实非常聪明,可以优化很多代码。但是它仍然只是一个计算机程序,而不是一个聪明的人,它可以查看您的代码并完全确定地知道程序员打算做什么。在某些情况下,编译器无法进行“完美的选择”优化,而退回到更安全的优化(对于某些目的而言可能太慢了),程序员将其关闭。人们倾向于依靠“智能编译器”来为他们工作的原因是这种问题持续存在的部分原因。

– C. M.
16 Mar 2 '16 at 0:46

除此之外,还有几个假设。首先是“ 99.9%”-这个数字来自哪里?在我看来,听起来“ 80%的统计数据都是在现场完成的”。其次,要处理的数据是一个整洁的数组..并非总是如此。确实,使数据符合“安全”操作的概念是程序员试图避免的,计算量大的数据操作的一部分,并且仅假设数据是“安全的”,或者假定编译器将“对其进行修复,以使其符合要求”。是。”等等。

– C. M.
16 Mar 2 '16 at 0:50

#6 楼

基本上,程序员是懒惰的人(包括我自己)。他们做类似使用gets()而不是fgets()的事情,并在堆栈上定义I / o缓冲区,并且没有寻找足够的内存来无意地覆盖内存(对于程序员,对于黑客,这是无意的:)。 />

评论


很难想象程序员会出于懒惰而使用C / C ++!

–德米特里·格里戈里耶夫(Dmitry Grigoryev)
16-2-24在10:48

@DmitryGrigoryev他们让我在学校学习C / C ++,但我懒于学习其他语言:)

–乔
16-2-24在11:38

@JOW以我的经验,用C#编写一个相当小的前端(几个按钮,HTTP请求,XML解析,文件IO),并且对该语言的先验知识为零,这仍然比我期望的用C ++ / MFC编写同一个应用程序来编码要快。是。

–德米特里·格里戈里耶夫(Dmitry Grigoryev)
16年2月24日在11:45

@BingBang读完后我感到不安。

– Gusdor
16-2-26在9:36

@Gusdor没有胆量,没有荣耀,伙计。

–砰砰
16-2-27在10:35

#7 楼

现有大量的C代码会未经检查地写入缓冲区。其中一些在库中。如果任何外部状态都可以更改写入的长度,则此代码是不安全的,否则是非常不安全的。

有大量现有的C代码确实限制了对缓冲区的写入。如果上述代码的用户发生了数学错误,并且编写了超出其预期范围的文字,则该漏洞与上述漏洞一样被利用。没有编译时的保证可以正确完成数学运算。

还有大量现有的C代码会根据内存中的偏移量进行读取。如果未检查偏移量是否有效,则可能会泄漏信息。

C ++代码通常用作与C互操作的高级语言,因此会遵循许多C构想,并且与C进行通信时会产生错误C API是常见的。

存在防止此类溢出的C ++编程样式,但只发生一个错误就可以使它们发生。

另外,指针悬空的问题,其中的内存资源被回收,并且指针现在指向的内存/生命周期与最初的内存/结构不同,从而允许某些类型的利用和信息泄漏。

这类错误-“围栏”错误,“悬空指针”错误-如此常见,而且很难完全消除,以至于许多语言都是使用专门设计来防止它们发生的系统开发的。

毫不奇怪,在旨在消除这些错误,这些错误几乎不会经常发生。它们有时仍会发生:运行该语言的引擎有问题,或者设置了与C / C ++案例的环境相匹配的手动情况(重用池中的对象,使用由使用者细分的公共大缓冲区等)。 )。但是由于这些用途很少见,因此问题发生的频率降低了。

C / C ++中的每个动态分配,每个缓冲区使用都存在这些风险。完美是无法实现的。

#8 楼

最常用的语言(例如Java和Ruby)编译为在VM中运行的代码。 VM旨在隔离机器代码,数据和通常的堆栈。这意味着常规语言操作无法更改代码或重定向控制流(有时有特殊的API可以执行此操作,例如用于调试)。

C和C ++通常直接编译为CPU的本机语言-这会带来性能和灵活性方面的好处,但是这意味着错误的代码可能会覆盖程序存储器或堆栈,从而执行不在原始程序中的指令。

这通常发生在C ++中(可能是故意)缓冲区溢出时。相比之下,在Java或Ruby中,缓冲区溢出将立即导致异常,并且(VM错误除外)不能覆盖代码或更改控制流。

评论


这与是否在VM上运行无关。您可以使VM具有与c相同的行为,就像您可以使程序直接编译为机器语言一样安全,就像说Java(例如ADA)一样

– Voo
16-02-29在13:19



理论上。在几乎所有实际情况下,Java都在可防止代码覆盖的VM上运行,而C / C ++则在不能这样做的裸机上运行。

–丰富
16-3-2在3:58

是。 ADA不能在VM上运行,并且可以像Java一样阻止大多数此类攻击。是否拥有VM完全与此无关(您认为VM的其他特色是什么,否则它是无法做到的?该地狱实际上仅引入了可能的安全漏洞,因为JIT需要可写和可执行的内存!)

– Voo
16 Mar 2 '16 at 7:30