初步分析
程序运行起来看起来似乎是一道常规的菜单堆题:
libc环境:
是Glibc 2.27-3ubuntu1.4,这个版本与2.31版本很像,都有key机制,一定程度上防止了double free的攻击。
回到程序,程序的功能有插入,展示和删除,我们具体用IDA打开来看看程序是个什么逻辑。
可以看到函数列表有非常多的函数(原题去除了符号表,笔者经过逆向重命名了一些函数符号),并且使用c++编写,逆向起来难度更大,如果采取常规的静态分析手段,可能会花费很大的精力,由于题目名字是cxx_and_tree,我们猜测整个程序是用树这种数据结构来存储信息,最经典的莫过于二叉树,我们可以来写个demo来测试程序,如果申请以下堆块,那么堆结构如下面的图:
add(0, 0x60, 'a')
add(4, 0x60, 'a')
add(2, 0x60, 'a')
add(9, 0x60, 'a')
add(3, 0x60, 'a')
add(7, 0x60, 'a')
其中0x40大小的为node部分数据,其余大小的为其data数据,将其画为二叉树长成如下样子:
左右子树根据其index分如上图,并且通过观察每个node的节点可以确定程序是用二叉树来存储数据。
经过逐步调试和逆向加深对程序的理解后,笔者分析node结构体如下:
struct node
{
__int64 idx; // 节点号
__int64 user_size; // 用户输入的size
__int64 *self_heap_buf; // 存储数据的buf
node *left; // 左孩子
node *right; // 右孩子
node *father; // 父节点
};
具体的漏洞和代码逆向请看下文。
漏洞分析与逻辑触发点
漏洞位于当我们删除某个二叉树节点的时候,如果该节点有左右子树,会调用一个memcpy的函数,这个函数的对于节点size的处理是有问题的。
在申请节点的时候,其size的算法是这样的:
做了一个类似于align的操作,这个操作是很安全的,人为扩展了一下chunk,使得我们能够申请的最大的size和其align之后最小的size一样大,但是下面的删除节点的操作就有bug了:
v2 = (unsigned __int64)tmp_target->user_size >> 3;
写个poc来看下我们能溢出的字节数量。
def poc():
for size in range(0x10, 0xff + 1):
biggerSize = ((size >> 3) + 1) * 8
smallerSize = (size >> 3) * 8
if biggerSize > smallerSize:
print("size:{}, biggerSize:{}, smallerSize:{}".format(hex(size), hex(biggerSize), hex(smallerSize)))
poc()
注意到我们在触发这个逻辑的时候,有部分size是比biggerSize要小的,最多可以溢出7字节。
整个删除节点的逻辑如下:
想要到达漏洞点所在的位置,则该节点必须同时拥有左右孩子节点才可以。
分析下如果该节点同时拥有左右孩子节点,那么删除该节点的时候发生的流程大致如下:
首先是获得该节点中右子树中最小的元素(按idx确定大小,因为下面一直走的是左子树的逻辑)
然后将其要替换的节点传入到带有bug的函数中,在此函数中,程序重新申请了一块buf,然后复制要替换节点的数据到新的buf中,值得注意的是,并没有像我们传统的数据结构中一通乱改指针,而是采用了一个复制的思想,但是新创建的buf的size给少了,控制得当能够溢出七个字节。
然后再往下的逻辑就是删掉刚才的右子树中的最小节点,因为其数据已经拷贝到原本要删除的节点当中。
在这里我有个疑问,既然之前选到了右子树的最小的节点,那么为什么还要判断其是否还有左子树呢?上面的分支应该永远不会进入,或许是出题人为了增加逆向难度,又或者是出题人面向ctrl+CV编程。
然后进入一个删除节点的函数:
unsigned __int64 __fastcall delete_leaf_node_or_right_children(struct node **father_node, struct node **to_delete_node, struct node **tmp_father_node)
{
struct node *v3; // rbx
struct node *v4; // rbx
struct node *v5; // rbx
struct node *v6; // rbx
unsigned __int64 v8; // [rsp+28h] [rbp-18h]
v8 = __readfsqword(0x28u);
if ( *to_delete_node == *father_node ) // only root node
{
if ( *((_DWORD *)father_node + 4) == 1 ) // only a node
{
v3 = *to_delete_node;
if ( *to_delete_node )
{
deleteNode0((__int64)*to_delete_node);
operator delete(v3);
}
*father_node = 0LL;
--*((_DWORD *)father_node + 4);
*to_delete_node = 0LL;
}
else // has right children
{
*father_node = (*father_node)->right;
(*father_node)->father = 0LL