Unlink利用原理

原理

一旦涉及到free内存,那么就意味着有新的chunk由allocated状态变成了free状态,此时glibc malloc就需要进行合并操作——向前以及(或)向后合并。这里所谓向前向后的概念如下:将previous free chunk合并到当前free chunk,叫做向后合并;将后面的free chunk合并到当前free chunk,叫做向前合并。

完整的unlink宏如下

define unlink(AV, P, BK, FD) {                                            
    FD = P->fd;                     
    BK = P->bk;                 
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  
    else {                                    
        FD->bk = BK;                    
        BK->fd = FD;                    
        if (!in_smallbin_range (P->size) && __builtin_expect (P->fd_nextsize != NULL, 0)) {
        if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
        || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    
          malloc_printerr (check_action,"corrupted double-linked list (not small)",P, AV);
            if (FD->fd_nextsize == NULL) {
                if (P->fd_nextsize == P)    
                  FD->fd_nextsize = FD->bk_nextsize = FD;
                else {                      
                    FD->fd_nextsize = P->fd_nextsize;
                    FD->bk_nextsize = P->bk_nextsize;
                    P->fd_nextsize->bk_nextsize = FD;
                    P->bk_nextsize->fd_nextsize = FD;
                  }                           
              } else {                      
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;
              }                                  
          }                                   
      }                         
}

最简单版本unlink宏如下

一、向后合并:

相关代码如下:

首先检测前一个chunk是否为free,这可以通过检测当前free chunk的PREV_INUSE(P)比特位知晓。在本例中,当前chunk(first chunk)的前一个chunk是allocated的,因为在默认情况下,堆内存中的第一个chunk总是被设置为allocated的,即使它根本就不存在。

如果为free的话,那么就进行向后合并:

1)将前一个chunk占用的内存合并到当前chunk; 2)修改指向当前chunk的指针,改为指向前一个chunk。 3)使用unlink宏,将前一个free chunk从双向循环链表中移除(这里最好自己画图理解,学过数据结构的应该都没问题)。

在本例中由于前一个chunk是allocated的,所以并不会进行向后合并操作。

二、向前合并操作:

首先检测next chunk是否为free。那么如何检测呢?很简单,查询next chunk之后的chunk的 PREV_INUSE (P)即可。相关代码如下:

整个操作与”向后合并“操作类似,再通过上述代码结合注释应该很容易理解free chunk的向前结合操作。在本例中当前chunk为first,它的下一个chunk为second,再下一个chunk为top chunk,此时 top chunk的 PREV_INUSE位是设置为1的(表示top chunk的前一个chunk,即second chunk, 已经使用),因此first的下一个chunk不会被“向前合并“掉。

介绍完向前、向后合并操作,下面就需要了解执行free()合并后或者因为不满足合并条件而没合并的chunk该如何进一步处理了。在glibc malloc中,会将合并后的chunk放到unsorted bin中(还记得unsorted bin的含义么?)。相关代码如下:

上述代码完成的整个过程简要概括如下:将当前chunk插入到unsorted bin的第一个chunk(第一个chunk是链表的头结点,为空)与第二个chunk之间(真正意义上的第一个可用chunk);然后通过设置自己的size字段将前一个chunk标记为已使用;再更改后一个chunk的prev_size字段,将其设置为当前chunk的size;然后更改后一个chunk的size字段的p位,将其设置为0,表示前一个chunk为空闲。

注意:上一段中描述的”前一个“与”后一个“chunk,是指的由chunk的prev_size与size字段隐式连接的chunk,即它们在内存中是连续、相邻的!而不是通过chunk中的fd与bk字段组成的bin(双向链表)中的前一个与后一个chunk,切记!。

三、Unlink部分安全检测机制

size大小检测

双链表冲突检测

Double Free检测

利用

通过溢出设置chunk 0数据,伪造fake_chunk

如图:

我们在malloc返回的ptr0(chunk 0)开始地方构造的数据:

这样的话将chunk 0的mem空间伪造成一个fake_chunk,其中fake_fd=p32(&fake_chunk-12) 代表伪造的chunk头在程序内存空间中的地址-12, fake_bk=p32(&fake_chunk-8) 这样做的话执行unlink操作时

这样就绕过了双向链表检查。

接下来绕过前后size检查,这个就很简单,只需将chunk 1的fake_prev_size覆盖为ptr0当前mem空间大小即80,并且fake_chunk的fake_size大小必须为ptr0当前mem空间大小加上p标志位=81.

覆盖chunk 1的fake_size&flag要正确,为偶数代表前一个chunk为空可合并,大小满足原本chunk大小,这样可以避免错误的size大小以致double free报错.

由于在 chunk 1 前面构造了一个伪造的空闲内存块,当free(chunk[1])时,就会对伪造的空闲内存块进行unlink操作:

从上可知,unlink后,buf[0]存的不再是ptr0 的地址了,而是&fake_chunk - 12 (即 如下图中的 &buf-12)。此时我们只关心buf数组的内存,其布局如下:

总结

程序中存在堆溢出且长度可观时,很容易构造出unlink;但是当程序没有长度溢出,或者堆大小固定时,我们可以构造chunk错位(伪造)的方式来构造unlink的空闲chunk;还有就是利用合并后被放入unsortedbin中的chunk,利用UAF ,对合并前的堆块进行构造。详细见2018强网杯silent2网鼎杯Pwn之babyheap

参考

Linux堆溢出漏洞利用之unlink – 阿里移动安全

Last updated

Was this helpful?