以前咱们深刻了解了glibc malloc的运行机制(文章连接请看文末▼),下面就让咱们开始真正的堆溢出漏洞利用学习吧。说实话,写这类文章,我是比较怂的,由于我当前从事的工做跟漏洞挖掘彻底无关,学习这部分知识也纯粹是我的爱好,于周末无聊时打发下时间,甚至我最初的目标也仅仅是能快速看懂、复现各类漏洞利用POC而已…鉴于此,后续的文章大体会由两种内容构成:1)各类相关文章的总结,再提炼;2)某些好文章的翻译及拓展。本文二者皆有,主要参考文献见这里。linux
首先,存在漏洞的程序以下:shell
在代码[3]中存在一个堆溢出漏洞:若是用户输入的argv[1]的大小比first变量的666字节更大的话,那么输入的数据就有可能覆盖掉下一个chunk的chunk header——这能够致使任意代码执行。而攻击的核心思路就是利用glibc malloc的unlink机制。安全
上述程序的内存图以下所示:数据结构
unlink攻击技术就是利用”glibc malloc”的内存回收机制,将上图中的second chunk给unlink掉,而且,在unlink的过程当中使用shellcode地址覆盖掉free函数(或其余函数也行)的GOT表项。这样当程序后续调用free函数的时候(如上面代码[5]),就转而执行咱们的shellcode了。显然,核心就是理解glibc malloc的free机制。wordpress
在正常状况下,free的执行流程以下文所述:函数
PS:鉴于篇幅,这里主要介绍非mmaped的chunks的回收机制,回想一下在哪些状况下使用mmap分配新的chunk,哪些状况下不用mmap?学习
一旦涉及到free内存,那么就意味着有新的chunk由allocated状态变成了free状态,此时glibc malloc就须要进行合并操做——向前以及(或)向后合并。这里所谓向前向后的概念以下:将previous free chunk合并到当前free chunk,叫作向后合并;将后面的free chunk合并到当前free chunk,叫作向前合并。spa
1、向后合并翻译
相关代码以下:指针
首先检测前一个chunk是否为free,这能够经过检测当前free chunk的PREV_INUSE(P)比特位知晓。在本例中,当前chunk(first chunk)的前一个chunk是allocated的,由于在默认状况下,堆内存中的第一个chunk老是被设置为allocated的,即便它根本就不存在。
若是为free的话,那么就进行向后合并:
将前一个chunk占用的内存合并到当前chunk;
修改指向当前chunk的指针,改成指向前一个chunk。
使用unlink宏,将前一个free chunk从双向循环链表中移除(这里最好本身画图理解,学过数据结构的应该都没问题)。
在本例中因为前一个chunk是allocated的,因此并不会进行向后合并操做。
2、向前合并操做
首先检测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不会被“向前合并“掉。
介绍完向前、向后合并操做,下面就须要了解合并后(或由于不知足合并条件而没合并)的chunk该如何进一步处理了。在glibc malloc中,会将合并后的chunk放到unsorted bin中(还记得unsorted bin的含义么?)。相关代码以下:
上述代码完成的整个过程简要归纳以下:将当前chunk插入到unsorted bin的第一个chunk(第一个chunk是链表的头结点,为空)与第二个chunk之间(真正意义上的第一个可用chunk);而后经过设置本身的size字段将前一个chunk标记为已使用;再更改后一个chunk的prev_size字段,将其设置为当前chunk的size。
注意:上一段中描述的”前一个“与”后一个“chunk,是指的由chunk的prev_size与size字段隐式链接的chunk,即它们在内存中是连续、相邻的!而不是经过chunk中的fd与bk字段组成的bin(双向链表)中的前一个与后一个chunk,切记!
在本例中,只是将first chunk添加到unsorted bin中。
如今咱们再来分析若是一个攻击者在代码[3]中精心构造输入数据并经过strcpy覆盖了second chunk的chunk header后会发生什么状况。
假设被覆盖后的chunk header相关数据以下:
prev_size = 一个偶数,这样其PREV_INUSE位就是0了,即表示前一个chunk为free。
size = -4
fd = free函数的got表地址address – 12;(后文统一简称为“free addr – 12”)
bk = shellcode的地址
那么当程序在[4]处调用free(first)后会发生什么呢?咱们一步一步分析。
1、向后合并
鉴于first的前一个chunk非free的,因此不会发生向后合并操做。
2、向前合并
先判断后一个chunk是否为free,前文已经介绍过,glibc malloc经过以下代码判断:
PS:在本例中next chunk即second chunk,为了便于理解后文统一用next chunk。
从上面代码能够知道,它是经过将nextchunk + nextsize计算获得指向下下一个chunk的指针,而后判断下下个chunk的size的PREV_INUSE标记位。在本例中,此时nextsize被咱们设置为了-4,这样glibc malloc就会将next chunk的prev_size字段看作是next-next chunk的size字段,而咱们已经将next chunk的prev_size字段设置为了一个偶数,所以此时经过inuse_bit_at_offset宏获取到的nextinuse为0,即next chunk为free!既然next chunk为free,那么就须要进行向前合并,因此就会调用unlink(nextchunk, bck, fwd);函数。真正的重点就是这个unlink函数!
在前文2.1节中已经介绍过unlink函数的实现,这里为了便于说明攻击思路和过程,再详细分析一遍,unlink代码以下:
此时 P = nextchunk, BK = bck, FD = fwd。
首先FD = nextchunk->fd = free地址– 12;
而后BK = nextchunk->bk = shellcode起始地址;
再将BK赋值给FD->bk,即(free地址– 12)->bk = shellcode起始地址;
最后将FD赋值给BK->fd,即(shellcode起始地址)->fd = free地址– 12。
前面两步还好理解,主要是后面2步比较迷惑。咱们做图理解:
结合上图就很好理解第3,4步了。细心的朋友已经注意到,free addr -12和shellcode addr对应的prev_size等字段是用虚线标记的,为何呢?由于事实上它们对应的内存并非chunk header,只是在咱们的攻击中须要让glibc malloc在进行unlink操做的时候将它们强制看做malloc_chunk结构体。这样就很好理解为何要用free addr – 12替换next chunk的fd了,由于(free addr -12)->bk恰好就是free addr,也就是说第3步操做的结果就是将free addr处的数据替换为shellcode的起始地址。
因为已经将free addr处的数据替换为了shellcode的起始地址,因此当程序在代码[5]处再次执行free的时候,就会转而执行shellcode了。
至此,整个unlink攻击的原理已经介绍完毕,剩下的工做就是根据上述原理,编写shellcode了。只不过这里须要注意一点,glibc malloc在unlink的过程当中会将shellcode + 8位置的4字节数据替换为free addr – 12,因此咱们编写的shellcode应该跳过前面的12字节。
当前,上述unlink技术已通过时了(但不表明全部的unlink技术都失效,详情见后文),由于glibc malloc对相应的安全机制进行了增强,具体而言,就是添加了以下几条安全检测机制。
该机制不容许释放一个已经处于free状态的chunk。所以,当攻击者将second chunk的size设置为-4的时候,就意味着该size的PREV_INUSE位为0,也就是说second chunk以前的first chunk(咱们须要free的chunk)已经处于free状态,那么这时候再free(first)的话,就会报出double free错误。相关代码以下:
该机制检测next size是否在8到当前arena的整个系统内存大小之间。所以当检测到next size为-4的时候,就会报出invalid next size错误。相关代码以下:
该机制会在执行unlink操做的时候检测链表中前一个chunk的fd与后一个chunk的bk是否都指向当前须要unlink的chunk。这样攻击者就没法替换second chunk的fd与fd了。相关代码以下:
通过上述3层安全检测,是否意味着全部unlink技术都失效了呢?答案是否认的,由于进行漏洞攻击的人脑洞永远比天大!以前恰好看到一篇好文][15,主讲在Android4.4上利用unlink机制实现堆溢出攻击。众所周知,Android内核基于linux,且其堆内存管理也是使用的glibc malloc,虽然在一些细节上有些许不一样,但核心原理相似。该文介绍的攻击方式就成功绕过了上述三层检测。
本文详细介绍了unlink攻击技术的核心原理,虽然上述介绍的unlink漏洞利用技术已经失效,而其余的unlink技术难度也愈来愈大,可是咱们仍是有必要认真学习,由于它一方面能够进一步加深咱们对glibc malloc的堆栈管理机制的理解,另外一方面也为后续的各类堆溢出攻击技术提供了思路。
从上文的分析能够看出,unlink主要仍是利用的glibc malloc中隐式链表机制,经过覆盖相邻chunk的数据实现攻击,那么咱们能不能在显示链表中也找到攻击点呢?请关注下一篇文章:基于fastbin的堆溢出漏洞利用介绍。
附:Linux技术分析系列文章
做者:走位@阿里聚安全,更多安全技术文章,请访问阿里聚安全博客