# Off by null
# 原理
在读入数据的时候,对数据长度的检查不严谨,导致了一个 NULL 字节的溢出。在堆块 size 为 0x100 时,溢出 NULL 字节会导致 prev_inuse 位被清零,该堆块的前一个堆块会被认为是 free(未分配)状态。因此可以通过 unlink 实现任意地址写,或者伪造 prev_size 达到 UAF 的效果。
# 流程
- 开辟三个堆块分别为 chunk0、chunk1、chunk2。
- 编辑 chunk1,输入特定数据使其溢出一个 NULL(\x00)字节,覆盖 chunk2 的 prev_inuse 位为 0,这时 chunk1 就会被认为 free 掉了。
# Unlink
Unlink 是把 free 掉的 chunk 从所属的 bins 链中,卸下来的操作(当然还包括一系列的检测机制)。它是在 free 掉一块 chunk (除 fastbin 的 chunk 外)之后,glibc 检查这块 chunk 相邻的上下两块 chunk 的 free 状态之后,做出的堆块合并引起的。
BK-P-FD==>BK-FD P | |
// 三个堆块 | |
FD=P->fd | |
BK=P->bk | |
// 确定堆块顺序 | |
FD->bk=BK<=>P->fd->bk=P->bk | |
BK->fd=FD<=>P->bk->fd=P->fd | |
// 将 P 前后两个 chunk 相连,从而分离中间的 chunk。 |
# 堆块合并
# 向前合并(高地址)
// 假设这里 p 为 chunk2 | |
if (!prev_inuse(p)) { | |
prevsize = p->prev_size; | |
size += prevsize; | |
// 确定堆块合并后的 size | |
p = chunk_at_offset(p, -((long) prevsize)); | |
// 通过 pre_size 找到前一个 chunk | |
unlink(av, p, bck, fwd); | |
// 通过 unlink 合并堆块 | |
} |
在 free chunk2 的时候。会先检测 chunk3 的 prev_inuse 值。如果为 0,说明 chunk2 已经被 free,直接返回;如果为 1,则会检测 chunk2 的 prev_inuse 值。如果 chunk2 的 prev_inuse 值为 1,说明 chunk1 被占用,则跳过向前合并,如果为 0,会根据 chunk2 的 prev_size 值找到 chunk1,合并这两个堆块,完成向前合并。
# 向后合并(低地址)
if (nextchunk != av->top) { | |
nextinuse = inuse_bit_at_offset(nextchunk, nextsize); | |
// 获得下一个 chunk 的 prev_inuse | |
if (!nextinuse) { | |
unlink(av, nextchunk, bck, fwd); | |
// 通过 unlink 合并堆块 | |
size += nextsize; | |
// 确定堆块合并后的 size | |
} else | |
clear_inuse_bit_at_offset(nextchunk, 0); |
在 chunk2 向前合并完成后,会检测下一个堆块是否为 top chunk,如果是,直接与 top chunk 合并;如果不是,继续检测 chunk3 的下一个堆块 chunk4 的 prev_inuse 值,如果为 1,说明 chunk3 被占用,跳过向后合并;如果为 0,合并 chunk3,完成向后合并。
# PS:
- 在堆块合并时,无论向前向后都只合并相邻的堆块,不会继续合并更前或更后的堆块。
- 属于 fastbin 大小的堆块在 free 时不会合并,而是直接放入 fastbin。
# 伪造 chunk 并绕过 unlink 检测
可以伪造 chunk 并通过 unlink 来修改 GOT 表从而 getshell。但是 unlink 会对 chunk 的 size 和双向链表完整性进行检查。
# size 检查
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) | |
malloc_printerr ("corrupted size vs. prev_size"); | |
//P 自己包含 size 信息,P 的下一个 chunk 也包含了 P 的 size 信息,将两者进行比较。 |
# 双向链表完整性检查
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) | |
malloc_printerr (check_action, "corrupted double-linked list", P, AV); | |
// 检查 P 前一个 chunk 的 bk 和后一个 chunk 的 fd 是否都指向 P。 |
# 流程
- 开辟三个堆块 chunk0、chunk1、chunk2。chunk2 应大于 fastbin 的范围。
- 在 chunk1 中伪造一个 chunk,并通过 off by null 修改 chunk2 的 prev_size 和 size 来绕过 size 检查。
- 因为伪造的 chunk 并不在链表中,所以让伪造的 chunk 的 fd 和 bk 都指向自己,就能绕过双向链表完整性检查。
// 在绕过双向链表完整性检查时,通常按照以下公式设置伪造 chunk 的 fd 和 bk。 | |
fd=chunk_addr-0x18 | |
bk=chunk_addr-0x10 | |
// 会有以下效果 | |
chunk->fd->bk==*(chunk->fd+0x18)==*(&chunk_addr-0x18+0x18)==chunk_addr | |
chunk->bk->fd==*(chunk->bk+0x10)==*(&chunk_addr-0x10+0x10)==chunk_addr | |
// 满足了检查条件 |
free chunk2,unlink 会合并伪造的 chunk 和 chunk2。
这时 bss 段上对应 chunk2 的指针 ptr 会从 [ptr] 修改为 [&ptr-0x18],就可以通过编辑 chunk2 对 GOT 表进行修改,改变其他 chunk 的指针。
chunk2
presize = 0
chunk0
fd: ptr-0x18 bk : ptr-0x10
chunk1
# libc2.29 之后
值得一提的是,在 libc2.29 之后,glibc 在向前合并时对 presize 添加了检测,导致我们无法修改正常 chunk 的 size,因此无法伪造 prev_size。
// 设这里的 p 为 chunk1 | |
if (!prev_inuse(p)) | |
{ | |
prevsize = prev_size (p); | |
size += prevsize; | |
p = chunk_at_offset(p, -((long) prevsize)); | |
if (__glibc_unlikely (chunksize(p) != prevsize)) | |
// 这里检测了 chunk0 的 size 和 chunk1 的 prevsize 是否相等 | |
malloc_printerr ("corrupted size vs. prev_size while consolidating"); | |
unlink_chunk (av, p); | |
} |
按照我们上面的思路,如果我们修改了 chunk2 的 prev_size,使其与伪造的 chunk 的 size 相同,这里会检查 chunk2 的 prev_size 和真正的 chunk1 的 size 是否相同,结果自然是不同的。
如果要伪造一个可以绕过检查的 chunk,需要利用到 largebin 残留的 fd_nextsize 和 bk_nextsize 两个指针,smallbin 残留的 bk 指针,以及 fastbin 的 fd 指针,会更加复杂。