- 概述
在前面的安全编码实践的文章里,我们讨论了GS编译选项和数据执行保护DEP功能。 结论是GS和DEP可以有效的缓解缓存溢出类型的安全漏洞的危害。关于这个结论,有两个大家需要值得注意的地方。
第一:GS和DEP是缓解(mitigation)措施。也就是说,代码本身仍然存在着安全漏洞,只是由于GS和DEP降低了其危害程度。
第二:GS和DEP存在其自身的局限性。例如,GS不是对每一个函数都适用,而DEP则需要一定的硬件支持。
那么,一个很自然的问题就是,有什么工具可以帮助我们在开发过程中,及早发现并修补代码中存在的安全漏洞?
答案之一是静态代码分析工具。本文会着重介绍微软提供的C/C++的静态代码分析工具:Prefast。对于托管代码(managed code),微软提供的静态代码分析工具是FxCop。关于FxCop,我们会在以后的安全编码实践的文章中专门介绍。
- Prefast介绍
2.1历史
Prefast是微软研究院提出的静态代码分析工具。主要目的是通过分析代码的数据和控制信息来检测程序中的缺陷。需要强调的是,Prefast检测的缺项不仅仅是安全缺陷,但是安全缺陷类型是其检测的最为重要的部分。Prefast推出后在微软内部得到了广泛的使用,并经历了若干格版本的升级。现在,微软将这个内部工具商业化,以提供给外部的开发人员使用。
2.2 如何获得Prefast
目前有两个办法可以获得Prefast工具。
- Prefast包括在Visual Studio 2005 /2008的团队版本(team edition)中。
- Prefast包括在Windows驱动程序开发包(Microsoft Windows Driver Kits)的开发环境中。
需要指出的是,Visual Studio的团队版本的价格要高于Visual Studio个人版本,而Windows驱动程序开发包是免费下载的,那它们提供的Prefast版本有什么区别?在Visual Studio的团队版本中,Prefast是直接和代码的开发过程集成的,使用非常方便。并且可以直接根据Prefast的输出结果创建相应的开发任务。而在Windows驱动程序开发包中,Prefast是作为一个单独的工具提供,没有像Visual Studio团队版本中一样与开发环境集成。
下载Windows驱动程序开发包可以通过http://connect.microsoft.com/, 注册Microsoft Connect后选择Windows Logo Kit (WLK), Windows Driver Kit (WDK) and Windows Driver Framework (WDF)即可。具体的步骤这里就不详细叙述了。
安装好WDK后Prefast就已经直接在其开发环境下使用了。
2.3使用Prefast
在Visual Studio的团队版本中,使用Prefast,打开Project Properties --> Configuration Properties --> Code Analysis -->Enable Code Analysis For C/C++ on build。选择 Yes(/analyze)即可。具体可参见图1。
如果直接使用CL.exe命令行编译器,采用/analyze编译选项即可。例如:cl test.cpp -W4 /EHsc /analyze
图1:VSTS中使用Prefast(Code Analysis)
使用在WDK中的Prefast,有以下几个重要命令。
- 运行Prefast:prefast build -cZ
- 查看Prefast输出结果:
- 命令行:prefast list
- GUI:prefast view
有关在WDK下使用Prefast的详细步骤,可以参见微软的PREfast Step-by-Step文档【1】。
- Prefast输出的警告(warning)信息
上面的介绍可以看出Prefast的使用操作并不难。成功应用Prefast的关键是要理解其输出的各类警告信息,并作出正确的评估和代码修改。文章前面我们提到过,Prefast检测的不仅仅是安全缺陷,但是安全缺陷类型是其检测的最为重要的部分。下面,我们就介绍一些和安全漏洞紧密相关的警告信息。以下给出的代码例子均来自于微软的文档Code Analysis for C/C++ Warnings【2】。
警告 C6001: using uninitialized memory <variable>,使用未初始化的变量。
例子:
#include "stdafx.h"
int f( bool b )
{
int i;
if ( b )
{
i = 0;
}
return i; // 当b为假时,变量i未初始化
}
大家也许会问,使用未初始化的变量会导致安全漏洞吗?这里我想特别强调的一点是,我们前面经常提到的安全漏洞是缓存溢出导致的,但是大家千万不要认为缓存溢出是导致安全漏洞的唯一原因。有各种各样的代码错误可以导致严重的安全漏洞,使用未初始化的变量也是其中的一种。关于未初始化的变量如何导致安全漏洞的详细信息超出了本文的范畴,有兴趣的读者可以参见微软SWI组的博客文章:MS08-014 : The Case of the Uninitialized Stack Variable Vulnerability【3】
修补:初始化变量。
警告C6029: possible buffer overrun in call to <function>: use of unchecked value,使用未验证的参数可能导致缓存溢出。
例子:
#include "windows.h"
void f(char *buff, DWORD cbLen, DWORD cbRead, HANDLE hFile)
{
if (!ReadFile (hFile, &cbLen, sizeof (cbLen), &cbRead, NULL))
{
// code ...
if (!ReadFile (hFile, buff, cbLen, &cbRead, NULL)) // warning 6029
{
// code ...
}
}
}
在上面这个例子中,第一次的ReadFile得到的cbLen值,直接作为最大读取的长度传递给第二个ReadFile。如果cbLen过大的话,就会导致写入buff过多的数据。
修补:验证参数。在第二个ReadFile之前验证cbLen。
if (cbLen <= sizeof (buff)) // check length
警告C6057: buffer overrun due to number of characters/number of bytes mismatch in call to <function>,字符(characters)数目和字节(bytes)数目的不匹配导致缓存溢出。
对于ANSI字符串类型,字符数目和字节数目是一致的。但是如果字符串的类型是UNICODE,字节数目就是字符数目的两倍。如果在应该传递字符数目的地方传递了字节数目,就可能导致缓存溢出。
例子:
#include<tchar.h>
#include<windows.h>
void f( HINSTANCE hInst, UINT uID )
{
TCHAR buff[128];
if ( LoadString ( hInst, uID, buff, sizeof buff ) ) // warning C6057
{
// code...
}
}
LoadString期望的参数是字符串缓存的字符数目。
修补:正确计算数组中元素的个数。
LoadString ( hInst, uID, buff, (sizeof buff)/(sizeof buff[0]) )
警告C6201: buffer overrun for <variable>, which is possibly stack allocated: index <name> is out of valid index range <min> to <max>,数组索引的越界可能导致缓存溢出。
例子:
void f()
{
int buff[25];
for (int i=0; i <= 25; i++) // i exceeds array bound
{
buff[i]=0; // initialize i
// code ...
}
}
修补:确保数组索引不越界。
警告C6202: buffer overrun for <variable>, which is possibly stack allocated, in call to <function>: length <size> exceeds buffer size <max>,使用缓存区时,给出的长度超出缓存区长度的最大值。
例子:
#include <memory.h>
void f( )
{
int intArray[5];
char charArray[5];
memset ((void *)charArray, 0, sizeof intArray);
// code ...
}
修补:正确的缓存长度。这里sizeof intArray应该是sizeof charArray。
警告C6204: possible buffer overrun in call to <function>: use of unchecked parameter <variable>,使用缓存区时,直接使用未经检查的参数可能导致缓存溢出。
例子:
#include<string.h>
void f(char *pCh)
{
char buff[10];
strcpy(buff, pCh);
}
pCh的值没有验证就直接使用了。
修补:验证传入的参数。加入检查:if (strlen(pCh) >= sizeof buff) return;
限於篇幅,我们这里无法一一列举所有和安全缺项紧密相关的重要警告信息,例如C6327,C6383,C6386等等。有关Prefast输出警告信息的详细列表,可以参见Code Analysis for C/C++ Warnings【2】。
- 静态代码分析工具的局限
静态代码分析工具是SDL(安全软件开发周期)提出的编码实践中非常重要的一环。但是,现实中不存在任何一种工具可以一下子解决所有的安全问题。静态C/C++代码分析工具是不可能发现代码中的所有安全漏洞的。就像GS和DEP一样,它也存在自身的局限性。
由于程序控制流和数据流的复杂程度,静态代码分析工具不可避免的存在:
- 漏报:我们上面举出的代码例子都是非常简单的。随着代码复杂度的增大,尤其是跨函数之间逻辑和数据的交互关系,往往给静态代码分析带来极大的困难。
- 误报:分析信息的不完备,会导致误报警告信息。
- 总结
Prefast是微软提供的针对C/C++程序的静态代码分析工具,可以有效的检测代码中存在的安全缺陷。强烈建议在开发过程中,采用Prefast或其他类型的静态代码分析工具,以降低程序中引入(安全)缺陷的可能。
- 参考文献
- PREfast Step-by-Step,http://www.microsoft.com/whdc/DevTools/tools/PREfast_steps.mspx,Microsoft http://msdn2.microsoft.com/en-us/library/ms680339(VS.85).aspx
- Code Analysis for C/C++ Warnings,http://msdn2.microsoft.com/en-us/library/a5b9aa09.aspx,Microsoft
- MS08-014 : The Case of the Uninitialized Stack Variable Vulnerability, http://blogs.technet.com/swi/archive/2008/03/11/the-case-of-the-uninitialized-stack-variable-vulnerability.aspx, SWI, Microsoft