CTF 夺旗 PWN 题: 二叉树的漏洞利用
初步分析
程序运行起来看起来似乎是一道常规的菜单堆题:
libc 环境:
是 Glibc 2.27-3ubuntu1.4,这个版本与 2.31 版本很像,都有 key 机制,一定程度上防止了 double free 的攻击。
1、200 份很多已经买不到的绝版电子书
2、30G 安全大厂内部的视频资料
3、100 份 src 文档
4、常见安全面试题
5、ctf 大赛经典题目解析
6、全套工具包
7、应急响应笔记
回到程序,程序的功能有插入,展示和删除,我们具体用 IDA 打开来看看程序是个什么逻辑。
可以看到函数列表有非常多的函数(原题去除了符号表,笔者经过逆向重命名了一些函数符号),并且使用 c++编写,逆向起来难度更大,如果采取常规的静态分析手段,可能会花费很大的精力,由于题目名字是 cxx_and_tree,我们猜测整个程序是用树这种数据结构来存储信息,最经典的莫过于二叉树,我们可以来写个 demo 来测试程序,如果申请以下堆块,那么堆结构如下面的图:
其中 0x40 大小的为 node 部分数据,其余大小的为其 data 数据,将其画为二叉树长成如下样子:
左右子树根据其 index 分如上图,并且通过观察每个 node 的节点可以确定程序是用二叉树来存储数据。
经过逐步调试和逆向加深对程序的理解后,笔者分析 node 结构体如下:
具体的漏洞和代码逆向请看下文。
漏洞分析与逻辑触发点
漏洞位于当我们删除某个二叉树节点的时候,如果该节点有左右子树,会调用一个 memcpy 的函数,这个函数的对于节点 size 的处理是有问题的。
在申请节点的时候,其 size 的算法是这样的:
做了一个类似于 align 的操作,这个操作是很安全的,人为扩展了一下 chunk,使得我们能够申请的最大的 size 和其 align 之后最小的 size 一样大,但是下面的删除节点的操作就有 bug 了:
写个 poc 来看下我们能溢出的字节数量。
注意到我们在触发这个逻辑的时候,有部分 size 是比 biggerSize 要小的,最多可以溢出 7 字节。
整个删除节点的逻辑如下:
想要到达漏洞点所在的位置,则该节点必须同时拥有左右孩子节点才可以。
分析下如果该节点同时拥有左右孩子节点,那么删除该节点的时候发生的流程大致如下:
首先是获得该节点中右子树中最小的元素(按 idx 确定大小,因为下面一直走的是左子树的逻辑)
然后将其要替换的节点传入到带有 bug 的函数中,在此函数中,程序重新申请了一块 buf,然后复制要替换节点的数据到新的 buf 中,值得注意的是,并没有像我们传统的数据结构中一通乱改指针,而是采用了一个复制的思想,但是新创建的 buf 的 size 给少了,控制得当能够溢出七个字节。
然后再往下的逻辑就是删掉刚才的右子树中的最小节点,因为其数据已经拷贝到原本要删除的节点当中。
在这里我有个疑问,既然之前选到了右子树的最小的节点,那么为什么还要判断其是否还有左子树呢?上面的分支应该永远不会进入,或许是出题人为了增加逆向难度,又或者是出题人面向 ctrl+CV 编程。
然后进入一个删除节点的函数:
分为两种情况删除,一是叶子节点,另外就是还有一个右孩子节点,删除的方法很普通,就是普通数据结构中学的删除方法一样。
漏洞利用
完整 exp 如下:
漏洞其实并不太好利用,分析原因如下:insert 节点的时候会额外申请别的堆块出来,整体的堆布局我们其实并不太好控制,所以漏洞利用的时候会有时不可控,我们需要反复的调试,现给出 exp 的书写思路。
泄露 libc 基址
在 2.27 下,只要循环填满 tcache 即可很容易的泄露出 libc
布置二叉树
可以看到,我们在输入 content 的时候会输入一些很奇怪的值,这个时候的值我们是无法确定的,必须结合后文来慢慢调试。
初始状态如图所示,这个时候我们去 free11,将会到达漏洞所在逻辑的位置处,让程序触发漏洞。
此时堆空间的布局如图所示:
可以看到这个时候其实已经利用了漏洞改写了下一个堆块的 size 位,形成了 overlap。
下面是关键操作,劫持 tcache 数组的 0x30 大小的 chunk 的 fd 为 hook。
此时的 bins 情况:
此时的二叉树为:
因为现在已经将 freehook 链入到 tcache 里面,下面我们的工作主要就是围绕怎么将其申请出来而努力,首先直接申请是肯定不行的,我也没有深究,因为申请的时候会首先申请两个 chunk 出来,然后将其 free 掉,然后再做一系列的 memcpy 操作,在这一系列的过程中,无法保证中间 chunk 的合法性能够绕过检查,所以我们还是利用漏洞点申请不同 size 的 chunk 的这一特性努力,我们可以逐个布置满足要求的二叉树节点,然后利用漏洞来申请出来这个 chunk。
第一轮申请
布置完如下 node,二叉树为:
堆布局为:
可以看到还有两个 node 就可以申请到 freehook。
清除一些无关的 node,为我们布置节点做好铺垫。
第二轮申请
此时二叉树为:
堆布局为:
清除一些节点来重新布置。
第三轮申请并 getshell
故技重施,最终可以申请到 hook 所在空间并 getshell。
本文到这里就结束了
评论