unlink
0x00 堆chunk的结构
一个 heap chunk
是如下的结构:
如果本 chunk
前面的 chunk
是空闲的,那么第一部分 prev_size
会记录前面一个 chunk
的大小,第二部分是本 chunk
的 size
,因为它的大小需要8字节对齐,所以 size
的低三位一定会空闲出来,这时候这三个位置就用作三个 Flag
(最低位:指示前一个 chunk
是否正在使用;倒数第二位:指示这个 chunk
是否是通过 mmap
方式产生的;倒数第三位:这个 chunk
是否属于一个线程的arena
)。之后的FD和BK部分在此 chunk
是空闲状态时会发挥作用。FD指向下一个空闲的 chunk
,BK指向前一个空闲的 chunk
,由此串联成为一个空闲 chunk
的双向链表。如果不是空闲的。那么从fd开始,就是用户数据了。
0x01 堆溢出unlink的思路
1.unlink是说从链表从卸下一个节点。
在free一块内存时,会查看该块前后相邻的两块是否空闲,如果空闲的话则把他们从原来的链表上卸载出来和当前块合并在一起。分为向前合并和向后合并。
向前合并:
- 查看下一个块是不是空闲的 – 下一个块是空闲的,如果下下个块(距离当前空闲块)的
PREV_INUSE(P)
位没有设置。为了访问下下个块,将当前块的大小加到它的块指针,再将下一个块的大小加到下一个块指针。 - 如果是空闲的,合并它。
- 现在将合并后的块添加到 unsorted bin 中。
向后合并:
- 查看前一个块是不是空闲的 –如果当前空闲块的
PREV_INUSE(P)
位没有设置, 则前一个块是空闲的,。 - 如果空闲,合并它。
unlink操作:
FD = P->fd; BK = P->bk; FD->bk = BK; BK->fd = FD;
假设执行free(q),对于向前合并,p就是指向q这个chunk块,对于向后合并,p指向的是q的前一个chunk块
2.unlink漏洞利用
现在假设我们已经申请了两块堆内存分别由chunk1和chunk2指针指向,并且存在一个指针ptr指向chunk1.如下图
现在我们通过向chunk1中输入内容构造一个伪chunk并覆盖chunk2头部,如下图
然后我们指向free(chunk2)指令,就会发生unlink.根据chunk2头部size的flag位显示chunk2前一个fake_chunk1空闲,于是发生向后合并。由于现在的glibc会对(P->fd)->bk和(P->bk)->fd进行一个验证,检测它们是不是指向的同一个块即P.根据unlink操作代码。我们会发现根据我们的构造,可以通过检测。
FD = P->fd; //FD = ptr - 0x0c BK = P->bk; //BK = ptr - 0x08 (FD -> bk == BK -> fd) FD->bk = BK; //ptr = ptr - 0x08 BK->fd = FD; //ptr = ptr - 0x0c
最终,ptr指向ptr - 0x0c,如下图
至此,我们可以通过向chunk1改写任意地址的值。例如,加入我们先向chunk1内输入‘a'*0x12 + read@got,然后再向chunk1内进行写操作时,就是改写read@got的值了。
3.32位与64位区别
64bit: FD = fake_fd = target_addr - 0x18 BK = fake_bk = target_addr - 0x10 //then FD + 0x18 = BK = target_addr - 0x10 BK + 0x10 = FD = target_addr - 0x18 // thus *target_addr = target_addr - 0x18 32bit: FD = fake_fd = target_addr - 0x0c BK = fake_bk = target_addr - 0x08 //then FD + 0x18 = BK = target_addr - 0x08 BK + 0x10 = FD = target_addr - 0x0c // thus *target_addr = target_addr - 0x0c