unlink漏洞(small bin)
当堆块free时,会检查相邻的后面的堆块(地址更小的),或者前面的堆块(地址更大的),是否空闲,如果空闲,那么需要进行堆块合并操作。
-
空闲的堆块一般以双向链表的形式组织(fast bin是单向链表,此攻击不适用),如果刚刚释放的堆块要与前面或者后面空闲的堆块进行合并操作,那么需要将前或后的堆块从双向链表中摘下来,合并成更大的堆块插入到unsort bin链表中。空闲堆块从(small bin)双向链表中摘下来的操作就是unlink。
-
这个漏洞能够写任何内存,为什么呢?
漏洞利用
首先需要两个相邻的堆块,其中一个堆块空闲,一个堆块占用,释放占用的堆块,引发两个堆块合并。正常的空闲堆块链接在空闲链表中,我们无法控制其中的fd和bk指针,所以方法是伪造一个空闲的堆块。libc判断相邻堆块空闲的方法是通过本堆块的size字段。
每个堆块大小是8的倍数,所以size字段最后3位是0,被libc作为标志位。其中最后一位如果为0,说明后面的相邻的堆块(地址更小的)是free的,为1说明正在使用。pre_size字段指明后一个堆块的起始位置。这两个字段可以判断相邻的后面堆块:是否分配和堆块的位置。那么unlink操作就根据这两个信息来发生。如果系统还会判断这个相邻的堆块是否在某个未分配的链表中,那么unlink攻击便实现不了,因为如果相邻堆块在某个空闲链表中,那么如何修改其中的bk和fd指针呢?答案是不可行。所以我们需要一个分配的堆块,来构造一个free堆块和一个已分配堆块。
具体操作
-
分配两个堆块,但不要过小,大于80字节就好。小于80字节应该是fastbin
-
前面分配的堆块用来伪造需要unlink的空闲堆块,那么需要设置堆块头和两个指针fd和bk。
-
伪造后面分配的堆块的头部,即pre_size和size字段,pre_size是整个堆块的大小(包含用户分配的大小和堆块头部),那么pre_size需要设置成前面分配的堆块的用户区大小,并且设置size字段最后一位为0。表示后面的伪造堆块是空闲的。free第二个分配的堆块时,系统检查发现相邻的后面的伪空闲堆块是空闲的,那么需要进行合并。
-
伪空闲堆块需要从空闲链表中unlink,实际上这个伪空闲堆块并不存在于任何空闲链表中。unlink之前需要进行一些简单的检查,这个检查是可以欺骗的:首先需要搞清楚"->"操作,"->"操作符左边的是指针,这个指针存放了某个内存的地址,操作符右边的是这个指针指向地址的某个偏移位置。合起来就是取指针指向地址的某个偏移处的内存。fd的偏移是3个机器位数,bk的偏移是4个机器位数。即在64位机器上,fd是8*3=24字节,bk是8*4=32字节;32位机器上,fd是4*3=12字节,bk是4*4=16字节。设伪空闲堆块的堆块头指针是p,那么需要检查:p->bk->fd==p && p->fd->bk==p
-
如何伪造伪空闲堆块上的fd和bk处的值才能绕过检查呢?fd = &p - 3*size(int); bk = &p - 2*size(int) 这样可以保证检查没有问题,否则提示double link错误。
-
unlink发生:FD->bk 和 BK->fd是p那个内存,设置成FD后,那么p = &p - 3*size(int)
FD = p->fd; BK = p->bk; FD->bk = BK; BK->fd = FD;
由上图所示,当ptr[0] = system_addr后,free函数的got表被改写成system函数的真实地址。只要之后再次调用free函数就会执行system函数,但是system函数需要参数"/bin/sh"才能弹出shell,可以再次申请空间,并且写入"/bin/sh"字符串,然后free就行了。
ptr2 = alloc(0x80)
ptr2 = "/bin/sh"
free(ptr2) //free在got表的地址已经变成了system的地址,而ptr2指向"/bin/sh"字符串
还有问题是:如何知道system函数的地址?
上图中ptr[3]=&free_got后,应该有打印函数可以打印ptr指向的值,即free_got处的值,既然leak到了free在libc上的值,那么system函数的值可以通过相对地址获得。
示例程序如下
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
int main(){
int malloc_size = 0x80;
uint64_t* ptr0 = (uint64_t*)malloc(malloc_size);
uint64_t* ptr1 = (uint64_t*)malloc(malloc_size);
ptr0[2] = (uint64_t)&ptr0 - 3*sizeof(uint64_t);
ptr0[3] = (uint64_t)&ptr0 - 2*sizeof(uint64_t);
uint64_t* ptr1_head = (uint64_t)ptr1 - 2*sizeof(uint64_t);
ptr1_head[0] = malloc_size;
ptr1_head[1] &= ~1;
free(ptr1);
char victim[10] = "hello";
ptr0[3]=(uint64_t)victim;
ptr0[0] = 0x4141414141;
printf("%s\n",victim);
return 0;
}
注意这个示例在ubuntu16.04 64位上通过,在ubuntu18.04 64位上测试失败,应该是libc做了一些防御。
参考连接: