本文要点概括: fastbin attack __malloc_hook与size错位构造 绕过calloc泄露内存的通用思想(堆块溢出“受孕”、fastbin attack利用、远交近攻“隔山打牛”) 边缘效应与耦合缓解(unsorted_bin中chunk再分配、清空bin环境) libc依赖: 有关不同libc版本下的堆地址
随着堆的学习,最近一直保持着有关libc堆漏洞利用的文章的更新,之前以babynote为例讲了unsorted bin attack,这次以0ctf2018 babyheap为例讲解一下fastbin attack的东西。
堆的知识细节很庞大,每次pwn一个challenge都会收获很多东西。之前是复现的babynote那道题,但毕竟是参考了别人的exp自己心里还是没底,而这次的babyheap的exploit开发则是彻头彻尾自己完成的,过程和结果令人惊喜:自己写出的有效exp之后和网上的exp进行了对比,发现思路有比较大的出入,也就意味着学到了更多的东西。
题目链接
一、逆向分析与漏洞挖掘丢进IDA,main函数F5,如下(函数名我已进行手动重命名):
0x01、new_log()如下:
我们的堆块就是通过该函数分配,索引表的结构就是传统的堆题目结构,由exist字段、大小字段和用户区指针构成;值得注意的是此处使用的内存分配函数是calloc而不是malloc,calloc分配chunk时会对用户区数据进行置空,也就是说之前的fd和bk字段都会被置为0,这在进行内存泄露时会造成一定的难度;返回的chunk下标也是传统的exist字段遍历法,下标从0开始。
0x02、edit_log()如下:可以看到,程序并没有对用户输入的Size长度进行检查,这就造成了任意长度输入,形成堆溢出漏洞;此外,输入没有尾补字符串结束符,有可能会造成内存泄露(该程序后经分析,内存泄露不利用此处缺陷)
0x03、delet_log()如下:可以看到,这段free函数写的是很安全的,首先对用户通过下标选择进行free的chunk在索引表层面做了存在性检查,如果exist字段为0说明已经free便不再继续执行free,这有利于防范double free;free成功后,相应的索引表的exist字段置空、堆指针置NULL也做到位了。总之该部分没有安全漏洞。
0x04、print_log()如下:其中sub_130F():
这个读内容函数也是安全的,首先作了存在性检查,如果exist字段为0就不会去读,也就是说只能读new过的记录;并且读取用的是write而不是puts,write读的长度也是索引表中记录的长度(即当初new的时候输入的长度的多大就只能读多大 )
0x05、逆向分析小结:该程序存在堆溢出漏洞,但是由于其他保护作的较好,在泄露内存阶段应该会遇到较大阻力;堆溢出漏洞可能带来Fastbin Attack的机会。
二、漏洞利用分析在进行具体分析之前,我们先粗略讲一下fastbin attack的相关知识:
(详细讲解请参考ctf wiki上的教程)
1.fastbin是单链表,按chunk大小递增一共有好几个,用户free一个chunk以后,如果大小是属于fastbin的、又不与top chunk相邻,就链入fastbin中大小对应的单链表
2.fastbin单链表是个栈,LIFO,链表结点(被free的chunk)的插入用的是头插法,即紧邻表头插入,fd指针则往链尾方向指向下一个chunk(此处的“头尾”是以表头为头)
3.大体上插入就是chunk->fd = fastbinY[x] ->fd ; fastbinY[x]->fd = chunk ; 而对应的拆卸过程就是 fastbinY[x]->fd = fastbinY[x]->fd->fd(看懂意思就好不要太较真,大家可以自行去看libc源码)4.fastbin的相关安全检查:首块double free检查,当一个chunk被free进fastbin前,会看看链表的第一个chunk是不是该chunk,如果是,说明double free了就报错;分配前size字段校验,从fastbin表中malloc出一个chunk时,拆卸前会检查要分配的这个chunk的size字段是不是真的属于它当前所在的fastbin表,如果size字段的值不是当前fastbin表的合法chunk大小值,则报错,其代码 ((((unsigned int)(sz)) >> (bitl == 8 ? 4 : 3)) 2);根据size算得应在的表的下标,再和当前所在fastbin的下标对比
5.fastbin chunk头部字段特点:presize为0,size的inuse位恒为1(不被合并,符合当时设计常驻较小块以提高效率的初衷)
6.fastbin attack:用过一定手段篡改某堆块的fd指向一块目标内存(当然其对应size位置的值要合法),当我们malloc到此堆块后再malloc一次,自然就把目标内存分配到了,就可以对这块目标内存为所欲为了(可以是关键数据也可以是函数指针)
下面正式开始分析:
我们的主要思路就是首先泄露得到libc的基地址,然后通过fastbin attack篡改libc中某个函数指针,最终在调用的时候实现劫持并get shell
0x01、泄露libc_base唯一有输出的地方就是程序的print_log()函数,只能利用这个函数泄露内存
而这个函数打印的东西都是chunk内的内容,自然想到应该是通过泄露chunk的fd和bk指针泄露libc_base地址
马上排除通过fastbin chunk泄露的可能性,因为fastbin chunk只有fd没有bk,而fd是往链尾指的而且是单链表,只能指向堆的地址,怎么也不可能指向fastbin表头,因此也无法通过偏移计算泄露libc_base
所以是通过unsorted bin来泄露libc_base!
阻碍:读的内存长度有限制,只能读当初new时输入的长度;calloc时会置空用户区数据,残存的fd和bk将被置零;chunk只有索引表exist指示存在时才能读
先考虑如何绕过calloc和exist:首先如果你要读fd和bk,就不能被置空,也就是说你读的fd和bk所在的堆块必须是free的,那么它的索引表exist肯定指示不存在不能读
所以现在看来,我们只能读exist即inuse 的堆块,又要读的出free的堆块里的内容
也就是说我们必须能够通过读一个exist即inuse 的堆块打印出某个free的堆块里的内容
要达到这个目的,唯一可能的情形就是:这个exist的堆块对应的索引表中的Size足够大,大到把某个free态的堆块也包含了进去,这样读这个exist的堆块时就可以读到free块的fd和bk
我们下面将用Size来代表这个足够大的长度值
那么这个大大的Size肯定是在new_log()之初就由用户输入了的,也就是说calloc时传入的大小就是这个Size,但是calloc时会置零,也就是说被包含进来的那个free态的堆块肯定不能是先free了再被这个exist的堆块包含进来(因为这样那个free态的堆块的fd和bk就置零没了),所以一定是calloc时还不是free的,calloc后再free掉,然后再读calloc到的堆块进行泄露
那么问题来了:calloc(Size)时如何能分配到一块包含了另一个占用态堆块的堆块呢?calloc到的堆块无非来自两种情况,要么是从bins中已有的块中直接拿出来的,要么就是从top chunk切下来的;显然,不能是从top chunk切下来的;所以是从bins中直接拿出了一个chunk,也就是说之前在bins中就已经存在这个大小为Size的chunk了(我们根据特点将这个大小为Size的堆块称作“怀孕块prgnt chunk”)
那么怎么构造这样一个bins中的prgnt chunk呢?或者说换种说法:怎么让它“怀上”肚子里的泄露目标chunk呢?有经验的pwn狗稍加思考就想到了:伪造size字段!方法自然是堆溢出!
只要能够将与“胎儿堆块”(fetus chunk)相邻的prev_chunk(即bins中的prgnt chunk)的size字段篡改的更大,大到把fetus chunk也包含进去了(也就是篡改为Size),那么在用户以Size为输入长度通过调用new_log执行calloc(Size)的时候,就会在bins里找到我们伪造出的这个size字段值为Size的prgnt chunk,分配出来就得到一个我们所需要的大小包含了一个inuse态的fetus chunk的prgnt chunk了
calloc到prgnt chunk后,我们只需要调用edit_log()编辑prgnt chunk来把还是inuse态的fetus chunk的presize字段和size字段写成合法值(被置零了),然后free掉fetus chunk(这时候就有fd和bk了),再调用print_log()读prgnt chunk就可以读出fetus chunk的fd和bk了(即前面所提到的calloc后再free掉,然后再读calloc到的堆块进行泄露)
显然prgnt chunk与fetus chunk都必须是unsorted_bin chunk,此外还需要一个保护堆块来殿后,防止合并进top chunk,然后在最前面还需要随便放一个堆块用来发起溢出,因此一共需要四个堆块,大小都是unsorted_bin chunk就行
泄露出的是main_arena__unsorted_bin的地址,通过偏移计算即可得到libc_base
0x02、Fastbin Attack先往fastbin里free一个chunk进去,溢出踩掉这个chunk的fd指针,把fd劫持到malloc_hook附近,然后连续calloc两次就得到一个指向malloc_hook附近的用户指针了,然后就可以将malloc_hook改写为我们的劫持目标地址(比如onegadget),之后再调用new_log()执行calloc的时候就可以把程序执行流劫持到onegadget然后get shell了
0x03、重要技术细节hook劫持、RELRO保护、错位构造size、onegadget
往常劫持函数指针我们常常是用GOT表劫持的手段,而仅就笔者目前的了解,就至少有两种情况是GOT表劫持行不通的:一个是RELRO保护全开、一个是fastbin须size错位
RELRO是一种加强对数据段保护的技术,当其完全开启时(full),GOT表就不会采用延迟绑定,而是在程序加载之初就一次性全部绑定,此后将GOT表属性设置为不可写,这样一来就无法篡改GOT表了
size错位就是我们今天遇到的情况,前面说过fastbin的安全检查之一就是size字段校验,因此如果我们想通过劫持fd至目标内存进而分配到目标内存,就必须保证在目标内存附近能够找到一个qword能够充当合法的size字段,绕过校验,这就是我们所说的size错位构造;而实践经验证明,在GOT表内,似乎并不能找到这样一个qword来错位构造size,因此fastbin attack攻击GOT表是行不通的
因此fastbin attack中我们选择攻击hook,先来讲一下hook:hook就是钩子函数,设计钩子函数的初衷是用于调试,基本格式大体是func_hook(*func,<参数>),在调用某函数时,如果函数的钩子存在,就会先去执行该函数的钩子函数,通过钩子函数再来回调我们当初要调用的函数,calloc函数与malloc函数的钩子都是malloc_hook:
(libc2.23源码中malloc的定义)
(IDA中的malloc的定义)
(libc2.23源码中calloc的定义)
(IDA中的calloc的定义)
综上四幅图可以看到,在调用malloc/calloc时,执行核心代码前都先判断了malloc_hook是否存在,如果存在的话都会先调用malloc_hook!
所以我们来看一下malloc_hook附近的内存布局:
(图.hook汇编窗口)
可以看到malloc_hook紧邻main_arena
我们fastbin attack都是攻击malloc_hook,也就是说在malloc_hook附近可以错位构造出一个合法的size字段,我们到hex界面看一下这个size是怎么构造出来的:
从3C4AF0到3C4B18,对照“图.hook汇编窗口”,各个qword分别是:
_IO_wfile_jumps align 20h__memolign_hook __realloc_hook
__malloc_hook align 20h
我们的攻击目标就是malloc_hook即0x3C4B10,这个位置需要处于分配到的chunk的用户区中,从这个位置往上找可以错位构造size字段的qword,就只能找到0x3C4AF0和0x3C4AF8,原因如下:
因为0x3C4AF8处的align 20h是固定不变的,永远都是00 00 00 00 00 00 00 00,注意其他几个位置在图中的hex都并不是实际运行时的值,实际运行时会附上真实的地址值,有经验的话应该能猜到这几个实际运行时libc地址长度都是6字节,且最高位字节为7f,这样一来就只能找到那一个位置可以错位构造size了,就是0x3C4AF0的最高字节7f加上往后的7个字节长度的00构成一串qword:7f 00 00 00 00 00 00 00,可以作为合法size字段值!
令sz = 0x7f,令bitl = 8,((((unsigned int)(sz)) >> (bitl == 8 ? 4 : 3)) 2)计算出的下标是5,因此对应chunk是属于fastbin[5]的: //这里的size指用户区域Fastbins[idx=0, size=0x10]
Fastbins[idx=1, size=0x20]
Fastbins[idx=2, size=0x30]
Fastbins[idx=3, size=0x40]
Fastbins[idx=4, size=0x50]
Fastbins[idx=5,