Update: 2020-11-10
简介
Ret2dlresolve 是高级 ROP 技巧之一,世界著名的黑客杂志 phrack 在某一期提到过这个攻击技巧,该手段用于没有较好的 gadget、函数、利用链的情况。ret2dlresolve 主要利用了 linux 下的延迟绑定机制,通过修改解析字符串,我们就可以控制最终解析出的函数,从而获取任意函数执行的能力。
延迟绑定
这一攻击技巧的基础就是linux下的延迟绑定机制,所以首先我们来了解一下什么是延迟绑定。
程序员编写一个程序会导入各种各样的函数,例如我们编写一个 hello world C语言程序
1 |
|
程序员只编写了两句有意义的代码,其中 printf 函数不是我们自己写的,而是从 C语言标准库中导入的。当开发一个大型项目的时候,通常会从标准库中导入很多函数,而某些函数可能从来都不会被调用,为了增加程序的执行效率,linux 引入的延迟绑定这一概念,当程序第一次调用导入函数的时候才去 libc 中寻找对应的地址,并将地址写入 GOT 表中,那些没有被调用的函数就不绑定。已经绑定的函数再次调用则直接去 GOT 表中寻找就好了。
实现延迟绑定的主要函数是 _dl_runtime_resolve,它接受两个参数,分别是 linkmap 和 offset,这个函数我们放在下面讲,首先来了解一下 ELF 的格式信息。
ELF .dynamic 节
这个节包含关于动态链接的信息,PE 或者 ELF 在运行程序的时候都需要进行动态链接,因为源代码中可能调用了动态库中的一些函数,引用其他程序代码时,在编译或者链接阶段不能确定地址到底是什么,因为当程序加载到内存之后地址都是动态的,所以需要重新进行地址定位。
.dynamic 节在动态链接的 ELF 文件中大同小异,结构一般如下(从 IDA 中看)
1 | LOAD:08049F14 ; ELF Dynamic Information |
里面包含许多子节,我们主要关注 DT_STRTAB、DT_SYMTAB、DT_JMPREL,这三项,他们是指向其他节的指针,DT_STRTAB 指向 .dynstr,DT_SYMTAB 指向 .dynsym,DT_JMPREL 指向 .rel.plt。
.rel.plt 节
从 IDA 中,这个节的结构为
1 | LOAD:080482B4 ; ELF JMPREL Relocation Table |
每一行都是一个结构体,结构体定义为
1 | typedef struct |
第一个成员是指向 GOT 表的指针,第二个成员是 .dynsym 节的偏移。
.dynsym 节
从 IDA 中来看,这个节的结构如下
1 | LOAD:080481CC ; ELF Symbol Table |
这个节的每一行也代表着一个结构体,定义如下
1 | typedef struct |
针对导入函数来说,比较主要的是 st_name,它指向了当前符号在 .dynstr 节中的偏移量。
.dynstr 节
这个节在 IDA 中结构如下
1 | LOAD:0804822C ; ELF String Table |
没有结构体定义,这个节的数据全都是字符串,两两之间用 ‘\x00’ 分割,而且开头和结尾都是 ‘\x00’。
如果将以上介绍的结构用一张图片表示
几点注意:
- r_info 指的是对应的结构体在 .dynsym 节中的下标(并不完全是,一会再讨论)
- st_name 指的是对应的字符串在 .dynstr 节中的偏移量,例如 .dynstr 节头部地址为 0x0804822C,目标字符串的地址是 0x08048246,那么 st_name = 0x08048246 - 0x0804822C = 26
第一次调用导入函数发生了什么
关于 ELF 格式或者各种节的详细介绍我们这里不涉及了,主要来看看当第一调用某个系统函数的时候发生了什么。
在 linux 下随便找一个可执行程序,用 gdb 打开找到一个系统函数单步跟踪。
以 read 函数为例,单步跟进。
红色方框圈出的就是主要代码,第一次调用 read 函数,首先会 jmp 到 GOT 表中寻找地址,但是此时 read 函数还没有被绑定到 GOT 表中。
然后执行了
1 | push 0 |
两步操作,最后通过 jmp 指令跳转到 _dl_runtime_resolve 继续执行。实际上这两句 push 就是将函数的参数入栈,调用函数原型为 dl_runtime_resolve(link_map,0×0),这个函数就会进行一系列操作解析出 read 函数的地址,将地址写入 read 的 GOT 表中,然后将控制权交给 read 继续执行。
_dl_runtime_resolve
我们简单了解一下这个函数的执行过程,它的代码是由汇编编写的,内部主要调用了 _dl_fixup 函数,这个函数的实现在 glibc/elf/dl-runtime.c 中,感兴趣的同学可以阅读源代码。
当程序调用 _dl_runtime_resolve 的时候,这个函数首先根据 linkmap 定位 .dynamic 节的地址,并从中取出 .dynstr、.dynsym、.rel.plt 这三个节的指针,然后利用 .rel.plt 头部指针和传入的第二个参数 offset 定位 .rel.plt 中的 Elf32_Rel 结构体,例如上面的例子传入的偏移量是 0,在 .rel.plt 中找到零号结构体就是
1 | Elf32_Rel <804A00Ch, 107h> ; R_386_JMP_SLOT read |
上面说了这个结构体包含两个成员,第一个是指向 GOT 表的指针 r_offset ,而第二个 r_info 的利用规则为 r_info >> 8
作为.dynsym
的下标,求出当前函数的符号表项Elf32_Sym
的指针,也就是说 0x107 >> 8 = 0x1 作为 .dynsym 节中的下标,取得 Elf32_Sym 结构体。在本例中取到的就是
1 | Elf32_Sym <offset aRead - offset byte_804822C, 0, 0, 12h, 0, 0> ; "read" |
(注意:.dynsym 节的第一个元素是 0)
取得 Elf32_Sym 结构之后,会根据 st_name 指针从 .dynstr 节中找到对应的字符串,这个 st_name 表示的是字符串相对 .dynstr 节起始地址的偏移量。例如本例中 st_name = offset aRead - offset byte_804822C = 0x08048246 - 0x804822C = 26
当找到了字符串之后,会在 libc 中查找这个函数的地址,并将地址赋值给 Elf32_Rel 的 r_offset 成员(也就是指向 GOT 表的指针)。
最后 _dl_runtime_resolve 将控制权转移给解析出来的函数,完成对应的功能。
总结一下流程
- 根据传入的 link_map 找到 .dynamic 节,并取出 .dynstr、.dynsym、.rel_plt 三个节。
- 根据传入的 offset 从 .rel_plt 中找到对应的 Elf32_Rel 结构体。
- 从 Elf32_Rel 结构体中找到 r_info 成员,根据这个成员的偏移量(r_info >> 8) 从 .dynsym 中找到 Elf32_Sym 结构体。
- 从 Elf32_Sym 结构体中找到 st_name 成员,根据这个偏移从 .dynstr 节中找到对应的字符串。
- 根据找到的字符串在 libc 中搜索对应的函数地址,并回填到 Elf32_Rel 的 r_offset 成员中。
- 将控制权交给解析出来的函数。
攻击思路
如果程序没有开启 RELRO 保护,即 checksec 显示 No RELRO 的时候,我们可以通过修改 .dynamic 节的 .dynstr 指针,将它指向一块可控内存,然后在对应的位置上面伪造好想要解析出来的函数名,例如 “system”。
上面的方法是最简单的,但是很多情况下题目都会开启部分 RELRO 保护,不能修改 .dynamic 的 .dynstr 指针(没有写权限),此时要用到另一种方法,那就是控制 _dl_runtime_resolve 的第二个参数。
通过上面介绍的函数流程,可以发现函数的解析链如下
1 | _dl_runtime_resolve(linkmap, offset) -> Elf32_Rel -> r_info -> Elf32_Sym -> st_name -> string -> r_offset |
当进行 ROP 等操作的时候,构造一个很大的 offset,并且在可控的内存空间伪造一个 Elf32_Rel 结构体,由于 libc 源代码中没有检查 offset 是否合法(取得这个结构体的算法是 .rel_plt + offset),当根据我们伪造的 offset 获取 Elf32_Rel 的时候就会落在伪造的结构体内部。
紧接着在伪造的结构体中构造好 r_info 偏移量,同样的,这个偏移量也可以伪造一个较大的值,使得 Elf32_Sym 结构体也落在可控内存区域。
在伪造的 Elf32_Sym 结构体中伪造 st_name,并在目标地址写入想要解析的函数名。
将上面三步操作用图片表示
这一攻击流程的核心点在于控制好各个位置的偏移量,可能需要多次调试才能确定精确的偏移。
举个栗子
ISCC 2019 的 PWN01,这道题没有给输出函数,只有一个栈溢出漏洞,并且将数据填写到了 bss 段。
主要代码如下
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
现将数据读入 bss 段,然后拷贝到栈上,导致栈溢出。
按照攻击思路,我们只需要构造好 ROP 链以及三个 fake 结构体即可完成利用。
构造 ROP 利用链,将返回地址劫持到 plt[0] 即 push linkmap 的位置,同时伪造好三个结构:Elf32_Rel、Elf32_Sym 和有效的字符串 “system”,当执行完 _dl_runtime_resolve 之后,就会将 system 的地址填充到 read 的 got 表中。
完整 EXP 如下
1 | # coding=utf-8 |
注意,虽然说 padding 填充什么都可以,但是当程序执行 system 函数中的 execve 时,需要满足一些必要的条件,由于我们已经将栈帧迁移到了 bss 段,所以 padding 最好填充为 ‘\x00’,否则在执行 execve 的时候会由于环境不正常导致syscall 执行失败。
再举个栗子
这是 2020 年铁三线上赛的一道 pwn 题,利用思路也是 ret2dlresolve,我们可以选择更简单的方式去利用,pwntools 内部集成了 Ret2dlresolvePayload 工具,可以 “一键” 实现攻击。
题目下载地址:链接:https://pan.baidu.com/s/1hkzR87BY8wqDAm_3Z8KSYw 提取码:5cac
官方例子
1 | >>> context.binary = elf = ELF(pwnlib.data.elf.ret2dlresolve.get('i386')) |
比较重要的是 Ret2dlresolvePayload,第一个参数是目标文件的 ELF 对象,第二个是想要解析的函数名,第三个是函数参数。
针对这道题,先依照官方的例子得到 payload,但是在执行 system 函数的时候发现由于 env 参数不为 0,会导致执行失败,所以将输出的 payload 中占位的字节改成 \x00 即可。
1 | from pwn import * |
- 本文作者: CataLpa
- 本文链接: https://wzt.ac.cn/2019/05/02/Ret2runtime_dlresolve/
-
版权声明:
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。