Ptmalloc算法:Unlink攻击
我们在利用 unlink 所造成的漏洞时,其实就是对 chunk 进行内存布局,然后借助 unlink 操作来达成修改指针(某个chunk的fd指针)的效果
这个指针可以修改为任何地址(fake pointer),那些下一次对该chunk进行操作时,就会向“fake pointer”中写入数据,以实现WAA
Unlink流程
unlink是一个宏操作,用于将某一个空闲chunk从其所处的双向链表中脱链
阉割版代码:
1 | void unlink(malloc_chunk *P, malloc_chunk *BK, malloc_chunk *FD) |
流程图:
在程序进行unlink操作时,还有一个检查:
1 | // 由于'P'已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查) |
简单来说就是:
chunkP的下一个chunk的上一个chunk是不是chunkP
chunkP的上一个chunk的下一个chunk是不是chunkP
Unlink攻击原理
unlink 操作虽然有检查,但并不是那么“智能”,程序只会 根据地址的相对位置 来推测此位置大概是什么数据,比如,程序会认为 chunk_head+0x8 的位置为 presize, 而不会去检查 chunk 本身的“完整性” ,我们就可以利用这一点来欺骗检查程序
buf[0] 中装有 chunk 的首地址,修改 fake chunk->FD 为 buf[0] - 0x18(buf[-3]),修改 fake chunk->BK 为 buf[0] - 0x10(buf[-2])
假设程序要对 fake chunk 进行 unlink 操作,而 FD,BK 中指向的地址显然不合法,看看程序是怎么检查的:
- 检查chunkP的下一个chunk的上一个chunk是不是chunkP,先获取FD指针指向的chunk,然后检查该chunk的BK指针是不是chunkP:fake chunk 的FD指针指向 buf[-3],而程序会把 buf[-3]+0x18 处当做它的BK指针(指向 buf[0]),符合检查
- 检查chunkP的上一个chunk的下一个chunk是不是chunkP,先获取BK指针指向的chunk,然后检查该chunk的FD指针是不是chunkP:fake chunk 的BK指针指向 buf[-2],而程序会把 buf[-2]+0x10 处当做它的FD指针(指向 buf[0]),符合检查
这只是一种伪造方法,核心点:程序只会根据地址的相对位置获取数据
Unlink利用姿势
一,针对 chunk_list ,劫持修改模块(两个chunk合并后unlink)
这种攻击的套路比较固定:
- 申请两个大小相同的chunk(大小不同也可以,我这里为了方便描述就这样规定)
- 找寻并泄露 chunk_list 的地址(chunk_list 就是存放chunk数据区地址的数组)
- 在chunk1内部进行伪造:
- 伪造 presize 为“0”
- 伪造 size 为“chunk1->size - 0x10”
- 伪造 FD 为“chunk_list - 0x18”
- 伪造 BK 为“chunk_list - 0x10”
- 通过 off-by-one 溢出1字节覆盖 chunk2->size
- 释放 chunk2
此后Unlink攻击完成,在 unsortedbin 中存储的地址变为“chunk_list - 0x18”,并且在 chunk_list 中残留的chunk1数据区地址也变为“chunk_list - 0x18”
接下来可以用修改模块进行 hook,GOT劫持,也可以把chunk申请到“chunk_list - 0x18”上
二,针对 heap ,实现 overlapping(三个chunk合并后unlink)
这种攻击可以直接在 heap 中打:
- 申请三个chunk
- 找寻并泄露 chunk1_head 的地址(chunk1的首地址)
- 在chunk1内部进行伪造:
- 伪造 presize 为“0”
- 伪造 size 为“fake->size+1”(使其可以索引到chunk3)
- 伪造 FD 为“chunk1_head + 0x18”
- 伪造 BK 为“chunk1_head + 0x20”
- 伪造 FD+0x10(BK+0x8)为“chunk1_head + 0x10”(fake chunk首地址)
- 在chunk2内部最后一个空间写入“fake->size”(原本chunk3->presize的位置,使其可以索引到 fake chunk)
- 通过 off-by-one 溢出1字节覆盖 chunk3->size
- 释放 chunk3
注意:“fake->size”的大小为“chunk1->size + chunk2->size - 0x10 ”
libc版本限制
glibc-2.23:
- 基本的unlink检查
绕过:用上述操作就可以绕过
glibc-2.27:
- 基本的unlink检查
- 对“下一个chunk的pre_size”的检查
1 | if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) |
标准的是申请三个堆 chunk0 chunk1 chunk2(控制 chunk1 为“buf[-3]”)
释放 chunk0,chunk1 的 prev_size 位置会留下 chunk0 的大小
释放 chunk2 glibc 会通过 prev_size 向上索引到 chunk0
chunk0 会被检测,这里 “chunk0的size” 会和 “chunk1的prev_size” 比较
注意:chunk2 需要申请一个 chunk3,防止 chunk2 释放后与“top chunk”合并
绕过:需要多伪造一个“fake_chunk0”,并通过“fake_chunk1”索引到“fake_chunk0”
glibc-2.29:
- 基本的unlink检查
- 对“上一个chunk的pre_size”的检查
- 在unlink之前:检查 “chunk0的size” 和 “chunk2的prev_size” 是否相等
1 | if (!prev_inuse(p)){ |
// 这里还没有搞清楚,以后遇到题目了再补充