转自:http://hi.baidu.com/david_jlu/blog/item/3f742b1b74284a1a8618bf80.html
/* DO NOT USE THIS FUNCTION!! There is no limit on how much it will read. */
下面让我们浏览一下gets的源码:
1 char * 2 gets(char *str){ 3 char *cp; 4 int c; 5 if ((stdin->flags & __SRD) == 0) 6 return NULL; 7 for (c = 0, cp = str; c != '\n'; cp++) { 8 if ((c = getchar()) == EOF) { 9 stdin->flags |= __SERR; 10 return NULL; 11 } 12 *cp = (char)c; 13 } 14 *--cp = '\0'; 15 return str; 16 } 一直读到‘\n’结束,看上去似乎没问题,但忽略了str的长度,如果输入的串超过str边界咋办? 所以要避免使用这个函数,取而代之用fget就可以有效的检查有没有越界: 1 char * 2 fgets(char *as, int n, FILE *f){ 3 int c; 4 char *s=as; 5 c = EOF; 6 while(n>1 && (c=getc(f))!=EOF){ 7 *s++=c; 8 --n; 9 if(c=='\n') break; 10 } 11 if(c==EOF && s==as 12 || ferror(f)) return NULL; 13 if(n) *s='\0'; 14 return as; 15 } 这是一个很典型的问题──缓冲区溢出(Buffer Overflow)。 1988年11月,许多组织不得不因为“Morris 蠕虫”而切断 Internet 连接,“Morris 蠕虫”使得 整个Internet的10%崩溃。2001年7月,一个名为“Code Red”的蠕虫病毒最终导致了全球 运行微软的IIS Web Server的300000多台计算机受到攻击。2003年1月,“Slammer”蠕虫利 用Microsoft SQL Server 2000中的一个缺陷,使得南韩和日本的部分Internet 崩溃,中断了 芬兰的电话服务,并且使得美国航空订票系统、信用卡网络和自动出纳机运行缓慢。所有这 些攻击都利用了缓冲区溢出的程序缺陷。为什么缓冲区溢出危害这么大呢?下面举一个例子: 1 void print_input(int a,int b) { 2 char str[3]; 3 gets(str); 4 puts(str); 5 } 6 int main(int argc, char * argv[]) { 7 print_input(1,2); 8 return 0; 9 }编译以后(gcc加-g)用gdb调试,设断点在gets后,直接continue到断点,看看当前状态
(gdb) bt
#0 print_input (a=1, b=2) at study.c:5
#1 0x080483e7 in main () at study.c:9
(gdb) p &a
$3 = (int *) 0xbfed95c0
(gdb) p &b
$4 = (int *) 0xbfed95c4
(gdb) p &str
$5 = (char (*)[3]) 0xbfed95b5
(gdb) x/3b 0xbfed95b5
0xbfed95b5: 0x41 0x42 0x00 #这里的‘1’和‘2’是我执行时输入的str
这里能看出栈区是从高地址向低地址延伸的,即高地址为栈底低地址为栈顶,当前内存状态:
0xbfed95b5 0xbfed95b6 0xbfed95b7 ………… 0xbfed95c0 0xbfed95c4
str[0] str[1] str[3] a b
中间还有8个字节,是什么呢?如果学过编译编译应该知道,肯定会有返回地址的,要不然执行
完print_input怎么返回main呢?!不信看看:
(gdb) x/4b 0xbfed95bc
0xbfed95bc: 0xe7 0x83 0x04 0x08 #这个就是返回地址0x080483e7
(gdb) x/4b 0xbfed95b8
0xbfed95b8: 0xd8 0x95 0xed 0xbf
(gdb) disassemble main #对main函数反汇编
Dump of assembler code for function main:
0x080483c2 <main+0>: lea 0x4(%esp),%ecx
0x080483c6 <main+4>: and $0xfffffff0,%esp
0x080483c9 <main+7>: pushl 0xfffffffc(%ecx)
0x080483cc <main+10>: push %ebp
0x080483cd <main+11>: mov %esp,%ebp
0x080483cf <main+13>: push %ecx
0x080483d0 <main+14>: sub $0x14,%esp
0x080483d3 <main+17>: movl $0x2,0x4(%esp)
0x080483db <main+25>: movl $0x1,(%esp)
0x080483e2 <main+32>: call 0x80483a4 <print_input>
0x080483e7 <main+37>: mov $0x0,%eax #函数执行完应该返回到这
0x080483ec <main+42>: add $0x14,%esp
0x080483ef <main+45>: pop %ecx
0x080483f0 <main+46>: pop %ebp
0x080483f1 <main+47>: lea 0xfffffffc(%ecx),%esp
0x080483f4 <main+50>: ret
End of assembler dump.
从上面可以看出,a变量左边紧挨着那个就是返回地址,即call 0x80483a4 <print_input>
的下一条指令,那还有一个空缺是什么呢?0xbfed95b8对应是堆栈指针sp,这个可有可无。
当前内存状态图:
0xbfed95b5 0xbfed95b6 0xbfed95b7 0xbfed95b8 0xbfed95bc 0xbfed95c0 0xbfed95c4
str[0] str[1] str[3] %sp ret地址 a b
现在的问题出在,gets根本不检查边界,倘若输入一个很长的串就会覆盖%sp和返回地址,
这就意味着cracker能够改写返回地址,当print_input完成时,它将返回──不过不是返回到
main函数
,而是返回到cracker想要执行的恶意代码。
附:C语言中的危险函数
函数 | 严重性 | 解决方案 |
gets | 最危险 | 使用 fgets(buf, size, stdin)。这几乎总是一个大问题! |
strcpy | 很危险 | 改为使用 strncpy。 |
strcat | 很危险 | 改为使用 strncat。 |
sprintf | 很危险 | 改为使用 snprintf,或者使用精度说明符。 |
scanf | 很危险 | 使用精度说明符,或自己进行解析。 |
sscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
fscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
vfscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
vsprintf | 很危险 | 改为使用 vsnprintf,或者使用精度说明符。 |
vscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
vsscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
streadd | 很危险 | 确保分配的目的地参数大小是源参数大小的四倍。 |
strecpy | 很危险 | 确保分配的目的地参数大小是源参数大小的四倍。 |
strtrns | 危险 | 手工检查来查看目的地大小是否至少与源字符串相等。 |
realpath | 很危险(或稍小,取决于实现) | 分配缓冲区大小为 MAXPATHLEN。同样,手工检查参数以确保输入参数不超过 MAXPATHLEN。 |
syslog | 很危险(或稍小,取决于实现) | 在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。 |
getopt | 很危险(或稍小,取决于实现) | 在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。 |
getopt_long | 很危险(或稍小,取决于实现) | 在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。 |
getpass | 很危险(或稍小,取决于实现) | 在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。 |
getchar | 中等危险 | 如果在循环中使用该函数,确保检查缓冲区边界。 |
fgetc | 中等危险 | 如果在循环中使用该函数,确保检查缓冲区边界。 |
getc | 中等危险 | 如果在循环中使用该函数,确保检查缓冲区边界。 |
read | 中等危险 | 如果在循环中使用该函数,确保检查缓冲区边界。 |
bcopy | 低危险 | 确保缓冲区大小与它所说的一样大。 |
fgets | 低危险 | 确保缓冲区大小与它所说的一样大。 |
memcpy | 低危险 | 确保缓冲区大小与它所说的一样大。 |
snprintf | 低危险 | 确保缓冲区大小与它所说的一样大。 |
strccpy | 低危险 | 确保缓冲区大小与它所说的一样大。 |
strcadd | 低危险 | 确保缓冲区大小与它所说的一样大。 |
strncpy | 低危险 | 确保缓冲区大小与它所说的一样大。 |
vsnprintf | 低危险 | 确保缓冲区大小与它所说的一样大。 |