heap unlink

在glibc2.3及之前,unlink宏早期定义如下

#define unlink( P, BK, FD ) \
{ \
    BK = P->bk; \
    FD = P->fd; \
    FD->bk = BK; \
    BK->fd = FD; \
}

按照这个宏的定义,它期望P->fdP->bk都是一个指向堆块头部的指针,所以FD->bk其实是将FD+0x18写入bk指针,BK->fd是将BK+0x10处写入fd指针。如此一来,该链表中没有任何一个堆块会指向P堆块,表现为P堆块从链表中脱离。

但是,当攻击者能够劫持P的fd和bk指针,事情就会发生变化。如果攻击者想要实现将A地址的数据写入B,只需要将P->fd指向A-0x18处,P->bk指向B,那么FD->bk实际上就是 *(A-0x18+0x18) = B

BK->fd就是 *(B+0x10) = A-0x18,虽然B是攻击者想要写入的数据,但在宏中此处会被视为一个内存地址,所以要求A和B+0x10处都可写


在之后的更新中,在执行unlink宏操作前,系统进行一些安全检查,其主要针对较早版本中fd和bk指针被劫持的情况。unlink宏要求被合并的堆块P的FD->bkBK->fd均指向该堆块

if (__builtin_expect (fd->bk != P || bk->fd != P, 0)) {
    malloc_printerr ("corrupted double-linked list");
}

该检查的进步在于不再信任P堆块的fd和bk代表的就是真实的FD和BK堆块,由于在正常的链表中,FD->bkBK->fd应当同时指向P,所以这被作为了检查的依据。如果攻击者仍然尝试通过篡改P->fdP->bk指针直接进行任意的数据写入,大概率会因为其fd指向地址的+0x18偏移未指向P而被阻止(当然对bk而言同理)

攻击者不太可能做到同时将目标地址的+0x18偏移和期望写入数据(视为内存地址)的+0x10偏移写为指向P的指针。但是,可以通过现有指向P的指针来绕过检查

P堆块通过malloc()函数申请,不论背后的具体实现是brk()还是mmap(),该函数都会返回一个指向P的指针,该指针存储在全局指针数组中,记作ptr

很巧的是,该数组中存储的指针指向用户数据起始位置(fd指针偏移处),所以如果直接从申请的堆块直接构造一个伪造的fake_P堆块,全局指针数组中的P指针就可以视作指向fake_P头部的指针。

在fake_P堆块中,构造其fd为&ptr-0x18,bk为&ptr-0x10。这样,FD->bkBK->fd都会是全局指针数组上的ptr,恰好指向fake_P头部。

然而对于该堆块的下一个堆块而言,上一个堆块的起始是P而不是fake_P,需要通过堆溢出篡改下一个堆块的pre_size条目和P标志位,使其识别上一个堆块为fake_P并且空闲

如果顺利,指向unlink宏后:

*(&ptr-0x18+0x18) = &ptr-0x10

*(&ptr-0x10+0x10) = &ptr-0x18

最后就是*(&ptr) = &ptr - 0x18,通过以上操作,全局指针数组中的指针被篡改为指向其附近的地址,而这个指针理论上是可以被随意操控的,如果再往后写入一些数据就可以覆盖全局指针数组上的其他指针


现在,unlink宏被更改为unlink_chunk()函数,引入了Large Bin 检查、指针保护等新防护特性,攻击的主要目标也从直接进攻变为绕过。

评论