为什么这么难使C不那么容易发生缓冲区溢出?


23

我正在大学上一门课程,其中一个实验室是对它们提供给我们的代码执行缓冲区溢出攻击。范围从简单的漏洞利用到改变堆栈上某个函数的返回地址以返回到另一个函数,一直到更改程序寄存器/内存状态然后返回到您调用的函数的代码,这意味着您调用的函数完全不会利用该漏洞。

我对此进行了一些研究,并且即使在现在,此类漏洞利用仍在很多地方使用,例如在Wii上运行自制程序以及iOS 4.3.1不受限制的越狱行为。

我的问题是为什么这个问题很难解决?显然,这是用于黑客攻击数百件事情的主要漏洞,但似乎很容易解决,只需截断超出允许长度的任何输入,然后对所有输入进行清理即可。

编辑:我想从另一个角度考虑问题-为什么C的创建者不通过重新实现库来解决这些问题?

Answers:


35

他们确实修复了库。

任何现代C标准库中包含的更安全的变体strcpystrcatsprintf,等等。

在C99系统上(大多数是Unix),您会发现它们的名称类似于strncatsnprintf,“ n”表示它采用的参数是缓冲区的大小或要复制的最大元素数。

这些功能可以用来更安全地处理许多操作,但是回想起来,它们的可用性并不高。例如,某些snprintf实现不保证缓冲区为空终止。strncat需要复制许多元素,但是许多人错误地传递了dest缓冲区的大小。

在Windows中,人们常常发现strcat_ssprintf_s中,“_s”后缀表示“安全”。它们也已进入C11中的C标准库,并提供了对发生溢出事件(例如截断与断言)的更多控制。

许多供应商提供了更多的非标准替代方案,例如asprintfGNU libc中的替代方案,它将自动分配适当大小的缓冲区。

您可以“仅修复C”的想法是一种误解。修复C不是问题-已经完成。问题在于修复数十年无知,疲倦或急忙的程序员编写的C代码,或者修复从安全无关紧要的上下文移植到安全重要的上下文的代码。尽管迁移到较新的编译器和标准库通常可以帮助自动识别问题,但是对标准库的任何更改都不能修复此代码。


11
+1是针对程序员的问题,而不是针对语言的问题。
Nicol Bolas'2

8
@Nicol:说“问题是程序员”是不公平的简化主义者。问题在于,多年来(数十年)C使编写不安全代码比编写安全代码更容易,尤其是因为我们对“安全”的定义的发展比任何语言标准都快,而且这种代码仍然存在。如果要尝试将其简化为单个名词,则问题是“ 1970-1999 libc”,而不是“程序员”。

1
程序员仍然有责任使用他们现在拥有的工具来解决这些问题。花费半天左右的时间,对这些事情在源代码中进行一些重复。
Nicol Bolas 2012年

1
@Nicol:尽管检测潜在的缓冲区溢出很简单,但通常很难确定这是一个真正的威胁,要弄清缓冲区是否溢出应该怎么办就不那么容易了。通常不考虑错误处理,因为您可以以意想不到的方式更改模块的行为,因此不可能“快速”实现改进。我们刚刚在数百万行的旧版代码库中完成了此操作,尽管花点时间进行练习会花费大量时间(和金钱)。
mattnz'2

4
@NicolBolas:不确定在哪家商店工作,但是我最后写C的用于生产用途的地方需要修改详细的设计文档,进行评审,更改代码,修改测试计划,查看测试计划,执行完整的工作。系统测试,检查测试结果,然后在客户现场重新认证系统。这是针对另一大陆的电信系统而编写的,该公司不再存在。最后我知道,源位于QIC磁带上的RCS存档中,如果可以找到合适的磁带驱动器,则该文件仍应可读。
TMN 2012年

19

说C实际上在设计上实际上是“容易出错的”,这并不是真的不正确。除了像这样的严重错误外gets,C语言在不失去吸引人们首先使用C的主要功能的情况下,实际上无法采用任何其他方式。

C被设计为一种系统语言,可以充当一种“便携式程序集”。C语言的主要特点是,与高级语言不同,C代码通常非常接近实际的机器代码。换句话说,++i通常只是一条inc指令,通过查看C代码,您通常可以大致了解处理器在运行时将执行的操作。

但是添加隐式边界检查会增加很多额外的开销,这些开销是程序员没有要求也可能不需要的。这种开销远远超出了存储每个数组长度所需的额外存储空间,或者超出了每次访问数组时检查数组范围的额外指令。指针算术呢?或者,如果您有一个带有指针的函数呢?运行时环境无法知道该指针是否在合法分配的内存块的范围内。为了对此进行跟踪,您需要一种严肃的运行时体系结构,该体系结构可以根据当前分配的内存块表检查每个指针,此时我们已经进入Java / C#风格的托管运行时领域。


12
老实说,当人们问为什么C不安全时,我想知道他们是否抱怨汇编不安全。
Ben Brocka

5
C语言很像在Digital Equipment Corporation PDP-11机器上的便携式汇编。同时在巴勒斯机有数组边界在CPU中检查,所以他们很容易得到正确的方案在硬件生命阵列检查在罗克韦尔柯林斯公司的硬件(主要是在航空使用。)
蒂姆Williscroft

15

我认为真正的问题不在于这些类型的bug是很难解决的,但他们是很容易使:如果您使用strcpysprintf和朋友在(看似)最简单的方法,可以工作,那么你可能已经打开了缓冲区溢出的门。在有人利用它之前,没有人会注意到它(除非您有很好的代码审查)。现在添加一个事实,即有许多中等水平的程序员,而且大多数时候他们都在时间压力下-而且您的代码配方中充斥着缓冲区溢出,以至于很难简单地修复它们,因为他们很多,他们藏得很好。


3
您实际上并不需要“非常好的代码审查”。您只需要禁止sprintf或将sprintf重新定义为使用sizeof()和指针大小错误的东西,等等。您甚至不需要代码审查,就可以通过SCM commit进行此类操作钩子和grep。

1
@JoeWreschnig:sizeof(ptr)一般是4或8。那是另一个C的局限性:仅给出指向数组的指针,就无法确定数组的长度。
MSalters

@MSalters:是的,一个int [1]或char [4]数组或任何可能为假肯定的数组,但实际上,您永远不会使用这些函数处理该大小的缓冲区。(我在这里不是从理论上讲的-我使用这种方法在大型C代码库上工作了四年,我从未达到将sprintfing转换为char [4]的限制。)

5
@BlackJack:大多数程序员都不傻-如果您强迫他们通过大小,他们会通过正确的。除非被迫,否则多数情况下它也不会传递大小。您可以编写一个宏,如果它是静态的或自动调整大小,它将返回数组的长度,但是如果给定指针,则返回错误。然后,您重新定义sprintf,以使用给出大小的宏调用snprintf。现在,您有了sprintf的版本,该版本仅适用于具有已知大小的数组,并迫使程序员以手动指定的大小调用snprintf。

1
这样的宏的一个简单示例将是#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / (sizeof(a) != sizeof(void *))触发编译时除零。我在Chromium中首次看到的另一个聪明的地方是#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / !(sizeof(a) % sizeof((a)[0])),将少数误报替换为一些误报-不幸的是,它对于char []毫无用处。您可以使用各种编译器扩展来使其更加可靠,例如blogs.msdn.com/b/ce_base/archive/2007/05/08/…

7

修复缓冲区溢出很困难,因为C几乎没有提供解决问题的有用工具。这是一个基本的语言缺陷,本地缓冲区没有提供保护它几乎,如果不是完全不可能用卓越的产品来取代他们,像C ++有没有std::vectorstd::array很难甚至在调试模式下找到缓冲区溢出。


13
“语言缺陷”是一个带有偏见的主张。库不提供边界检查是一个缺陷。语言不是避免开销的有意识选择。这种选择是允许更std::vector高效地实现更高层次结构的一部分。并vector::operator[]为速度超过安全做出相同选择。这样做的安全性vector来自于使它更容易随身携带,这与现代C库采用的方法相同。

1
@Charles:“ C只是不提供任何类型的动态扩展缓冲区作为标准库的一部分。” 不,这与它无关。首先,C确实通过提供了它们realloc(C99还允许您通过运行时确定但通过任何自动变量使用恒定大小来调整堆栈数组的大小,几乎总是更喜欢char buf[1024])。其次,问题与扩展缓冲区无关,它与缓冲区是否携带大小以及在访问它们时检查该大小有关。

5
@Joe:问题不在于本地数组坏了。因为它们无法替代。首先,vector::operator[]在调试模式下进行边界检查(本机数组无法做到),其次,C中没有办法将本机数组类型换成可以进行边界检查的方式,因为没有模板,也没有运算符超载。在C ++中,如果要从T[]移到std::array,实际上可以换出typedef。在C语言中,无法实现这一目标,也无法编写具有等效功能的类,更不用说接口了。
DeadMG 2012年

3
@Joe:除了它永远不能是静态大小的,而且你永远也不能使其通用。用C编写任何起到std::vector<T>std::array<T, N>C ++ 相同作用的库都是不可能的。没有办法设计和指定可以做到这一点的任何库,甚至没有标准库。
DeadMG

1
我不确定“它永远不会是静态大小”的意思。正如我要使用的那个术语一样,std::vector它也永远不会是静态大小的。至于泛型,您可以使它像C所需要的那样成为泛型-在void *上进行少量基本操作(添加,删除,调整大小)以及所有其他专门编写的操作。如果您要抱怨C没有C ++样式的泛型,那超出了安全缓冲区处理的范围。

7

问题不在于C 语言

IMO,要克服的唯一主要障碍是对C的理解很不好。在参考手册和讲义中,数十年来的不良实践和错误信息已被制度化,从一开始就毒害了每一代新一代的程序员。给学生一个简单的I / O功能(如gets1或)的简短描述,scanf然后留给自己的设备使用。他们没有被告知这些工具在哪里或如何发生故障,或如何防止这些故障。他们没有被告知使用fgetsstrtol/strtod因为这些被认为是“高级”工具。然后,他们在专业领域释放了他们的破坏力。并不是很多经验丰富的程序员对此有所了解,因为他们受到了同样的大脑损坏的教育。太疯狂了 我在这里以及在Stack Overflow和其他站点上看到了很多问题,很明显,问这个问题的人是由一个根本不知道他们在说什么的人教的,当然您不能只说“您的教授错了,”因为他是一名教授,而您只是互联网上的某个人

然后,您会遇到很多不屑于任何答案的人群,“根据语言标准……”,因为他们在现实世界中工作,并且根据他们的看法,该标准不适用于现实世界。我可以和一个受过不良教育的人打交道,但是任何坚持要愚昧无知的人只会给这个行业带来麻烦。

如果正确地讲授该语言并着重于编写安全代码,就不会有缓冲区溢出问题。这不是“困难”,不是“高级”,只是谨慎。

是的,这真是令人大跌眼镜。


1值得庆幸的是,尽管它将永久保留在40年的遗留代码价值中,但该语言终于从语言规范中剔除。


1
虽然我大体上同意你的观点,但我认为你仍然有点不公平。我们认为“安全”也是时间的函数(而且我发现您作为专业软件开发人员的时间比我长得多,所以我相信您对此很熟悉)。从现在起的十年后,会有人讨论为什么2012年的所有人都使用支持DoS的哈希表实现,我们是否对安全一无所知?如果在教学中存在问题,那就是我们过于专注于教授“最佳”实践,而不是最佳实践本身在发展,这是一个问题。

1
老实说。您可以使用just编写安全的代码sprintf,但这并不意味着该语言没有缺陷。Ç 有瑕疵,有缺陷的-就像任何语言-那我们承认这些缺陷,所以我们可以继续解决这些问题是很重要的。

@JoeWreschnig-虽然我同意更大的观点,但我认为支持DoS的哈希表实现与缓冲区溢出之间存在质的差异。前者可以归因于您周围不断发展的环境,而第二种则没有借口。缓冲区溢出是编码错误,周期。是的,C没有刀片护罩,如果您不小心,会割伤你的;我们可以争论这是否是语言的缺陷。这与几乎没有给任何学生的事实正交在学习语言时安全指导。
约翰·博德

5

问题是管理短视而不是程序员无能的问题之一。请记住,一个90,000行的应用程序只需要一个不安全的操作即可完全不安全。在根本不安全的字符串处理之上编写的任何应用程序都是100%完美的,这几乎是不可能的,这意味着它是不安全的。

问题在于,不安全的费用没有向正确的收件人收取(销售应用程序的公司几乎永远不会退还购买价),或者在做出决定时不清楚(“我们必须运送不管在三月!”)。我可以肯定地说,如果您考虑了长期成本以及用户的成本,而不是公司的利润,那么使用C或相关语言编写的代码将更加昂贵,甚至可能如此昂贵,以至于在许多情况下,这显然是错误的选择。如今,传统观念认为这是必不可少的领域。但是,除非引入更严格的软件责任,否则这不会改变-行业内没人希望。


-1:将管理归咎于一切邪恶的根源并不是特别具有建设性。忽略历史要少一些。答案几乎被最后一句话赎回。
mattnz

对安全性感兴趣并愿意为此付费的用户可能会引入更严格的软件责任。可以说,可以通过对违反安全性的行为处以严厉的惩罚来引入它。如果用户愿意为安全性付费,那么基于市场的解决方案将会奏效,但事实并非如此。
David Thornley,2012年

4

使用C的强大功能之一是它使您能够以自己认为合适的任何方式来操纵内存。

使用C的最大弱点之一是它使您可以按照自己认为合适的任何方式来操作内存。

任何不安全功能都有安全版本。但是,程序员和编译器并不严格强制使用它们。


2

为什么C的创建者不通过重新实现库来解决这些问题?

可能是因为C ++已经做到了,并且与C代码向后兼容。因此,如果您想在C代码中使用安全的字符串类型,则只需使用std :: string并使用C ++编译器编写C代码即可。

底层的内存子系统可以通过引入保护块并对其进行有效性检查来帮助防止缓冲区溢出-因此所有分配都添加了4个字节的“ fefefefe”,当写入这些块时,系统会抛出摆动。它不能保证防止内存写入,但是它将表明出了点问题,需要修复。

我认为问题在于旧的strcpy等例程仍然存在。如果删除它们以支持strncpy等,那将有所帮助。


1
完全删除strcpy等将使增量升级路径变得更加困难,从而导致人们根本无法升级。现在,您可以切换到C11编译器,然后开始使用_s变体,然后禁止使用非_s变体,然后修复现有用法,无论实际可行的时间段如何。

-2

很容易理解为什么无法解决溢出问题。C在几个方面存在缺陷。当时,这些缺陷被视为可以容忍甚至是功能。几十年后的今天,这些缺陷是无法修复的。

编程社区的某些部分不希望这些漏洞被堵塞。只需看看所有从字符串,数组,指针,垃圾回收开始的火焰之战...


5
哈哈,糟糕透顶的答案。
Heath Hunnicutt 2012年

1
要解释为什么这是一个错误的答案:C确实确实存在许多缺陷,但是允许缓冲区溢出等与它们几乎没有关系,但是与基本语言要求无关。设计一种语言来完成C的工作并且不允许缓冲区溢出是不可能的。社区中的某些人不想放弃C允许他们使用的功能,通常有充分的理由。关于如何避免其中一些问题也存在分歧,这表明我们对编程语言设计没有完全的了解。
David Thornley,2012年

1
@DavidThornley:一个人可以设计一种语言来完成C的工作,但要做到这一点,以使普通的惯用做事方式至少允许编译器合理地检查缓冲区溢出(如果编译器选择这样做)。memcpy()可用和仅是有效复制数组段的标准方法之间存在巨大差异。
supercat 2014年
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.