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/