抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

BlackBird的博客

这世界上所有的不利状况,都是当事者能力不足导致的

ret2dl_resolve

这个貌似,,咕了很长时间的文章了……在Lacanva师傅的监督下,,,终于不咕了……呜呜呜~

前置知识

本节文章实验所用代码:

#include<stdio.h>
#include<stdlib.h>
int main(){
    char s[16]
    puts("Input:");
    read(0, s, 0x100);
    puts(s);
    puts("This is a test.");
    return 0;
}

.Dynamic段

由于_dl_runtime_resolve主要是链接的过程,所以我们先研究一下ELF文件里面的.Dynamic段。我们在IDA的stucture里面可以看到dyn结构体

00000000 Elf32_Dyn struc ; (sizeof=0x8, align=0x4, copyof_4) 00000000 ; XREF: LOAD:_DYNAMIC/r 00000000 ; LOAD:08049668/r ... 00000000 d_tag dd ? 00000004 d_un Elf32_Dyn::$A263394DDF3EC2D4B1B8448EDD30E249 ? 00000008 Elf32_Dyn ends

对应的声明是:

typedef struct
{
  Elf32_Sword   d_tag;                  /* Dynamic entry type */
  union
    {
      Elf32_Word d_val;                 /* Integer value */
      Elf32_Addr d_ptr;                 /* Address value */
    } d_un;
} Elf32_Dyn;

Elf32_Sword d_tag用于表示该结构体中d_un的类型,在内存中:

image-20210918141628430

这些内容也可以通过readelf来获得:

blackbird@ubuntu ~/C/tmp> readelf -d test

Dynamic section at offset 0x660 contains 24 entries: 标记 类型 名称/值 0x00000001 (NEEDED) 共享库:[libc.so.6] 0x0000000c (INIT) 0x80482a8 0x0000000d (FINI) 0x80484f4 0x00000019 (INIT_ARRAY) 0x8049658 0x0000001b (INIT_ARRAYSZ) 4 (bytes) 0x0000001a (FINI_ARRAY) 0x804965c 0x0000001c (FINI_ARRAYSZ) 4 (bytes) 0x6ffffef5 (GNU_HASH) 0x804818c 0x00000005 (STRTAB) 0x804820c 0x00000006 (SYMTAB) 0x80481ac 0x0000000a (STRSZ) 79 (bytes) 0x0000000b (SYMENT) 16 (bytes) 0x00000015 (DEBUG) 0x0 0x00000003 (PLTGOT) 0x804974c 0x00000002 (PLTRELSZ) 24 (bytes) 0x00000014 (PLTREL) REL 0x00000017 (JMPREL) 0x8048290 0x00000011 (REL) 0x8048288 0x00000012 (RELSZ) 8 (bytes) 0x00000013 (RELENT) 8 (bytes) 0x6ffffffe (VERNEED) 0x8048268 0x6fffffff (VERNEEDNUM) 1 0x6ffffff0 (VERSYM) 0x804825c 0x00000000 (NULL) 0x0

这里我们主要强调以下这些内容:

  • DT_STRTAB

    处于.dynamic的地址加0x40的位置

    该元素保存着字符串表地址

    ,包括了符号名,库名,和一些其他的在该表中的字符串。指向.dynstr

    image-20210918143844658

  • DT_SYMTAB

    处于.dynamic的地址加0x4c的位置

    该元素保存着符号表的地址,对32-bit类型的文件来说,关联着一个Elf32_Sym入口。指向.dynsym

    image-20210918144331457

    对于每一个Elf32_Sym都有:

    typedef struct
    {
      Elf32_Word    st_name;  
      Elf32_Addr    st_value;
      Elf32_Word    st_size;
      unsigned char st_info;
      unsigned char st_other;
      Elf32_Section st_shndx;
    }Elf32_Sym;
    

    我们可以对应着声明分别看一下部分内容:

    • st_name:符号的名字;

      但它并不是一个字符串,而是字符串表中的一个索引值,在字符串表中该索引值的位置上存放的字符串就是该符号名字的实际文本;如果此值不为0,则它就代表符号名字在字符串表中的索引值;如果此值为0,则表示此符号没有名字;

    • st_info:符号的类型和属性;

      该字段由一系列的二进制位构成,标识了"符号绑定(symbol binding)"、"符号类型(symbol type)"和"符号信息(symbol information)"三种属性

  • DT_JMPREL

    处于.dynamic的地址加0x80的位置

    包含了重定位表的信息:

    image-20210918161057454

    声明:

    typedef struct {
         Elf32_Addr r_offset;    // 对于可执行文件,此值为虚拟地址  函数got地址
         Elf32_Word r_info;      // 动态符号符号表索引
     } Elf32_Rel;
    #define ELF32_R_INFO(sym, type)        (((sym) << 8) + ((type) & 0xff))
    

    我们以read函数为例,read函数的got表地址是0x8049758;read函数在symtable中的偏移为1,类型为7。

延迟绑定机制

在程序运行时,有很多函数在程序执行时不会被用到,比如错误处理或者 用户比较少用的功能模块等,所以不需要所有函数在一开始就链接好。 延迟绑定(Lazy Binding) 的基本思想是 函数第一次被调用时才进行绑定(符号查找、重定位等),如果没有则不进行绑定。要实现 延迟绑定 需要使用到名为 PLT(Procedure Linkage Table)的方法。 而通常延迟绑定机制又是通过调用 _dl_runtime_resolve函数来实现的,这也正是此函数没有延迟绑定的原因。

_dl_runtime_resolve函数

_dl_runtime_resolve是重定位函数,该函数会在进程运行时动态修改函数地址来达到重定位的效果。此函数无无延迟绑定机制, 需要两个参数,一个是 reloc_arg ,就是函数自己的 plt表项 push的内容,一个是 link_map,这个是公共 plt 表项 push进栈的,通过它可以找到.dynamic的地址

_dl_fixup()函数

_dl_fixup()函数在elf/dl_runtime.c中实现,用于解析导入函数的真实地址,并改写got

调试

这一段实验用的代码:

#include<stdio.h>
#include<stdlib.h>
int main(){
    char s[16];
    puts("Input:");
    read(0, s, 0x100);
    puts(s);
    puts("This is a test.");
    return 0;
}

我们已经做了充足的准备,下来可以开始调试了, 调试的时候遇到什么问题可以再往前参考。

我们在第一个puts的时候单步步进:

image-20210920145314242

我们进入这个函数后进入了程序的.plt段:

image-20210920150823963

我们发现,.plt段相当于是专门为后面_dl_runtime_resolve函数寻找函数真实地址传递参数用的。第一个参数reloc_arg,对于每一个函数都不一样,即该函数在Elf_JMPREL(ELF JMPREL Relocation Table)中的偏移:

image-20210920170208465

第一个参数压栈了以后,来到了.plt的开头,压入第二个参数link_map,`_GLOBAL_OFFSET_TABLE_+4,也就是got`偏移4的内存:

image-20210920151732552

image-20210920150513135

我们可以看到,在link_map中的第三个参数就是.Dynamic段的地址。

接下来就进入了_dl_runtime_resolve函数, 一开始直接进行传参:

image-20210920154025716

从注释中我们清晰的看到,_dl_runtime_resolve一开始将plt压栈的参数放置到寄存器中,然后_dl_fixup函数从寄存器中取参。

我们在glibc/elf/dl-run-time.c中找到_dl_fixup函数的定义,然后对着源码和IDAGDB,,,emmm……调着看着,它大概主要干了这么几件事:

  1. link_map访问.dynamic,取出.dynstr, .dynsym, .rel.plt的指针

  2. .rel.plt +第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel

  3. rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym

  4. .dynstr + sym->st_name得出符号名字符串指针

  5. 在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT

  6. 调用这个函数

    _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->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);
    }
    

    emm……要是懒得看字的话,大致就是这亚的一个流程:

    攻击方式

    我们现在对整个流程有一个大致的了解,我们现在考虑一下针对这个过程如何攻击???我们的目标是获取system函数的地址,也就是LOOKUP_VALUE_ADDRESS函数的参数为指向system字符串的指针时的返回地址,那么我们很自然想到的就是修改DT_STRTAB,但是我们在IDA里面明显能看到这一段是不可写的,也就是DT_STRTABDT_SYMTABDT_JMPREL都是不可写的。那么我们既然修改不了他的,,,那我们就自己伪造一个(最好在.bss段上吧……)。对了,.Dynamic段是有可写权限的,那如果可以的话,直接修改.Dynamic段,把.dynstr.dynsym段或者其他段定位到我们伪造的地方上。

    大致流程就是栈迁移+伪造+ROP……

    胡思乱想:

    ​ 如果对ROP长度有限制或者ROP不大能控制的话,那么修改.Dynamic段,应该有用???

    就这些吧……

参考资料

从o开始的pwn学习之超详细ret2dl_resolve

ELF文件-符号表

技术讨论 | Ret2dl_resolve漏洞利用分析

评论