堆漏洞之简单的unlink利用

本文详细介绍了unlink漏洞的原理及其利用方式,通过伪造堆块使程序在进行堆块合并时发生错误,进而实现对任意内存地址的写操作。文章还提供了一个具体的示例程序,展示了如何通过该漏洞控制free函数的got表地址,最终执行system函数获取shell。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

unlink漏洞(small bin)

       当堆块free时,会检查相邻的后面的堆块(地址更小的),或者前面的堆块(地址更大的),是否空闲,如果空闲,那么需要进行堆块合并操作。

  1. 空闲的堆块一般以双向链表的形式组织(fast bin是单向链表,此攻击不适用),如果刚刚释放的堆块要与前面或者后面空闲的堆块进行合并操作,那么需要将前或后的堆块从双向链表中摘下来,合并成更大的堆块插入到unsort bin链表中。空闲堆块从(small bin)双向链表中摘下来的操作就是unlink。

  2. 这个漏洞能够写任何内存,为什么呢?

漏洞利用

        首先需要两个相邻的堆块,其中一个堆块空闲,一个堆块占用,释放占用的堆块,引发两个堆块合并。正常的空闲堆块链接在空闲链表中,我们无法控制其中的fd和bk指针,所以方法是伪造一个空闲的堆块。libc判断相邻堆块空闲的方法是通过本堆块的size字段。

        每个堆块大小是8的倍数,所以size字段最后3位是0,被libc作为标志位。其中最后一位如果为0,说明后面的相邻的堆块(地址更小的)是free的,为1说明正在使用。pre_size字段指明后一个堆块的起始位置。这两个字段可以判断相邻的后面堆块:是否分配和堆块的位置。那么unlink操作就根据这两个信息来发生。如果系统还会判断这个相邻的堆块是否在某个未分配的链表中,那么unlink攻击便实现不了,因为如果相邻堆块在某个空闲链表中,那么如何修改其中的bk和fd指针呢?答案是不可行。所以我们需要一个分配的堆块,来构造一个free堆块和一个已分配堆块。

具体操作

  1.  分配两个堆块,但不要过小,大于80字节就好。小于80字节应该是fastbin

  2. 前面分配的堆块用来伪造需要unlink的空闲堆块,那么需要设置堆块头和两个指针fd和bk。

  3. 伪造后面分配的堆块的头部,即pre_size和size字段,pre_size是整个堆块的大小(包含用户分配的大小和堆块头部),那么pre_size需要设置成前面分配的堆块的用户区大小,并且设置size字段最后一位为0。表示后面的伪造堆块是空闲的。free第二个分配的堆块时,系统检查发现相邻的后面的伪空闲堆块是空闲的,那么需要进行合并。

  4. 伪空闲堆块需要从空闲链表中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

  5. 如何伪造伪空闲堆块上的fd和bk处的值才能绕过检查呢?fd = &p - 3*size(int); bk = &p - 2*size(int) 这样可以保证检查没有问题,否则提示double link错误。

  6. 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做了一些防御。 

参考连接:

  1. http://www.cnblogs.com/shangye/tag/ctf/
  2. https://github.com/shellphish/how2heap/blob/master/glibc_2.26/unsafe_unlink.c            
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值