堆溢出基础
堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块。堆溢出漏洞轻则可以使得程序崩溃,重则可以使得攻击者控制程序执行流程。
不难发现,堆溢出漏洞发生的基本前提是:
- 程序向堆上写入数据;
- 写入的数据大小没有被良好地控制。
与栈溢出所不同的是,堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据,因此我们一般无法直接通过堆溢出来控制EIP,一般来说,我们利用堆溢出的策略如下:
- 1、覆盖与其物理相邻的下一个 chunk 的如下内容:
- prev_size
- size,主要有三个比特位,以及该堆块真正的大小:
NON_MAIN_ARENA、IS_MAPPED、PREV_INUSE、the True chunk size- chunk content,从而改变程序固有的执行流。
- 2、利用堆中的机制(如
unlink
等 )来实现任意地址写入(Write-Anything-Anywhere
)或控制堆块中的内容等效果,从而来控制程序的执行流。
关键步骤
堆溢出中比较重要的几个步骤如下:
寻找堆分配函数
通常来说堆是通过调用glibc函数malloc
进行分配的,在某些情况下会使用calloc
分配。calloc
与malloc
的区别是 calloc 在分配后会自动进行清空,这对于某些信息泄露漏洞的利用来说是致命的。
calloc(0x20); |
除此之外,还有一种分配是经由realloc
进行的,realloc
函数可以身兼malloc
和free
两个函数的功能。
|
realloc
的操作并不是像字面意义上那么简单,其内部会根据不同的情况进行不同操作:
1、当 realloc(ptr,size) 的 size 不等于 ptr 的 size 时:
如果申请 size > 原来size:
- 如果 chunk 与 top chunk 相邻,直接扩展这个 chunk 到新 size 大小;
- 如果 chunk 与 top chunk 不相邻,相当于 free(ptr),malloc(new_size)。
如果申请 size < 原来 size:
- 如果相差不足以容得下一个最小 chunk(64 位下 32 个字节,32 位下 16 个字节),则保持不变;
- 如果相差可以容得下一个最小 chunk,则切割原 chunk 为两部分,free 掉后一部分。
2、当 realloc(ptr,size) 的 size 等于 0 时,相当于 free(ptr);
3、当 realloc(ptr,size) 的 size 等于 ptr 的 size,不进行任何操作。
寻找危险函数
通过寻找危险函数,我们快速确定程序是否可能有堆溢出,以及有的话,堆溢出的位置在哪里。常见的危险函数如下:
- 输入:
gets
,直接读取一行,忽略'\x00'
- scanf
- vscanf
- 输出:
- sprintf
- 字符串处理函数:
strcpy
,字符串复制,遇到'\x00'
停止strcat
,字符串拼接,遇到'\x00'
停止- bcopy
确定填充长度
这一部分主要是计算我们开始写入的地址与我们所要覆盖的地址之间的距离。 一个常见的误区是 malloc 的参数等于实际分配堆块的大小,但是事实上ptmalloc
分配出来的大小是对齐的。这个长度一般是字长的 2 倍,比如 32 位系统是 8 个字节,64 位系统是 16 个字节。但是对于不大于 2 倍字长的请求,malloc 会直接返回 2 倍字长的块也就是最小 chunk,比如 64 位系统执行malloc(0)
会返回用户区域为 16 字节的块。
|
堆内存结构如下:
//根据系统的位数,malloc会分配8或16字节的用户空间 |
注意:chunk_hear.size = 用户区域大小 + 2 * 字长
还有一点是之前所说的用户申请的内存大小会被修改,其有可能会使用与其物理相邻的下一个 chunk 的prev_size
字段储存内容。回头再来看下之前的示例代码:
|
观察如上代码,我们申请的 chunk 大小是24
个字节。但是我们将其编译为 64 位可执行程序时,实际上分配的内存会是 16 个字节而不是 24 个:
0x602000: 0x0000000000000000 0x0000000000000021 |
16 个字节的空间是如何装得下 24 个字节的内容呢?答案是借用了下一个块的prev_size
域。我们可来看一下用户申请的内存大小与 glibc 中实际分配的内存大小之间的转换:
/* pad request bytes into a usable size -- internal version */ |
当req=24
时,request2size(24)=32
。而除去 chunk 头部的 16 个字节。实际上用户可用 chunk 的字节数为 16。而根据我们前面学到的知识可以知道 chunk 的 prev_size 仅当它的前一块处于释放状态时才起作用。所以用户这时候其实还可以使用下一个 chunk 的 prev_size 字段,正好 24 个字节。实际上 ptmalloc 分配内存是以双字为基本单位,以 64 位系统为例,分配出来的空间是 16 的整数倍,即用户申请的 chunk 都是 16 字节对齐的。
堆中的经典漏洞及利用
堆同栈一样,存在许多经典的漏洞利用方式,下面对典型的漏洞利用技术进行学习记录。
Off-By-One
严格来说off-by-one
漏洞是一种特殊的溢出漏洞,off-by-one 指程序向缓冲区中写入时,写入的字节数超过了这个缓冲区本身所申请的字节数并且只越界了一个字节。
漏洞原理
off-by-one 是指单字节缓冲区溢出,这种漏洞的产生往往与边界验证不严和字符串操作有关,当然也不排除写入的 size 正好就只多了一个字节的情况。其中边界验证不严通常包括:
- 使用循环语句向堆块中写入数据时,循环的次数设置错误导致多写入了一个字节;
- 字符串操作不合适(比如
strlen()
函数计算字符串长度时不考虑结束符\x00
,而strcpy()
函数在复制的时候会复制\x00
,二者一起用就会导致off-by-one)。
单字节溢出被认为是难以利用的,但是因为 Linux 的堆管理机制ptmalloc
验证的松散性,基于 Linux 堆的 off-by-one 漏洞利用起来并不复杂,并且威力强大。
此外,需要说明的一点是off-by-one
是可以基于各种缓冲区的,比如栈
、bss 段
等等,但是堆上的 off-by-one 是 CTF 中比较常见的。我们这里仅讨论堆上的 off-by-one 情况。
利用思路
1、溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据。也可使用 NULL 字节溢出的方法;
2、溢出字节为 NULL 字节:在 size 为256的时候,溢出 NULL 字节可以使得 prev_in_use
位(记录前一个 chunk 块是否被分配)被清,这样前块会被认为是 free 块。
(1) 这时可以选择使用 unlink 方法(见 unlink 部分)进行处理;
(2) 另外,这时 prev_size
域就会启用,就可以伪造 prev_size
,从而造成块之间发生重叠。此方法的关键在于 unlink 的时候没有检查按照 prev_size
找到的块的大小与prev_size
是否一致。
最新版本代码中,已加入针对 2 中后一种方法的 check ,但是在 2.28 前并没有该 check :
/* consolidate backward */ |
Chunk Extend and Overlapping
chunk extend
是堆漏洞的一种常见利用手法,通过extend
可以实现chunk overlapping
的效果。这种利用方法需要以下的时机和条件:
- 程序中存在基于堆的漏洞
- 漏洞可以控制
chunk header
中的数据
漏洞原理
chunk extend 技术能够产生的原因在于 ptmalloc 在对堆 chunk 进行操作时使用的各种宏。
1、在 ptmalloc 中,获取 chunk 块大小的操作如下,一种是直接获取 chunk 的大小,不忽略掩码部分,另外一种是忽略掩码部分:
/* Like chunksize, but do not mask SIZE_BITS. */ |
2、在 ptmalloc 中,获取下一 chunk 块地址的操作如下:
/* Ptr to next physical malloc_chunk. */ |
即使用当前块指针加上当前块大小。
3、在 ptmalloc 中,获取前一个 chunk 信息的操作如下:
/* Size of the chunk below P. Only valid if prev_inuse (P). */ |
即通过 malloc_chunk->prev_size 获取前一块大小,然后使用本 chunk 地址减去所得大小。
4、在 ptmalloc,判断当前 chunk 是否是 use 状态的操作如下:
|
即查看下一 chunk 的 prev_inuse 域,而下一块地址又如我们前面所述是根据当前 chunk 的 size 计算得出的。
通过上面几个宏可以看出,ptmalloc 通过 chunk header 的数据判断 chunk 的使用情况和对 chunk 的前后块进行定位。简而言之,chunk extend 就是通过控制 size 和 pre_size 域来实现跨越块操作从而导致 overlapping 的。与 chunk extend 类似的还有一种称为 chunk shrink 的操作。
下面介绍各类型的 extend,参考的环境是 64 位的,因此偏移是 8 字节。
类型1-对 inuse 的 fastbin 进行 extend
该利用的效果是通过更改第一个块的大小来控制第二个块的内容。示例程序如下:
int main(void) |
当两个 malloc 语句执行之后,堆的内存分布如下:
0x602000: 0x0000000000000000 0x0000000000000021 <=== chunk 1 |
之后,我们把 chunk1 的 size 域更改为0x41
,0x41 是因为 chunk 的 size 域包含了用户控制的大小和 header 的大小。如上所示两个chunk大小之和为 0x40。在题目中这一步可以由堆溢出得到。
0x602000: 0x0000000000000000 0x0000000000000041 <=== 篡改大小 |
执行 free 之后,我们可以看到 chunk2 与 chunk1 合成一个 0x40 大小的 chunk,一起释放了:
Fastbins[idx=0, size=0x10] 0x00 |
之后我们通过 malloc(0x30) 得到 chunk1+chunk2 的块,此时就可以直接控制 chunk2 中的内容,我们也把这种状态称为 overlapping chunk。
call 0x400450 <malloc@plt> |
类型2-对 inuse 的 smallbin 进行 extend
通过之前深入理解堆的实现部分的内容,我们得知处于 fastbin 范围的 chunk 释放后会被置入 fastbin 链表中,而不处于这个范围的 chunk 被释放后会被置于 unsorted bin 链表中。 以下这个示例中,我们使用 0x80 这个大小来分配堆(作为对比,fastbin 默认的最大的 chunk 可使用范围是 0x70
)
int main() |
在这个例子中,因为分配的 size 不处于 fastbin 的范围,因此在释放时如果与 top chunk 相连会导致和 top chunk 合并。所以我们需要额外分配一个 chunk,把释放的块与 top chunk 隔开。
0x602000: 0x0000000000000000 0x00000000000000b1 <===chunk1 篡改size域 |
释放后,chunk1 把 chunk2 的内容吞并掉并一起置入 unsorted bin:
0x602000: 0x0000000000000000 0x00000000000000b1 <=== 被放入unsorted bin |
[+] unsorted_bins[0]: fw=0x602000, bk=0x602000 |
再次进行分配的时候就会取回 chunk1 和 chunk2 的空间,此时我们就可以控制 chunk2 中的内容
0x4005b0 <main+74> call 0x400450 <malloc@plt> |
类型3-对 free 的 smallbin 进行 extend
这个类型的利用是在第二种类型的基础上进行的,这次我们先释放 chunk1,然后再修改处于 unsorted bin 中的 chunk1 的 size 域。
int main() |
两次 malloc 之后的结果如下:
0x602000: 0x0000000000000000 0x0000000000000091 <=== chunk 1 |
我们首先释放 chunk1 使它进入 unsorted bin 中:
unsorted_bins[0]: fw=0x602000, bk=0x602000 |
然后篡改 chunk1 的 size 域:
0x602000: 0x0000000000000000 0x00000000000000b1 <=== size域被篡改 |
此时再进行 malloc 分配就可以得到 chunk1+chunk2 的堆块,从而控制了 chunk2 的内容。
Chunk Extend/Shrink 可以做什么?
一般来说,这种技术并不能直接控制程序的执行流程,但是可以控制 chunk 中的内容。如果 chunk 存在字符串指针、函数指针等,就可以利用这些指针来进行信息泄漏和控制执行流程。
此外通过 extend 可以实现 chunk overlapping,通过 overlapping 可以控制 chunk 的 fd/bk 指针从而可以实现 fastbin attack 等利用。
类型4-通过 extend 后向 overlapping
这里展示通过 extend 进行后向 overlapping,这也是在 CTF 中最常出现的情况,通过 overlapping 可以实现其它的一些利用。
int main() |
在 malloc(0x50) 对 extend 区域重新占位后,其中 0x10 的 fastbin 块依然可以正常的分配和释放,此时已经构成 overlapping,通过对 overlapping 的进行操作可以实现 fastbin attack。
类型5-通过 extend 前向 overlapping
这里展示通过修改 pre_inuse 域和 pre_size 域实现合并前面的块
int main(void) |
前向 extend 利用了 smallbin 的 unlink 机制,通过修改 pre_size 域可以跨越多个 chunk 进行合并实现 overlapping。
Unlink(没搞特别懂)
漏洞原理
我们在利用 unlink 所造成的漏洞时,其实就是对 chunk 进行内存布局,然后借助 unlink 操作来达成修改指针的效果。
unlink
操作就是把一个双向链表中的空闲块拿出来(例如 free 时和目前物理相邻的 free chunk 进行合并)。其基本的过程如下:
古老的unlink
在最初 unlink 实现的时候,其实是没有对 chunk 的 size 检查和双向链表检查的,即没有如下检查代码:
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查) |
这里我们以 32 位为例,假设堆内存最初的布局是下面的样子:
现在有物理空间连续的两个 chunk(Q,Nextchunk),其中 Q 处于使用状态、Nextchunk 处于释放状态。那么如果我们通过某种方式(比如溢出)将 Nextchunk 的 fd 和 bk 指针修改为指定的值。则当我们 free(Q) 时:
- 1、glibc 判断这个块是 small chunk
- 2、判断前向合并,发现前一个 chunk 处于使用状态,不需要前向合并
- 3、判断后向合并,发现后一个 chunk 处于空闲状态,需要合并
- 4、继而对 Nextchunk 采取 unlink 操作
那么 unlink 具体执行的效果是什么样子呢?我们可以来分析一下:
- 1、FD=P->fd = target addr -12
- 2、BK=P->bk = expect value
- 3、FD->bk = BK,即 *(target addr-12+12)=BK=expect value
- 4、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 |
此时:
- 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) == P
fakeBK -> fd == P
<=>*(fakeBK + 8) == P
当满足上述两式时,可以进入 Unlink 的环节,进行如下操作:
fakeFD -> bk = fakeBK
<=>*(fakeFD + 12) = fakeBK
fakeBK -> fd = fakeFD
<=>*(fakeBK + 8) = fakeFD
如果让 fakeFD + 12 和 fakeBK + 8 指向同一个指向 P 的指针,那么:
*P = P - 8
*P = P - 12
即通过此方式,P 的指针指向了比自己低 12 的地址处。此方法虽然不可以实现任意地址写,但是可以修改指向 chunk 的指针,这样的修改是可以达到一定的效果的。
如果我们想要使得两者都指向 P,只需要按照如下方式修改即可:
需要注意的是,这里我们并没有违背下面的约束,因为 P 在 Unlink 前是指向正确的 chunk 的指针。
// 由于P已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。 |
此外,其实如果我们设置 next chunk 的 fd 和 bk 均为 nextchunk 的地址也是可以绕过上面的检测的。但是这样的话,并不能达到修改指针内容的效果。
利用思路
1、条件
- UAF ,可修改 free 状态下 small bin 或是 unsorted bin 的 fd 和 bk 指针
- 已知位置存在一个指针指向可进行 UAF 的 chunk
2、效果
使得已指向 UAF chunk 的指针 ptr 变为 ptr - 0x183、思路
设指向可 UAF chunk 的指针的地址为 ptr:- 修改 fd 为 ptr - 0x18
- 修改 bk 为 ptr - 0x10
- 触发 unlink
ptr 处的指针会变为 ptr - 0x18。
Use After Free
漏洞原理
简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况:
- 1、内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃;
- 2、内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转;
- 3、内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。
而我们一般所指的Use After Free
漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为dangling pointer
。
一个简单的示例代码:
|
运行结果如下:
➜ use_after_free git:(use_after_free) ✗ ./use_after_free |
可见被free掉的chunk又被成功访问了。
- Post Title: 堆溢出基础
- Post Author: ggb0n
- Post Link: http://ggb0n.cool/2020/06/01/堆溢出基础/
- Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
1.TCTF2020部分题解
2.第五空间pwn题练习
3.堆溢出-Tcache_Attack
4.堆溢出-Housese_Of_XXX
5.堆溢出基础
6.入坑二进制