PWN堆house-of-muney
前置知识
- 需要了解
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的利用思想。
相关结构体
- 注:
strtab、gnu_hash、JMPREL Relocation Table,其实并不是结构体,而都是一个Section
link_map结构体
1 | struct link_map |
Elf64_Dyn结构体
1 | typedef struct |
Elf64_Rela结构体
1 | typedef struct |
Elf64_Sym结构体
1 | typedef struct { |
相关段
- 这里主要是符号解析过程相关的
section
1 | Elf64_Ehdr |
符号解析
-
在
ret2dlreolve的时候有大致了解了一下符号解析的过程(主要是了解)_dl_fixup函数中对伪造符号表比较重要的过程,并没有很详细的了解符号解析的过程,在进行house-of-muney之前,再详细介绍一下符号解析的过程。 -
整个符号解析的过程其实就是在调用
_dl_runtime_resolve开始的。所以我们先来查看一下_dl_runtime_resolve,示例程序可以使用ret2dlreolve这个程序进行调试。 -
下面是
_dl_runtime_resolve具体调用过程:
1 | _dl_runtime_resolve |
_dl_runtime_resolve
- 对于该函数并没有什么特别的,最重要的其实就是在该函数调用的时候会调用
_dl_fixup。_dl_runtime_resolve的作用直接看汇编会更好理解 - 下面放出该函数的汇编形式,
_dl_runtime_resolve主要就是干了这么几件事情:- 调整栈帧,保存寄存器相应的值
- 将之前
push的俩个参数值传递给rdi、rsi并调用_dl_fixup - 调用完
_dl_fixup会将其返回值(rax寄存器保存的值)给r11寄存器 - 恢复寄存器,恢复栈帧,最后
jmp r11调用该函数
1 | endbr64 |
_dl_fixup
- 接下来在
/glibc/elf/dl-runtime.c文件中找到_dl_fixup源码,源码位置如下:
1 | _dl_fixup ( |
_dl_lookup_symbol_x
- 该函数在
glibc/elf/dl-lookup.c中出现,这个函数稍微理解一下全过程就行,重点是下面一个函数。
1 | /* 在已加载对象的符号表中搜索符号 UNDEF_NAME 的定义,可能还会指定所需的符号版本。 |
do_lookup_x(*重要)
- 该函数也是在
glibc/elf/dl-lookup.c中出现,这个函数是house-of-muney核心利用过程,我们伪造的结构体就是在这个函数中被使用的。
1 | static int |
-
符号解析的过程总结:
-
需要伪造的值如下:
bitmask_word:字符串哈希和掩码进行与运算的结果buckethasharr:看文章说需要多伪造几个- 修改目标结构体
Elf64_Sym中st_value成员,符号表中,除了st_value修改为目标地址外,其他成员保持不变(劫持地址,上面三个都是伪造值)
环境
-
house-of-muney环境还真是难搞,搞了将近一个下午了,记录一下,这里找到了一个更详细的poc。house-of-muney的poc只有特定libc版本的,所以就去搞了一下Docker。在Docker里面编译。瞎折腾半天QAQ到晚上才发现并不是环境问题,而是我编译的时候没有选用延迟绑定命令QAQ -
首先先要下载对应的libc文件,去到
ubuntu官网下载2.31-0ubuntu9.9 : libc6 : amd64 : Focal (20.04) : Ubuntu

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


- 之后编写如下
dockerfile,使用docker bulit .命令创建Docker镜像
1 | FROM ubuntu:20.04 |
- 创建完后使用
docker run命令用该镜像创建一个容器。这时候访问libc即可确定libc.so.6是9.9版本的

- 接下来我们要使用该容器编译如下poc(实验二中的实验代码),首先我们先要更新包,然后安装
gcc、vim
1 | apt-get update |
- 接下来使用
vim将代码写入到lab1.c文件中

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

实验
- 找了好多篇文章,都是使用
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 | // gcc -Wl,-z,lazy -g -o lab1 lab1.c |
- 接下来就进行调试操作,首先是一个初始化操作,并且将
/bin/sh字符串给复制到地址0xdeadb000中去。

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


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

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

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


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



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



实验二
1 | /* |
利用总结
- 通过申请很大的堆块size为
A,将堆块的位置申请到libc.so.6的地址前面- 然后修改该堆块的size位为
A+X,使得其能覆盖到.dynsym/.gnu.hash/etc.,此时libc的符号表还是只读状态。- 然后将修改了
size位的堆块进行释放,这个时候.dynsym/.gnu.hash/etc.这些段也连带着被取消映射- 再次申请一个大堆块,至少需要申请的大小需要
A+X,内核将重新分配给刚刚释放一样的堆地址给用户使用此时libc的.dynsym/.gnu.hash/etc.这些段就变成可读可写了- 最后伪造这些相关结构即可。
题目1_ciscn2023半决赛华中赛区_muney
题目2_pwn_me
- 不知道什么比赛的题目
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 iyheart的博客!

