# Off by null

# 原理

​ 在读入数据的时候,对数据长度的检查不严谨,导致了一个NULL字节的溢出。在堆块size为0x100时,溢出NULL字节会导致prev_inuse位被清零,该堆块的前一个堆块会被认为是free(未分配)状态。因此可以通过unlink实现任意地址写,或者伪造prev_size达到UAF的效果。

# 流程

  1. 开辟三个堆块分别为chunk0、chunk1、chunk2。
  2. 编辑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:

  1. 在堆块合并时,无论向前向后都只合并相邻的堆块,不会继续合并更前或更后的堆块。
  2. 属于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。

# 流程

  1. 开辟三个堆块chunk0、chunk1、chunk2。chunk2应大于fastbin的范围。
  2. 在chunk1中伪造一个chunk,并通过off by null修改chunk2的prev_size和size来绕过size检查
  3. 因为伪造的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
//满足了检查条件
  1. free chunk2,unlink会合并伪造的chunk和chunk2。

  2. 这时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指针,会更加复杂。

編集日

*~( ̄▽ ̄)~[お茶]を一杯ください

Coldwinds WeChat 支払う

WeChat 支払う

Coldwinds Alipay

Alipay