roarctf_2019_realloc_magic
总结
做完这道题后总结如下:
-
realloc
功能比较多,使用需要谨慎 -
可利用修改
stdout
结构体的flags
和_IO_write_base
来泄露libc
中的地址 -
利用
main_arena
来劫持stdout
结构体
题目分析
checksec
首先checksec
一下,发现保护全开:
函数分析
然后将题目拖进IDA分析,首先看main函数: 可以看到,main函数并不复杂,一个菜单加上3个选项。
-
menu:
-
re:
-
fr:
-
ba:
这里需要注意,分配内存函数使用的是realloc(void* ptr, size_t size)
,这个函数的功能很多,查看源码后发现其功能有:
- 当
ptr == nullptr
的时候,相当于malloc(size)
, 返回分配到的地址 - 当
ptr != nullptr && size == 0
的时候,相当于free(ptr)
,返回空指针 - 当
size
小于原来ptr
所指向的内存的大小时,直接缩小,返回ptr
指针。被削减的那块内存会被释放,放入对应的bins
中去 - 当
size
大于原来ptr
所指向的内存的大小时,如果原ptr
所指向的chunk
后面又足够的空间,那么直接在后面扩容,返回ptr
指针;如果后面空间不足,先释放ptr
所申请的内存,然后试图分配size
大小的内存,返回分配后的指针
可以看到,realloc
函数功能很多,也很危险,使用不当的话会引来严重的安全问题。
ba
函数可以将realloc_ptr
置为空,但是只有一次使用机会,re
函数会释放内存,但是没有置为空,存在double free
的漏洞。
题目使用的是ubuntu 18
的环境,对应的libc
的版本为2.27
,考虑使用tcache attack
。
解题思路
漏洞找到了,而一般的tcache attack
也很简单,就是直接修改tcache bin chunk
的next
指针,可以进行任意地址写。所以,初步的解题思路是:
初步解题思路
- 利用
fr
函数进行tcache dup
- 修改
chunk
的next
指针,覆盖__free_hook
,为one_gadget
- 修改后触发
fr
函数,获取shell
思路没啥问题,但是中间有几个关键的问题:
存在的问题
- 分配函数是
realloc
,所以如果指针ptr
不置为空,就无法达到malloc
的效果,ptr
所指向的chunk
要么扩大,要么缩小,要么换一片内存段进行内存分配,没有办法从bins
里面取出chunk
- 题目里似乎没有泄露地址的函数,要想往
__free_hook
写入one_gadget
需要libc
的基地址
问题解决方案
- 回忆一下刚刚总结的
realloc
函数的特点,可以发现,在上图的re
函数第7
行,将realloc_ptr
接收返回后的指针,那么如果realloc_ptr != 0 && size==0
,就会触发free(realloc_ptr)
,并且将realloc_ptr
置为0
。所以,第一个问题就解决了。 - 当题目没有泄露地址的函数或功能的时候,可以通过劫持
stdout
结构体,修改flags
和_IO_write_base
来泄露libc
中的地址,进而获取到libc
的基地址。攻击原理就不详述了,这位师傅写的很好:利用IO_2_1_stdout_泄露信息。最后需要将stdout
结构体的flags
修改为0x0FBAD1887
,将_IO_write_base
的最后一个字节覆盖为0x58
。劫持stdout
可以借助main_arena
来操作,只需要修改低字节的几个地址即可。
最终解决思路
由以上分析,可以总结出最终的解题思路为:
- 首先分配一块合适大小的内存块A。这段内存用于调用
realloc
往后面扩张,覆写tcache bin chunk
的size
和next
指针。 - 利用
re
函数将realloc_ptr
指针置为空,然后分配一块大小在small bin chunk
范围的内存块B,如大小为0x80。这是为了之后能得到unsorted bin
- 利用
re
函数将realloc_ptr
指针置为空,然后随意分配一块内存块C,用于隔开top chunk
。 - 利用
re
函数将realloc_ptr
指针置为空, 申请大小为0x80的内存,得到了刚刚释放的那块内存B。然后利用fr
函数和re
函数将realloc_ptr
释放8次,使得tcache bin
和unsorted bin
存在重合,同时realloc_ptr
所对应的chunk
的fd
和bk
指针,都指向了main_arena + 96
。 - 重新将内存块A申请回来,然后扩张,修改内存块A下面的内存块B的
size
为0x51
,这里可以修改为任意在tcache bin
范围内的值,是为了避免再次调用realloc(realloc_ptr, 0)
的时候,又改变了tcache bin
链上的指针。保证能将内存申请到stdout
附近。 - 然后申请内存到
stdout
结构体附近,修改flags
和_IO_write_base
的值。泄露出libc
的地址,计算得到__free_hook
地址和one_gadget
的地址。 - 接下来不能利用
re
来清空realloc_ptr
指针,程序会挂掉,因为绕不过检查。这里选择使用ba
函数,来将指针置为空。 - 然后重复上面的1-4步,修改
__free_hook
的值为one_gadget
,触发fr
函数,获取shell
。
编写exp
根据最终的解题思路,编写exp并调试,过程记录如下:
定义好函数:
|
|
执行思路的1-4步:
|
|
看一下此时的bins
:
然后修改内存块B的size
和next
指针,劫持到stdout
,同时泄露出地址
|
|
这里调试的时候可以发现,_IO_2_1_stdout_
的低两个字节和main_arena + 96
不同,理论上需要改这两个字节,实际上最后一个字节一直是0x60
,所以只需要改一个字节就行了。此处为本地调试,可以手动查看要修改的内容,然后填上去。
输入0xb7
后,修改成功:
然后分配到stdout
结构体,修改flags
等,泄露出地址:
计算一下基地址,__free_hook
的地址等:
重复一下上面的过程,在_free_hook
附近写上one_gadget
即可:
|
|
之后就可以拿到shell:
最后贴一下完整的exp
|
|
注意:在实际打的时候,需要爆破一个字节。
exp说明
这份exp是我专门用来刷BUUCTF上面的题目的,有需要的小伙伴可以拿去用。主要是利用click
包装了一下命令行参数,方便本地调试和远程攻击。
- 输入
python3 exp.py -h
可以获取帮助:
调试的时候,首先需要进入tmux
,然后可以指定是否分屏调试,以及断点设置等。目前可支持设置函数地址断点和函数名断点。
- 输入
python3 expcopy.py roarctf_2019_realloc_magic -t 1 -gb puts
是这样的:
可以开始调试,并且断在puts
函数处。
- 如果本地调通了需要远程打直接输:
python3 exp.py filename -p 25622
就可以了。这一题不能直接远程打,需要改下脚本进行爆破。
也可以自己定制命令,省去做题输入命令,改脚本的时间。