5.5 寻找堆栈溢出的挑战
当然,编写安全应用程序的最好方法是编写不带缺陷的软件。即便这是可能的,也仍然存在大量缺陷且可能存在安全漏洞的遗留代码——例如,倾向于各种缓冲区溢出的 “b?”。有多种工具来审核代码并从细节中找出可能存在的溢出。
每一个程序都是可利用的,或者用它的源代码,或者仅仅作为一个二进制。显然,对于寻找产生溢出的缺陷,这种数据需要完全不同的方法。源代码审核工具根据它们的用途可以被划分为几个种类:
● 词汇静态代码分析器 这些工具对于它们在源代码中所看到的通常有“坏”模式集。它们经常只是找一些被频繁滥用的函数的情况,比如gets()函数。这些工具可以像grep一样简单或者更加复杂,比如RATS (www.securesoftware.com/ download _rats.htm)、ITS4 (www.cigital.com/its4/)和Flawfinder (www.dwheeler.com/flawfinder/)。
● 语义静态代码分析器 这些工具不仅可以查找坏掉的函数的“种类”情况,而且可以考虑上下文。例如,可以规定一个缓冲区是64字节长。那么如果程序中其他的地方置入了禁止入内的元素,工具就会将这种情况作为一个可能的缺陷报告出来。SPLINT程序(www.splint.org) 是这种类型的工具之一。编译器警告也是一个很好的参考。
● 静态源代码分析的人工智能或学习引擎 Application Defense Developer软件对于超过13种的语言,可以通过多种方法来识别源代码问题。通过词汇识别和语义(也叫作前后关系)分析相结合,以及一个专家学习系统,这种攻击可以被识别出来。更多关于源代码安全组的信息可以在www.applicationdefense.com找到。
● 动态(执行时间)程序追踪器 这些是用于探测内存漏洞的调试程序,但是它们探测各种缓冲区溢出也是非常敏捷的。这些工具包括Rational Purify (http://www-306.ibm.com/software/awdtools/purify/)、Valgrind (http://valgrind.kde
.org/)和ElectricFence (http://perens.com/FreeSoftware/)。
二进制审核是一个更复杂、更不发达的领域。主要方法包括:
● 有错误植入的黑盒子测试和压力测试,也叫作fuzzing Fuzzing是一种检测者使用设计好的脚本量给程序提供大量大小和结构都不相同的输入的方法。它通常可用于指定这种输入应该如何构造,以及根据程序的行为工具应该如何发生变化。
● 反转工程学 这个进程需要将二进制代码反编译到汇编语言列表中,如果可能的话,将反编译到高级语言中。第二个任务在C/C++程序中更加复杂,但是对于Java这样的语言就相当简单。 尽管Java并不会受到缓冲区溢出的困扰。
● 特定缺陷二进制审核 这个方法包括一个分析器应用程序,读取编译好的程序并根据某些提示进行扫描,以找到缓冲区溢出。它可以被当作类似于源代码的词汇和语义分析,仅仅在汇编级别中。这个范围内最广为人知的程序是Bugscan(www.logiclibrary.com /bugscan.html)。
现在来回顾一下这些技术中的一部分是如何用于查找可能的堆栈溢出的。
5.5.1 词汇分析
仅用grep就可以做最简单的词汇分析。首先,找到所有固定长度的字符串缓冲区:
[root@gabe book]# grep -n 'char.*/[' *.cook]# grep -n 'char.*/['
bof.c:6: char buffer[8]; /* 一个8字节的字符型缓冲区 */
exploit.c:5:char shellcode[] =
exploit.c:32: char buffer[2048];
offbyone.c:5: char buffer[256];
offbyone.c:11: main(int argc, char *argv[])
pointer.c:4:int main(int argc, char *argv[])
pointer.c:9: char buffer[128];
stack-1.c:9: char buffer[15]="Hello buffer!"; /* 一个15字节的字符型缓冲区 */
stack-2.c:17: char buffer[15]="Hello World"; /*一个10字节的字符型缓冲区 */
stack-3.c:13: char buffer[8]; /* 一个8字节的字符型缓冲区 */
stack4.c:13: char buffer[8]; /*一个8字节的字符型缓冲区 */
然后为本章之前所列出的不安全的函数查找源。比如,用一些前面的例子:
[blah]$ grep –nE'gets|strcpy|strcat|sprintf|vsprintf|scanf|sscanf|
fscanf|
vscanf|vsscanf|vfscanf|getenv|getchar|fgetc|get|read|fgets|strncpy|
strncat|snprintf|vsnprint' *.c
bof.c:14: fread( buffer, sizeof( char ), 2048, badfile );
stack3.c:15: strcpy(buffer,"AAAAAAAAAAAAAAAAAAAA");
stack4.c:21: fread( buffer, sizeof( char ), 1024, badfile );
这个列表可以确切地找出一些(而非全部)易受攻击的函数。
当然,并非所有的这些结果都必然会导致溢出(在实际的例子中,只有一小部分是可攻击的错误),但是这至少是进一步探测的起点。接着,可以回顾已发现的实例,密切关注gets、strcpy、strcat和sprintf等函数。常见的错误包括用strncat来复制一个超过缓冲区/数组限度的空字节,或者用strncpy式的字符串好像它们是以空字符终止(这没必要是真的)的。strcat和strcpy理论上应该只能用于静态字符串,这些静态字符串预先被分配了空间,包括拖尾的零字节的空间。
可能存在的缺陷的另一个明显的标志是各种DIY字符串复制函数。如果您看到一些像my_strcpy这样的函数,就弄懂它!用数学方法做,并检测当一个零字节被加到字符串末端时,它是不会超过缓冲区1个字节的,就像:
bufer[sizeof(buffer)-1] = '/0'
而不是像:
bufer[sizeof(buffer)] = '/0'
而如果一个程序有gets的实例,它是易受攻击的——修补它(对于一个输入循环用合适的检查方法改变gets)或者有些人会利用它。
刚才描述的进程可以通过用一些“steroids上的grep” 工具,也叫做词汇分析器,而变得简单一些。下面就是来自Flawfinder (www.dwheeler.com/flowfinder)的输出:
[root@gabe book]# flawfinder stack-3.c
Flawfinder version 1.26, (C) 2001-2004 David A. Wheeler.
Number of dangerous functions in C/C++ ruleset: 158
Examining stack-3.c
stack-3.c:13: [2] (buffer) char:
Statically-sized arrays can be overflowed. Perform bounds checking,
use functions that limit length, or ensure that the size is larger than
the maximum possible length.
stack-3.c:15: [2] (buffer) strcpy:
Does not check for buffer overflows when copying to destination.
Consider using strncpy or strlcpy (warning, strncpy is easily misused).
Risk
is low because the source is a constant string.
Hits = 2
Lines analyzed = 29 in 0.74 seconds (118 lines/second)
Physical Source Lines of Code (SLOC) = 23
Hits@level = [0] 0 [1] 0 [2] 2 [3] 0 [4] 0 [5] 0
Hits@level+ = [0+] 2 [1+] 2 [2+] 2 [3+] 0 [4+] 0 [5+] 0
Hits/KSLOC@level+ = [0+] 86.9565 [1+] 86.9565 [2+] 86.9565 [3+] 0 [4+]
0 [5+] 0
Minimum risk level = 1
Not every hit is necessarily a security vulnerability.
There may be other security vulnerabilities; review your code!
正如所看到的,它并不是非常精确。其他类似的免费工具还包括RATS (www.securesoftware.com/rats.php)和ITS4 (www.cigital.com/its4)。词汇工具一般说来是不精确的,因为它们只能找出最简单的错误——比如gets()的使用。例如,它们不能从缓冲区被定义的地方到向其中复制东西时的地方一直追踪它的大小;这就是所谓的语义分析开始发挥作用的地方。
5.5.2 语义分析器
有这样一种类型的分析器,曾经用过它但不了解它:C编译器。例如,如果用–Wall选项运行GCC,它会侦查资料,如没有用过的变量或者显而易见的内存分配问题,但是它对探测堆栈缓冲区溢出没有太大作用。还有一些尝试工作,例如,GCC更多地关注可能的堆栈溢出,但是它们还不能成为主流的发展趋势。
这里只有最简单的检查。如果编译以下程序:
#include <stdio.h>
int main (void)
{
char buffer[10];
printf("Enter something: ");
gets(buffer);
return 0;
}
会得到下面的输出:
#gcc –o gets gets.c
/tmp/ccIrG9Rp.o: In function `main':
/tmp/ccIrG9Rp.o(.text+0x1e): the `gets' function is dangerous and should
not be used.
Splint (www.splint.org)是相当智能的。它能检查“正常”的源代码,但是当代码用特殊标记标注,通知检查者某种变量或者参数必须以空字符终止或者限制长度的时候,它的工作效果是最好的。即使没有这些标注,它也会侦测到可能的缓冲区溢出:
[root@gabe book]# splint offbyone.c +bounds-write -paramuse
-exportlocal -retvalint -exitarg -noret
Splint 3.0.1.7 --- 24 Jan 2003
offbyone.c: (in function func)
offbyone.c:8:18: Possible out-of-bounds store:
buffer[i]
Unable to resolve constraint:
requires i @ offbyone.c:8:25 <= 255
needed to satisfy precondition:
requires maxSet(buffer @ offbyone.c:8:18) >= i @ offbyone.c:8:25
A memory write may write to an address beyond the allocated buffer.
(Use
-boundswrite to inhibit warning)
Finished checking --- 1 code warning
[root@gabe book]#
5.5.3 应用程序保护
这一部分阐明了缓冲区溢出是如何被修补的,以及在修补旧的缺陷时,新的缺陷是如何被引入的。这里将检查两种情况:一个是在OpenBSD FTP软件中的off-by-one缺陷,一个是在Apache1.3.31和1.3.33中的本地溢出。
5.5.4 OpenBSD 2.8 ftpd的off-by-one错误
2000年,在OpenBSD分布机上的FTP软件中用于处理目录名单的代码部分内,发现了一个缓冲区溢出。易受攻击的代码部分如下所示(/src/libexec/ftpd/ftpd.c):
replydirname(name, message)
const char *name, *message;
{
char npath[MAXPATHLEN];
int i;
for (i = 0; *name != '/0' && i < sizeof(npath) - 1; i++, name++) {
npath[i] = *name;
if (*name == '"')
npath[++i] = '"';
}
npath[i] = '/0';
reply(257, "/"%s/" %s", npath, message);
}
在<sys/param.h>中,MAXPATHLEN被定义为1024字节。这里for()循环正确地从变量i跳跃到< 1023,这样当循环结束时,超过npath[1023]的字节不用写/0。然而,因为i在嵌套中被增加,正如++i,它可以等于1024,且npath[1024]超过已分配的缓冲区空间。因而一个空字节被写入npath[1024],覆盖EBP的最低有效位字节。这种情况可以作为off-by-one溢出被攻击。可以通过改变逻辑来修补缺陷:
replydirname(name, message)
const char *name, *message;
{
char *p, *ep;
char npath[MAXPATHLEN];
p = npath;
ep = &npath[sizeof(npath) - 1];
while (*name) {
if (*name == '"' && ep - p >= 2) {
*p++ = *name++;
*p++ = '"';
} else if (ep - p >= 1)
*p++ = *name++;
else
break;
}
*p = '/0';
reply(257, "/"%s/" %s", npath, message);
}
p和ep两个指针的使用保证了只有在未到达缓冲区npath[1023]的末端时才能插入结束引语标记。指针p也总是小于ep,因而也不会比&npath[sizeof(npath)]–1大,因此当
*p='/0';
被执行时,这个空字节永远不会超过已分配的空间。
5.5.5 Apache htpasswd缓冲区溢出
近来在Bugtraq和Full Disclosure列表中有一个名为“local buffer overflow in htpasswd for apache 1.3.33 not fixed in 1.3.31”的公告,公告的作者注意到apache 1.3.33中的htpasswd.c可能易受本地缓冲区溢出的影响,并提供了它的补丁(这并非官方补丁)。有问题的代码是:
static int mkrecord(char *user, char *record, size_t rlen, char *passwd,
int alg)
{
char *pw;
char cpw[120];
char pwin[MAX_STRING_LEN];
char pwv[MAX_STRING_LEN];
char salt[9];
...
<skipped>
...
memset(pw, '/0', strlen(pw));
/*
* 检查一下,看看缓冲区是否足够大到可以容纳用户名、无用信息和分隔符。
*/
if ((strlen(user) + 1 + strlen(cpw)) > (rlen - 1)) {
ap_cpystrn(record, "resultant record too long", (rlen - 1));
return ERR_OVERFLOW;
}
strcpy(record, user);
strcat(record, ":");
strcat(record, cpw);
return 0;
}
正如所看到的,这个代码包含一个“坏”函数strcpy()和strcat()的实例。在这个特例中,这两个函数可能是可利用的,也可能不是(这将留作练习)。上面提及的公告的作者后来提供了它的补丁,把函数strcpy()变为strncpy():
--- htpasswd.orig.c 2004-10-28 18:20:13.000000000 -0400
+++ htpasswd.c 2004-10-28 18:17:25.000000000 -0400
@@ -202,9 +202,9 @@
ap_cpystrn(record, "resultant record too long", (rlen - 1));
return ERR_OVERFLOW;
}
- strcpy(record, user);
+ strncpy(record, user,MAX_STRING_LEN - 1);
strcat(record, ":");
- strcat(record, cpw);
+ strncat(record, cpw,MAX_STRING_LEN - 1);
return 0;
}
这个补丁仅是把两个函数都换成了它们的“安全”变量。不幸的是,这个代码又引入了另一个缺陷——对strncat()函数的最后一次调用使用了被复制字符串的错误长度。本函数中最后一个参量应该是被复制字符的数目——换句话说,是缓冲区中的剩余部分,而不是它的总长度。如果像在补丁中这样被留下,变量record仍然可能溢出。
本文转自
http://book.csdn.net/bookfiles/228/10022810714.shtml