unlink是利用glibc malloc 的内存回收机制造成攻击的,核心就在于当两个free的堆块在物理上相邻时,会将他们合并,并将原来free的堆块在原来的链表中解链,加入新的链表中,但这样的合并是有条件的,向前或向后合并。但这里的前和后都是指在物理内存中的位置,而不是fd和bk链表所指向的堆块。
以当前的chunk为基准,将preivous free chunk合并到当前chunk称为向后合并,将后面的free chunk合并到当前chunk就称为向前合并。
向后合并
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(p, bck, fwd);
}
#define chunk_at_offset(p, s) ((mchunkptr)(((char*)(p)) + (s)))
unlink的定义如下:
#define unlink(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); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (P->size) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
assert (P->fd_nextsize->bk_nextsize == P); \
assert (P->bk_nextsize->fd_nextsize == P); \
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弄清楚。
#define unlink(P,BK,FD){
FD=P->fd;
BK=P->bk;
FD->bk=BK;
BK->fd=FD;
...
}
unlink可以简单看作上面的代码部分,将链表中的P脱链,把之前P的下一个chunk与P的上一个chunk连接,使P离开链表。
首先检测前一个chunk是否为free状态,通过检测当前free chunk的PREV_INUSE(P)标志位,如果为0表示free状态,但内存中第一个申请的chunk的前一个chunk一般都被认为在使用中,所以不会发生向后合并。
如果不是内存中的第一个chunk且它的前一个chunk标记为free状态时,发生向后合并:
首先修改chunk的size位大小为两个chunk size之和
再将指针移动到前一个chunk处
最后调用unlink将前一个chunk从它所在的链表中移除。
向前合并
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
/* consolidate forward */
if (!nextinuse) {
unlink(nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);
#define clear_inuse_bit_at_offset(p, s)\
(((mchunkptr)(((char*)(p)) + (s)))->size &= ~(PREV_INUSE))
向前合并不修正P的指针,只增加size大小。
在合并后glbc malloc会将合并后新的chunk加入unsorted bin中,加入第一个可用的chunk之前,更改自己的size字段将前一个chunk标记为已用,再将后一个chunk的previous size改为当前chunk的大小。
利用unlink改写got表执行shellcode的第一种姿势(理解来自 阿里@走位)
如果两个相邻的chunk在第一个chunk写入数据时发生了溢出,覆盖了后一个chunk的数据为特殊含义的数据,那么就有可能使程序执行特定的代码,从而控制程序流程达到相应的目的。
当我们通过溢出改写数据如下时会满足特别的条件:
previous size => 一个偶数
size =>-4
fd => free@got addr-12
bk =>shellcode addr
当覆盖数据后,因为改写的是下一个chunk的数据,当free当前第一个chunk时,先考虑会不会向后合并,这时因为第一个chunk 的前一个总是占用的,即使他根本不存在,所以当第向后chunk被free后不会发生向后合并,再判断向前合并,
首先去检测下一个chunk是否处于free状态
需要通过next->next chunk的size标志位检测,当我们设置next chunk的size为-4时,next chunk的previous size字段会被看作next->next chunk的size字段,此时又因为next chunk的previous size 字段为偶数,即next->next chunk->size为偶数,P标志位为0,表示next chunk为free状态,满足向后合并的条件,触发unlink。
当满足unlink的条件时,内存的变化
利用两个临时变量FD、BK将后一个chunk从原来的free链表中解链
首先FD=P->fd;BK=P->bk;这时FD的值为free@got-12,BK为shellcode地址
再次FD->bk=BK;BK->fd=FD;因为这时FD,BK是强制被看作两个chunk的,所以它的bk与fd相对的地址与一个正常chunk是一样的,FD->bk与FD的地址相差12,而FD为free@got-12,那么FD->bk的位置就是free@got的位置,被赋值了BK=shellcode,这时当执行free函数时转而执行shellcode,达成了通过unlink修改free@got表的目的。
利用unlink的第二种姿势
将一个大的chunk伪造成两个较小的堆块,在填充数据时通过第二个伪造堆块的size标志位P使前一个伪chunk处于free状态,这时因为这个大的堆块本来就处于inuse状态,所以第二个伪堆块的下一个堆块size位P=1,表示前一个chunk处于使用状态,当free后面的伪堆块时会将前一个伪堆unlink,
图中红色边框为一个大的申请的堆块,黄色为第一个伪堆块,蓝色为第二个伪堆块,通过数据填充构造上图的内存布局,因为ptr为申请内存时返回的指针,所以ptr一开始就指向fake_prev,要绕过unsafe_unlink,将第一个伪堆块的fd=ptr-0x18,bk=ptr-0x10,这样当检查FD->bk=p的时候成功绕过了检测,满足了unlink可以实现后续的利用。