# 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指针,会更加复杂。