堆块chunk介绍及unlink漏洞利用原理

发布时间 2023-04-04 16:20:21作者: 3rdtsuki

堆块chunk介绍及unlink漏洞利用原理

chunk结构

当进程动态分配内存时,系统会在堆中创建一个chunk(堆块)。chunk包含chunk头和chunk体两部分

chunk头中有两个字段:

  • prev_size:前一个chunk的size,前指的之前分配的内存,也就是低地址相邻的chunk
  • size:当前chunk的size,size字段的低3位A,M,P不用于计算size,其中末位的PREV_INUSE字段表示前一堆块是否正在使用(1为使用,0为空闲)

chunk体分为两种情况

  • 对于非空闲/正在使用的堆块,chunk体就是当前堆块存放的数据。
  • 空闲的堆块会被一个双向链表(空闲链表)连接。chunk体的前两个size_t的空间存放两个指针fd和bk,fd指向链表中的前一个chunk(低地址),bk指向链表中的后一个chunk(高地址)。之后的空间是空闲的,即全0

image

chunk结构代码如下

struct chunk{
    size_t prev_size;
    size_t size;//低3位为A,M,P
    union{
        struct{
            chunk* fd;
            chunk* bk;
        };
        char userdata[n];
    }
}

注意事项

(1)使用size_t *p = (size_t*)malloc(0x80)分配堆空间后返回的指针p指向的不是chunk的基址&chunk,而是&chunk+2*sizeof(size_t),即跳过了prev_size和size。所以实际上chunk的大小还要加上chunk头,为0x80+0x8*2=0x90(在64bit系统下,size_t=8)

(2)在编写代码时,往往无法获得chunk结构的对象,要想修改chunk的fd和bk的值,需要使用*(&chunk+2*sizeof(size_t)) = value*(&chunk+3*sizeof(size_t)) = value,或者使用chunk[2]和chunk[3]来修改

(3)当PREV_INUSE=1时,prev_size无意义,一般填0即可

chunk的free过程

如果我们要free一个chunk,设其指针为p,free(p)会进行如下操作:

  • 检查物理上与p前后相邻的chunk是否也是空闲的
  • 如果是,则将空闲的chunk与p进行合并,将空闲的chunk从空闲链表中删除(unlink),将合并后的chunk加入空闲链表

如果free的chunk大小小于0x80,则会放入fastbin

unlink(p)时,首先会进行一个检查

// fd bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      \
  malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \

然后执行四句代码完成删除

FD=p->fd;
BK=p->bk;
FD->bk=BK;
BK->fd=FD;

unlink漏洞利用原理

由上面的介绍可知,free一个堆块时会在链表中unlink原来空闲的chunk,而unlink的四句代码可以被利用来进行非法操作,方法如下

首先需要堆中有连续的两个chunk p和f(假设size均为0x90个字节),还需要可以控制指针p。然后在p中构造一个略小的fake_chunk,只差一个chunk头,其大小为0x80。现在要将fake_chunk伪装成一个空闲的chunk,进行如下操作

  • 通过堆溢出(p[16]和p[17])设置f->prev_size=sizeof(fake_chunk), f->PREV_INUSE=0,这让系统认为f的前一个chunk是fake_chunk,并且是空闲的
  • 设置fake_chunk->prev_size=0,fake_chunk->PREV_INUSE=1,这让系统认为fake_chunk前面的chunk不是空闲的。prev_size=0的原因参见注意事项(3)
  • 设置fake_chunk->fd=fake_chunk-3*sizeof(size_t)fake_chunk->bk=fake_chunk-2*sizeof(size_t),这是利用unlink的关键

此时堆结构如下
image

现在,执行free(f)。系统先检查与f相邻的fake_chunk,发现是空闲的,所以要从链表中unlink(fake_chunk),四行代码会执行为:

FD=fake_chunk->fd;//FD=fake_chunk-3*sizeof(size_t)
BK=fake_chunk->bk;//BK=fake_chunk-2*sizeof(size_t)
FD->bk=BK;//*(fake_chunk-3*sizeof(size_t)+3*sizeof(size_t))=BK
BK->fd=FD;//*(fake_chunk-2*sizeof(size_t)+2*sizeof(size_t))=FD

最后一行执行后会使得*(fake_chunk)=fake_chunk-3*sizeof(size_t),也就是说unlink使得fake_chunk指针指向了fake_chunk-3*sizeof(size_t)这个地址,我们就可以任意修改其中的内容了。

之所以要在p中偏移两个size_t构造fake_chunk而不是直接将p改为fake_chunk,是因为要绕过unlink(fake_chunk)时的检查

// fd bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      \
  malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \

必须使得FD->bk==BK->fd==fake_chunk,我们构造的fake_chunk会使得FD->bk=fake_chunk-3*sizeof(size_t)+3*sizeof(size_t)=fake_chunk,绕过这个检查。

参考:https://bbs.kanxue.com/thread-273402.htm