接触的第一道多线程题。做的过程中多次参考了 glibc 源码,有了一些自己的想法,便在此记录。
基本情况
ELF文件,libc 版本为 2.23,保护全开,无 seccomp。
程序分析
主函数
主函数的流程可以概括为:用户输入线程数后,主线程在循环中依次创建线程。锁 mutex 控制线程的创建,一次仅允许一个子线程执行“任务”,而锁 stru_2030E0 的作用是阻塞主线程,直到所有子线程的“任务”结束。
值得注意的是主线程没有回收任何子线程的资源。
最后给出了 _IO_2_1_stdout_
的地址,有一次修改 chunk 、两次 malloc, read 0x60 的机会,最后调用 strdup
,其内部会调用 malloc
。(看上去要用 fastbin attack)
子线程例程
堆菜单题的模板。有几点注意:
- 只有一个指针
chunk_ptr
。 - add 只有两次,show 和 delete 只有 一次。
- delete 有 UAF 漏洞。
- add 时会 malloc 两次,大小为 size 和 0x10,分别存放用户输入和 size。delete 中两者均被 free。
- 退出时解锁并 sleep 一段较长时间。
思路
在做这题之前我对多线程下的内存模型及 glibc 的做法只有一点模糊的感觉(连多线程程序都没写过),好在这题使用的多线程 API 都是最基本的。并且执行流程基本上是线性的,比较简单。所以直接用 gdb 调试,配合 vmmap 和 heap 指令尝试“管中窥豹”。
前提
ptmalloc2 libc-2.23 中使用 arena 记录 chunk 和 bin 的详细信息,heap_info 记录子线程的堆信息。(这部分内容请参考 CTFwiki 和源码)
为子线程分配堆的算法我不了解(CTFwiki 上也没有介绍),导致后面做不出来。看了gls的文章 才明白这题的考点。
本题的核心就是堆分配的算法。然而我暂时没时间去看源码、调试,所以下面的总结都是做此题得到的经验性质的结论。
调试的发现:奇怪的sleep
刚开始分析程序时,我对子线程 return 前的 sleep 感到疑惑,然后我开始调试:
1 | >> |
初步调试后我发现:若在前一子线程 exit 前就为当前子线程分配 chunk,会出现新的大小为 0x21000 的区域,反之则不会出现。
然后我查看了前一子线程的 arena,在 sleep 时 arena.attached_threads 为 1,例程结束(return)后 arena.attached_threads 为 0。如果这之后当前子线程申请 chunk,arena.attached_threads 又会变为 1:
1 | binmap = {0, 0, 0, 0}, |
可以总结出两个结论:
- 子线程 return 后就算没有被回收,操作系统依然会做一些处理。
- 一个堆可以被不同线程反复利用。
这样看来,本题中的有两种分配 chunk 的策略:
- 在前一子线程 sleep 时,之后的子线程立刻申请 chunk。chunk 可能会分配到新 mmap 的区域(新的 arena),或者旧的,甚至是
main_arena
。(后两点我在看了题解后才直到) - 等待前一子线程 return 后再申请,会分配到旧 arena 上。(至少在此题 debug 过程中没遇到其它情况)。
从 pwn 的角度来看,两种策略均有用处:前一种可以分配到其它线程的堆上去搞破坏,后一种可以增加 add、delete 等函数的使用次数。此题的题解完全依赖前一种策略。但后一种在此题中也有妙用,后面再说。
漏洞利用
gls的文章里写得很清楚,简化一下就是:
- 不断申请堆直到申请到
main_arena
(通过泄漏地址判断),delete 一个 0x71 的 chunk 进入 fastbin。(子线程中进行) - 利用 UAF,使用 fastbin attack 修改
__realloc_hook
和__malloc_hook
。(主线程中进行)
下面是堆分配的情况:
可以看到,两个线程可以共用 arena。
不提供libc地址?
如果程序不提供 libc 地址,该怎么泄漏呢?借助上面提到的第二种分配策略,我们可以泄漏在 non_main_arena 中的 libc 地址。(我一开始硬要提前泄漏 libc 地址,搞了半天,成功了,却发现没什么用。。。)
arena.next
arena 以循环链表的方式链接起来,其 next 域指向下一个 arena。而 main_arena 位于 libc 中,所以子线程 arena 的 next 域就有 libc 地址。
fastbin attack
接下来要解决的问题是,如何将一个 chunk 分配到此处?
如果用 fastbin attack,要解决 size 域和如何修改 fd 的问题:
size 域:arena 的 next 域的上方有
binmap
、bins,其中 bins 数组中指针紧挨着排列,没有可以凑的 0x7f。修改 fd:可以 double free,但 size 域只能为 0x20,且要有 non_main_arena flag,因为在
__libc_malloc
中有检查:
(我一开始还以为这里的 assert 只有 debug 时才发挥作用。。。)
这时在上方的 binmap
就有用了。
binmap
构思
在这题前,我对 binmap
的模糊印象是:如果各种 bin 里都找不到大小合适的 chunk,它会帮忙找更大的 chunk。至于结构、原理什么的完全不知道。
做题时我去翻 CTFwiki,上面给的简介是:“ptmalloc 用一个 bit 来标识某一个 bin 中是否包含空闲 chunk 。”
再看看它和 next 域的位置关系(蓝框为 binmap 区域,绿框为 next 域):
与 size 域联想一下:如果我们能通过一系列的 add 和 delete 操作将 binmap 凑为 0x24,那就能将一个 fastbin chunk 申请到此处,泄漏 libc 地址。甚至还能修改 arena 的 next 域 (能达成什么效果就不知道了,因为我目前对涉及 arena 分配的算法一窍不通)。
方法
binmap
的 bit 代表对应的 bin 中是否有 chunk。比如 0x24 的二进制为 100100,代表大小为 0x20 和 0x50 的 bin 有chunk。接下来看看它如何设置这些 bit。
glibc 中的 binmap
和一些方法:
由上图知 markbin(m, i)
宏用来设置 binmap
中的 bit,查找后发现它只在一处被使用——遍历 unsortedbin 的循环的末尾:
这意味着只要有一个 chunk 从 unsortedbin 取下,放到其它 bin 里,binmap
的对应 bit 就会被设置。
但我没有看到 unmarkbin(m, i)
的踪影,就去搜av->binmap
。
整个 malloc.c
文件中 av->binmap
只出现了三次,且都在 _int_malloc
中取更大块的部分,其中清除位的操作:
不是在 chunk 取出时检测并清除,而是在找更大 chunk 时没找到才清除。
泄漏libc exp
总结出 bit 的设置和清除规律后,接下来搞好堆风水就好了。大脑的苦力活,搞了挺长时间。(感觉总是不能一步到位,经常搞着搞着就发现错误了)。
1 | from pwn import * |
一些问题
问题还挺多的,今天没时间写了,改天再说。