2012年4月23日星期一

关于缓冲区溢出攻击的一次小测试

我们写C/C++程序的时候,用scanf,gets,strcpy什么的,编译总会有warning,这个函数是不安全的云云。到现在才明白这个所谓的不安全是怎么回事,这个关于C的历史遗留问题真的有够严重的。。。
不安全的代码,产生的bug就不是bug那么简单了,而是exploit。这个buffer overflow attack (缓冲区溢出攻击)在安全界的知名度是不言而喻的,基本上属于最经典的漏洞之一。各种利用工具和shellcode何其多哉!

抱着亲身了解下缓冲区溢出攻击的过程,顺便体验下linux下强大的gdb的心态,就拿gets()写了个小程序练下手。

//attack.c
#include<stdio.h>
int main()
{
char s[8];
gets(s);
printf(s);
return 0;
}


生成的attack可执行文件用objdump反汇编:

080483f4 <main>:
 80483f4: 55                    push   %ebp
 80483f5: 89 e5                 mov    %esp,%ebp
 80483f7: 83 e4 f0              and    $0xfffffff0,%esp
 80483fa: 83 ec 20              sub    $0x20,%esp
 80483fd: 8d 44 24 18           lea    0x18(%esp),%eax
 8048401: 89 04 24              mov    %eax,(%esp)
 8048404: e8 fb fe ff ff        call   8048304 <gets@plt>
 8048409: 8d 44 24 18           lea    0x18(%esp),%eax
 804840d: 89 04 24              mov    %eax,(%esp)
 8048410: e8 0f ff ff ff        call   8048324 <printf@plt>
 8048415: b8 00 00 00 00        mov    $0x0,%eax
 804841a: c9                    leave  
 804841b: c3                    ret    
 804841c: 90                    nop
 804841d: 90                    nop
 804841e: 90                    nop
 804841f: 90                    nop





然后gdb attack
先尝试一下程序运行是否正确:

(gdb) run
Starting program: /home/shanks/Documents/attack 
hello
hello


输入小于8字节的时候,程序运行是OK的~


开始debug,先设置断点:

(gdb) break *0x80483f4      //设置在main程序入口处
Breakpoint 1 at 0x80483f4
(gdb) run
Starting program: /home/shanks/Documents/attack 
Breakpoint 1, 0x080483f4 in main ()


然后查看寄存器和栈帧情况:



可以得到的信息有,esp为0xbffff38c,ebp为0,eip指向main
注意下面的栈帧信息,进入main函数前保存的返值为 0x4f3683f3(这个到后面会被溢出修改)
单步执行,例行的ebp入栈,新ebp(0xbffff388)指向旧ebp(0)
查看属实:
(gdb) print $ebp
$3 = (void *) 0xbffff388
(gdb) print *(int*)($ebp)
$5 = 0

继续单步
and一句 最低位置0,其实我也不太清楚具体作用,猜测是alignment的原因,但也不排除是编译器为了阻碍溢出攻击故意让esp有不确定的偏移量?(这个可能性略低 = =)

anyway,现在esp为:
(gdb) print $esp
$6 = (void *) 0xbffff380
sub栈延伸后为0xbffff360



继续,将esp+18的地址放到eax中
(gdb) print /x $eax
$10 = 0xbffff378

这个其实就是栈中为char s[8]开辟的内存空间,其具体地址为37f - 378 (前面那段一样的0xbffff就省略了)刚好8字节。同时注意存放顺序,也就是,378对应的是s[0](首地址),379对应s[1],当然,以一般的内存视图看起来,这个是反序的

继续,将eax,也就是char数组首地址,放入esp所指位置。这里其实效果和push eax差不多,都是将gets的参数入栈,之后调用gets(),函数细节就略过了,ret之后,我们输入的字符串就会出现在以378开始的内存单元中。即char数组s[8]。但问题是,如果我们输入的字符串不止8字节呢??

(gdb) break *0x8048409
Breakpoint 2 at 0x8048409
(gdb) continue 
Continuing.
00001111222233334444     //输入20字节,加上后面的'\0'共21字节


然后可以预计,从378-37b 也就是S[0] 到 s[3] 都是0。然后37c-37f都是1,依次类推。则388-38b都是4,转换成十六进制ASCII码也就是34。问题来了!前面说道388处存放的是旧ebp(也就是0),这里将会被覆盖。也就是原ebp会丢失,破坏了栈的结构,而且这里不止ebp被破坏,返值eip的最低位也被换成0(也就是字符串结束符'\0')


看看实际结果:

(gdb) print /x ($ebp)
$16 = 0xbffff388
(gdb) print /x *(int*) ($ebp)
$18 = 0x34343434
(gdb) print /x *(int*)($ebp+4)
$24 = 0x4f368300

(gdb) continue 
Continuing.


Program received signal SIGSEGV, Segmentation fault.
0x4f34c8f8 in ?? ()




完全符合预测
可以看到,缓冲区溢出攻击是需要高精确度计算的攻击手段。实际应用中,必然没有俺这么容易就能利用的exploit。但是小小测试一下,对理解攻击手段,堆栈操作,构造特殊代码,编写shellcode还是有点帮助的~

没有评论:

发表评论