在看雪发的第二篇帖子,回归老本行pwn。
在经典的2.23版本的glibc下,off-by-null算得上是堆利用的一种中等难度的利用技巧,简单来说,这种漏洞发生在读入数据的过程,存在多输入一个字节并且该字节固定为\x00的问题,利用这一个看似不可控却很有作用的字节,攻击者就可以精心构造出堆上的布局,进行任意地址的读写,构造的过程也可以说十分巧妙。
不过慢慢在各种CTF赛题的狂轰乱炸之下,off-by-null也算形成了比较固定的公式,开始被玩坏了。
想到目前已经2020年了,希望出题人也好,选手也好,不要再把思路局限于2.23了,甚至于2.27也算是一个相对过时的版本了,因为除了多了一个tcache机制,其实等于裸奔的fastbins,把tcache塞满之后,其余的区别并不大。所以在这里,我会详细解析一下在各项保护更加完善的2.29下,是如何进行off-by-null的,希望可以起到一些抛砖引玉的作用。
先从构造起来最简单最基本的2.23说起,其实也是一个自己复习知识的过程,由简入繁,才能更好地理解问题。
这里我画了一个简单的2.23下off-by-null的构造流程图,如果你对这个过程足够熟练,看着这个图就可以撸出2.23下off-by-null的exp了:
这里说前向合并或者后向合并容易产生歧义,所以接下来,我会用高低地址来表示。先简述一下流程:
这里主要利用到了free时的两个机制,一个是不满足fastbin的时候,会判断是否向低地值合并:
p就是chunk2,因为此时chunk2的P被off-by-null盖成了0,所以开始向低地值合并,通过prevsize找到了需要合并的chunk,这里也就是chunk0,然后触发第二个机制,也就是unlink。
unlink的作用就是在bin双向链表上把其中的一个chunk卸载下来,曾经稚嫩的unlink还是个宏,后来地位上升了,现在早已经变成函数了。部分能用得到的宏代码:
这里的P已经就是图中的chunk0了。有一段该死的检测:
主要是在检测P是否在一个真正的双向链之中。这也正是off-by-null构造的巧妙之处,chunk0被我们提前丢进了unsortedbin之中,所以当对其检测时,就利用了系统已经帮我们构造好了的链表绕过了检测。(这里的chunk0的fd和bk里面的值相同,都是0x7f....这种,这就是unsortedbin这条双向链的链表头,该链因为只插进了chunk0这一个元素,所以链表头里的fd和bk也会指向chunk0)
别急,接下来还有一步要处理,低地址检测之后还会检测是否要和高地址合并:
这里也就是检测下一个chunk的下一个chunk的P位,因为当前chunk是否被使用是标记在比它地址更高的下一个堆块的P位上的。这也是图里步骤2要构造的东西,为了不让系统执行向高地址合并,用正常的chunk,或者用自己伪造的chunk都是可以的。
之后chunk0直到chunk2,同时连带着中间的这么一大块空间,就都被送进了unsortedbin,当我们再次从unsortedbin切割的时候,就会分配出中间的各种地址,达成堆块复用的效果了。
2.27我就不介绍了,把tcache填满了,其他的跟2.23没什么区别。
2.29的代码进行了更加完善的保护,在向低地值合并的时候,加了一行代码:
检测了chunksize(p) != prevsize
,以图为例,就是检测chunk0的size和chunk2的prevsize是否相等。仔细想想,这让我们之前的那种利用方式就变成了不可能。chunk2的prevsize是我们为了索引到chunk0而构造的,而chunk0和chunk2中间还存在准备被复用的chunk,所以这种构造方式,这个保护是绕不过去的。
2.23的链表构造,是系统帮助我们构造出来的。2.29也同样要想办法让系统帮我们构造出来。这里的手法的核心思想就是利用系统的残留数据。
首先根据上述的知识,要达成的目的就是伪造出一个chunk0,使其能够绕过这两个检测。
一个是向低地值合并的检测:
另一个就是unlink时候的检测
接下来的操作全程关闭aslr,为了保证100%的成功率。
这里的思路是利用largebin残留的fd_nextsize和bk_nextsize两个指针,smallbin残留的bk指针,以及fastbin的fd指针。构造出一个如下的布局:
2.29下的off-by-null的题有2019-TCTF-FINAL-babyheap,不过这道题因为有off-by-null发生在edit功能中而不是add中,所以也很简单。
所以接下来,我会以2019-BALSN-CTF-plaintext为例,来详细介绍off-by-null的流程。
当然,因为原来的题目文件在我本地有点问题,所以我重新编译了一份文件,源代码和文件我会放到附件里。(我这个文件也有点问题,因为我是在2.27环境编译,在2.29环境运行的,不过关注点还是主要放在利用手法上嘛hh)
1.首先做一些提前的布置。
用到的最关键的堆块主要是15,0-7的目的都是为了填充堆空间,来调整所需堆块的偏移,这里不完全相同,根据情况自行调整。8-14正好7个chunk是为了对tcache进行操作。16是防止15和topchunk合并。这样布局之后,delete 15 会将其放入unsortedbin:
可以看到此时chunk15堆地址的第二字节(因为是小端序)是\x00,这就是我们前面填充的原因,因为off-by-null不可避免的要在输入后补入\x00,堆地址如果是这个样子,那么我们覆盖第一个字节后,第二个字节被补\x00后就相当于不会产生变化。
而chunk15的size也要尽量的大,因为后续会对他各种切割,并且要保证切割后的大小移然超过tcache的限制,方便我们后续直接让他切割后的部分作为被off-by-null的目标而不用处理tcache。
2.对fake chunk的fd和bk进行布局。
当malloc的请求大于当前unsortedbin的size时,会触发malloc源码中的双循环机制,将unsortedbin里的chunk按bk顺序依次压入对应的bin中,然后再从topchunk切割出对应size的空间。
所以上一步的chunk15就被放进了largebin,同时此时因为它所在的largebin中只有它一个chunk,那么此时的fd_nextsize与bk_nextsize会指向其本身,关于largebin的规则这里就不展开说了,有些复杂和乱,如果有时间我会再详细介绍一下largebin规则的。
这里的chunk17的请求,因为size小于largebin里的size,所以会在largebin里切下来一小块,然后剩余部分放到unsortedbin里面。关于chunk17里所填入的值都是根据后续产生的指针不断调整出来的,这里暂时是无法观察出这几个值都是怎么来的。不过其作用是伪造出一个fake chunk:
3.构造出fake chunk的fd,使fd->bk = fake chunk,满足条件
前面的18-21还是从刚才进到unsortedbin的chunk15中切出来的,之后用到第1步准备的7个tcache填满对应的他tcache_entry链,之后20和18就会被放入fastbin中:
清空tcache防止后续被干扰,之后申请一个超过对应fastbin的请求,就会触发机制把当前fastbin里的压入到smallbin中,当然用不了0x400那么大,不过写的时候没注意,之后不想改了hh。
注意fastbin取链是遍历fd,而smallbin或者其他任何双向链的bin取链都是遍历bk,当然largebin有所不同,所以此时申请0x28,就会从smallbin的bk取对应的chunk分配给我们,而这个chunk的bk如图所示,残留着堆指针可以被我们操作:
smallbin申请之后,会有一个机制将对应smallbin链中余下的chunk放入对应的tcache里,图中可以看到。然后可以看到刚才取出来的chunk的bk被我们覆盖成了fake chunk的地址,这也就是第2步里为什么覆盖成\x40的原因。
4.使fake chunk的bk->fd = fake chunk,满足条件。
chunk 17就是fake chunk,fake chunk的bk是之前通过largebin残留的bk_nextsize,这里指向包裹它的正常堆块,所以这里要改变其fd,要用到fastbin残留的fd指针。不用tcache的原因,是因为tcache在2.29下多了一个key的机制,也就是在bk所对应的位置会加一个指向其tcache的指针,这里不细说了。反正会破坏掉我们对fake chunk伪造的size区域。
进入fastbin:
分配之后:
5.触发off-by-null。
首先还是清空tcache防止被干扰,然后分配一个chunk23用于接下来对chunk24进行off-by-null。chunk25则是防止进行off-by-null的chunk24被合并掉。这里的0x5f8也是通过上述的那些size分配调整出来的,可以正好分配出剩余的unsortedbin,而且对应的size为0x601。这样的size有两个好处,一是足够大,这样就不用考虑还要先填满tcache,第二就是进入就是off-by-null之后,我们就不需要再对nextchunk进行伪造了,借助系统帮我们完成。
对chunk24进行修改后:
可以看到通过prevsize可以找到我们的fake chunk,同时也是第2步里,fake chunk的size选择为0x521的理由。
关键位置我都框出来了,这时可以来人肉判断一下是否能通过两个检测:
向低地值合并的检测。
unlink时候的检测。
很明显全部满足!bingo!最后free chunk24构造off-by-null成功:
6.泄露libc和getshell。
最后有了堆块复用就随便玩了,后续步骤就不说了。
2.29下的off-by-null到这里就算分析完了,思路是来自于EX师傅的博客,我只是详细地对其中的关键点解释了一下。
当然2.29下还有一些其他的很有意思的新思路和利用手法,这里因为我在过几天的某比赛的出题里用到了这种手法,所以我会在比赛结束后再分享给大家。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)