2008年8月26日星期二

使用GDB调试缓冲区溢出程序

今天在阅读完 Linux下缓冲区溢出攻击的原理及对策,后对文章中的一个例子进行了验证。但是对example2进行调试时,发现由于gcc版本不同,因此在自己的机器无法调试通过。通过GDB对该程序进行调试,最后找出了问题,过程如下:
例子中的源代码如下
1 int function(int a, int b, int c) {
2 char buffer[14];
3 int sum;
4 int *ret;
5
6 ret = buffer + 20;
7 (*ret) += 10;
8 sum = a + b + c;
9 return sum;
10 }
11
12 void main() {
13 int x;
14
15 x = 0;
16 function(1,2,3);
17 x = 1;
18 printf("%d\\n",x);
19 }

这段代码的原理,在原文中有解释,简单的将就是通过定义的buffer变量的地址,定位到保存function函数返回main函数时的返回地址的 位置,然后修改返回地址的值,将c源码中第17行x=1的赋值运算跳过。使最后x的输出为0。而在本机上没有成功的原因是由于function函数在调运 时,为变量分配空间时,由于编译器存在差异,因此分配的空间大小不同导致。那么如何能够知道准确的ret address的值呢?让我们使用gdb来进行调试。
首先编译源码
gcc -g -o example2 example2.c
gdb example2

(gdb) disassemble main
Dump of assembler code for function main:
0×0804840d : lea 0×4(%esp),%ecx
0×08048411 : and $0xfffffff0,%esp
0×08048414 : pushl 0xfffffffc(%ecx)
0×08048417 : push %ebp
0×08048418 : mov %esp,%ebp
0×0804841a : push %ecx
0×0804841b : sub $0×24,%esp
0×0804841e : movl $0×0,0xfffffff8(%ebp)
0×08048425 : movl $0×3,0×8(%esp)
0×0804842d : movl $0×2,0×4(%esp)
0×08048435 : movl $0×1,(%esp)
0×0804843c : call 0×80483c4
0×08048441 : movl $0×1,0xfffffff8(%ebp)
0×08048448 : mov 0xfffffff8(%ebp),%eax
0×0804844b : mov %eax,0×4(%esp)
0×0804844f : movl $0×804852c,(%esp)
0×08048456 : call 0×8048320
0×0804845b : add $0×24,%esp
0×0804845e : pop %ecx
0×0804845f : pop %ebp
0×08048460 : lea 0xfffffffc(%ecx),%esp
0×08048463 : ret
End of assembler dump.
(gdb) disassemble function
Dump of assembler code for function function:
0×080483c4 : push %ebp
0×080483c5 : mov %esp,%ebp
0×080483c7 : sub $0×28,%esp
0×080483ca : mov %gs:0×14,%eax
0×080483d0 : mov %eax,0xfffffffc(%ebp)
0×080483d3 : xor %eax,%eax
0×080483d5 : lea 0xffffffee(%ebp),%eax
0×080483d8 : add $0×14,%eax
0×080483db : mov %eax,0xffffffe8(%ebp)
0×080483de : mov 0xffffffe8(%ebp),%eax
0×080483e1 : mov (%eax),%eax
0×080483e3 : lea 0×7(%eax),%edx
0×080483e6 : mov 0xffffffe8(%ebp),%eax
0×080483e9 : mov %edx,(%eax)
0×080483eb : mov 0xc(%ebp),%eax
0×080483ee : add 0×8(%ebp),%eax
0×080483f1 : add 0×10(%ebp),%eax
0×080483f4 : mov %eax,0xffffffe4(%ebp)
0×080483f7 : mov 0xffffffe4(%ebp),%eax
0×080483fa : mov 0xfffffffc(%ebp),%edx
0×080483fd : xor %gs:0×14,%edx
0×08048404 : je 0×804840b
0×08048406 : call 0×8048330 <__stack_chk_fail@plt>
0×0804840b : leave
0×0804840c : ret
End of assembler

main+47行的call function指令,除了会将执行权交给function外,还会将function返回后执行的下一条指令的地址0×08048441压入栈中,这个是call指令自动执行的,同时通过function中第一条指令:

0×080483c4 : push %ebp

将寄存器ebp的值也压入栈中,

0×080483c5 : mov %esp,%ebp

将寄存器esp的值交给ebp

0×080483c7 : sub $0×28,%esp

完成空间申请等..

我们来观察寄存器的状态的变化

(gdb) break *0×080483c7

在call function后break一下

(gdb)r

(gdb) info registers

我们只关心esp和ebp的值:

esp 0xbfc53500
ebp 0xbfc53528

目前的堆栈数据结构为:

—————–
0×08048441 返回main时,所执行的下一条指令的地址
—————–
0xxxxxxxxx 原先ebp中的值
—————– <—该处的地址值为ebp中的值0xbfc53528
buffer
buffer
buffer <—该处的地址值在gdb中使用p &buffer能得知为0xbfc53516
…..
—————–

我们知道ebp保持的值和存放返回地址内存区域的差距是4个字节,我们可以通过gdb进行验证0xbfc3528+4处保存的内容是否是正确的返回地址:

(gdb) x 0xbfc5352c

0xbfc5352c: 0×08048441 没有问题

而&buffer的内存地址和ebp中的值相差为0×12,换算成十进制为18,加上ebp值和返回地址之间相差的4个字节。

因此需要将源码中第6行改为

ret = buffer + 22;

重新编译后,返回值为0,与目标相符

勘误:

正如Border所说, 第七行 (*ret) += 10;应改为 (*ret) += 7;

没有评论: