1.要测试的程序源码
我们将一个占用32bytes空间的字符串拷贝到只分配了16bytes空间的字符数组中,由于字符数组所占空间不能满足要拷贝的字符串大小,因此会产生缓冲区溢出的情况。
#include <stdio.h>
#include <string.h>
#define ATTACK_BUFF_LEN 1024
char attackStr[ATTACK_BUFF_LEN] = "01234567890123456789========ABCD"; // 32 bytes
void overflow()
{
char buff[16]; // 最多容纳16 bytes
strcpy (buff, attackStr);
}
int main(int argc, char * argv[])
{
overflow();
return 0;
}
2. 运行时的栈
要了解缓冲区溢出攻击原理,我们必须先要了解C程序函数运行时的栈存储情况。我们以下面代码为例观察运行时栈的存储情况:
int cal(int a,int b)
{
int x = 2;
int y = 3;
return x*a + y*b;
}
void main()
{
int a = 1;
int b = 2;
int c = cal(a,b);
}
通过以上图示我们可以观察到,对于栈空间的分配是从高地址到低地址的,当某一函数要调用另一函数时首先要将要传入的参数逆序推入堆栈,然后将返回地址推入堆栈。
分析要测试程序可知,程序流程为 mian 函数调用 overflow函数,overflow 函数调用 strcopy 函数,运行时的栈存储情况应如下图所示:
3. 实验过程
本实验通过 Linux 命令行编译运行调试该程序,观察缓冲区的溢出情况
- 为了快速观察到实验结果,用以下命令关闭地址随机化机制:
sudo sysctl -w kernel.randomize_va_space=0
- 使用以下命令编译 attack_overflow.c 函数
gcc -fno-stack-protector -o attack_overflow attack_overflow.c
其中 -fno-stack-protector 参数是用来防止 C 编译时添加防止缓冲区溢出的指令
- 使用以下命令对程序进行调试
gdb attack_overflow
会出现以下界面
4. 输入 disas 命令对 main 函数与 overflow 函数进行反汇编
(gdb) disas main
Dump of assembler code for function main:
0x08048428 <+0>: lea 0x4(%esp),%ecx
0x0804842c <+4>: and $0xfffffff0,%esp
0x0804842f <+7>: pushl -0x4(%ecx)
0x08048432 <+10>: push %ebp
0x08048433 <+11>: mov %esp,%ebp
0x08048435 <+13>: push %ecx
0x08048436 <+14>: sub $0x4,%esp
0x08048439 <+17>: call 0x804840b <overflow>
0x0804843e <+22>: mov $0x0,%eax
0x08048443 <+27>: add $0x4,%esp
0x08048446 <+30>: pop %ecx
0x08048447 <+31>: pop %ebp
0x08048448 <+32>: lea -0x4(%ecx),%esp
0x0804844b <+35>: ret
End of assembler dump.
(gdb) disas overflow
Dump of assembler code for function overflow:
0x0804840b <+0>: push %ebp
0x0804840c <+1>: mov %esp,%ebp
0x0804840e <+3>: sub $0x18,%esp
0x08048411 <+6>: sub $0x8,%esp
0x08048414 <+9>: push $0x804a040
0x08048419 <+14>: lea -0x18(%ebp),%eax
0x0804841c <+17>: push %eax
0x0804841d <+18>: call 0x80482e0 <strcpy@plt>
0x08048422 <+23>: add $0x10,%esp
0x08048425 <+26>: nop
0x08048426 <+27>: leave
0x08048427 <+28>: ret
End of assembler dump.
- 在 overflow 函数的汇编代码以下几段设置断点
0x0804840b <+0>: push %ebp
0x0804841d <+18>: call 0x80482e0 <strcpy@plt>
0x08048427 <+28>: ret
通过以下命令:
(gdb) b*(overflow+0)
Breakpoint 1 at 0x804840b
(gdb) b*(overflow+18)
Breakpoint 2 at 0x804841d
(gdb) b*(overflow+28)
Breakpoint 3 at 0x8048427
(gdb)
- 运行该程序,并通过以下指令显示即将执行的指令的汇编代码。
运行程序:
(gdb) r
显示即将执行指令的汇编代码:
(gdb) display/i $eip
1: x/i $eip
=> 0x804840b <overflow>: push %ebp
显示即将执行的指令我们可知,程序已经执行到了第一个断点处即 overflow 汇编指令的第一条 0x0804840b <+0>: push %ebp,我们观察此时的运行时的栈顶存储情况:
(gdb) x/x $esp
0xbffff04c: 0x0804843e
(gdb) x/i 0x0804843e
0x804843e <main+22>: mov $0x0,%eax
(gdb)
根据指令所显示的栈顶存储情况可知,当前栈顶的地址为 0xbffff04c ,其中所存储的内容为地址 0x0804843e ,我们通过查看 0x0804843e 地址,可以看到其中存储的是 main 函数在调用完 overflow 函数后所执行的第一条指令,由此我们可以得知该地址记录了调用完 overflow 的返回地址。此时运行时的栈存储情况如下图所示:
- 继续调试该程序
(gdb) c
Continuing.
Breakpoint 2, 0x0804841d in overflow ()
1: x/i $eip
=> 0x804841d <overflow+18>: call 0x80482e0 <strcpy@plt>
(gdb)
此时,程序已经开始调用 strcopy 函数了,我们观察运行时的栈存储情况:
(gdb) x/x $esp
0xbffff020: 0xbffff030
(gdb)
0xbffff024: 0x0804a040
(gdb)
strcopy函数传参为:strcpy (buff, attackStr),由于C语言默认将参数逆序推入堆栈,因此,src(全局变量attackStr的地址) 先进栈 (高地址),des(overflow()中buff的首地址) 后进栈 (低地址),与上面的地址相结合我们可以推测,此时栈顶地址为 0xbffff020 其中存储的内容为 0xbffff030 应该为 buff 变量的首地址,此栈顶地址为 0xbffff024 其中存储的内容为 0x0804a040 应该为 attackStr 变量的首地址,我们通过指令观察一下:
(gdb) x/i 0x0804a040
0x804a040 <attackStr>: xor %dh,(%ecx)
(gdb)
次栈顶中果然存储的是全局变量 attackStr 的首地址,此时我们可以的到运行时的栈存储状况如下图所示:
- 计算存储返回地址的首地址 0xbffff04c 与 buff 首地址 0xbffff030 的差值
(gdb) p 0xbffff04c-0xbffff030
$1 = 28
(gdb) p/x 0xbffff04c-0xbffff030
$2 = 0x1c
(gdb)
可以看到两地址之间的差值为28bytes,也就是说当 attackStr 所占的存储空间大于28bytes时其多余的部分就会将返回地址所覆盖,attackStr的长度为32字节,其中最后的4个字节为 ABCD
我们知道 attackStr 的首地址为 0x0804a040 通过下图计算我们可以得知其最后的四个字节为:0x44434241。因此我们可以得知因此,在执行strcpy(des, src)之后,返回地址由原来的 0x0804843e 变为 ABCD(0x44434241),即返回地址被改写。
(gdb) x/x 0x0804a040+0x1c
0x804a05c <attackStr+28>: 0x44434241
(gdb)
- 继续调试程序观察返回地址的变化:
(gdb) c
Continuing.
Breakpoint 3, 0x08048427 in overflow ()
1: x/i $eip
=> 0x8048427 <overflow+28>: ret
(gdb) x/x $esp
0xbffff04c: 0x44434241
(gdb)
通过指令的反馈我们可以看到,即将执行的指令为ret,执行ret等价于以下三条指令:
- eip的值=esp指针指向的堆栈内容
- 跳转到eip执行指令
- esp=esp+4
执行ret之前的esp的内容为 ABCD,即 0x44434241,可以推断执行ret后将跳到地址 0x44434241 去执行。由于 0x44434241 是不可访问的地址,因此发生段错误。esp=0x44434241,正好是 ABCD 倒过来,这是由于IA32默认字节序为little_endian(小端字节序,低字节存放在低地址)。
通过修改 attackStr 的内容(将 ABCD 改成期望的地址),,就可以设置需要的返回地址,从而可以将 eip 变为可以控制的地址,也就是说可以控制程序的执行流程。