前言
参考博客如下:
CTF的Pwn题里面,通常就会遇到一些加了沙盒的题目,这种加沙盒的题目,在2.29之后的堆题中,通常为以下两种方式
- 劫持
__free_hook
,利用特定的gadget,将栈进行迁移 - 劫持
__malloc_hook
为setcontext+61
的gadget,以及劫持IO_list_all
单链表中的指针在exit结束中,在_IO_cleanup
函数会进行缓冲区的刷新,从而读取flag
因为setcontext + 61
从2.29之后变为由RDX寄存器控制寄存器了,所以需要控制RDX寄存器的指向的位置的部分数据
1 | <setcontext+61>: mov rsp,QWORD PTR [rdx+0xa0] |
但是如果将exit函数替换成_exit
函数,最终结束的时候,则是进行了syscall来结束,并没有机会调用_IO_cleanup
,若再将__malloc_hook
和__free_hook
给ban了,且在输入和输出都用read和write的情况下,无法hook且无法通过IO刷新缓冲区进行调用,这时候就涉及到ptmalloc源码里面了
利用条件
- 能够触发
_malloc_assert
,通常由堆溢出导致 - 能够任意地址写,修改
_IO_file_sync
和IO_helper_jumps + 0xA0 and 0xA8
利用原理
glibc
中ptmalloc
部分,从以前到现在都存在一个assret
断言的问题,此处存在一个fflush(stderr)
的函数调用,其中会调用_IO_file_jumps
中的sync
指针
GLibc 2.32/malloc:288
1 | static void |
这里放一个我常用来查询glibc源码的网址:Glibc source code - Bootlin
在_int_malloc
中存在一个 assert (chunk_main_arena (bck->bk));位置可以触发,此外当top_chunk
的大小不够分配时,则会进入sysmalloc中
GLIBC 2.32/malloc.c:2394
1 | ...... |
此处会对top_chunk的size|flags
进行assert判断
- old_size >= 0x20;
- old_top.prev_inuse = 0;
- old_top页对齐
通过这里也可以触发assert
下面手动实现进入assert后,可以想到fflush和fxprintf都和IO有关,可能需要涉及IO,一步步调试看看可以发现在fflush
函数中调用到了一个指针:位于_IO_file_jumps
中的_IO_file_sync
指针,且观察发现RDX寄存器的值为IO_helper_jumps
指针,多次调试发现RDX始终是一个固定的地址
如果存在一个任意写,通过修改 _IO_file_jumps + 0x60
的_IO_file_sync
指针为setcontext+61
修改IO_helper_jumps + 0xA0 and 0xA8
分别为可迁移的存放有ROP的位置和ret指令的gadget位置,则可以进行栈迁移
例题介绍
以NepCTF 2021年中NULL_FxCK为例
题目分析
沙箱开启
保护全开
禁用malloc_hook, free_hook
漏洞分析
整个程序只有在modify里有一次off by null 的机会,无其他漏洞
思路整理
这里的思路其实有点公式化的味道,就和我们做数学题一样,都是通过题目给的条件来思考利用的手法。给了off_by_null就思考会用到堆块的合并导致的重叠。重叠带来的好处:
- 通过切割堆块能使我们在合并的堆块内部任意地址写main_arena,这个任意地址可能是note数组的一个堆指针的fd,那么我们就可以泄露libc了
- 合并的时候的unlink能使得我们在已知堆块的fd和bk上写一个堆地址,这样就可以弥补一开始的截断带来的不能泄露堆块,然后成功泄露出堆块了
堆风水泄露libc和堆地址
首先伪造一个chunk头,利用off by null unlink吞并几个Allocated状态的chunk. 然后add分割使得chunk进入unsortedbin,从而泄露libc. 同时包含,largebin中提前包含heap信息,从而泄露heap地址
house of wiki
发现这题中的exit
被换成了_exit
,而_exit
是不会存在house of pig
里面的那条链子的,它直接就是一个exit
的系统调用然后程序就结束了,所以任何打exit
的链子都不能直接拿来用。遇到这种问题,有的师傅就开辟了一条名为house of kiwi
的链子。主要是打__malloc_assert
断言,有一个位于_IO_file_jumps+0x60
的稳定的跳转指针sync
和稳定的rdx——_IO_helper_jumps
,而且这两个地方在gdb里都是有符号表的(比banana好找多了2333):
那么我们通过两次任意地址写就行了?非也。因为还需要触发assert,看看malloc.c的源码,ctrl f输入”assert”。发现有80多个。这里介绍其中一种做法:当top_chunk的大小不够分配时,则会进入sysmalloc中
1 | assert ((old_top == initial_top (av) && old_size == 0) || |
发现很多检测,我们注意到对topchunk的prev_inuse的检测,只要把topchunk的size位的prev_inuse置为0,申请一个比它大的堆块就可以触发了。我们发现,至少需要改三个地址,也就是执行三次任意地址写。从这道题的严苛条件,不能用tcache poison等简单手法。
TLS段tcache struct attack
我们都知道,malloc_init会在heapbase段开设一个内存用于管理tcache。而这个管理tcache的地址,是可以从heapbase被我们劫持到另一个地方的,这是因为实际寻找的时候,是找到TLS段的管理tcache的地址,只不过malloc_init函数预设成了heapbase+0x10而已(注意,是heapbase+0x10而不是heapbase),我们可以在gdb中找到这段区域:
通过largebin attack
劫持这段为可控堆块,在上面布置任何我们想写的东西,malloc
对应位置size大小就能够申请出来并且改写了(这里的偏移要调一调,不过也可以拿exp的模板直接来用,也就是)
通过改稳定的跳表sync为setcontext+61(因为setcontext会将[rdx+0xa0]设置为rsp,将[rdx+0xa8]设置为rip),将稳定的rdx _IO_helper_jumps
设置为_IO_helper_jumps+0xa0
为存orw链,+0xa8为ret指令,并改top_chunk的size,然后申请一个比它的size大的堆块触发assert就可get_shell了
总结
这道题考察了高版本的off_by_null,large bin attack,house of kiwi , TLS attack
exp
1 | # -*- encoding: utf-8 -*- |