运行时和编译时的安全性检查
2007-03-23 21:30:31 来源:WEB开发网摘要:说明 Visual C++ 编译器和库在帮助开发人员提高其应用程序可靠性和安全性方面的功能。
样例应用程序中包含六个测试,用来模拟程序员可能会犯的某些错误,或者模拟对未采取任何保护措施的程序进行恶意输入所产生的某些后果:
1.通过多次运行循环程序覆盖缓冲区。由于只定义了一个缓冲区,因此覆盖缓冲区将影响包含返回地址的堆栈的剩余部分。
2.
当另一个缓冲区也在堆栈中时覆盖缓冲区。
3.
不覆盖缓冲区。
4.
使用未初始化的变量。
5.
执行可能导致丢失信息的转换。
6.
以更复杂的方式使用未初始化的变量。
其中的某些测试与 /GS 和 /RTC 选项相关。
/GS -- 缓冲区安全性检查如果使用 /GS 进行编译,将在程序中插入代码,以检测可能覆盖函数返回地址的缓冲区溢出。如果发生了缓冲区溢出,系统将向用户显示一个警告对话框,然后终止程序。这样,攻击者将无法控制应用程序。用户也可以编写自定义的错误处理例程,以代替默认对话框来处理错误。
在返回地址之前将插入一个专门的 cookie(系列字节),以使得任何缓冲区溢出都将更改该 cookie。在函数返回之前,将测试 cookie 的值。如果 cookie 值已被更改,将会调用处理程序。服务器或服务可能会要求进行不同的处理,而不是显示一个对话框,请参阅 MSDN,以获取有关编写自己的处理程序的详细信息。
此 cookie 由 C 运行库在程序启动时生成,攻击者将无法知晓 cookie 值,并且在每次运行程序时,该值都不相同。由于使用了 CRT,因此 C 运行库不会象发生误报时那样初始化两次以及重复生成 cookie。
此编译器选项适用于已发布的代码。用于编译样例代码的命令提示代码为:
cl /O2 /ML /GS /EHsc GS-RTC.cpp
(/O2 选项将打开优化功能。它不是调试版本。)运行此命令将创建 gs-rtc.exe。
测试 1 设计用于显示 /GS 选项的功能:
void Test1()
{
char buffer1[100];
for (int i=0 ; i < 200; i++)
{
buffer1[i] = 'a';
}
buffer1[sizeof(buffer1)-1] = 0;
cout << buffer1 << endl;
}
其中的 for 循环将执行很多次,从而使返回地址以及其他地址溢出。要运行测试 1,请使用 /GS 选项编译程序,然后执行以下命令:
gs-rtc 1
此时将显示以下对话框:
图 1. 使用 /GS 选项时生成的缓冲区溢出错误
作为对比,下面为不使用 /GS 选项编译相同的代码:
cl /Od /MLd /EHsc /ZI GS-RTC.cpp /link
再次运行测试,此时系统将显示以下对话框:
图 2. 不使用 /GS 选项时生成的错误。
使用缓冲区溢出攻击方式的攻击者将使用精心设计、可为攻击者提供控制能力的地址来覆盖返回地址:此样例仅在返回地址上写入了多个 a(十六进制的 61)。
/GS 将无法检测到不覆盖返回地址、但会破坏其他内存并导致结果错误的溢出。请尝试使用到目前为止所显示的任意一条编译命令来运行测试 2。
gs-rtc 2
虽然测试 2 覆盖内存,但 /GS 选项却无法检测出来。在此代码的优化版本中,删除了循环语句后面的空终止符分配,因此未出现覆盖所引起的后果。通常这种情况不会发生。
/RTC
RTC 表示运行时检查。RTC 有若干子选项。与 /GS 不同,/RTC 设计用于调试版本,而不用于优化代码。与 /GS 相同的是,如果您不喜欢默认对话框,可以编写自己的处理程序。
使用 Microsoft® Visual Studio® 调试应用程序时,RTC 对话框可以为您提供在错误发生之处调试应用程序的选项。此外,还可以在 Visual Studio 中创建若干个配置,每个配置都包含各种选项的不同组合,例如,对发布版本使用 /GS 选项,而对调试版本使用一个或多个 /RTC 选项。
/RTCs - 堆栈帧运行时错误检查此选项在保护堆栈不被破坏方面采取了若干措施。
• 在每次调用函数时,将所有局部变量初始化为非零值。这样可以防止以前的调用对堆栈中的值的无意使用。• 验证堆栈指针能够检查到堆栈破坏,例如,在一个位置将函数定义为 __stdcall,而在另一个位置将函数定义为 __cdecl 可导致堆栈破坏。
• 检测局部变量的溢出和不足。这与 /GS 不同,因为它仅适用于调试版本,并且检测缓冲区的两端以及所有缓冲区是否遭到破坏。
cl /Od /MLd /ZI /EHsc /RTCs GS-RTC.cpp
此命令将关闭优化 (/Od),并设置 _DEBUG 预处理器定义。
使用此命令编译样例后,请再次尝试运行 测试 1。此时它将会捕捉到覆盖操作。
测试 2 说明了不包括返回地址的堆栈溢出:
void Test2()
{
char buffer1[100];
char buffer2[100];
buffer1[0] = 0;
for (int i=0 ; i <= sizeof(buffer2); i++)
{
buffer2[i] = 'a';
}
buffer2[sizeof(buffer2)-1] = 0;
cout << buffer2 << '-' << buffer1 << endl;
}
此循环使 buffer2 溢出一个字符,因为它使用 <= 代替了 <。它将覆盖 buffer1 的第一个字符。使用 /RTCs 编译器开关进行编译并使用 gs-rtc 2 运行时,系统将显示下面的对话框:
图 3. 使用 gs-rtc 2 选项时生成的错误。
测试 3 对缓冲区不足进行了说明:
void Test3()
{
char buffer1[100];
char buffer2[100];
memset(buffer1,'a',sizeof(buffer1)-1);
buffer1[sizeof(buffer1)-1]=0;
memset(buffer2,'b',sizeof(buffer2)-1);
buffer2[sizeof(buffer2)-1]=0;
*(buffer1-1) = 'c';
cout << buffer1 << endl;
cout << buffer2 << endl;
}
(要运行测试 3,请使用命令“gs-rtc 3”。)在本例中,buffer2 的最后一个字节被覆盖,并且对话框将显示 buffer1 出现的问题。如果不进行运行时检查,此测试的输出结果为:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
由于这一写入操作破坏了 buffer2 的终止符,因此,所有将 buffer2 用作空结束字符串的操作都将获得一个长度为期望值两倍的字符串。
测试 4 显示了 /RTCs 选项如何将未初始化变量设置为“标志”值:
void Test4()
{
unsigned int var;
cout << hex << var;
}
在使用 /RTCs 编译之后运行 测试 4时,将得到十六进制值 cccccccc。如果不使用 /RTCs 进行编译,将得到堆栈中剩余的随机值。
/RTCc - 检测导致数据丢失的分配此选项将插入代码,以便在分配导致数据丢失时向您发出警报,这样可以确保在转换为较小类型时从不丢失数据。测试 5 说明以下内容。
void Test5(int value)
{
unsigned char ch;
ch = (unsigned char)value;
}
请执行以下命令来编译此代码:
cl /Od /MLd /ZI /EHsc /RTCc GS-RTC.cpp
请在命令提示符处使用其他数字来运行此命令。例如,此命令将触发错误:
gs-rtc 5 300
图 4. 在 /RTCc 开关中使用大于 255 的数值时所生成的错误。
无符号字符最多可容纳 255 个字符,因此向程序中输入 300 将导致数据丢失。使用 gs-rtc 5 200 再次运行命令,此时将不会出现错误。
如果要转换为较小类型,并要故意丢失上面的数位,则可以使用如下所示的掩码:
ch = (unsigned char)(value & 0xFF);
/RTCu - 报告使用了未初始化的变量当访问未初始化的变量时,此选项将会发出警告。测试 6 包含三个子测试,代码如下:
void Test6(int value)
{
int uninitialized;
int var;
switch (value) {
case 3:
uninitialized = 4;
case 2:
var = 5 * uninitialized;
break;
case 1:
int *var2;
var2= &uninitialized;
var = 5 * uninitialized;
break;
}
}
(请注意,执行 case 3 中的语句后,程序将直接转到 case 2 中执行。)使用以下命令编译此代码:
cl /Od /MLd /ZI /EHsc /RTCu GS-RTC.cpp
运行 gs-rtc 6 2 时,系统将显示以下对话框:
图 5. 使用 gs-rtc 6 2 时所生成的错误
运行 gs-rtc 6 3 时,系统将不显示对话框,因为变量已被初始化。"但是,即使使用了未初始化的变量,gs-rtc 6 1 也不会出现错误,因为编译器不跟踪可以通过指针来初始化的变量。
结论下表总结了使用每种编译器选项的各种测试及结果。
常规编译 | /GS | /RTCs | /RTCu | /RTCc | |
gs-rtc 1 |
返回地址被破坏。程序终止(但仍可能受到缓冲区溢出攻击)。 |
检测到缓冲区溢出。程序终止。 |
检测到数据破坏 |
不适用 |
不适用 |
gs-rtc 2 |
Buffer1 使用随机字符进行了扩展,因为终止空字符被覆盖。 |
Buffer1 使用随机字符进行了扩展,因为终止空字符被覆盖。 |
检测到数据破坏 |
不适用 |
不适用 |
gs-rtc 3 |
使用 buffer1 的所有内容扩展Buffer2 |
使用 buffer1 的所有内容扩展Buffer2 |
检测到数据破坏 |
不适用 |
不适用 |
gs-rtc 4 |
输出随机整数 |
不适用 |
输出 cccccccc |
不适用 |
不适用 |
gs-rtc 5 200 |
无 |
不适用 |
不适用 |
不适用 |
无 - 不截断 |
gs-rtc 5 300 |
无 |
不适用 |
不适用 |
不适用 |
检测到数据丢失 |
gs-rtc 6 1 |
无 |
不适用 |
不适用 |
不适用 |
未检测到使用了未初始化变量 -- 使用指针屏蔽 |
gs-rtc 6 2 |
无 |
不适用 |
不适用 |
不适用 |
检测到使用了未初始化的变量 |
gs-rtc 6 3 |
无 |
不适用 |
不适用 |
不适用 |
无,变量已经初始化 |
更多精彩
赞助商链接