Unlink
原理
我们在利用 unlink 所造成的漏洞时,其实就是对 chunk 进行内存布局,然后借助 unlink 操作来达成修改指针的效果。
我们先来简单回顾一下 unlink 的目的与过程,其目的是把一个双向链表中的空闲块拿出来(例如 free 时和目前物理相邻的 free chunk 进行合并)。其基本的过程如下

下面我们首先介绍一下 unlink 最初没有防护时的利用方法,然后介绍目前利用 unlink 的方式。
古老的 unlink
在最初 unlink 实现的时候,其实是没有对 chunk 的 size 检查和双向链表检查的,即没有如下检查代码。
这里我们以 32 位为例,假设堆内存最初的布局是下面的样子

现在有物理空间连续的两个 chunk(Q,Nextchunk),其中 Q 处于使用状态、Nextchunk 处于释放状态。那么如果我们通过某种方式(比如溢出)将 Nextchunk 的 fd 和 bk 指针修改为指定的值。则当我们free(Q)时
glibc 判断这个块是 small chunk
判断前向合并,发现前一个 chunk 处于使用状态,不需要前向合并
判断后向合并,发现后一个 chunk 处于空闲状态,需要合并
继而对 Nextchunk 采取 unlink 操作
那么 unlink 具体执行的效果是什么样子呢?我们可以来分析一下
FD=P->fd = target addr -12
BK=P->bk = expect value
FD->bk = BK,即 *(target addr-12+12)=BK=expect value
BK->fd = FD,即*(expect value +8) = FD = target addr-12
看起来我们似乎可以通过 unlink 直接实现任意地址读写的目的,但是我们还是需要确保 expect value +8 地址具有可写的权限。
比如说我们将 target addr 设置为某个 got 表项,那么当程序调用对应的 libc 函数时,就会直接执行我们设置的值(expect value)处的代码。需要注意的是,expect value+8 处的值被破坏了,需要想办法绕过。
当前的 unlink
**但是,现实是残酷的。。**我们刚才考虑的是没有检查的情况,但是一旦加上检查,就没有这么简单了。我们看一下对 fd 和 bk 的检查
此时
FD->bk = target addr - 12 + 12=target_addr
BK->fd = expect value + 8
那么我们上面所利用的修改 GOT 表项的方法就可能不可用了。但是我们可以通过伪造的方式绕过这个机制。
首先我们通过覆盖,将 nextchunk 的 FD 指针指向了 fakeFD,将 nextchunk 的 BK 指针指向了 fakeBK 。那么为了通过验证,我们需要
fakeFD -> bk == P<=>*(fakeFD + 12) == PfakeBK -> fd == P<=>*(fakeBK + 8) == P
当满足上述两式时,可以进入 Unlink 的环节,进行如下操作:
fakeFD -> bk = fakeBK<=>*(fakeFD + 12) = fakeBKfakeBK -> fd = fakeFD<=>*(fakeBK + 8) = fakeFD
如果让 fakeFD + 12 和 fakeBK + 8 指向同一个指向P的指针,那么:
*P = P - 8*P = P - 12
即通过此方式,P 的指针指向了比自己低 12 的地址处。此方法虽然不可以实现任意地址写,但是可以修改指向 chunk 的指针,这样的修改是可以达到一定的效果的。
如果我们想要使得两者都指向 P,只需要按照如下方式修改即可

需要注意的是,这里我们并没有违背下面的约束,因为 P 在 Unlink 前是指向正确的 chunk 的指针。
此外,其实如果我们设置next chunk 的 fd 和 bk 均为 nextchunk 的地址也是可以绕过上面的检测的。但是这样的话,并不能达到修改指针内容的效果。
利用思路
条件
UAF ,可修改 free 状态下 smallbin 或是 unsorted bin 的 fd 和 bk 指针
已知位置存在一个指针指向可进行 UAF 的 chunk
效果
使得已指向 UAF chunk 的指针 ptr 变为 ptr - 0x18
思路
设指向可 UAF chunk 的指针的地址为 ptr
修改 fd 为 ptr - 0x18
修改 bk 为 ptr - 0x10
触发 unlink
ptr 处的指针会变为 ptr - 0x18。
2014 HITCON stkof
基本信息
可以看出,程序是 64 位的,主要开启了 Canary 和 NX 保护。
基本功能
程序存在 4 个功能,经过 IDA 分析后可以分析功能如下
alloc:输入 size,分配 size 大小的内存,并在 bss 段记录对应 chunk 的指针,假设其为 global
read_in:根据指定索引,向分配的内存处读入数据,数据长度可控,这里存在堆溢出的情况
free:根据指定索引,释放已经分配的内存块
useless:这个功能并没有什么卵用,本来以为是可以输出内容,结果什么也没有输出
IO 缓冲区问题分析
值得注意的是,由于程序本身没有进行 setbuf 操作,所以在执行输入输出操作的时候会申请缓冲区。这里经过测试,会申请两个缓冲区,分别大小为 1024 和 1024。具体如下,可以进行调试查看
初次调用 fgets 时,malloc 会分配缓冲区 1024 大小。
分配之后,堆如下
当分配16大小的内存后,堆布局如下
当使用 printf 函数,会分配 1024 字节空间,如下
堆布局如下
此后,无论是输入输出都不会再申请缓冲区了。所以我们最好最初的申请一个 chunk 来把这些缓冲区给申请了,方便之后操作。
但是,比较有意思的是,如果我们是 attach 上去的话,第一个缓冲区分配的大小为 4096 大小。
基本思路
根据上面分析,我们在前面先分配一个 chunk 来把缓冲区分配完毕,以免影响之后的操作。
由于程序本身没有 leak,要想执行 system 等函数,我们的首要目的还是先构造 leak,基本思路如下
利用 unlink 修改 global[2] 为 &global[2]-0x18。
利用编辑功能修改 global[0] 为 free@got 地址,同时修改 global[1] 为puts@got 地址,global[2] 为 atoi@got 地址。
修改
free@got为puts@plt的地址,从而当再次调用free函数时,即可直接调用 puts 函数。这样就可以泄漏函数内容。free global[1],即泄漏 puts@got 内容,从而知道 system 函数地址以及 libc 中 /bin/sh 地址。
修改
atoi@got为 system 函数地址,再次调用时,输入 /bin/sh 地址即可。
代码如下
2016 ZCTF note2
分析程序
首先,我们先分析一下程序,可以看出程序的主要功能为
添加note,size限制为0x80,size会被记录,note指针会被记录。
展示note内容。
编辑note内容,其中包括覆盖已有的note,在已有的note后面添加内容。
释放note。
仔细分析后,可以发现程序有以下几个问题
在添加note时,程序会记录note对应的大小,该大小会用于控制读取note的内容,但是读取的循环变量i是无符号变量,所以比较时都会转换为无符号变量,那么当我们输入size为0时,glibc根据其规定,会分配0x20个字节,但是程序读取的内容却并不受到限制,故而会产生堆溢出。
程序在每次编辑note时,都会申请0xa0大小的内存,但是在 free 之后并没有设置为NULL。
第一个问题对应在ida中的代码如下
其中i是unsigned类型,a2为int类型,所以两者在for循环相比较的时候,a2-1的结果-1会被视为unsigned类型,此时,即最大的整数。所以说可以读取任意长度的数据,这里也就是后面我们溢出所使用的办法。
基本思路
这里我们主要利用发现的第一个问题,主要利用了 fastbin 的机制、unlink 的机制。
下面依次进行讲解。
基本操作
首先,我们先把note可能的基本操作列举出来。
生成三个note
构造三个 chunk,chunk0、chunk1 和 chunk2
其中这三个 chunk 申请时的大小分别为0x80,0,0x80,chunk1 虽然申请的大小为0,但是 glibc 的要求 chunk 块至少可以存储 4 个必要的字段(prev_size,size,fd,bk),所以会分配 0x20 的空间。同时,由于无符号整数的比较问题,可以为该note输入任意长的字符串。
这里需要注意的是,chunk0 中一共构造了两个 chunk
chunk ptr[0],这个是为了 unlink 时修改对应的值。
chunk ptr[0]'s nextchunk,这个是为了使得 unlink 时的第一个检查满足。
当构造完三个 note 后,堆的基本构造如图1所示。
释放 chunk1-覆盖 chunk2-释放 chunk2
对应的代码如下
首先释放 chunk1,由于该chunk属于fastbin,所以下次在申请的时候仍然会申请到该chunk,同时由于上面所说的类型问题,我们可以读取任意字符,所以就可以覆盖chunk2,覆盖之后如图2所示。
该覆盖主要是为了释放chunk2的时候可以后向合并(合并低地址),对chunk0中虚拟构造的chunk进行unlink。即将要执行的操作为unlink(ptr[0]),同时我们所构造的fakebk和fakefd满足如下约束
unlink成功执行,会导致ptr[0]所存储的地址变为fakebk,即ptr-0x18。
获取system地址
代码如下
我们修改ptr[0]的内容为 ptr 的地址-0x18,所以当我们再次编辑 note0 时,可以覆盖ptr[0]的内容。这里我们将其覆盖为atoi的地址。 这样的话,如果我们查看note 0的内容,其实查看的就是atoi的地址。
之后我们根据 libc 中对应的偏移计算出 system 的地址。
修改atoi got
由于此时 ptr[0] 的地址 got 表的地址,所以我们可以直接修改该 note,覆盖为 system 地址。
get shell
此时如果我们再调用 atoi ,其实调用的就是 system 函数,所以就可以拿到shell了。
2017 insomni'hack wheelofrobots
基本信息
动态链接64位,主要开启了 canary 保护与 nx 保护。
基本功能
大概分析程序,可以得知,这是一个配置机器人轮子的游戏,机器人一共需要添加 3 个轮子。
程序非常依赖的一个功能是读取整数,该函数read_num是读取指定的长度,将其转化为 int 类型的数字。
具体功能如下
添加轮子,一共有 6 个轮子可以选择。选择轮子时使用函数是read_num,然而该函数在读取的时候
read_num((char *)&choice, 5uLL);读取的长度是 5 个字节,恰好覆盖了 bender_inuse 的最低字节,即构成了 off-by-one 漏洞。与此同时,在添加 Destructor 轮子的时候,并没有进行大小检测。如果读取的数为负数,那么在申请calloc(1uLL, 20 * v5);时就可能导致20*v5溢出,但与此同时,destructor_size = v5仍然会很大。移除轮子,直接将相应轮子移除,但是并没有将其对应的指针设置为 NULL ,其对应的大小也没有清空。
修改轮子名字,这个是根据当时申请的轮子的大小空间来读取数据。之前我们已经说过 destructor 轮子读取大小时,并没有检测负数的情况,所以在进行如下操作时
result = read(0, destructor, 20 * destructor_size);,存在几乎任意长度溢出的漏洞。启动机器人,在启动的时候会随机地输出一些轮子的名称,这个是我们难以控制的。
综上分析,我们可以知道的是,该程序主要存在的漏洞 off-by-one 与整数溢出。这里我们主要使用前面的off-by-one 漏洞。
利用思路
基本利用思路如下
利用 off by one 漏洞与 fastbin attack 分配 chunk 到 0x603138,进而可以控制
destructor_size的大小,从而实现任意长度堆溢出。这里我们将轮子1 tinny 分配到这里。分别分配合适大小的物理相邻的 chunk,其中包括 destructor。借助上面可以任意长度堆溢出的漏洞,对 destructor 对应的 chunk 进行溢出,将其溢出到下一个物理相邻的 chunk,从而实现对 0x6030E8 处 fake chunk 进行 unlink 的效果,这时 bss 段的 destructor 指向 0x6030D0。从而,我们可以再次实现覆盖bss 段几乎所有的内容。
构造一个任意地址写的漏洞。通过上述的漏洞将已经分配的轮子1 tinny 指针覆盖为 destructor 的地址,那么此后编辑 tinny 即在编辑 destructor 的内容,进而当我们再次编辑 destructor 时就相当于任意低地址写。
由于程序只是在最后启动机器人的时候,才会随机输出一些轮子的内容,并且一旦输出,程序就会退出,由于这部分我们并不能控制,所以我们将
exit()patch 为一个ret地址。这样的话,我们就可以多次输出内容了,从而可以泄漏一些 got 表地址。其实,既然我们有了任意地址写的漏洞,我们也可以将某个 got 写为 puts 的 plt 地址,进而调用相应函数时便可以直接将相应内容输出。但是这里并不去采用这种方法,因为之前已经在 hitcon stkof 中用过这种手法了。在泄漏了相应的内容后,我们便可以得到 libc 基地址,system 地址,libc中的 /bin/sh 地址。进而我们修改 free@got 为 system 地址。从而当再次释放某块内存时,便可以启动shell。
代码如下
题目
参考
malloc@angelboy
https://gist.github.com/niklasb/074428333b817d2ecb63f7926074427a
note3
介绍
ZCTF 2016的一道题目,考点是safe unlink的利用。
题目介绍
题目是一个notepad,提供了创建、删除、编辑、查看笔记的功能
保护如下所示
功能概述
程序New功能用来新建笔记,笔记的大小可以自定只要小于1024字节。
所有的笔记malloc出来的指针存放在bss上全局数组bss_ptr中,这个数组最多可以存放8个heap_ptr。 而且heap_ptr对应的size也被放在bss_ptr数组中。current_ptr表示当前笔记,bss布局如下。
Show功能是无用的功能,edit和delete可以编辑和释放note。
漏洞
漏洞存在于edit功能中,这里面在获取用户输入的id号之后并没有进行验证。如果输入的id是负数的话依然可以执行。 在get_num函数中存在整数溢出漏洞,我们可以获得一个负数。
因此我们可以使得edit读入cuurent_ptr,使用的size是note7_ptr
首先创建8个note,然后edit note3使current_ptr指向note3,之后使用-1溢出note3
我们使用的溢出数据是用于构造一个fake chunk来实现safe unlink的利用,具体的原理可以看这一章节的讲解。
之后释放note4,note3与note4就会合并。note3_ptr会指向note0_ptr的位置。这样我们通过不断的修改note0_ptr的值和edit note0就可以实现任意地址写数据。
但是题目没有提供show功能,所以无法进行任意地址读,也就无法泄漏数据。 这里采用的办法是把free的got表改为printf的值,然后在bbs中一块空白的区域写入"%x"。 这样当free这块区域(这块区域在ptr_array中,所以可以直接传递给free),就可以泄漏出栈中的数据。 通过栈中的libc地址求出system的地址就可以利用任意地址写获得shell
完成的exp如下
Last updated