PWN堆house-of-atum
- 参考博客:Glibc堆利用之house of系列总结 - roderick - record and learn!
- 参考博客:[House Of Atum-原理 | Pwn进你的心](https://ywhkkx.github.io/2023/03/10/House Of Atum-原理/)
- 参考博客:原创]BCTF 2018 House of Atum分析-Pwn-看雪-安全社区|安全招聘|kanxue.com
前置知识
-
在
glibc2.31
版本之前,tcache
的检查机制比较不太严格,在glibc2.31
的时候才出现了key
机制,所以在glibc2.31
之前,实现tcache double free
利用是非常容易的。甚至我们还可以把同一个chunk
连续释放8
次,第8
次释放就会将该chunk
放入fastbin
中,当然此时该chunk
本身还存留在tcache
中。 -
这时我们甚至可以利用
UAF
漏洞,连续释放同一个chunk
8次,使得该chunk
被放入fastbin
中,该chunk
被放入fastbin
的同时本身还是留在tcache_bin
中。 -
house of atum
的适用范围:glibc2.26——glibc2.30
。 -
house of atum
利用的前提条件:- 需要在
glibc
对应版本中。 - 需要存在
UAF
漏洞,这里的UAF
漏洞需要满足两点,其一是:可以进行double free
利用;其二是:edit after free
,即该堆块被释放后还可以向这个堆块写入内容。
- 需要在
-
进行
house of atum
这个利用方式之前,我们先要了解tcache_bin
中的运行机制,而tcache_bin
这个运行机制就不在这里多说明了,已经单独写一篇博客了。 -
其实最好是先学习一下
tcache double free
这个理由再来学习这个,但是问题不大。
实验
实验1
- 这个利用方式在
how2heap
这个仓库中没有理由的代码,这时我们就自己写一个程序来动态调试一下程序的运行情况。 - 该程序的编译的
gcc
版本为gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
,但是由于我使用的docker
环境中,glibc2.27
已经打了补丁,所以我将该编译好的程序复制到WSL
中,然后使用glibc-all-in-one
这个项目,配合patchelf
,这样就能得到实验的环境。 - 实验的源码如下:
1 |
|
patchelf
完之后就可以开始进行动态调试了。我们就先申请一个堆块。
- 然后我们先释放一次堆块,我们所申请的堆块就被放入了
- 这时我们再释放
6
次这个堆块,这时我们tcachebins
中存放0x20
大小的堆块就会被放满一样的堆块,这时我们再释放一个堆块,这个堆块就会被放入fastbin
中。
- 现在我们第
8
次释放堆块,查看bins
的状态,此时这个chunk
的fd
指针已经被置0
,并且该堆块除了已经放入到tcachebin
中,也会被放入到tcachebin
中。
实验2
- 接下来我们一步一步完善我们的利用方式。
1 |
|
- 这时我们将这个代码编译后
patchelf
,进行调试并且画图理解。我们先来查看堆块释放7
次,将tcachebin
填满之后就会出现如下图所示。
- 然后当我们再释放一次堆块,此时
tcachebin
对应存储着0x20
的size
已经被填满了,这时这个堆块就会被放入fastbin
中,如下图所示,并且此时该堆块
的fd
指针会被设置为0
- 这时我们再申请与之前释放的相同大小的堆块,此时就会从
tcachebin
中去取空闲的堆块。取完之后,tcachebin
这个数组元素的值就会被设置为0
,但是tcachebins
有一个count
标志用于表明该tcachebin
中存放空闲chunk的数量,但是这时tcachebin
相当于没有空闲的堆块。 - 但是我们看到
fastbin
数组中还有空闲的堆块。
- 这时我们修改我们申请过来的堆块的
fd
指针,将该指针的值设置为chunk_pre_size_addr - 0x10
如下图所示。
- 这时我们申请一个堆块,此时我们还会申请到
0x55731847a250
这个位置,但是0x55731847a240
会被链到tcachebin
链表上,这样我们
- 然后我们再次申请一个堆块,此时
malloc()
返回的地址对应的就会是fake_fd
这边,对应着我们前面两次申请的堆块的prev_size_addr
,这时我们就可以通过这次申请过来的堆块写入数据,从而达到修改chunk
的prev_size
、和chunk
的size
、P
、N
、M
这三个标志位。从而进行后续的利用操作。
利用方式
- 利用条件:
- 需要存在
UAF
漏洞,并且可以使用tcachebin double free
。 - 需要泄露堆的地址。
- 需要泄露
- 需要存在
- 申请
chunk A
,大小在fastbin
范围内 - 释放
A
,连续释放8
次,此时,A
的fd
被清0
,A
也被放置到了fastbin
里面 - 申请一个
chunk
,将其fd
修改为A - 0x10
,此时tcache
中的counts
为6
- 再申请一个
chunk
,从fastbin
里面取,但是会把fastbin
里面剩余的一个chunk
链入到tcachebin
- 再次分配就会分配到地址
A-0x10
处,就可以修改原来A
的presize/size
等
house of atum_level_1
- 题目来源:BCTF2018-house-of-atum
- 题目附件:搜索一下就能找到
- 涉及知识点:
house of atum
、tcachebin下触发unlink
、unsortedbin泄露libc地址
、tcachebin_UAF申请任意地址
、简单的__free_hook的利用
,总的来说也是一套组合利用,由于tcachebin下触发unlink
、unsortedbin_attack
这两个利用还没怎么了解,所以利用完house of atum
就卡住了。 - 拿到附件之后我们需要使用
glibc-all-in-one
项目对应的glibc2.27
版本进行patchelf,这样该程序才能正常运行。
level_1分析1
- 我们先运行一下这个程序看一下这个程序的基本运行逻辑。我们运行一下这个程序发现一开始就会弹出一个菜单。基本上就是
增、删、改、查
这一字面意思。
- 然后我们就输入选择,我们选择
1
后,程序会要求用户输入内容。
- 然后我们再选择
2
看看,我们选择2
后,程序会要求我们输入要内容对应堆块的索引值idx
,之后让我们输入新的内容。
- 我们先来选择第
4
个选项,发现程序要求我们输入索引,输入后会输出对应索引的堆块。
- 最后我们选择第
3
个选项,发现就是清除用户指定索引的堆块。
- 运行完一遍程序后我们就可以查看一下这个程序的保护机制了,发现保护全开。
- 大致了解程序的运行逻辑之后,我们就使用
IDA
对这个程序进行逆向一下。结合之前我们运行的程序,我们大致可以分析出main
函数其实就是一个类似于菜单的。
- 查看一下
initialize()
函数,我们发现这个函数其实就是一个输入、输出初始化的一个函数。
- 查看一下
menu()
这个函数,这个函数会输出菜单,输出菜单后还会调用getint()
- 而
getint()
这个函数实现的功能就是字面意思,也就是获取用户的选项输入
。
- 接下来就是看按照
增
、删
、改
、看
这个顺序查看一下菜单对应的各个函数。 - 先查看
alloc()
函数- 该函数首先会遍历一下
已经申请并且存放在notes数组中的堆块个数
,我们会发现notes
这个是一个全局变量,并且是指针数组类型。 - 如果已经有两个堆块了,就会告诉用户
太多notes
,并且不能再申请堆块了 - 如果还没有申请两个堆块,程序接下去就会申请一个
0x48
大小的堆块。 - 并且让用户向里面输入内容。
- 该函数首先会遍历一下
- 接下来查看
改
即(delete()
这个函数),主要就是让用户输入对应的notes
数组的索引,然后直接释放对应索引的堆块。 - 程序会提示是否要清除堆块,如果输入
y
就会将notes
对应的索引的元素设置为0
。注意这里就存在UAF漏洞,我们可以选择不clear对应索引的堆块
- 之后查看
edit
这个函数,这个函数就先会让用户输入对应的idx
,然后向对应的堆块输入新的内容。
- 最后再查看
show()
,这个函数就是输出对应堆块的内容。注意这个函数可以配合UAF漏洞使用,从而泄露出堆的地址
- 程序分析完之后我们再来查看一下全局变量,看看是否有全局变量被我们遗漏的。发现并没有全局变量被我遗漏的。只有一个
notes
这个全局变量。
level_1分析2
- 接下来我们进行动态调试,查看一下程序的基本逻辑,我们先写好基本的交互脚本。
1 | from pwn import * |
- 到这里我们先思考一个问题,这边我们的
notes
只能是申请两个堆块,这时我们就要先仔细思考一下,我们只申请一个堆块,就只对这个堆块进行利用是否可以。我们会发现当我们只申请一个堆块,就来进行house of atum
的利用时,虽然我们可以修改到我们所申请堆块的prev_size
位和size
位,但是这对后续就会出现死路一条。 - 首先我们一开始申请的
0x250
这个堆块是与输入输出缓冲区有关的,而在我们申请堆块相邻高地址处就是top_chunk
了。 - 这时就会出现一个问题,我们没办法修改相邻的两个堆块中的内容,这时如果我们修改了
中间chunk
的prev_size
和size
位,进行一些unlink
等操作就会报错,因为我们没办法修改其他堆块从而绕过检查。
- 这个时候我们就需要先申请两个堆块了,其中
idx=0
是我们要进行house of atum
利用的堆块。而idx=1
对应的堆块是帮助我们绕过检查的堆块。 - 然后我们将
idx=1
对应的堆块,在fd偏移0x30到0x40
这个地址处写入payload = p64(0)*7+p64(0x11)
,因为tcache
一开始设计就是为了提高堆分配的效率
,所以当我们释放idx=1
对应的堆块时,它被放入tcachebin
中,但是里面写入的数据还是存在的。所以我们就可以利用这个一特性先写入payload = p64(0)*7+p64(0x11)
,这样我们在之后修改它相邻低地址堆块对应的prev_size和size位
时就可以绕过unlink
这个检查。此时堆中内容如下:
1 | from pwn import * |
- 在做完绕过准备后,我们先要泄露出堆块的地址,这样我们再能进行
house of atum
这个利用,这时我们先连续释放2
个idx=0
对应的堆块,释放之后,这个堆块对应的fd
指针,就是指向自己堆块结构中fd
指针的地址。如下图所示
- 接下来我们就可以进行
house of atum
的利用了- 我们先释放
idx=1
处的堆块,因为如果idx=1
处的堆块没有被释放,我们就不能连续申请两个堆块,就没办法利用house of atum
了。 - 先释放
idx=1
处的堆块后,再释放5
次idx=0
处的堆块就行。 - 第
5
次释放idx=0
处的堆块时,要将notes[0]
处的元素设置为0
,这样做也是为了后续我们能连续申请两个堆块。
- 我们先释放
1 | from pwn import * |
- 操作所对应的
exp
,操作完之后我们再看一下对应的内存,发现堆块的布局如下我们连续释放的堆块已经被放入fastbin中,并且其fd指针已经被设置为0了
- 并且此时我们发现,我们之前写入的
0x11
还保留在对应的堆块中。
- 这时我们就可以进行
house of atum
的利用
1 | from pwn import * |
- 先申请一个堆块,将这个堆块的
fd
指针修改为chunk_addr = chunk_addr -0x10 - 0x20
,也就是这个位置
- 然后我们再申请一个堆块,这个堆块的内容随便填一下,这样我们就可以得到如下的
bins
,我们会发现0x*****240
即上图中对应红框框起来的下面一个位置,已经被放入到了tcachebin
中。 - 但是此时我们的
idx=0
和idx=1
都有堆块存放了,所以我们这时需要释放一个堆块,才能将放入tcachebin
中的堆块给申请回来。而释放idx=0
或者idx=1
其中一个都没什么问题,他们指向的都是0x******260
,并且存放0x50
大小的堆块对应的tcachebin
已经存满7
块了,所以我们释放这两个堆块的其中一个,都会被放入fastbin
中。我们选择释放掉idx=1
- 释放完
idx=1
后,我们就可以将对应的0x******240
这个堆块给申请回来,并且就可以从0x******240
开始往里面写数据,在写数据的时候可以覆盖到原来我们堆块的prev_size
和size
位。这时我们就可以保持prev_size=0x0
并且size=0x91
为我们后续触发unlink
操作做准备。我们计算0x91
这个位置,刚好就是下图所示的范围。 - 此时我们
idx=1
指向的地址为0x*****240
,idx=0
指向的地址为0x********260
- 接下来我们就要实现在
tcachebin
下的如何触发unlink
- 首先我们需要让
tcachebin
对应的位置存满7
个堆块,然后我们第8
次释放后就会放入fastbin
中,并且查看是否能合并,如果能和其他相邻的堆块合并,就会进行合并操作,合并后的堆块就会被放入unsortedbin
这个堆块中。所有我们就要释放idx=0
对应的堆块8
次 - 这里还要注意下,在第
8
次释放的时候,我们要将notes[0]
中的数据设置为0
。这样我们之后才能申请堆块。
1 | from pwn import * |
- 我们来查看效果,
unlink
后的bins
和堆内存。我们发现图中的堆块已经成功被触发了unlink
操作。
- 并且我们发现放入
unsortedbin
中的堆块,fd
、bk
对应的值为main_arena+96
的位置,由于notes[0]
处的元素已经被设置为0
了这时我们就可以使用show(1)
来泄露堆块
- 接下来就是泄露操作了
1 | from pwn import * |
- 但是在进行
show(1)
操作时,我们还要对idx=1
处的堆块进行编辑。因为0x*****240
这边都是空字节,在使用puts
函数输出时,并不会将main_arena+96
这个地址给泄露出来。所以我们要先填充b'a'*0x20
,再调用show(1)
进行地址的泄露。
- 并且泄露之后我们还需要将对应地址
0x****260
的prev_size
、size
复原,要不然在下次申请的时候会程序就会报错。 - 泄露的结果如下:
- 接下来我们就要对
unsortedbin
这个运行进制进行利用了,我们先来看看目前bins
的布局
- 此时利用的是
tcachebin_UAF然后申请到任意地址
,我们已经泄露了libc
的地址,这时我们就可以劫持hook
从而getshell
,这里我们选择劫持__free_hook
这个函数指针为system('/bin/sh')
。所以我们先要将堆块申请到__free_hook
这个位置。 - 并且由于
tcachebins
在取出堆块的时候并没有对chunk
的size
位做检测,所以并不要寻找合适的size
对应的内存进行申请,直接申请__free_hook-0x20
这个位置即可。
- 这时我们还需要修改已经被放入
tcachbin
中的size
位为0x51
,这样我们申请的时候就可以申请到处于0x*****260
处的堆块。 - 并且我们还要修改
fd
的值为free_hook-0x20
,这样在申请完0x*******260
这个堆块的时候free_hook-0x20
这个地址就会被链到tcachebin
上,具体效果如下:
- 但是此时
idx=0
和idx=1
都已经有堆块了,所以我们申请到0x******260
这个堆块后,又要将其释放。但是该堆块会被放入fasbin
中,所以我们下次申请时就会申请到__free_hook-0x20
这个位置,接下来将/bin/sh
先写入到free_hook-0x10
的位置,然后将system_addr
填入__free_hook
中。
1 | payload = p64(0)*0x3 + p64(0x51) + p64(free_hook-0x20) |
- 填入之后再使用
free
调用释放这个堆块,这时free(0x7fcd558de8d8)
,就相当于执行system("/bin/sh")
,0x7fcd558de8d8
这个内存地址中存储的就是/bin/sh\x00
这个字符串。这样就能取得shell
了
level_1_exp
- 具体的
exp
如下:
1 | from pwn import * |
利用失效
glibc2.31
后利用基本失效了
1 | // glibc ≥ 2.30 |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 iyheart的博客!