PWN堆house-of-einherjar
- 14号差不多了解了
house of spirit
的堆利用方法,现在就来学习house-of-einherjar
的堆利用。 - 在学习
house of einherjar
之前,要先学习unlink
的这个堆漏洞的利用,这样会对unlink
的流程以及free()
的过程会更加清晰。 house of einherjar
的这个堆利用适用范围``2.23—— 至今`
前置知识
-
之前学习了
unlink
技术,有一段时间没有做unlink
的相关题型,所以先简单的总结一下unlink
的过程,以及堆的前向合并以及后向和合并。并且重点强调堆中的隐式链表技术(该技术在house of spirit
中有稍微提过一点) -
这里继续放出
free()
和unlink
的源码,接下来要看源码,理清一下free()
的流程,这样有助于更好的学习house of spirit
这个堆利用方式
free函数流程
- 通过略读
free
函数,大概理清了free
函数的具体流程
堆管理隐式链表技术
- 堆块在被释放的时候如果被放入了
bins
中他是会被链表这个结构给管理起来的。 - 而我们在按顺序申请堆块的时候,每个堆块都有在物理地址上相邻的堆块。对于每个堆块如果找到物理地址上相邻的其他两个堆块,这就是堆管理的隐式链表技术。
- 我们就用如下示例程序对堆管理的隐式链表技术进行一个比较详细的说明。
1 |
|
- 我们直接对编译好的
test
程序进行动态调试。使用ni
命令将该程序执行到调用完free
之后,执行ret
指令之前。这时我们使用vis
命令查看一下堆块会看到如下结果。图中红色框的都是每个堆块的prev_size
位和size
位。所谓隐式链表,也就是该堆块并没有指针去指向下一个chunk。而是使用自己的prev_size
和size
从而去确定低地址的堆块的位置,和高地址堆块的位置。- 在图中对于
绿色
区域,它的prev_size=0x160
,而它的size=0x60
- 所以我们就可以通过绿色这个堆块的起始地址
p = 0x107c1a0
通过p-p.prev_size=0x107c040
从而计算出与该堆块的物理地址相邻且更低的堆块的起始地址。 - 要计算与该堆块的物理地址相邻且更高的堆块的起始地址,就可以通过
p=0x107c1a0
,p+(p.size>>3)<<3=0x107c200
得到 - 注意
prev_size
这个数据,只有在该堆块prev_inuse
这个位为0
的时候会有效,当prev_inuse
为1
的时候prev_size
是无效的,不会起作用。
- 在图中对于
free函数相关源码
__libc_free源码
__libc_free
glibc2.23--malloc.c
第2933行开始
1 | void |
_int_free源码
_int_free源码
glibc2.23--malloc.c
第3836行开始
1 | /* |
unlink源码
unlink宏定义
glibc2.23--malloc.c
第1413行开始
1 |
其他源码
其他源码
glibc2.23--malloc.c
第1303行获取size的值
1 |
glibc2.23--malloc.c
第1219行,用于将free(addr)
这个addr
回退到chunk的头部
而不是chunk中size
之后的地址。
1 |
利用方式
前向合并和后向合并
- 之前对于源码中的注释
consolidate backward
和consolidate forward
到底哪个是前向合并哪个是后向合并有点搞不清楚。 - 这里先对自己对堆块的认知做一个统一,按照注释中的意识,我们将地址增加的方向当做前,地址减少的方向当做后。
- 所以之前在
unlink
中所写的可能有点相反了。
- 了解了堆管理隐式链表技术和之前学习的
unlink
,就可以来学习house of einherjar
的漏洞利用方式了。与该利用方式相关的代码是unlink
和_int_free
中的代码前向合并
和后向合并
的部分代码。unlink
的代码上面有,这边主要是截取_int_free
的部分代码。- 这边注释所说的
consolidate backward
就是我们释放的堆块与其后面的堆块合并,叫做后向合并 - 这边注释所说的
consolidate forward
就是我们释放的堆块与其前面的堆块合并,叫做前向合并 - 由于后向合并会更新p指针,使其指向低地址的堆块。所以在
unlink
时,p
指针都是指向两个要合并堆块中低地址的那一块。 house of einherjar
主要用到的是堆块的后向合并
(正常堆块为前、伪造的堆块在后或者能溢出修改p
位的堆块在前、伪造的堆块在后)
- 这边注释所说的
1 | /* consolidate backward */ |
利用原理
-
接下来就详细说明一下
house of einherjar
具体的利用过程。house of einherjar
是利用unlink
的过程和堆管理隐式链表技术
的不安全进行漏洞利用的。- 在堆块中,两个物理相邻的
chunk
会共享prev_size
字段,尤其是当低地址的chunk
处于使用状态的时(此时高地址的堆中prev_inuse
位为1
)高地址的chunk
的prev_size
字段可以被低地址的chunk
使用。因此,我们有希望可以通过写低地址的chunk
覆盖高地址chunk
的prev_size
字段。 - 一个chunk的
prev_inuse
位标记了其物理地址相邻的低地址chunk的使用状态,而且该位是和prev_size
物理相邻。 - 后向合并时,
p
指针会被更新,更新到两个合并堆块中所处地址更小的一个堆块。即p = chunk_at_offset(p, -((long) prevsize));
- 在堆块中,两个物理相邻的
-
首先我们的利用过程如下(这边我给出两种利用方式)其中一种方式是博客普遍上有的,另一种应该可以(刚学没遇到)。
-
利用方式:这边我们先申请了两个堆块(实际上要三个,防止与
top_chunk
相邻的堆块与top_chunk
合并),将其命名为chunk0
和chunk_1
,他们的物理地址的结构如图所示。(图中一些数据可能要重新布置,图中size为0x40
可能无法利用house of einherjar
,具体设置细节看实验或者利用原理的最后)
- 我们先伪造一个
fake_chunk
,fake_chunk
的具体要求伪造要求之后再说。
- 我们对
chunk0
进行写操作,此时需要off-by-one
漏洞,这样我们将chunk0
的数据溢出到chunk1
中,并且修改了chunk1中prev_size
的值和也修改chunk1中p
位的值为0
。其中prev_size
的值应该为chunk1_addr - fake_chunk_addr
(二者要么都用prev_size
的地址相减,要么都用fd指针所存储的地址相减)。
- 这时我们将
chunk1
释放,就会触发后向合并
。我们就会更新p
指针为p = chunk_at_offset(p, -((long))
,此时p
就指向了fake_chunk
。这时合并后就会出现这么一大块的空间。并且合并后的堆块会被放入unsorted_bin
中被管理
-
之后我们再使用
malloc
申请一个堆块,这个堆块这个堆块就会从fake_chunk开始,切割一部分然后返回给用户使用。这样我们就达到了任意地址写的目的。 -
注意:由于要计算
prev_size
的大小,所以需要泄露堆地址
和fake_chunk
的地址。 -
在这个过程中我们需要修改的数据如下:(可以先看实验之后再来看这边)
- 我们要通过溢出修改高地址堆块的
prev_size
为该堆地址
到fake_chunk
的偏移地址 - 我们还要修改更高地址堆块的
P
位为0
,如果该堆块的size
不是0x100
整数倍的话,还需要修改该堆块的size
位使得其是0x100
的整数倍,并且要确保M
和M
位为0
。(其实也可以修改size
位为0x00)
- 我们要通过溢出修改高地址堆块的
- 我们
fake_chunk
的所需要伪造的地方:prev_size
位:如果能伪造就尽量伪造成我们所要unlink
的另一个堆块的size大小(即上面溢出的堆块的size)。(貌似glibc2.23
,对prev_size
位检查没那么高)经过测试fake_chunk的prev_size位不需要伪造- 对于
P
位:一定要置0,这样才能触发unlink
,对于M
和N
位需要保持0
即可 - 对于
size
位:一定要和b
堆块的prev_size
相同大小(这样才能绕过检查机制) - 对于
fd
、bk
、fd_nextsize
、bk_nextsize
都指向fake_chunk
的开头,至少要满足fd
、bk
指向fake_chunk
的开头这样才能绕过unlink
的检查机制。当申请的堆块比较大时就会多出两个元数据,即fd_nextsize
、bk_nextsize
。这样我们对于fd
、bk
、fd_nextsize
、bk_nextsize
赋值相同的值,就可以绕过unlink
的检查。
实验
- 这时
how2heap
上glibc2.23
的house of einherjar.c
的源码
源码
1 |
|
- 为了能认真让自己看一遍这个代码,我决定将汉化源码
1 |
|
- 接下来进行动态调试,我们先申请了一个
0x38
的堆块。
- 然后我们在栈上伪造了一个
fake chunk
,伪造的fake chunk
数据如下
- 之后又申请了一个
0xf8
大小的堆块。
- 这时我们再通过
off-by-one
溢出,修改b
的p
位为0
(其他俩位在申请时默认为0)
- 之后我们先泄露了
b
的地址和fake chunk
的地址,并计算这两个的偏移,将计算的结果写入到b
的prev_size
位中,同时还需要改变fake chunk
的size
位(这样貌似是为了绕过检查)
- 此时我们再释放堆块
b
,在释放的过程中就发生了unlink
使得堆块b
和fake_chunk
合并,结果就会变成如下这样。这时我们的堆就会被合并到栈上。(用heap
或者heap -v
命令再查看栈就会出现问题,查看到的堆块并不是我们下次分配要分割的堆块起始地址。下次要分割返回的起始地址其实在fake_chunk
那边,看下一步malloc就清楚了)
- 然后我们再申请一个
0x200
大小的堆块。
house of einherjar_level_1
-
题目来源:
CTFhub
的house of einherjar
,题目环境是glibc2.23
,但是为了方便动态调试,我就使用ubuntu22.04
,即glibc2.35
版本,之后动态调试查看fd
指针的时候才会使用glibc2.23
的环境,因为高版本的glibc
存储fd
的指针的值为进行异或加密 -
这题在
2月2号
尝试打了一下,但是由于题打太少了,思维给实验的那种利用方式给限制了,这题并不是在bss
段或者是栈
上伪造堆块,而是构造堆叠,进行堆排布。(但是收获还是有的,弄好了docker
环境中的gdb
调试) -
今天
2月3号
打算再来尝试一下这题(找了好半天看了wp之后才知道要用堆叠,并且这题还要用到malloc_hook
,打完这题马上就学malloc_hook
) -
这题本地打一下就行,打远程还是有点问题
level_1_分析1
- 接下来检查一下程序的保护机制,发现保护开的很全,这时候就
- 接下来将该程序拖入
IDA
对该程序进行逆向分析,同时配合程序的运行,这样可以更好的理清程序的运行逻辑 - 先来查看一下
main
函数的大致逻辑- 先是
init
对该程序进行输入输出初始化,让该程序无缓冲输入,但是标准输出好像不是无缓冲输出 - 接下来打印
menu
菜单,1.create
、2.show
、3delete
、4.edit
、5.exit
- 接下来就是让用户输入选项,输入选项之后就进入用户所选择选项的相应函数
- 先是
- 接下来查看
add()
函数- 先让用户输入
ID
,也就是申请堆块返回地址存储的索引 - 然后让用户输入
size_long
,也就是所要申请堆块的大小 - 经过判断后,满足条件就调用
malloc()
函数申请用户指定的堆块大小,并将返回的堆块地址存储到chunk
数组中 - 之后将
size_long
存储在size
中
- 先让用户输入
- 现在来查看
delete()
函数- 让用户输入book的
ID
- 之后使用
free()
函数释放指定ID
的堆块 - 之后还将存储释放堆块的地址置
0
所以不存在UAF
漏洞
- 让用户输入book的
- 接下来就查看
show
函数- 将用户指定的堆块内容输出到屏幕上,这边还存在着数组越界引用的漏洞。(这个数组越界引用的漏洞并没有什么用好像)
- 最多就是利用
%s
这个字符串格式化对堆块的地址进行泄露
- 接下来查看
edit()
这个函数- 在这里先会读取上一次申请堆块的大小
- 之后我们选择要
edit
的堆块ID
- 之后就向我们指定的堆块中写入数据
- 注意由于我们读取的是上一次操作堆块的大小,所以与我们所指定的堆块大小无关,这就可能造成堆溢出的操作。(这个程序逻辑需要动态调试和认真读汇编代码才能理清楚代码逻辑)
- 假如我们在
edit
之前申请堆块ID为14,这时我们edit
的size就是ID14的size。当我们在edit
之前是释放ID为10的堆块,这时我们edit
的size就是ID为10的堆块的size
- 我们在
.bss
段中发现了全局变量number
,但是这个number
在程序运行过程中没有被使用,这个全局变量就很可能是用来伪造堆块
level_1_分析2
- 现在进行gdb动态调试,这时候我们这边有一个堆溢出,这样我们可以通过堆风水对堆进行巧妙的排布,然后再通过堆溢出,最后使用
edit()
函数就可以泄露出堆地址。但是泄露完这个堆地址后并没有什么用 ,因为我们没办法对.bss
段的地址或者栈
上的地址,使用这种利用方式是行不通的。 - 那么我们就直接利用
unlink
机制,两个堆unlink
后,堆块就会被放入unsorted_bin
,由于unsorted_bin
是双向链表,这样我们就可以利用这个链表指针,通过edit()
函数将libc的地址泄露出来。接下来我们进行动态调试。这样我们就要先使用house_of_einherjar
堆块伪造技术,对堆块进行堆叠。 - 由于后申请的堆块处于更高地址,最后申请的堆块与
top
chunk相邻,为了不让合并后的堆块与top_chunk
合并,我们在申请我们所需要理由的堆块后最后还要申请一个堆块,用于阻隔合并后的堆块和top_chunk
。(并且这个堆块要的size要比之前的大一点,这样我们才能够进行堆溢出操作。) - 注意:house of einherjar利用的是堆块的后向合并,这时我们要修改低地址的堆块为空闲堆块,再free高地址的堆块所以这个堆块的prev_inuse位就需要为1。这时我们需要绕过
unlink
的检查机制
1 | if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ |
- 这时我们就需要泄露
chunk
的地址,这时我们就要先泄露堆块的地址,我们先创建一个堆块ID6,大小为0x10
用于堆溢出,然后我们再申请两个堆块ID7、ID8大小都为0x20,最后防止被释放的堆块合并top
我们再申请一个堆块ID9大小为0x40
- 这时我们先释放ID8的堆块(后申请的堆块),再释放ID7的堆块(先申请的堆块)。这两个堆块就放入了
fastbin
中,堆块ID7的fd
指针会指向堆块ID8的prev_size_addr
。
- 这时我们就可以通过溢出ID6,溢出到
ID7
的fd指针,再使用show()
函数,打印出ID6堆块内容的同时就会将ID7的fd
指针打内容打印出来,这样我们就可以泄露ID8的prev_size_addr
的地址从而就可以泄露堆块的地址
- 这样我们就可以进行堆块溢出
- 这样堆块的地址就可以被泄露出来了。
- 现在我们就可以开始伪造堆块,这样就可以进行
house of einherjar
的利用。结合利用原理,这时我们先要申请一个堆块ID0大小为0x10,这样我们就可以进行对高地址的的堆块进行溢出。然后我们要申请一个size
位为0x100整数倍的堆块。所以我们申请一个堆块ID1大小为0xf8
的堆块,然后我们再申请一个堆块ID2大小为0x10
,用于另一个堆块的溢出。之后我们继续申请一个堆块ID3大小为0xf8
的堆块。最后由于edit的堆块的size是调用上一个ID堆块的size,所以我们还要申请一个堆块ID4大小为0x40(这样我们在编辑堆块ID2的时候就可以进行溢出操作)。这时我们就要溢出修改ID3的prev_size
和prev_inuse
这两个位
- 然后我们还要再申请一个堆块ID5大小为0x40,这样我们才能在修改
ID0
的时候发生溢出,从而修改ID1堆块的size
位、fd
、bk
这三个
- 之后我们就可以释放堆块
ID3
这样我们就可以将该堆块释放,并且触发unlink
进行后向合并,并且可以绕过unlink
的检查机制,这样我们ID1、ID2、ID3
就可以合并(其实是ID1和ID3合并)因为我们修改了size和prev_size。所以合并的时候就会连同ID2的堆块一起合并,放入unsorted_bin中。但是堆块ID2的地址还是保存在chunk[ID]
数组中我们还可以对这个堆块进行show
、edit
操作,这时我们再申请一个堆块ID1
大小为0xf8
。 - 这时就会从
unsorted_bin
合并的堆块中分割0xf8
大小。这时剩下的堆块,其fd、bk指针就恰好为堆块ID2中的fd
、bk
、由于unsorted_bin
使用的是双向链表,此时的fd
、bk
指针就会被指向main_arena+88
地址处,这时我们就可以show(ID2)
从而泄露libc地址
level_1_分析3
- 接下来我们就可以利用
ID2
这个堆块进行UAF漏洞,这样我们就可以通过堆风水构造double free
,劫持malloc_hook
和realloc_hook
- 我们可以通过这些来计算偏移
1 | libc_addr = main_area - 88 - 0x10 - libc.sym['__malloc_hook'] |
- 接下来我们就构造
double
,这时我们需要去__malloc_hook
找可以伪造的堆块。我们需要这样的堆块,使得我们double free
从而之后申请到这个地址的堆块,从而可以修改__malloc_hook
、realloc_hook
的地址,从而劫持hook
指针为onegadget
这样去getshell
。(虽然这个地址的size并不是0x70
、0x71
,但是在double free
的时候这个堆块也会被放入fastbin
链表中)
- 接下来我们构造
double free
,我们现在就利用一个ID2
去构造UAF
,由于我们要劫持hook
所要申请到的地址size位为0x7f
,这样我们就要将其放入0x70
的fastbin
中。 - 所以我们先申请一个堆块ID10,大小为
0x68
,这样申请的堆块size
才会为0x70
。此时chunk[10]
存储的堆地址与chunk[2]
存储的堆地址是相同的。此时我们释放chunk[10]
这个堆块,这样该堆块的就会被放入fastbin
链表,该堆块的fd
指针就会被启用,这时我们就使用edit()
对chunk[2]
修改,从而修改fd
指针,使其指向malloc_hook - 0x23
- 之后我们就先申请一个先申请一个
ID11
大小为0x68
,然后我们再申请一个堆块ID13
这样我们就可以将malloc_hook-0x23
的这个堆块地址申请回来,然后我们可以修改malloc_hook
为realloc+16
、改realloc_hook
为ogg+6
。 - 此时我们的
ogg
为如下,这里我们ogg+6
的原因是,当我们直接hook到ogg
的时候rcx
不是一个正常地址,会导致段错误,所以我就ogg+6
直接不执行某个汇编代码,这样就不会出现段错误
- 之后我们再创建一个堆块,触发调用
malloc
,这样就可以触发malloc_hook
,从而getshell
level_1_exp
- exp如下:
1 | import os |
house of einherjar_level_2
level_2_分析1
level_2_分析2
level_2_分析3
level_2_exp
总结
- 对于
house of einherjar
的这种堆利用方式,相同点就是触发堆块的unlink
的向后合并,可以有以下几种思路:- 第一种就是像实验那样,在栈上伪造堆块,并且存在堆溢出,这样在堆合并后再次申请堆块就会申请到栈上,这样我们就可能可以修改栈上的返回地址,从而劫持程序的执行流。
- 第二种就是在堆块上的利用现有的堆通过堆溢出中
off-by-one
造成堆叠,然后通过堆风水和堆排布得到double free
等漏洞,之后就可以申请任意地址,这样可能可以劫持hook
、got
表等地址。(例题:level1)