Heap Exploitation-unlink
Heap Exploitation - unsafe unlink
原理
unlink是glibc里的一个宏,是一段代码片段,通常在内存管理系统中使用。为了避免堆内存过度碎片化,当一个非fast bins的堆块被释放时,libc会查看前后堆块是否是释放状态,是就取出合并,这就是unlink。
unlink主要是从双向链表结构中移除一个节点,该节点通常用于跟踪空闲内存块。目的是将某一空闲的chunk从其所处的bin中脱链。在malloc_consolidate()函数中将fast bins中的空闲chunk,整理到unsorted bins中,在malloc()函数中用于将unsorted bins中的空闲chunk整理到small bins中或者large bins中,以及在malloc()中获得堆空间时,均可能调用unlink()宏。
在执行free()时会执行int_free()函数,int_free()函数中调用了unlink宏
|
unlink源码:
/* Take a chunk off a bin list */ |
在源码中,首先会通过 FD = P->fd; BK = P->bk;
来将前向和后向指针分别存储。然后进行错误检查,执行FD->bk = BK; BK->fd = FD;
来调整P节点前后的指针,绕过P来移除P节点。之后会检查节点的大小,对更大的节点有额外的检查和调整方法,由于large bins中的一个bin会包含不同大小的chunk,所以还会额外处理fd_nextsize和bk_nextsize。
ulink过程图示:
利用
简单来说,由于C语言通过计算偏移来寻找相应的结构体成员,就算堆块的某些成员变量被篡改,libc仍然会认为该位置就是原来的数据,那么就可以通过unlink这一操作来指向伪造的fake fd和fake bk。
这里简单说一下原理,然后通过复现例题理解。
检查机制
在unlink前会检查
FD->bk != P || BK->fd != P |
简单来说就是检查前置结点的fd指针和后置结点的bk指针指向的是否都是该结点
条件
- UAF(Use After Free),可修改 free 状态下 small bins 或是 unsorted bins 的 fd 和 bk 指针
- 已知位置存在一个指针指向可进行UAF的chunk
思路
虽然有检查机制,但是还是比较容易绕过的,设指向可 UAF chunk 的指针的地址为 ptr
修改 fd 为 ptr - 0x18
修改 bk 为 ptr - 0x10
触发 unlink
ptr 处的指针会变为 ptr - 0x18。
示例复现
静态分析
检查保护,64位开启NX和Canary
函数逻辑就是,检查输入的数字,输入数字执行对应的函数。
输入1进入函数,会申请一个对应输入大小的chunk,并将chunk的内存指针存储到一个变量中,判断chunk是否创建成功,成功就会将该变量放在bss段的一个未初始化变量中,并输出成功分配的内存块数量。
fgets(s, 16, stdin); |
输入2进入对应函数,首先接收输入,判断对应输入下边的位置是否有chunk,如果有就将再次接收输入,使用fread函数将输入的字符写入地址处,如果输入一个不限制长度的字符串就会造成堆溢出。
fgets(s, 16, stdin); |
输入3会进入释放chunk的函数,如果输入的下标有被创建的chunk就释放。
v3 = __readfsqword(0x28u); |
动态分析
1
先申请一个chunk,查看chunk可以看到有四个
第二个是申请的chunk,可以看到申请了0x20的空间(user data 10 + prev_size 8 + size 8),显示0x21是因为prev_inuse标志位为1。能看到还有另外三个chunk,最后一个size很大的是top chunk。
整个堆会在初始化后被当成一个free chunk,成为top chunk,每次用户申请内存时,如果bins中没有合适的chunk,就会从top chunk中进行划分,如果top chunk的大小不够,则调用brk()拓展堆的大小。然后从新的top chunk中进行划分。
第一个和第三个实际上是由于程序本身没有进行setbuf操作,所以在初次使用fget()和printf()执行输入输出时会申请缓冲区。
那这样就不能利用chunk1进行操作了,chunk1是被两个输入输出申请的chunk包住了,没法利用,也不好利用,不如直接再申请两个chunk2和chunk3,这时候再申请的两个chunk就是连在一起的了,就方便操作,然后用chunk2来溢出到chunk3,执行流程就比较好控制了。
接下来需要分析一下溢出的效果是什么,如何控制。
2
之前知道在编辑时可以构造堆溢出,比如申请三个chunk,编辑第二个,让第二个溢出到第三个。
申请三个chunk分别为chunk1、chunk2、chunk3,编辑第二个chunk,将其内容改为48字节大小的内容。
能够看到chunk3消失了
查看chunk2地址的内容可知,未修改前chunk2地址的内容:
修改后chunk2地址的内容
其实就是由于溢出,chunk3的prev_size和size被修改了,导致了识别的上的问题(可能),不过查看0x602140处存放chunk地址的数组发现chunk3也消失了,此处还没弄清楚,应该也是原来指向的地址被修改了导致的。但是确定了可以利用溢出来修改chunk3的chunk头信息。
实际做题中,第一个chunk的大小无所谓,因为用不到。第二个chunk最好是0x30,第三个不能小于0x80,防止free掉被放到fast bins中。
3
利用方法,简单来说就是在chunk2内的data部分来伪造一个chunk,伪造为释放状态,利用堆溢出修改后一个chunk也就是这里的chunk3的prev_size和size,将chunk3的prev_size修改为伪造的chunk大小,并将size的P位置0,以此来绕过检查并触发unlink,触发unsafe unlink。
构造fake chunk:
- prev_size:prev_size这里实际上与chunk2相关,但是是不需要合并chunk2的,所以prev_size置零即可
- size:fake chunk只需要fd和bk来完成unlink,而由于需要伪造为释放状态,即P位为0,所以为0x20
接下来要构造fake chunk的fd和bk。
在这里需要将0x602140的作为一个chunk来看,将其作为BK。将0x602138处的数据也作为chunk来看,也就是FD。
让fake chunk的fd指针指向0x602140,并让bk指针指向0x602138。
为什么要这么设置呢?前面说过:
由于C语言通过计算偏移来寻找相应的结构体成员,就算堆块的某些成员变量被篡改,libc仍然会认为该位置就是原来的数据
在源码中:
//取要脱链结点的fd指针和bk指针 |
也就是说,bk指针是通过 + 0x18来找到的,fd指针是通过起始地址 + 0x10来找到的,二者均通过计算偏移来寻找成员,那么就可以将上面的代码理解为:
P->fd->bk == P <=> *(p->fd+0x18) == P |
那么就可以设置fd和bk如下:
P->fd = &P-0x18 |
这样实际上就完成了对检查的绕过,利用偏移寻找结构体成员时,找到的仍旧是正确的结点,但是fd和bk指向的是bss段某处的地址。
这样的话就可以利用已有条件,将fd和bk修改指向为可以控制的某段空间,在这里就是已知的bss段。
前面分析源码已知了global数组的地址为0x602140,就可以按以下进行设置:
target = 0x602140 + 0x10 //global[2] |
修改chunk2后,fake chunk就与FD和BK构造了一个双向链表。
第一个payload:
payload1 = p64(0) + p64(0x30) |
修改后效果
查看chunk2和chunk3
4
构造好了fake chunk就要想办法触发unlink,这里再次说明一下unlink是如何实现的。
在上面构造了fake chunk,以此来实现绕过检测,并将bk和fd指针指向可写的bss段上,并且现在chunk3的prev_size已经被设置为了fake chunk的大小,size的prev_inuse也被置0。此时free掉chunk3,glibc检查chunk3的chunk头就会发现是free状态,将上一个chunk也就是fake chunk拿出来进行合并。
fake chunk就会被从刚才构造的双向链表中摘除,摘除过程中会执行FD->bk = BK; BK->fd =FD;
实际上改变的是同一处的地址,但是由于先后顺序,最终被改为0x602138,此时我们就控制了0x602138到0x602150之间,可以在这段区域来继续进行操作。
free掉chunk3即可,gdb调试看一下。
成功控制了这段内存区域。
5
控制这段区域后,由于该题没有后门,需要泄露libc基址。
而由于之前按将原本global数组中存放chunk2地址的位置改写为了0x602138,所以实际上如果修改chunk2的内容是不会写入到chunk2内的,会写到0x602138处。
那么就可以通过修改chunk2来对global数组来进行部署函数的GOT表地址,再次修改global数组时就会修改GOT表中的真实地址。实现GOT表劫持。
第二个payload:
payload2 = 'a' * 0x8 |
布置好后,由于chunk0处为free函数的GOT表地址,可以将其修改为put函数的PLT地址,这样调用free就会调用puts函数。如果释放chunk1,就会实现:
free(chunk1) -> free(puts_got) -> puts(puts_got) |
将puts函数的真实地址输出。并利用真实地址计算偏移得出system函数的真实地址和binsh的真实地址。
最后还是老一套,将chunk2的atoi函数GOT地址改为system函数地址,调用atoi就会调用system函数,等待输入时输入/bin/sh地址,就是调用system(/bin/sh),getshell。
EXP
#!usr/bin/env python |