目前最常见的安全问题是缓冲区溢出。这个特别的安全问题引发的病毒感染可能比其它原因引发的病毒感染数量的总和还要多。市场上几乎每个应用程序和操作系统都存在黑客可能利用的缓冲区溢出漏洞。这个问题对于微软Windows来说是如此严重,以至于微软在产品的新版本(例如Windows XP Service Pack 2)中采用了一种完全不同的解决方法。本文的目的是帮助你更清晰地认识缓冲区溢出,并提供了几种用于减少(或者是消除)Visual C++应用程序缓冲区溢出问题的技术。
导航:
什么是缓冲区溢出
缓冲区溢出证明了一个观点:除非你看着用户与你的应用程序交互操作,否则你根本就不知道用户会向应用程序输入什么样的数据。
验证数据的范围
编程语言提供的大多数数据范围反映的都是下层硬件的实际情况,而不是现实世界的需要。例如,当你在代码中把某个值定义为Int32的时候,就意味着用户输入的值应该在-2,147,483,648到2,147,483,647之间。
验证数据的长度
有些数据类型不太容易进行快速检查。例如,字符串可以包含任意数量的字符,其数量最多只受到.NET框架组件和机器的限制。当然,很少人真的需要这么长的字符串。通常开发者要求字符串有一个最小和最大的长度范围。
排除非法的字符
黑客经常在输入信息中包含一些额外的非法字符,以了解会发生什么情况。例如,黑客通常会通过添加特定的字符建立脚本。在很多情况下,系统在没有提供任何警告的情况下就会执行脚本,赋予黑客访问系统的权利。
提供高级的用户帮助
很多开发者都不能把帮助与良好的安全性联系到一起,但是良好的帮助的确可以减少用户犯错误来提高安全性。
什么是缓冲区溢出
缓冲区溢出证明了一个观点:除非你看着用户与你的应用程序交互操作,否则你根本就不知道用户会向应用程序输入什么样的数据。这些攻击依赖于一些奇怪的想法:黑客给应用程序提供的输入信息可能超过了缓冲区的长度,结果这些额外的(超出缓冲区长度的)信息覆盖了缓冲区控制之外的内存。在某些情况下,这些内存实际上保存着可执行信息(heap memory overrun,堆存储泛滥),从而使应用程序不运行原始的可执行代码,而是运行黑客的代码;在另外一些情形中,黑客则覆盖了应用程序的栈页面(stack memory overrun,栈存储泛滥)。
有些黑客甚至于分析你的代码,查找位置以供堆或栈存储泛滥利用。但是在有些情况下,当黑客试图向某个字段输入一些信息,查看发生什么情况的时候,这种利用可能被发现。例如,黑客可能试图输入一段简单的代码,看你的应用程序是否会执行它。不管该黑客是如何发现漏洞的,其结果都是相同的:你的应用程序失去了对黑客代码的控制权--黑客现在可以享受那些曾经是你的应用程序才能享受的权力了。
很多开发者认为黑客会通过某些秘密的通道来利用他们所建立的程序,但是很多利用方法是非常简单的--让操作系统显示命令提示符这样的行为在某些情况下就足以获取控制权了。如果系统的安全性稍微有一点松懈,黑客就可以获取服务器的控制权。至少,命令提示符允许黑客探测系统的状况,采用其它的某种方式来获取更多的访问权。黑客不需要在第一次尝试的时候就获得系统的控制权。他们所需要的是获取累积起来的点点滴滴的控制权。
很明显,如果要保证应用程序免受缓冲区泛滥的伤害,你就必须为应用程序提供某种保护措施。控制缓冲区泛滥的最好的方法是检查程序收到的所有输入信息,即使这些信息来自受信任的来源。本文考虑了每个程序应该执行的四个基本的检查:检查数据范围、验证数据长度、排除非法字符、为用户提供足够的帮助以确保良好的输入。
验证数据的范围
编程语言提供的大多数数据范围反映的都是下层硬件的实际情况,而不是现实世界的需要。例如,当你在代码中把某个值定义为Int32的时候,就意味着用户输入的值应该在-2,147,483,648到2,147,483,647之间。这个数字是依赖于硬件条件的,计算机使用31位存储数据,1位存储符号(2^31 = 2,147,483,648)。但是,你的应用程序可能没有查明可接受的范围。
当硬件需求与应用程序的现实需求不一致的时候,你就必须在应用程序中包含特定的代码来检查潜在的错误条件。你在代码中可能希望接受1到40,000的数字,它超出了Int16的值范围,但是在Int32的值范围中。列表1显示了这类检查的例子。
列表1.检查数据范围错误
请注意,这段代码首先使用Parse()方法把输入信息转换成Int32类型。这种简单的转换可以为很多输入方面的问题进行定位。在这个例子中,代码使用System::OverflowException异常检查值是否太大或太小,使用System::FormatException异常检查值的格式是否正确。在代码确保输入信息是一个合理的Int32值之后,接着检查实际的输入范围。
值的数据类型是最容易检查的,因为它们都有特定的范围。值与对象不同,它没有隐藏的元素,使开发者感到惊讶的地方很少。
一般来说,用于验证值数据类型的所有事务是在代码中定义上下边界,接着对值进行检查。
当我们使用对象的时候,数据值验证的问题就出现了。例如,你希望用户把几个字符串中的一个作为输入信息,那么使用列表框来减少用户的输入选择是有帮助的。当用户面对只有数个选项的列表框的时候,他们是不可能输入无效信息(例如脚本)的。
有时候你必须为问题设计独特的方案。例如,你如何确保某个特定的方法接收数量固定的、范围不连续的输入信息?在这种情况下枚举(enumeration)可能会节约时间。列表2显示了在代码中如何把枚举用于自动化的数据范围变化。
类表2:使用枚举检查数据的范围
请注意,DisplayString()的声明需要一个SomeStrings枚举类型的输入信息(参数)。调用者不可能使用其它的任何输入类型,这意味着DisplayString()方法自动地受到了保护。例如,你不可能把某个脚本作为输入信息,因为它不是正确的类型。
验证数据的长度
有些数据类型不太容易进行快速检查。例如,字符串可以包含任意数量的字符,其数量最多只受到.NET框架组件和机器的限制。当然,很少人真的需要这么长的字符串。通常开发者要求字符串有一个最小和最大的长度范围。因此,你不需要验证接收到的是否是字符串,只需要验证它的长度是否正确。否则,其他人可能发送任意长度的字符串,而这样就会导致缓冲区泛滥。列表3显示了通过验证每个参数的数据长度来防止发生问题的例子。
列表3:验证数据的长度
验证过程发生在ProcessData()方法中,该方法把输入的字符串、最小的字符串长度、最大的字符串长度作为输入信息。请注意,这段代码首先验证输入参数是否正确。UpperLimit参数必须比LowerLimit参数大。这部分代码演示了良好的编程习惯--永远不要相信你接收到的输入信息。请注意,这部分代码产生System::ArgumentException异常而不是通用的异常。虽然特定的异常表现更好,但是大多数开发者还是使用通用的异常。如果.NET框架组件不能为你的代码需求提供特定的异常,你应该建立定制的异常。
代码接着验证字符串。如果字符串的字符数量太多或者太少,代码就产生 System::Security::SecurityException异常。在这儿使用安全性异常是正确的,因为这类事件就会导致安全性异常。用户可能决定输入长字符串以创造缓冲区溢出的条件。即使用户只是犯了一个错误,你引发这个安全性异常意味着你至少可以验证这个异常的起因,而不是简单地跳过去。
这个例子的测试代码在btnDataLength_Click()方法之中。这段代码在try...catch代码块中执行以确保异常都会被捕捉到。真正的检查只是一个简单的if语句。这段代码为每个异常都包含了catch语句。如果你希望确保应用程序注意到任何安全性异常并适当地作出处理,那么捕捉异常就很重要了。
排除非法的字符
黑客经常在输入信息中包含一些额外的非法字符,以了解会发生什么情况。例如,黑客通常会通过添加特定的字符建立脚本。在很多情况下,系统在没有提供任何警告的情况下就会执行脚本,赋予黑客访问系统的权利。对于这种利用方式来说,Web应用程序比桌面应用程序受的影响更大,但是两者你都必须受到保护。
幸运的是,.NET框架组件提供了强大的合格表达式(regular expression)支持。合格表达式定义了可接受的字符串输入,因此你可以轻易地检测到非法的字符。列表4显示了使用合格表达式的一个方法。
列表4:使用合格表达式
代码开头包含了Regex对象。在这种情况下,唯一可以接受的输入是字母(甚至于不能包含空格)。合格表达式旁路了大量的输入信息。实际上,为ASP.NET应用程序提供的很多验证支持中定义了很多的默认模板。其要点在于你可以建立一个字符串,它定义了可接受的输入信息,包含了输入样式(例如电话号码)。
Regex对象可以执行很多比较操作。在例子中它使用Matches()方法对比字符串的长度和参照的数字。当这两个数字匹配的时候,输入信息就是正确的。否则,输入信息就包含了非法的字符,CheckChars()方法会引发异常。
提供高级的用户帮助
很多开发者都不能把帮助与良好的安全性联系到一起,但是良好的帮助的确可以减少用户犯错误来提高安全性。例如,良好的帮助文件可以通过显示应用程序希望接收的信息,从而防止某类用户输入错误信息。减少输入错误可以使我们彻底地分析遗留的错误信息,并最终减少不正确输入带来的安全风险。
帮助可以来自于所有形式,包括有用的错误消息。某些数据类型会提出一些特殊的挑战,而你的应用程序必须处理这些问题以确保数据完整性和安全性。例如,日期就是经常会出现问题的一个数据输入条目。首先,你必须考虑日期的格式。用户可能输入1 June 2003、06/01/2003、June 1, 2003、2003/06/01或其它可接受的变量。你应该约束自己的应用程序,只允许一种日期格式以便于检查日期信息的有效性。但是错误消息和帮助文件可以告诉用户必须使用哪种格式,这样用户使用错误格式输入一个有效日期的时候就不会感到沮丧(因为有帮助提醒格式)。
无论你怎样做,仍然有一些用户试图滥用系统。他们可能使用错误的格式输入日期,甚至于输入根本不包含日期的信息。但是,通过提供良好的帮助,你就拥有了用于询问用户的基本要素了。你可以调用安全性措施来确保用户知道这种行为是不可接受的。减少缓冲区溢出是一个主动的过程。你必须防止无效的输入、为用户提供良好的帮助、并给决心忽视规则的用户惩罚性的措施。
导航:
什么是缓冲区溢出
缓冲区溢出证明了一个观点:除非你看着用户与你的应用程序交互操作,否则你根本就不知道用户会向应用程序输入什么样的数据。
验证数据的范围
编程语言提供的大多数数据范围反映的都是下层硬件的实际情况,而不是现实世界的需要。例如,当你在代码中把某个值定义为Int32的时候,就意味着用户输入的值应该在-2,147,483,648到2,147,483,647之间。
验证数据的长度
有些数据类型不太容易进行快速检查。例如,字符串可以包含任意数量的字符,其数量最多只受到.NET框架组件和机器的限制。当然,很少人真的需要这么长的字符串。通常开发者要求字符串有一个最小和最大的长度范围。
排除非法的字符
黑客经常在输入信息中包含一些额外的非法字符,以了解会发生什么情况。例如,黑客通常会通过添加特定的字符建立脚本。在很多情况下,系统在没有提供任何警告的情况下就会执行脚本,赋予黑客访问系统的权利。
提供高级的用户帮助
很多开发者都不能把帮助与良好的安全性联系到一起,但是良好的帮助的确可以减少用户犯错误来提高安全性。
什么是缓冲区溢出
缓冲区溢出证明了一个观点:除非你看着用户与你的应用程序交互操作,否则你根本就不知道用户会向应用程序输入什么样的数据。这些攻击依赖于一些奇怪的想法:黑客给应用程序提供的输入信息可能超过了缓冲区的长度,结果这些额外的(超出缓冲区长度的)信息覆盖了缓冲区控制之外的内存。在某些情况下,这些内存实际上保存着可执行信息(heap memory overrun,堆存储泛滥),从而使应用程序不运行原始的可执行代码,而是运行黑客的代码;在另外一些情形中,黑客则覆盖了应用程序的栈页面(stack memory overrun,栈存储泛滥)。
有些黑客甚至于分析你的代码,查找位置以供堆或栈存储泛滥利用。但是在有些情况下,当黑客试图向某个字段输入一些信息,查看发生什么情况的时候,这种利用可能被发现。例如,黑客可能试图输入一段简单的代码,看你的应用程序是否会执行它。不管该黑客是如何发现漏洞的,其结果都是相同的:你的应用程序失去了对黑客代码的控制权--黑客现在可以享受那些曾经是你的应用程序才能享受的权力了。
很多开发者认为黑客会通过某些秘密的通道来利用他们所建立的程序,但是很多利用方法是非常简单的--让操作系统显示命令提示符这样的行为在某些情况下就足以获取控制权了。如果系统的安全性稍微有一点松懈,黑客就可以获取服务器的控制权。至少,命令提示符允许黑客探测系统的状况,采用其它的某种方式来获取更多的访问权。黑客不需要在第一次尝试的时候就获得系统的控制权。他们所需要的是获取累积起来的点点滴滴的控制权。
很明显,如果要保证应用程序免受缓冲区泛滥的伤害,你就必须为应用程序提供某种保护措施。控制缓冲区泛滥的最好的方法是检查程序收到的所有输入信息,即使这些信息来自受信任的来源。本文考虑了每个程序应该执行的四个基本的检查:检查数据范围、验证数据长度、排除非法字符、为用户提供足够的帮助以确保良好的输入。
验证数据的范围
编程语言提供的大多数数据范围反映的都是下层硬件的实际情况,而不是现实世界的需要。例如,当你在代码中把某个值定义为Int32的时候,就意味着用户输入的值应该在-2,147,483,648到2,147,483,647之间。这个数字是依赖于硬件条件的,计算机使用31位存储数据,1位存储符号(2^31 = 2,147,483,648)。但是,你的应用程序可能没有查明可接受的范围。
当硬件需求与应用程序的现实需求不一致的时候,你就必须在应用程序中包含特定的代码来检查潜在的错误条件。你在代码中可能希望接受1到40,000的数字,它超出了Int16的值范围,但是在Int32的值范围中。列表1显示了这类检查的例子。
列表1.检查数据范围错误
System::Void btnDataRange_Click(System::Object * sender, System::EventArgs * e) { Int32 TestData; // 保持输入的值 try { // 永远需要首先尝试分析数据 TestData = Int32::Parse(txtInput1->Text); } catch (System::OverflowException *OE) { // 溢出错误处理 MessageBox::Show(S"Type a value between 1 and 40,000.", S"Input Error", MessageBoxButtons::OK, MessageBoxIcon::Error); return; } catch (System::FormatException *FE) { //溢出错误处理 MessageBox::Show(S"Type the number without extra charaters.", S"Input Error", MessageBoxButtons::OK, MessageBoxIcon::Error); return; } // 测试特定的数据范围 if (TestData < 1 || TestData > 40000) //溢出错误处理 MessageBox::Show(S"Type a value between 1 and 40,000.", S"Input Error", MessageBoxButtons::OK, MessageBoxIcon::Error); } |
请注意,这段代码首先使用Parse()方法把输入信息转换成Int32类型。这种简单的转换可以为很多输入方面的问题进行定位。在这个例子中,代码使用System::OverflowException异常检查值是否太大或太小,使用System::FormatException异常检查值的格式是否正确。在代码确保输入信息是一个合理的Int32值之后,接着检查实际的输入范围。
值的数据类型是最容易检查的,因为它们都有特定的范围。值与对象不同,它没有隐藏的元素,使开发者感到惊讶的地方很少。
一般来说,用于验证值数据类型的所有事务是在代码中定义上下边界,接着对值进行检查。
当我们使用对象的时候,数据值验证的问题就出现了。例如,你希望用户把几个字符串中的一个作为输入信息,那么使用列表框来减少用户的输入选择是有帮助的。当用户面对只有数个选项的列表框的时候,他们是不可能输入无效信息(例如脚本)的。
有时候你必须为问题设计独特的方案。例如,你如何确保某个特定的方法接收数量固定的、范围不连续的输入信息?在这种情况下枚举(enumeration)可能会节约时间。列表2显示了在代码中如何把枚举用于自动化的数据范围变化。
类表2:使用枚举检查数据的范围
请注意,DisplayString()的声明需要一个SomeStrings枚举类型的输入信息(参数)。调用者不可能使用其它的任何输入类型,这意味着DisplayString()方法自动地受到了保护。例如,你不可能把某个脚本作为输入信息,因为它不是正确的类型。
验证数据的长度
有些数据类型不太容易进行快速检查。例如,字符串可以包含任意数量的字符,其数量最多只受到.NET框架组件和机器的限制。当然,很少人真的需要这么长的字符串。通常开发者要求字符串有一个最小和最大的长度范围。因此,你不需要验证接收到的是否是字符串,只需要验证它的长度是否正确。否则,其他人可能发送任意长度的字符串,而这样就会导致缓冲区泛滥。列表3显示了通过验证每个参数的数据长度来防止发生问题的例子。
列表3:验证数据的长度
System::Boolean ProcessData(String *Input, Int32 UpperLimit, Int32 LowerLimit) { StringBuilder *ErrorMsg; // 错误信息 // 检查输入信息错误 if (UpperLimit < LowerLimit) { // 建立错误消息 ErrorMsg = new StringBuilder(); ErrorMsg->Append(S"The UpperLimit input must be greater than "); ErrorMsg->Append(S"the LowerLimit number."); // 定义新的错误 System::ArgumentException *AE; AE = new ArgumentException(ErrorMsg->ToString(),S"UpperLimit"); // 抛出错误 throw(AE); } // 检查数据长度错误条件 if (Input->Length < LowerLimit || Input->Length > UpperLimit) { // 建立错误信息 ErrorMsg = new StringBuilder(); ErrorMsg->Append(S"String is the wrong length. Use a string "); ErrorMsg->Append(S"between 4 and 8 characters long."); // 定义新的错误 System::Security::SecurityException *SE; SE = new SecurityException(ErrorMsg->ToString()); //抛出错误 throw(SE); } // 如果数据是正确的就返回true return true; } System::Void btnDataLength_Click(System::Object * sender, System::EventArgs * e) { try { // 处理输入文本 if (ProcessData(txtInput2->Text, 8, 4)) // 显示正确输入的结果信息 MessageBox::Show(txtInput2->Text, "Input String", MessageBoxButtons::OK, MessageBoxIcon::Information); } catch (System::Security::SecurityException *SE) { // 显示错误输入的错误信息 MessageBox::Show(SE->Message, "Input Error", MessageBoxButtons::OK, MessageBoxIcon::Error); } catch (System::ArgumentException *AE) { // 显示错误输入的错误信息 MessageBox::Show(AE->Message, "Argument Error", MessageBoxButtons::OK, MessageBoxIcon::Error); } } |
验证过程发生在ProcessData()方法中,该方法把输入的字符串、最小的字符串长度、最大的字符串长度作为输入信息。请注意,这段代码首先验证输入参数是否正确。UpperLimit参数必须比LowerLimit参数大。这部分代码演示了良好的编程习惯--永远不要相信你接收到的输入信息。请注意,这部分代码产生System::ArgumentException异常而不是通用的异常。虽然特定的异常表现更好,但是大多数开发者还是使用通用的异常。如果.NET框架组件不能为你的代码需求提供特定的异常,你应该建立定制的异常。
代码接着验证字符串。如果字符串的字符数量太多或者太少,代码就产生 System::Security::SecurityException异常。在这儿使用安全性异常是正确的,因为这类事件就会导致安全性异常。用户可能决定输入长字符串以创造缓冲区溢出的条件。即使用户只是犯了一个错误,你引发这个安全性异常意味着你至少可以验证这个异常的起因,而不是简单地跳过去。
这个例子的测试代码在btnDataLength_Click()方法之中。这段代码在try...catch代码块中执行以确保异常都会被捕捉到。真正的检查只是一个简单的if语句。这段代码为每个异常都包含了catch语句。如果你希望确保应用程序注意到任何安全性异常并适当地作出处理,那么捕捉异常就很重要了。
排除非法的字符
黑客经常在输入信息中包含一些额外的非法字符,以了解会发生什么情况。例如,黑客通常会通过添加特定的字符建立脚本。在很多情况下,系统在没有提供任何警告的情况下就会执行脚本,赋予黑客访问系统的权利。对于这种利用方式来说,Web应用程序比桌面应用程序受的影响更大,但是两者你都必须受到保护。
幸运的是,.NET框架组件提供了强大的合格表达式(regular expression)支持。合格表达式定义了可接受的字符串输入,因此你可以轻易地检测到非法的字符。列表4显示了使用合格表达式的一个方法。
列表4:使用合格表达式
代码开头包含了Regex对象。在这种情况下,唯一可以接受的输入是字母(甚至于不能包含空格)。合格表达式旁路了大量的输入信息。实际上,为ASP.NET应用程序提供的很多验证支持中定义了很多的默认模板。其要点在于你可以建立一个字符串,它定义了可接受的输入信息,包含了输入样式(例如电话号码)。
Regex对象可以执行很多比较操作。在例子中它使用Matches()方法对比字符串的长度和参照的数字。当这两个数字匹配的时候,输入信息就是正确的。否则,输入信息就包含了非法的字符,CheckChars()方法会引发异常。
提供高级的用户帮助
很多开发者都不能把帮助与良好的安全性联系到一起,但是良好的帮助的确可以减少用户犯错误来提高安全性。例如,良好的帮助文件可以通过显示应用程序希望接收的信息,从而防止某类用户输入错误信息。减少输入错误可以使我们彻底地分析遗留的错误信息,并最终减少不正确输入带来的安全风险。
帮助可以来自于所有形式,包括有用的错误消息。某些数据类型会提出一些特殊的挑战,而你的应用程序必须处理这些问题以确保数据完整性和安全性。例如,日期就是经常会出现问题的一个数据输入条目。首先,你必须考虑日期的格式。用户可能输入1 June 2003、06/01/2003、June 1, 2003、2003/06/01或其它可接受的变量。你应该约束自己的应用程序,只允许一种日期格式以便于检查日期信息的有效性。但是错误消息和帮助文件可以告诉用户必须使用哪种格式,这样用户使用错误格式输入一个有效日期的时候就不会感到沮丧(因为有帮助提醒格式)。
无论你怎样做,仍然有一些用户试图滥用系统。他们可能使用错误的格式输入日期,甚至于输入根本不包含日期的信息。但是,通过提供良好的帮助,你就拥有了用于询问用户的基本要素了。你可以调用安全性措施来确保用户知道这种行为是不可接受的。减少缓冲区溢出是一个主动的过程。你必须防止无效的输入、为用户提供良好的帮助、并给决心忽视规则的用户惩罚性的措施。