HEAP
本文最后更新于 2025年6月19日 中午
参考
UAF
简介
UAF全称:Using-After-Free
- 浅显理解:在free没有置零的情况下,可以做到malloc申请同样大小空间的堆,会返回上一个申请过这个大小的地址(当然有很多限制,这里先不讲)
原理
例题
ctfshowpwn141
根据原理,我们只需要将chunk->func的地址改为backdoor即可
EXP
1 |
|
代码解读
这里要注意,只有chunk->content具有读写能力
为什么前两个Note的content的大小要满足 >= 4 && != 8
- 根据原理,我们知道,如果size=8,那么我们通过流程可知,chunk[2]->content —> chunk[1]—>content,从而不能修改func的地址到backdoor
为什么最后一个Note的content的大小一定是8
- 保证它分配的地址一定是chunk[0]—>func,从而利用content的读写性去修改其地址
Heap block overlap
chunk的结构(堆)
prev_size:若前一个物理相邻的chunk是free chunk,则表示其大小。否则用于存储前一个chunk的数据
- x8664下prev_size 和 size+A+M+P 各自占8个字节
- i386下prev_size 和 size+A+M+P 各自占4个字节
- chunk的起始大小为:Initial_size = prev_size + (size+A+M+P) == 0x10 (x64)/0x8(x32)
- chunk的最小大小为(x64):0x20 (0x21) [Initial_size+0x10]/(x32):0x10 (0x11) [Initial_size+0x8]
要注意一点
最终chunk大小的计算如下:
1 |
|
1 |
|
Off-By-One
例题
ctfshowpwn142
整理一下其结构体类型得到如下:
Create Heap
首先申请了两个地址的空间
第一个八字节地址用来存放 heaparray[i]->size
第二个八字节地址空间是一个地址,指向这个堆的内容:heaparray[i]->content
Edit Heap
往heaparray[i]->content 存放的地址里写东西
大小是heaparray[i]->size+1,即申请堆的内容空间大小+1
存在堆溢出(off-by-one)
- Show Heap
- 打印堆heaparray[i]->content内容的空间大小以及内容
Delete Heap
分别释放堆,并置零,不存在UAF
heaparray[i]->content
heaparray[i]->size
EXP
1 |
|
代码解释
堆变化
Unlink
Fake_thunk模板
1 |
|
thunk结构
Unlink: 将一个空闲的 chunk 从双向链表中移除,更新BK、FD的bk、fd(BK、FD都是一个thunk)
简单来说将一个thunk从一个双向链表中删除,但是保证这个双向链表不被破坏
- fd:指向下一个空闲的 chunk
- bk:指向上一个空闲的 chunk
个人理解:
Unlink有点像连带责任,P走了,那么BK、FD就要必须更新新的信息
原理
1. 原先的双向链表
2. Unlink后的双向链表(Unlink(P))
也就说Unlink(P)
- BK->fd = P->fd = FD
- FD->bk = P->bk = BK
那也就说
当我们设置了P->bk
的值,那么FD->bk
也会被我们设置
当我们设置了P->fd
的值,那么BK->fd
也会被我们设置
而FD、BK又是由P->fd、P->bk来决定的
这就实现了任意内存写
a. fd != bk
P->fd = 0x1234
P->bk = 0x5678
那么
BK = 0x5678
FD = 0x1234
((size_t)BK+2) = P->fd == 0x1234
((size_t)FD+3) = P->bk == 0x5678
能看到,两个不同的地址*((size_t*)BK+2)``*((size_t*)FD+3)
,通过Unlink实现了覆写
b. fd = bk
P->fd = 0x1234
P->bk = 0x1234
BK = 0x1234
FD = 0x1234
((size_t)BK+2) = P->fd == 0x1234
((size_t)FD+3) = P->bk == 0x1234
- *((size_t*)thunk+2) = P->fd == 0x1234*
- *((size_t)thunk+3) = P->bk == 0x1234
依据这个特性,那么我们就可以实现定点写入
c. 定点
写入
fd
的位置
P->fd = target-1
P->bk = target
BK = target
FD = target-1
((size_t)BK+2) = P->fd
((size_t)FD+3) = P->bk
- *((size_t*)thunk+3) = P->fd
- *((size_t*)thunk+3) = P->bk
如果只考虑一个(FD或者BK的话)那么会更简单
比如说我只考虑BK
那么
P->fd = value
P->bk = target_addr
就是实现了*((size_t*)target_addr+2) = value
反之:
P->fd = target_addr
P->bk = value
就是实现了*((size_t*)target_addr+3) = value
再简化一下
只考虑BK
P->fd = value
P->bk = target_addr - 2
*((size_t*)target_addr) = value
只考虑FD
P->fd = target_addr - 3
P->bk = value*((size_t*)target_addr) = value
3. 无check
在早期,是不存在检查的,也就说,直接通过P->fd、P->bk的设置,触发Unlink(P)就可以实现任意内存写的操作
4. check
现实很残酷,有洞,总要修的,但是洞是修不完的
1 |
|
增加check后,我们来理解下fd、bk的检查
- FD->bk != P
- BK->fd != P
要想绕过check
FD->bk = P
BK->fd = P
(P->fd)->bk = P
(P->bk)->fd = P
来看看我们之前的图:绿色代表FD、BK能够通过check的指向,红色代表我们触发Unlink后FD、BK的指向
FD->bk = BK
BK->fd = FD
(P->fd)->bk = BK
(P->bk)->fd = FD
这样看,我们就可以通过定点这个知识点(上边)可以轻松解决
把BK和FD看成一个大的target_thunk,在target_thunk中BK和FD重合,且在target_thunk中存在P的地址
就可以实现绕过了,上图理解下
target_thunk中存在P的地址
通过P->bk、P->fd布局,使得FD->bk = P、BK->fd = P
但是一般来说都是只知道P地址的存储地址,那么我们来转换一下
这样的结果是P_ptr被覆盖成了P->fd == P_Addr-3,至于为什么不是P_Addr-2,我也不造了,个人猜测是P->bk先覆盖了,P->fd后覆盖
触发
流程
伪造thunk(fake_thunk),
修改fake_thunk所在thunk的下一个thunk(这里叫next_thunk)的
prev_size
、size上的 P flag(通常默认是1)
,通过next_thunk的修改来设置fake_thunk的状态是free
prev_size = fake_thunk->size(不包含P,即P=0)
size = size-1 (如果size的个位上存在1)
next_thunk的size有要求,必须>= 90,即malloc(0x80)[amd64下]
不然回收的时候(free(next_thunk))会将next_thunk收到fastbin中,导致触发不了Unlink
- free(next_thunk):触发Unlink(P)
解释下size上的P flag
:PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。
prev_size
:若前一个物理相邻的chunk是free chunk,则表示其大小。否则用于存储前一个chunk的数据
例题
ctfshowpwn143
add函数,可以看出它的布局
edit函数存在堆溢出
根据原理,我们来构造fake_thunk,并触发Unlink
EXP
1 |
|
代码解释
fakefd->List[1] - 0x18(3*8)
fakebk->List[1] - 0x10(2*8)
AMD64下size_t的大小为8、i386下size_t的大小为4
Unsorted bin Attack
Unsortedthunk模板
1 |
|
原理
这个攻击有点像Unlink,但是可能更高效一点(在进行攻击的时候)也有缺点,就是单纯的Unsorted bin Attack下,任意内存写入的数据不可控,简单理解下
当我们申请的
thunk->size >= 0x90
的时候,再free(thunk)
,该thunk会进入unsortedbin
unsortedbin是个双向链表管理器, 通俗来讲,它有个最高等级的(虚假的这里我用虚线来表示)
tmp_thunk
来管理双向链表
当我要将thunk1从这个双链中脱出
- 更新tmp_thunk->bk
- 更新thunk0->fd
1 |
|
- victim实际上就是unsortedbin中链表的最后一个thunk(这里就是thunk1)
- 那么bck就是thunk0
- unsorted_chunks (av)就是tmp_thunk
翻译一下就是
tmp_thunk->bk = bck == victim->bk
bck->fd = tmp_thunk
- tmp_thunk->bk = victim->bk
- victim->bk->fd = tmp_thunk
- tmp_thunk->bk = victim->bk
- victim->bk+2 = tmp_thunk
看得难受?没事,上图
那也就说,当我们控制了victim->bk值,那么我们就能将
tmp_thunk
写入任何地址上,但是<font style="color:#DF2A3F;">tmp_thunk</font>
的值我们无法控制看表达式
- victim->bk = target-2
- tmp_thunk->bk = target-2
- victim->bk+2 [target] = tmp_thunk
此外还要注意一点,待free的thunk不能与
top thunk
物理相邻,不然free后无法进入unsortedbin
例题
ctfshowpwn144
edit:存在堆溢出漏洞
EXP
1 |
|
代码解释
House Of Spirit
这个操作是通过伪造fastbin的单向链表来实现的
可以实现任意内存构造thunk
原理
fastbin的管理机制
- 当thunk->size < 0x90,free(thunk)时会被回收到fastbin中
[AMD64下]
- 当fastbin中为空时,free(thunk),该thunk->fd = 0
- 当fastbin中不为空时,free(thunk),该thunk->fd指向上一个被回收入fastbin中的thunk
- 申请同样大小的thunk时,返回链表的头thunk,该thunk从fastbin中脱链,thunk->fd被content覆盖
看文字总是有点不够生动,看图吧
fastbin添加thunk
这里有两种
- 手动free(fake_thunk),实现fastbin添加fake_thunk,但是在free的时候有要求
- fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
- fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
- fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。
- fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem 。
- fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。
- 通过修改fastbin中的thunk->fd,实现fastbin添加fake_thunk,同样有要求
- 能够写入fastbin中的thunk的内存,一般是堆溢出
- thunk->size <= fake_thunk->size < thunk->size + size_t*2
a
的理解很简单:如果不能写入thunk的内存,那么fd就无法修改,也就无法实现fastbin添加
b
的理解就有丢丢复杂:要求fake_thunk->size 必须存在值,且值不能超过thunk->size + size_t2或者小于thunk->size。什么意思呢,就是说((size_t*)fake_thunk+1)必须有值;后半句是说,由于你是修改fastbin中已有的thunk->fd,那么就认为你的fake_thunk->size和thunk->size一样,但是可以不完全一样,上图来看看比较好理解
例题
ctfshowpwn144
edit:存在堆溢出
我们的目的是修改magic的值,那么我们就需要布局fake_thunk(第二种添加方法)
EXP
1 |
|
代码解释
在布局fake_thunk时
stdin@@GLIBC_2_2_5,在elf文件映射内存的时一般都在高位地址上,也就是0x000070~0x00007F,那么就可以构造一个thunk->size = 0x71,实现添加fake_thunk
这里还有个问题,就是说,如果我们把fake_thunk == 0x0602090,那么fake_thunk->size ==0,就无法申请到fake_thunk的地址,那么我们就需要把[0x70~0x7f]从fake_thunk->prev_size挪到fake_thunk->size上。其在高三位上,那么stdin@@GLIBC_2_2_5-3,把它顶出去就ok