glibc2.29以上的off-by-null
前言
其实网上已经有很多类似的文章,我本着复习的态度就再写一下,回顾一下整个流程。
off-by-null 说白了就是一个 chunk overlap 的过程,这个过程分为向上合并和向下合并,接下来就结合源码具体分析一下这个过程是怎么实现的。
一些宏定义
首先得知道 ptmalloc 是如何对 chunk 进行操作的·:是通过定义各种功能的宏
获取 chunk 的大小
1 | /* Get size, ignoring use bits */ |
获取下一 chunk 块地址
1 | /* Ptr to next physical malloc_chunk. */ |
获取前一个 chunk 信息
1 | /* Size of the chunk below P. Only valid if prev_inuse (P). */ |
判断当前 chunk 是否是 use 状态
1 |
|
向下合并
1 | /* consolidate forward */ |
可以看到,它一开始会检查下一个 chunk 的 inuse 位是否为 0 ,因为下一个 chunk 的 inuse 位会标志当前 chunk 的使用状态。
如果为 0 ,说明当前 chunk 为空闲状态。于是触发 unlink ,将当前 chunk 的下一个 chunk 取出 chunk 链,再将当前 chunk 的 size 变为 size + nextsize
。
向上合并
1 | /* consolidate backward */ |
相对于向下合并更加复杂,首先检查自己本身的 inuse 位是否为 0,即检查上一个 chunk 是否为空闲 chunk。
为 0,说明空闲。于是先获取上一个 chunk 的 size 大小,将其保存在 prevsize 中,然后将自己的 size 改为 size + 上一个 chunk 的 size
再将指向自己的指针 p ,减去上一个 chunk 的 size,使其指向上一个 chunk 所在的位置。
最后 unlink 将 上一个 chunk 从 chunk 链取出。
整个过程说白了就是把 上/下一个 chunk t 了,然后通过改变自己 chunk 的 size (向上合并时还要改变指针位置) 替代掉被 t 的chunk。
2.27
1 | // In /glibc/glibc-2.27/source/malloc/malloc.c#L1404 |
主要的检查:
第一个:
1 | if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) |
此时如果我们将相应大小的tca bin 填满,free chunk0 ,chunk0 会链入unsorted bin,此时 chunk0 的 fd 和 bk 都是 0x7f…..,同时 unsorted bin 只有 chunk0 ,所以链表头的 fd 和 bk 也都指向 chunk0。
第二个:
1 | if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) |
检查要脱链的 chunk 的 size 是否和 下一个 chunk 的 prev_size 相等。
于是可以构造如下 chunk
大致流程就是:
- 创建四个 chunk,这里直接对应 chunk 0,1,2,3(chunk3 是为了防止 off-by-null 的时候整个堆块与 top chunk 合并,chunk3 的 prev_size 要注意)
- free chunk0,此时 chunk1 的 prev_size 会留下 chunk0 的大小
- 通过 chunk1 off-by-null 覆盖到 chunk2,修改 chunk2 的 prev_size 和 size 的 p 位(prev_size 修改成 chunk0+chunk1 的大小)
- free chunk2 触发 unlink 合并
- 将 chunk0 进行 Unlink 操作,通过 chunk0 的 size 域找到 nextchunk 就是 chunk1 ,检查 chunk0 的 size 与 chunk1 的 prev_size 是否相等。
- 由于第二步中已经在 chunk1 的 prev_size 域留下了 chunk0 的大小,因此,检查通过
2.29
1 | // In /glibc/glibc-2.29/source/malloc/malloc.c#L1460 |
unlink 内部没有多大变化。
向上合并时发生了一点变化:
1 | /* consolidate backward */ |
多了一行:
1 | if (__glibc_unlikely (chunksize(p) != prevsize)) |
说白了就是会检查 chunk0 的 size 和 chunk2 的 prevsize 是否相等,这使得用传统的构造方式进行 chunk overlap 不可能实现。
于是就有了另一种方法,利用残留指针伪造 fake chunk,将 fake chunk 的 size 写为 chunk2 的 prevsize,不就绕过检查了吗。
直接用例题进行讲解
例题
题目是 2022 年春秋杯的 torghast
大概流程是通过加血的方式打过 level3,然后就可以进入菜单,这个过程就不细说了,主要讲讲 off-by-null 的过程。
edit 函数存在 off-by-null 漏洞
1 | unsigned __int64 edit() |
下面来看具体的构造过程
1 | add(1,0x600,'b')#large |
创建一个 large bin 大小的 chunk,free 掉之后,再 add 一个比它大的 chunk3,chunk1 会被放入 large bin。
1 | add(4,0x28,'aaaaaaaa') |
此时我们再 add 一个 chunk,chunk1 被放入 unsorted bin 并且被切割。large bin 残留的 fd_nextsize 和 bk_nextsize 残留在 chunk4 中。
此时就可以泄露出 libcbase 和 heapbase
1 | add(5,0x28,'aaaaaaaa') |
伪造一个 0x101 大小的 fake chunk,使得 (fake chunk -> fd) -> bk == fake chunk
,(fake chunk -> bk) -> fd == fake chunk
从而绕过 unlink 的一个检查:
1 | if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) |
1 | add(8,0x4f0,'a') |
此时我们先将 unsorted bin 全部都申请完,再利用 chunk7 进行 off-by-null,将 chunk8 的 prev_size 改为 0x100(对应 fake chunk 的 size),size 后两位覆盖成 ‘\x00’
这样就可以绕过另外两个检查:
unlink 的检查:
1 | if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) |
结合 next_chunk 的宏定义一起看
1 | /* Ptr to next physical malloc_chunk. */ |
向上合并的检查:
1 | /* consolidate backward */ |
再 dele(8),就成功 chunk overlap 了,可以看到 chunk4 ~ chunk8 被合并成了一个 0x601 的 chunk
这里还有一个小细节要注意的是,chunk8 的 size 被覆盖成了 0x500,但是 chunk8 下面的那个 chunk 的 prev_size 位记录了原 chunk8 的大小,如果 off-by-null 之前,这个 unsorted bin 的大小后两位不是 ‘\x00’,就会因为下一个 chunk 的 prev_size 和 0x500 不相等而报错。所以要在之前就将 chunk8 的后两位变为 ‘\x00’,说白了就是 off-by-null 覆盖 size 这一步不能改变 unsorted bin 的大小。
EXP
1 | from pwn import* |
参考链接
https://bbs.pediy.com/thread-257901-1.htm#msg_header_h2_1
https://www.anquanke.com/post/id/208407#h3-5
https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/chunk-extend-overlapping/