这个教程试着向读者展示最基本的栈溢出攻击和现代Linux发行版中针对这种攻击的防御机制。为此我选择了最新版本的Ubuntu系统(12.10),因为它默认集成了几个安全防御机制,而且它也是一个非常流行的发行版。安装和使用都很方便。我们选择的系统是X86_64的。读者将会了解到栈溢出是怎样在那些默认没有安全防御机制的老系统上面成功的溢出的。而且还会解释在最新版本的Ubuntu上这些保护措施是如何工作的。我还会使用一个小例子来说明如果不阻止一个栈上面的数据结构被溢出那么程序的执行路径就会失去控制 。
尽管本文中使用的攻击方式不像经典的栈溢出的攻击方式,而更像是对堆溢出或者格式化字符串漏洞的利用方式,尽管有各种保护机制的存在溢出还是不可避免的存在。现在如果你还不懂这些,不要担心,我会在下面的文章中详细的讲解。
2. 使用的系统关于不同版本的ubuntu 系统中默认启用的安全控制机可以看这里:https://wiki.ubuntu.com/Security/Features
-----------------------------------
$ uname -srp && cat /etc/lsb-release | grep DESC && gcc --version | grep gcc
Linux 3.5.0-19-generic x86_64
DISTRIB_DESCRIPTION="Ubuntu 12.10"
gcc (Ubuntu/Linaro 4.7.2-2ubuntu1) 4.7.2
-----------------------------------
3. 经典的栈溢出首先让我们回到从前,一切都很简单,向栈上面复制草率的复制数据很容易导致程序的执行完全失控。可以看下面的例子(没有考了到许多保护机制).
-----------------------------------
$ cat oldskool.c
#include <string.h>
void go(char *data) {
char name[64];
strcpy(name, data);
}
int main(int argc, char **argv) {
go(argv[1]);
}
-----------------------------------
在测试之前,我们需要禁用系统的 ASLR ,你可以这么来做:
-----------------------------------
$ sudo -i
root@laptop:~# echo "0" > /proc/sys/kernel/randomize_va_space
root@laptop:~# exit
logout
-----------------------------------
在很老的机器上面也许还不存在这个包含机制。为了同时禁用掉其他的保护(主要是编译器生成的运行时栈检测代码) 我们可以这样来编译我们的例子:
$ gcc oldskool.c -o oldskool -zexecstack -fno-stack-protector -g
下面来看看这个示例程序,我们可以看到 我们在函数中在栈上面分配了64字节的缓冲区,然后把命令行的第一个参数复制到这个缓冲区里面。程序没有检测第一个参数的长度是不是大于64字节就直接调用strcpy 来复制数据了,众所周知,这样会导致栈溢出。 现在为了得到程序控制权限,我们需要知道这样一个事实,就是任意一个C函数在进入一个函数之前,都会把它即将执行的下一条指令的地址压到栈中(也就是call指令做的事情 把call的下一条指令压栈,这样函数就知道要返回哪个地址继续执行了)。我们把这个地址叫做函数返回地址或者叫 “已保存的指令的指针”。在我们的例子里面 返回地址就是我们在执行完我们的 go()函数后下一步要执行的那条指令的地址。这个地址就仅挨着我们的 name[64] 这个缓冲区。因为栈的工作方式(译者注:也就是栈是向低地址衍生的,也就是说最后进栈的保存在栈最低的地址处),如果用户的数据超过了缓冲区的长度,那么输入的数据就会覆盖掉函数的返回地址(译者注:因为往缓冲区里面写数据是从低地址向高地址写,所以当写完函数分配缓冲区,下面的4个字节就是函数的返回地址了)。函数返回的时候就会跳到错误的地址处去执行,一个攻击者就能通过把他们要执行的机器码复制到一个缓冲区中,然后把返回地址指向那个缓冲区来劫持程序的执行流程。然后攻击者就可以随意的让程序做一些他们想做的事情,也许是因为好玩也行是为了利益。废话不多说,让我来给你们演示下吧 如果你看不懂下面使用的命令,你可以在 看一下GDB 的使用教程。
-----------------------------------
$ gdb -q ./oldskool
Reading symbols from /home/me/.hax/vuln/oldskool...done.
(gdb) disas main
Dump of assembler code for function main:
0x000000000040053d <+0>: push %rbp
0x000000000040053e <+1>: mov %rsp,%rbp
0x0000000000400541 <+4>: sub $0x10,%rsp
0x0000000000400545 <+8>: mov %edi,-0x4(%rbp)
0x0000000000400548 <+11>: mov %rsi,-0x10(%rbp)
0x000000000040054c <+15>: mov -0x10(%rbp),%rax
0x0000000000400550 <+19>: add $0x8,%rax
0x0000000000400554 <+23>: mov (%rax),%rax
0x0000000000400557 <+26>: mov %rax,%rdi
0x000000000040055a <+29>: callq 0x40051c
0x000000000040055f <+34>: leaveq
0x0000000000400560 <+35>: retq
End of assembler dump.
(gdb) break *0x40055a
Breakpoint 1 at 0x40055a: file oldskool.c, line 11.
(gdb) run myname
Starting program: /home/me/.hax/vuln/oldskool myname
Breakpoint 1, 0x000000000040055a in main (argc=2, argv=0x7fffffffe1c8)
11 go(argv[1]);
(gdb) x/i $rip
=> 0x40055a : callq 0x40051c
(gdb) i r rsp
rsp 0x7fffffffe0d0 0x7fffffffe0d0
(gdb) si
go (data=0xc2 ) at oldskool.c:4
4 void go(char *data) {
(gdb) i r rsp
rsp 0x7fffffffe0c8 0x7fffffffe0c8
(gdb) x/gx $rsp
0x7fffffffe0c8: 0x000000000040055f
-----------------------------------