静态代码安全检查工具是一种能够帮助程序员自动检测出源程序中是否存在安全缺陷的软件。它通过逐行分析程序的源代码,发现软件中潜在的安全漏洞。本文针对 C/C++语言程序设计中容易存在的多种安全问题,分别分析了问题的根源,给出了具体可行的分析及检测方法。最后通过对静态代码安全检查工具优缺点的比较,给出了一些提高安全检查效果的建议。
软件漏洞的出现,除了程序员缺少编写高质量安全程序的意识外,编程语言本身的不安全性也使得程序员更容易在无意中编写出存在安全问题的代码。在众多编程语言中,C/C++语言是目前公认的最容易引起安全问题的语言,黑客往往就利用这些安全问题产生的漏洞来绕过安全策略,以达到网络攻击的目的。针对这种情况,在程序运行前,采用静态代码安全检查工具对源程序进行安全检查是一种很有效的方法。它面对的是问题本身而非征兆,所以有时它比动态监测更有效。
1 C/C++ 语言静态代码安全检查工具
静态代码安全检查工具的工作类似于软件测试中的静态测试。它们之间的不同之处在于软件测试是为了找出软件中的错误,而静态代码安全检查的主要目的是解决软件的安全问题,并以查找软件中容易被黑客利用的漏洞为目标。它的基本工作原理是:从前至后逐行读入源程序代码,定位可能的嫌疑,再逐步进行深入分析,直至得到确定的分析结果,最后根据不同的分析结果,依安全策略对其进行处理并报告处理结果。
2 C/C++ 语言静态代码安全检查原理分析
静态代码安全检查的工作过程是:首先读入不安全函数列表,然后先对欲扫描的源程序进行词法分析。根据不安全函数列表,有些函数会被找出来,并做相应的处理;对于需要进行语法分析的函数再做进一步语法分析,确定这些函数是否会引起安全问题,并做相应的处理。重复此过程直到分析完所有源程序,最后报告结果。
具体地说,针对不同类型的安全问题有以下几个方面的分析处理方法。
2.1 缓冲区溢出问题的解决途径
缓冲区溢出问题是目前软件中存在的最普遍的问题。从目前来看,找出了缓冲区溢出问题也就找出了绝大部分的安全问题。缓冲区溢出的最根本原因就是未检查动态缓冲区边界,当源数据长度超出缓冲区长度时产生溢出。要静态地分析出源程序代码中是否存在此类问题,首先就要计算出缓冲区长度。
针对缓冲区的不同类型,可有以下 4 种方法计算缓冲区长度:
(1) 字符串常量:如“satecode scan”,其缓冲区长度为字符数+ 1。它有两种存在方式,一是直接在函数中使用,另一种是出现在变量定义或赋值语句中。不论哪种都可以通过语法分析回归法计算;
(2) 静态缓冲区:如buf[1024],buf[MAX_len],对于前一种表示方式,可以直接根据 1024 计算缓冲区的大小。对于第 2 种情况,通过检查宏定义、常量定义,一般就可以确定缓冲区的大小;
(3) 动态缓冲区:动态缓冲区可以是通过new 进行分配,也可以通过 alloc、malloc 进行分配。对于前一种分配方法,需要考虑所分配的基类型,然后计算缓冲区长度。对于后一种分配方式,可直接通过表达式计算缓冲区大小;
(4) 指针引用:通过引用指针或数组下标,从而引用预先设好的缓冲区的一部分。对于这种情况,先用以上方法求出基缓冲区的大小,再通过表达式求值计算出其偏移。
除此之外,预填数据方法也可以检测出缓冲区溢出。例如:对于 strcpy (buf1, buf2),在调用前加入以下语句:memset
(buf1,'A',sizeof(buf2))。如果 buf2 比buf1 大,则在调试阶段发生缓冲区溢出。
此方法的特点是对于可能引起缓冲区溢出的函数,在调试阶段(debug),预填满源缓冲区数据,使溢出发生在调试阶段, 避免将不安全因素带到运行期。
具体地把C/C++中可能引起缓冲区溢出的函数分为以下几类,针对不同类的函数分别采用不同的分析与处理。
3.1.1 两个参数的字符串拷贝函数
此类的函数包括 strcpy、_mbscpy、strcat、wcscat 等。其特点是函数有两个参数,从一个参数向另一个参数拷贝字符串,当目标参数缓冲区长度小于源参数缓冲区长度时,发生缓冲区溢出。处理此类函数采用数据流跟踪的方法检查缓冲区长度。
例如下面一段程序:
(1) void transdata(char *str)
(2) {char buffer[24];
(3) strcpy(buffer,str); /*把buf[256] 中的内容拷到buffer[24] 里去*/
(4)
(5) char buf [256];
(6) for(i=0;i<255;i++)/*往buf[256]里写入 255 个M*/
(7) buf[i]='M';
(8) buf[255]=0;
(9) transdata (buf);
此段程序中,当程序调用 transdata(buf)时就会发生溢出。检查这个错误,首先在遇到 strcpy(buffer,str) 时,检查目的参数buffer,并找到在此之前出现的(第2 行),检测出它的长度应是24 个字节;接着,再检查源参数 str,发现它是经第 1 行(char *str)→ 第 9 行(buf)-->第 5 行(char buf[256])的数据流得到的,并且检测出其长度是 256 字节。此时,已经能初步确定可能产生溢出了。也可以报告存在缓冲区溢出的位置(第 3 行)。但是,如果要更精确地定位,则需要利用语法分析器继续从第 5 行定义数组buf[256]开始检查所有路径,这样就可以检测出在调用transdata(buf)时确实会发生溢出,最后报告造成溢出的路径(第 3、2、5、6、7、8 行)。
3.1.2 3 个参数的字符串函数
这类函数包括 memcpy、strncpy、_mbsncpy、strncat、wcsncat等。其特点是有 3 个形式参数,例如memcpy(buf,"M", count),当 count 说明的字节数大于 buf 的缓冲区长度时,发生溢出。处理此类函数同样采用数据流跟踪的方法。在前面这个例子中,就是要检查并比较 count 的大小是否超过了buf 的缓冲区的大小。
3.1.3 格式化控制的字符串处理函数
这类函数有两种不同的情况:一种包括 printf、fprintf。其特点是函数不能确定数据参数在什么地方结束,因此缓冲区溢出情况一般发生在说明的参数的个数与格式化字符串不匹配时。此类问题要分析格式化字符串与参数是否匹配。
例如下面的一段程序:
(1)int data=1234567890;
(2) printf("data=%d%n\n",data, &data); /* 显示data 的值, 并把显示字符的长度写到变量data 中*/
(3) printf("data=%d\n",data);此程序正常结果是: data=1234567890
data=10
若第2 行写成printf("%d%n\n",data),执行时就会把显示内容的长度写到变量data 存储那个数值所在的指向的内存里[2]。当然,此地址不能被访问。但如果精心设计这个输入值,就会造成缓冲区溢出攻击。分析时,当遇到 printf 时,先用词法分析器分析并记录两个双引号中含有“%”且非“%%”的个数,然 后分析其参数的个数是否与之匹配,就能发现此类问题。
另一类函数包括sprintf、swprintf,它通过格式化字符串进行输出,当字符串缓冲区小于格式化串所说明的长度时,会发生缓冲区溢出。此类问题要检查格式化字符串的动态长度, 并与实际区长度进行比较。
3.1.4 向缓冲区中读入字符串函数
其中一类函数包括 scanf、fscanf、sscanf 等。当说明的缓冲区小于实际读入的字符串长度时,发生缓冲区溢出。分析处理方法:跟踪说明缓冲区的参数在程序中的出现,检查其缓冲区长度,并提示用户使用带有限制输入字符长度的格式化字符串。如程序段:
char buffer[20]; scanf("%s",& buffer);
检查时,先分析出&buffer 所指向的缓冲区的大小,并发现%s 未受限制,说明可能产生溢出。接着采用提示用户使用scanf("%20s",& buffer)替换的处理方法[3]。
另一类函数包括 fgets、fgetc、gets、getc。如果限制读入数据大小的参数值超出目标缓冲区长度就会发生溢出。处理类函数采用数据流跟踪方法检查这两个数值。如 fgets (char *sint n,FILE *stream),此函数的功能是从输入流 stream 中读入字符,并存到 s 串中。这里,要分析 s 与 n 在程序中的定义,检查 s 的长度是不是小于n 的值。需要说明的是,强烈建议不使用 gets、getc,而是用fgets、fgetc 替代。
3.2 关于内存泄漏问题的解决途径
内存泄漏的原因是动态分配了内存,但没有释放,造成分配的内存不能再被使用。一般的情况是堆内存的泄漏,另外还包含系统资源的泄漏,比如核心态HANDLE、GDI Object、SOCKET、Interface 等[4]。
由于泄漏发生在程序运行时,因此要检测出内存泄漏问题不太容易。静态安全检查可以采用控制流跟踪的方法,通过分析所有可能的路径,以达到发现内存泄漏的目的,适用于new/delete、alloc/free、malloc/free、GlobalAlloc/GlobalFree 等函数。根据内存泄漏发生方式的不同,可以分以下几种情况分析。
3.2.1 忘记释放内存造成的内存泄漏
动态分配内存后,没有调用 delete 或free 等释放。这种内存泄漏只要分析所有路径是否存在只使用new、malloc 分配内存,但没有用 delete 或free 释放内存的情况即可。
3.2.2 delete 或free 的调用方法不正确造成的内存泄漏
此类问题比较常见,造成的后果也比较严重。处理此类问题采用语法分析的方法进行路径分析。
例如下面一段程序:
void function(int size)
{
char* p= new char[size]; if( size>=512 ){
printf“( Error!”); return;
}
//using the string pointed by p; delete p;
}
显然程序可能没到出口处就结束了,这样就造成了内存泄漏。
检查这类问题可以先采用 3.2.1 中的方法分析所有路径, 然后再检查是否存在没有使用 delete 或 free 释放内存就结束程序的情况。
3.2.3 隐式内存泄漏
此类问题比较特殊,程序在运行过程中不停地分配内存, 但是直到结束的时候才释放内存。严格地说这里并没有发生内存泄漏,因为最终程序释放了所有已申请的内存。但是对于一个服务器程序,如果不及时释放内存可能导致最终耗尽系统的所有内存。检查此类问题要从释放内存空间处入手,检查释放过程是否只在调用析构函数时出现。如果是,则再分析程序是否会出现不调用析构函数而再分配内存的路径,从而发现是否会发生隐式内存泄漏。需要说明的是,由于此类问题一般在异常的情况发生时出现,程序本身又往往是正常的,所以不太容易静态地检查出来,因此这种方法也只能分析出已知的特殊情况。
3.3 空指针引用问题的解决途径
此类函数包括 open、fopen。所谓空指针就是没有指向任何合法的存储空间的指针。如果对打开文件的过程未做检查,在打开文件失败的情况下,就会产生空指针,并被黑客利用。
例如下面一段程序:
FILE *in=null, *out=null;
out=fopen("\\Test\testnum1.txt","r"); in=fopen("\\Test\testnum2.txt", "w"); while(! feof(out))
{fputc(fgetc(out),in)};
fopen 打开文件失败时,就会产生空指针。另外,如果文件打开后不检查文件属性,文件属性也容易被修改[5]。分析处理此类问题采用语法分析的方法,检查是否对文件打开过程进行了严格检查。
3.4 随机数问题的解决途径
在C/C++程序中涉及许多随机数的选取,但系统提供的rand 是一个伪随机数。其内部的实现使得根据给定的种子产生的输出值可能重复,从而造成了随机数可能被黑客猜到的结果。分析处理此类函数的方法是:建议用其它的健壮的数据源取代。可能产生伪随机数的函数包括rand、drand48、erand48、jrand48、lrand48、mrand48、random 等。
4 结 论
C/C++ 语言的静态代码安全检查工具能够在程序运行之前发现源程序潜在的安全漏洞,大大降低了出现安全漏洞的概率,对提高程序的安全性具有重要意义。但是代码检查非常耗费时间,而且静态代码安全检查需要知识和经验的积累。对较复杂的问题,静态代码安全检查工具很可能检查不出来。所以,一方面强烈建议程序员时刻保持高质量程序设计的思想,进行主动防错设计。另一方面,对那些比较重要的系统, 提醒使用多种安全检查手段相结合方法。例如,在静态检查的基础上,再采用动态资源监控、漏洞扫描、入侵检测等方法以确保系统安全。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。