gets引发的血案

转自: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 低危险 确保缓冲区大小与它所说的一样大。

猜你喜欢

转载自blog.csdn.net/swust_long/article/details/7498171