
1.3.2 C存在的问题
C是一种灵活、可移植的高级编程语言,它已经广泛使用逾40年,但在安全社区中它却是灾星。那么C的哪些特性使得用它编程易于犯错从而导致安全缺陷呢?
C语言的目标是成为一种内存耗用微小的轻量级语言。C的这种特征使得当程序员误以为某些事情会由C自动处理(而实际上并不会)时,就可能会导致漏洞的出现。如果程序员熟悉某些表面看上去相似的语言,如Java、Pascal或者Ada,那他们更容易误以为C会为其提供更多的保护。这些错误的假设导致程序员容易犯这样的一些错误:对数组的越界不加保护,不处理整数操作的溢出和截断,以及用错误的实参数目调用函数等。
C语言标准的原章节包含一些指导原则。其中,第6点揭示了该语言中绝大多数安全问题的根源:
要点6:保持C精神。C精神的一些方面,可以归纳为下列短语:
(a)信任程序员。
(b)不要阻止程序员做他需要做的东西。
(c)保持语言的小而简单。
(d)对一种操作只提供一种方法。
(e)即使不能保证可移植性,也要使它快速运行。
箴言(a)和(b)直接违背安全性。在2007年春季的WG14伦敦会议上,代表讨论了C11的章节,有观点认为(a)应修改为“信任与核查”。WG14认为(b)点是C语言持续成功的关键。
C标准[ISO/IEC 2011]定义了以下几种行为。
特定于语言环境的行为(locale-specific behavior):该行为取决于每个实现的文档记录的当地国籍惯例、文化和语言。特定于语言环境的行为的一个例子是,对于26个小写拉丁字母以外的其他字符,islower()函数是否返回true。
未指定行为(unspecified behavior):使用一个未确定的值,或C标准提供两种或更多可能性的其他行为,并规定,在任何情况下选择使用哪一种没有进一步的要求。未指定行为的一个例子是:函数参数的求值顺序。
实现定义的行为(implementation-defined behavior):每个实现的文档记录如何作出选择的未指定行为。实现定义行为的一个例子是,当一个有符号整数右移位时高位 [1]的传播。
未定义行为(undefined behavior):使用一个不可移植的或错误的程序构造,或使用错误数据,国际标准并没有规定要求的行为。
附录J,“可移植性问题”,列举了这些行为在C语言中的具体例子。
实现是一组特定的软件,它在一个特定的翻译环境下运行,具有特定的控制选项,在一个特定的执行环境下执行程序的翻译,并支持执行其功能。实现基本上是编译器命令行(包括选定的标志或选项)的同义词。更改任何标志或选项都可能会导致产生显著不同的可执行文件,因此实现被看作是单独的实现。
C标准还介绍了如何确定未定义行为,如下所示。
如果违反在约束之外出现的“应当”或“不得”的规定,那么其行为是未定义的。本国际标准中另有用“未定义行为”一词指示的未定义行为,或遗漏任何明确定义的行为。特别强调,这三者没有任何区别,它们都描述“行为是不确定的”。
C标准委员会划分未定义行为的原因如下:
·为了许可实现者可以不捕获一些难以诊断的程序错误。
·为了避免定义有利于某个实现策略,而不利于另一个实现的不起眼的角落的情况。
·为了找出可能符合语言扩展的区域:实现者可以提供未正式定义行为的定义来增强语言。
合格的实现可以通过各种方式处理未定义行为,如完全无视该情况,并产生不可预知的结果;对环境的特点以一个已记录的方式翻译或执行程序(产生或不产生诊断消息);终止翻译或执行(产生诊断消息)。
未定义的行为非常危险,因为它们不必由编译器诊断,也因为所产生的程序可能产生任何行为。本书描述的大部分安全漏洞都是在代码中利用未定义行为的结果。
未定义行为存在的另一个问题是编译器优化。因为编译器没有义务产生未定义行为的代码,所以这些行为是优化的对象。通过假设未定义行为不会发生,编译器可以生成具有更好性能特性的代码。
编译器的编写者越来越多地利用C编程语言中的未定义行为来改善优化。通常情况下,这些优化会干扰开发人员对源代码进行因果分析(也就是说,分析后一阶段的结果对前面结果的依赖性)的能力。因此,这些优化消除了软件中的因果关系,使得产生软件故障、缺陷和漏洞的概率增加。
正如附录J的标题所建议的,未指定的、未定义的、实现定义的、特定于语言环境的行为都是可移植性问题。未定义行为是最有问题的,因为它们的行为在一个版本的编译器中可能良好地定义,但在其后续版本中可能彻底改变。C标准要求,实现记录并定义所有实现定义、特定语言环境的特点和所有扩展。
从语言的历史中我们可以看出,在C编程语言的初始阶段,可移植性并不是一个主要目标,但当将语言移植到不同的平台,并最终标准化时,移植性逐渐变得重要。当前的C标准确定了可移植程序的两个级别:符合(conforming)和严格符合(strictly conforming)。
严格符合程序只使用那些由C标准规定的语言和库的功能。严格符合程序可以使用有条件的功能,如果这种使用由适当的条件(包含预处理指令)守卫。它不能产生依赖于任何未指定、未定义或实现定义的行为的输出,也不能超过任何最低执行限制。符合程序是对一个符合标准的实现而言可以接受的程序。严格符合程序的目的是要最大限度地在符合的实现之间移植。符合程序可以依赖于一个符合标准的实现不可移植的特性。
可移植性要求,以独立于底层机器的体系结构的抽象水平对逻辑进行编码并转化或编译成底层的表示。问题出在对这些抽象的语义以及它们如何翻译成机器级别指令的不精确理解。这种理解的缺失导致了不当的假设、安全缺陷和漏洞。
C语言缺乏类型安全性。类型安全包括两方面含义:保持性(preservation)和前进性(progress)[Pfenning 2004]。保持性要求如果变量x的类型为t,那么如果x具有值v,则v的类型也为t。前进性要求对一个表达式的计算不会以非预期的方式进行,即要么得到一个值(且计算结束),要么存在某种方式对其进行继续处理。通俗地说,类型安全就是要求对某特定类型的操作,其结果仍然是原来的类型。C语言起源于两种无类型的语言,因此仍然保留着很多无类型语言的或弱类型语言的特征。例如,可以通过显式类型转换将指向某一类型的指针转换为指向另一种类型的指针,而当对转换后的指针进行解引用(dereferenced)时,其行为就是未定义的。还可以用隐式转换合法地对不同长度的带符号和不带符号的数混合操作,并且产生不可表示的结果。这种类型安全的缺乏导致了很大范围的安全缺陷和漏洞。
出于这些原因,C程序员的责任是开发杜绝未定义行为的代码,无论是否有编译器的帮助。
总之,虽然C语言包含了一些经常被误用,从而导致安全缺陷的因素,但C仍然是一种广为流行的语言,在许多情况下是多种应用程序的首选语言。这些问题中的部分问题可以通过对语言标准、编译器以及相关工具等的改进加以解决。短期来看,改善现状最有效的方式就是通过让开发人员了解常见的安全缺陷以及相应的缓解策略,教他们如何进行安全的程序设计。从长远来看,必须对C语言标准、兼容编译器及库做进一步的改进,使其继续作为开发安全系统的可行语言。