堆溢出+malloc_hook劫持
前几天遇到XCFT上的堆题,搞了很久才搞出来,也学到了很多东西,在此分享一下
题目链接: 题目 (xctf.org.cn)
64位,dynamically,开了Canary,开了Full
代码分析
程序有4个选项
选项1:申请一片堆空间,大小自定义,可以写入数据(可以申请10次,第11次会被强行释放)
//在堆中写入数据的指针会被写入bss段
选项2:可以释放一片堆空间,下标自定义(0~9)
选项3:可以更改某一次申请堆空间的数据(大小可以不同)
//必须在bss段检测到选项1生成的指针才会执行
漏洞分析
首先有UAF,申请的指针没有置空
其次还有数组越位:
可以发现,下标“v0”只检查了上界,没有检查下界,造成其可以为负数
没有开NX,那么堆中可以写入shellcode
在本函数中,没有检测大小就直接写入,明显的堆溢出漏洞
入侵思路
本程序开了Full,GOT表不可改,堆中的shellcode不能借助函数的plt来控制CS:IP
所以这里选择控制malloc_hook来执行shellcode
Hook
Hook直意为钩子又叫做回调函数,是一种特殊的消息处理机制,它可以监视系统或者进程中的各种事件消息,截获发往目标窗口的消息并进行处理
在程序中设置钩子,用来在malloc, realloc,free的时候,对其进行检查,可以看到对应的函数调用后的地址是什么
原理:
函数的指针可以指向不同的函数,从而完成不同的功能
设计理念:
我们在写main函数的时候,可能还不知道它会完成什么功能,这时候留下函数指针作为接口,可以挂上不同的函数完成不同的功能,究竟执行什么功能由钩子函数的编写者完成
案例:
1 | #include "stdio.h" |
这里的函数“fun1”和函数“fun2”就是hook
把函数指针fun指向fun1和fun2的过程称为“挂钩子”
// 在嵌入式系统中,底层不知道应用层需要完成什么功能, 往往会提供像这样子的函数回调方式供应用层使用
malloc_hook
malloc_hook本质上讲是一个指针变量,指向一个“检查函数”
1 | void * function(size_t size, void * caller) |
//其中caller是表示在栈中调用malloc的函数的时候的函数地址,%p可以打印出它的地址
在执行malloc时,会检测__malloc_hook的值,如果malloc_hook的值存在(已经挂钩),将调用malloc_hook指向的call rax的地址(malloc调用之后的地址)
简单来说:执行malloc时,malloc_hook也会执行
案例:
1 |
|
malloc_hook指向了fun的首地址(挂钩子)
程序在执行malloc时就会执行fun,而不是“检查函数”(钩子已切换)
利用:
malloc_hook位于main_arena上方0x10的位置,可以通过fake chunk来overwrite该值实现getshell
堆管理机制:bin
一个链表被称为一个bin,简单来说bin就是free chunk的容器
用户 free 掉的内存并不是都会马上归还给系统,ptmalloc 会统一管理 heap 和 mmap 映射区中空闲的 chunk,当用户进行下一次分配请求时,ptmalloc 会在空闲的 chunk 中选择一个合适的分配给他,这样就避免了频繁地系统调用
ptmalloc 把大小相似的 chunk,用双向链表连接起来,这样就形成了一个 bin。ptmalloc 一共维护了 128 个这样的 bin,并使用数组来存储这些 bin 如下:
从第2个到第64个bin是small bin,small bin中的chunk大小相同,small bin是一个双向链表
在某一条bin中(chunk大小相同)按照「先入先出」进行排序,也就是说,刚被释放的放在前面
//所以UAF中:刚刚释放的内存空间,可以被立刻重新申请
unsorted bin是一段特殊的bin,由free chunks组成的循环双链表,当释放较小或较大的chunk的时候,如果系统没有将它们添加到对应的bins中,系统就将这些chunk添加到unsorted bin中
在使用malloc申请内存时,如果在“fastbin”和“smallbin”中都没有找到合适的free chunk,程序就会在unsorted bin中进行遍历,寻找合适的free chunk,并且对其进行排序并重新分配到bins中
堆管理机制:chunk
1.allocated chunk:当前chunk是被应用层用户所使用的
2.free chunk:当前chunk是空闲的,没有被应用层用户所使用
3.top chunk:
当一个chunk处于一个arena的最顶部(即最高内存地址处)的时候,就称之为top chunk
该chunk并不属于任何bin,而是在系统当前的所有free chunk(无论那种bin)都无法满足用户请求的内存大小的时候,将此chunk当做一个应急消防员,分配给用户使用
4.last remainter chunk:
如果在bins链中存在freechunk时,当我们去malloc的时候,malloc的请求大小比freechunk的大小小,那么arena就会切割这个freechunk给malloc使用,那么切割之后剩余的chunk就被称为“last remainder”
堆管理机制:ptmalloc算法
malloc的时候,不论malloc的大小,首先会去检查每个bins链(除去fastbins链)是否有与malloc相等大小的freechunk,如果没有就去检查bins链中是否有大的freechunk可以切割
如果存在,那么就切割大的freechunk,那么切割之后剩余的chunk成为last remainder,并且last remainder会被放入到unsorted bin中
如果没有大的free chunk可以切割,程序就会查询top chunk,如果top chunk的大小比用户请求的大小要大的话,就将该top chunk分作两部分:1.用户请求的chunk,2.新的top chunk,否则,就需要扩展heap或分配新的heap了——在main arena中通过sbrk扩展heap,而在thread arena中通过mmap分配新的heap
可以作出以下示意图:
//切割unsortedbin其实就是 _int_malloc 函数的for大循环的第一步,切割smallbins、largebins其实就是 _int_malloc 函数的for大循环的第三步
堆溢出漏洞
堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数
堆溢出和栈溢出不同,因为堆上没有IP控制指令,所以不能通过堆溢出来控制IP
那么堆溢出的作用是:
1.覆盖下一个相邻的chunk的内容
2.利用堆的机制(如unlink等)来实现任意地址的写入( Write-Anything-Anywhere),或控制堆块中的内容等效果,从而来控制程序的执行流
案例:
1 |
|
这个程序的主要目的是调用 malloc 分配一块堆上的内存,之后向这个堆块中写入一个字符串,如果输入的字符串过长会导致溢出 chunk 的区域并覆盖到其后的 top chunk 之中(实际上 puts 内部会调用 malloc 分配堆内存,覆盖到的可能并不是 top chunk)
1 | 0x602000: 0x0000000000000000 0x0000000000000021 <===chunk |
1 | 0x602000: 0x0000000000000000 0x0000000000000021 <===chunk |
//chunk的头两个8字节:一个为“prev_size”,另一个为“size”
入侵的最终目的是为了通过malloc_hook来执行shellcode
我们知道:执行malloc的时候会执行malloc_hook挂钩的函数,malloc_hook默认挂钩“检查函数”,只要把 malloc_hook 赋值为shellcode的首地址,那么在执行malloc后就可以执行shellcode了
要想malloc_hook可以挂钩shellcode,那么shellcode的地址必须确定,所以地址随机化的堆空间并不适合写入shellcode,要选择一片可写入的,较稳定的内存段(比如bss段)
最后可以利用堆溢出来完成Write-Anything-Anywhere,把shellcode写入bss段
本程序可以利用堆溢出修改chunk的结构,通过unlink修改buf数组中指针指向的位置(使其指向bss段),利用选项三把shellcode写入bss段,然后再利用选项三修改malloc_hook为bss段上shellcode的地址,最后执行 shellcode,详细过程如下:
1.调用选项一在堆上创建两个chunk(0x90),此时堆结构如下:
2.使用选项三修改chunk0的数据区(fk&bk),在其中伪造一个chunk,并通过堆溢出修改chunk 1的pre_size
伪造chunk的payload:
1 | # pre_size, size |
payload写入后的堆结构:
3.使用选项二删除thunk 1(还未合并)
ptmalloc通过伪造的P位识别到伪造chunk为free,于是把chunk 1和伪造chunk进行后向合并
此时,“控制权”交给了伪造chunk,所有指向chunk1的指针都会指向伪造chunk
//装入bss段的buf[1]中装有的指向chunk1的指针不会指向伪造chunk(它在合并之前就已经写定)
unlink之前会进行检查:
伪造chunk->fd->bk 是否 就是伪造chunk
伪造chunk->bk->fd 是否 就是伪造chunk
只要按照上述payload中fd&bk的排布是符合条件的,这里有些复杂:
chunk->fd:“buf - 0x18”,buf[-3]
chunk->bk:“buf - 0x10”,buf[-2]
虽然buf[-3],buf[-2]这两个地方根本就没有chunk,但只要unlink检测到对应位置的对应值正确就行了
可以发现:chunk->fd->bk和chunk->bk->fd都占用了buf[0]这个位置,就是伪造chunk的首地址
unlink之中的操作就有些微妙了:
伪造chunk->fd->bk(指针buf[0]) = 伪造chunk->bk(地址buf[-2])
伪造chunk->bk->fd(指针buf[0]) = 伪造chunk->fd(地址buf[-3])
先让buf[0]中的指针指向buf[-2],然后再指向buf[-3]
//前3步操作的根本目的,就是unlink改变buf[0]中指针的指向,方便改变数组buf的结构
4.使用选项三编辑“buf[0]”(实际上写入了“buf[-3]”),在buf中伪造一个chunkX
伪造chunkX的payload:
1 | payload=p64(0)+p64(0)+p64(0)#填充buf[-3]~buf[-1] |
payload写入后的buf结构:
buf[1]中写入了buf[0]的地址,buf[0]中写入了bss段的地址
buf[4]就是“chunkX”的首地址,pre_size为“0”,size为“0x20”
5.使用选项一申请两个大小为“0x100”的chunk(chunk2,chunk3)
前面伪造chunk和chunk1合并后,在top chunk中存放有一个“0x90+0x90+0x10”大小的free chunk
在申请第一段“0x100”的chunk时,top chunk中的chunk被切割为“0x90”
在申请第二段“0x100”的chunk时,top chunk中的chunk大小不够,需要继续申请
//这里是为了把top chunk消耗干净,让chunk3成为新的top chunk
buf结构:
堆结构:(chunk2,chunk3都处于allocate状态)
6.使用选项二删除chunk2,此时chunk2不会进行合并,直接被收入unsorted bin(作为最后一个)
堆结构:
7.使用选项三修改buf[2],修改chunk2结构的payload如下:
1 | payload=p64(0)+p64(buf+0x8*4)#修改fd&bk |
chunk2已经是free状态,修改其fd为“0”,修改其bk为“buf+0x8*4”(buf[4])
修改后的chunk2结构:
因为“chunk2”属于“unsorted bin”,其bk指向“buf[4]”(“chunkX”的首地址)
这样的话之前在buf中伪造的“chunkX”也进入了unsorted bin(被强行连接)
8.使用选项一添加一个和chunk2一样大小的chunk4,这样unsorted bin中只剩下chunkX了
这里需要先分析一下ptmalloc的运算过程:
申请chunk4时,程序在small bins中没有找到结果,于是在unsorted bin中进行遍历
因为“chunk2”排在“chunkX”前面,所以程序搜索到“chunk2”后终止,并不会重新分配“chunkX”
9.所以选项三在buf[1](buf)写入payload:
1 | payload=p64(bss) + p64(buf) + p64(0) * 4 + '\x10' |
目的是修改buf[6]为“malloc_hook”
buf[1]中写入了buf[0]的地址,buf[0]中写入了bss段的地址
buf[4]是chunkX,在它就是unsorted bin中的最后一位,所以它的fd&bk应该指向一个固定偏移main_arena+XX,这里不需要知道它具体是多少,只要知道它和malloc_hook只有最后两位不同就可以了
在buf[1]中写入payload,而payload会实际写入buf[0],前面的数据会依次填充“buf[0]~buf[5]”,而最后一位数据“\x10”会填入fd,因为计算机采用小端序,所以“\x10”会覆盖fd指针指向内容的最后两个字节,它正是main_arena+XX,它会被覆盖为malloc_hook(3C4B10)
10.使用选项三在buf[0]中写入shellcode:
buf[0]装有bss段的首地址,所以shellcode会被写入bss段,且首地址已知
11.使用选项三在buf[6]中写入shellcode的地址:
buf[6]中装有malloc_hook,所以malloc_hook会和shellcode挂钩
12.使用选项一随便申请一段内存:
程序调用malloc时,会同时调用malloc_hook,就相当于调用了shellcode
unlink函数
1 | >void unlink(malloc_chunk *P, malloc_chunk *BK, malloc_chunk *FD) |
unlink是一个宏操作,用于将某一个空闲chunk 从其所处的双向链表中脱链
解释unlink函数:
FD->bk = BK:让P结构体的下一个结构体中的bk变为P本身的bk
BK->fd = FD:让P结构体的上一个结构体中的fd变为P本身的fd
就相当于跳过了P这个结构体,把FD和BK连接
unlink检查:
当前这个chunkP的后一个chunk的fd == chunkP 是否成立
当前这个chunkP的前一个chunk的bk == chunkP 是否成立
unlink攻击
unlink是利用glibc malloc的内存回收机制造成攻击的,核心就在于当两个free的堆块在物理上相邻时,会将他们合并,并将原来free的堆块在原来的链表中解链,加入新的链表中,但这样的合并是有条件的,向前或向后合并
将要进行向前还是向后合并,关键是看前一个和后一个chunk是否为free
1.将要被free的chunk的P位为“1” > 进行向后合并(前一个chunk为free)
2.将要被free的chunk的next chunk为free > 进行向前合并(后一个chunk为free)
//前向合并可以合并top chunk,这点经常被利用
向前合并&向后合并(先合并,后unlink)
一,向后合并:(向后扩充,需要free的chunk作为“合成材料”)
通过P位检测前一个chunk是否为free,将要free的chunk会根据它自己的 size 来获取 前一块chunk的size ,并把它自己的size加到前一块chunk的size中,然后让新的chunk进入unlink流程
例子:
当chunk2将要进行free时,发现chunk1已经是free状态,先unlink chunk1,然后进行向后合并
最终指向chunk1和chunk2的指针也会指向新的chunk
二,前向合并:(向前扩充,需要free的chunk作为“合成材料”)
通过inuse_bit_at_offset检测后一个chunk是否为free,将要free的chunk会把它自己size加上nextsize(后一个chunk的大小),然后让新的chunk进入unlink流程
//这里pre_size的值没有改变,所以不需要进行操作
当chunk1将要进行free时,发现chunk2已经是free状态,先unlink chunk2,然后进行向前合并
最终指向chunk1和chunk2的指针也会指向新的chunk
Arena- 管理线程中的堆
Arena 是一种管理系统仿真模拟软件,它给c语言提供了接口,使其可以使用它的功能
功能:
一个线程申请的1个/多个堆包含很多的信息:二进制位信息,多个malloc_chunk信息……
而Arena就是用于管理这些信息的
特点:
1.一个线程只有一个arnea,并且这些线程的arnea都是独立的不是相同的
2.主线程的arnea被称为main_arena,子线程的arnea称为thread_arena
malloc_hook劫持(arena)
先看malloc_trim函数,它可以之前分配的内存还给系统,源码如下:
1 | int |
在IDA中的反汇编如下:
对比发现:“dword_3C4B20”就是“&main_arena”
//通常通过“malloc_trim”来快速锁定“main_arena”的地址
因为main_arena的偏移(main_arena+0x58)和malloc_hook很近,只有两位不同
所以可以通过main_arena来实现malloc_hook劫持,而“malloc_trim”中就有“main_arena”
OFF BY ONE
所谓OFF BY ONE就是利用堆溢出一个字节到下一个堆块,使得目前堆块与下一堆块合并成一个堆块,此时堆块的大小就是我们溢出的那一字节(覆盖了pre_size)
//也可以溢出多个字节,完全覆盖size位也不是不行
并且堆块的fd(前驱指针)以及bk(后继指针)都会指向main_arena+XX的地址
malloc机制中的unsorted bin、small bins以及large bins中的双向链表中的第一个chunk以及最 后一个chunk中的 fd\bk 字段,都会指向arena的固定偏移
//不同的libc版本可能固定偏移有差别
完整exp:
1 | from pwn import * |