大古:一开始就用红色形态作战不就行了吗
前言
glibc
高版本逐渐移除了__malloc_hook/__free_hook/__realloc_hook
等等一众 hook
全局变量。
利用手段向 IO_FILE 靠拢,但是随着版本越来越高,堆利用手段也变少,IO_FILE 的问题也逐渐减少。
large bin attck
一个范围的bin,保证了其内部有序性。在 浅析largebin attack文章中有张图方便理解
同样大小的bin按照free的时间顺序进行排序
- fd, bk: 相同大小堆的双向链表,按照时间先后排序
- fd_nextsize, bk_nextsize: 大小不同的双向链表
- 如果只有一个,fd, bk指向 main_arena fd_nextsize 和 bk_nextsize 指向自己
直接使用 how2heap 2.36 的 large bin attack 进行演示(Glibc >= 2.30 都可以使用)。
- 漏洞的点在开头的注释中给出,就是最后一句赋值语句导致的,victim(正在链入largebin)的size小于已经存在的bin
- malloc两个大chunk p1,p2,两个 0x18 是防止 相邻的unsorted bin 合并 以及 被top_chunk合并。
- 这里注意的是:p1 的 size 大于 p2,但是不要差太多,在同一个largebin 里
- free p1,将 p1 放入large bin 中
- free p2,修改 p1 的 bk_nextsize 为 &target-0x20
- 将 p2 放入largebin中
- target 值就变成了 p2 的地址
1 |
|
达到一个任意地址写成堆地址的目的。
Glibc 2.29 之前,unsortedbin attack 和 largebin attack 都是攻击 bk 指针,但是后来加了一句检查
在攻击时,fd,bk,fd_nextsize 可以随便覆盖内容,在经过malloc后会修复fd,因为fd指向 size 较小的 victim
IO 流
这里一般指 存在一条链,某个函数 使用 vtable 的函数指针来调用函数。
程序使用exit退出程序
- 从main函数退出,glibc会调用exit
- 显示调用 exit 函数退出程序
malloc_assert: house of kiwi 提出,触发下面的条件选一个
- topchunk的大小小于MINSIZE(0X20)
- prev inuse位为0
- old_top页未对齐
- 但是从libc 2.36 发生了一点变化,移除IO操作,也就是从libc 2.36不能使用
- libc 2.37 直接没有这个函数了。
libc 2.35:
- 两个函数(fflsh, fxpeintf)都涉及IO操作。
1 | static void |
FSOP
FSOP就是通过劫持_IO_list_all的值(如large bin attack修改)来执行_IO_flush_all_lockp函数,这个函数会根据_IO_list_all刷新链表中的所有文件流.
当程序从 main 函数返回或者执行 exit 函数的时候,均会调用 fcloseall 函数,调用链如下
- 最后会遍历
_IO_list_all
存放的每一个IO_FILE
结构体 - 如果满足条件的话,会调用每个结构体中
vtable->_overflow
函数指针指向的函数。
1 | exit |
vtable 函数调用过程,就是调用跳表,比如说调用 __overflow
IO_validate_vtable
函数负责检查vtable
的合法性,会判断vtable
的地址是不是在一个合法的区间。如果vtable
的地址不合法,程序将会异常终止。- 最后就是调用 vtable 里面的函数
1 |
检查函数
- 检查此结构体的 vtable 与
__io_vtables
全局变量表偏移 - 在这个表里的表就能通过检查。
1 | static inline const struct _IO_jump_t * |
所以现在劫持vtable都差不多在这个表里找一个能符合条件的表进行利用。
比如挟持到 _wide_data
相关的表,因为这个表含有vtable,并且函数调用没有检查。
- 而与其相关的表有3个 找
_IO_wfile_jumps
开头的表存在三个
1 |
house of apple
有三个版本,这里是 version 2.0,控制函数执行流。
- IO 流:exit 或者 malloc_assert
- 能泄露出
heap
地址和libc
地址 - 能使用一次
largebin attack
(一次即可)
wide_data 结构体
- 其中也存在一个 vtable
- 由上面的FSOP知道,在调用
_wide_vtable
虚表里面的函数时,同样是使用宏去调用,但是没有检查,因此更好利用
1 | struct _IO_wide_data |
假设劫持了vtable 到 IO_wdata_jumps
之后,调用overflow
- 因为是宏展开,进入
_IO_wfile_jumps
的 overflow 函数。 - 而这个函数执行流如下
1 | wint_t _IO_wfile_overflow(FILE *f, wint_t wch) { |
主要看其中的函数调用,这里主要看作者的几条连
链1:_IO_wfile_overflow
控制函数执行流,但是需要绕过某些检查。伪造fp
_flags
设置为~(2 | 0x8 | 0x800)
,如果不需要控制rdi
,设置为0
即可;如果需要获得shell
,可设置为sh;
,前面有两个空格vtable
设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap
地址(加减偏移),使其能成功调用_IO_wfile_overflow
即可_wide_data
设置为可控堆地址A
,即满足*(fp + 0xa0) = A
_wide_data->_IO_write_base
设置为0
,即满足*(A + 0x18) = 0
_wide_data->_IO_buf_base
设置为0
,即满足*(A + 0x30) = 0
_wide_data->_wide_vtable
设置为可控堆地址B
,即满足*(A + 0xe0) = B
_wide_data->_wide_vtable->doallocate
设置为地址C
用于劫持RIP
,即满足*(B + 0x68) = C
,比如说C为system函数
1 | _IO_wfile_overflow |
链2:_IO_wfile_underflow_mmap
控制函数执行流
_flags
设置为~4
,如果不需要控制rdi
,设置为0
即可;如果需要获得shell
,可设置为sh;
,注意前面有个空格vtable
设置为_IO_wfile_jumps_mmap
地址(加减偏移),使其能成功调用_IO_wfile_underflow_mmap
即可_IO_read_ptr < _IO_read_end
,即满足*(fp + 8) < *(fp + 0x10)
_wide_data
设置为可控堆地址A
,即满足*(fp + 0xa0) = A
_wide_data->_IO_read_ptr >= _wide_data->_IO_read_end
,即满足*A >= *(A + 8)
_wide_data->_IO_buf_base
设置为0
,即满足*(A + 0x30) = 0
_wide_data->_IO_save_base
设置为0
或者合法的可被free
的地址,即满足*(A + 0x40) = 0
_wide_data->_wide_vtable
设置为可控堆地址B
,即满足*(A + 0xe0) = B
_wide_data->_wide_vtable->doallocate
设置为地址C
用于劫持RIP
,即满足*(B + 0x68) = C
1 | _IO_wfile_underflow_mmap |
链3:_IO_wdefault_xsgetn
控制函数执行流
_flags
设置为0x800
vtable
设置为_IO_wstrn_jumps/_IO_wmem_jumps/_IO_wstr_jumps
地址(加减偏移),使其能成功调用_IO_wdefault_xsgetn
即可_mode
设置为大于0
,即满足*(fp + 0xc0) > 0
_wide_data
设置为可控堆地址A
,即满足*(fp + 0xa0) = A
_wide_data->_IO_read_end == _wide_data->_IO_read_ptr
设置为0
,即满足*(A + 8) = *A
_wide_data->_IO_write_ptr > _wide_data->_IO_write_base
,即满足*(A + 0x20) > *(A + 0x18)
_wide_data->_wide_vtable
设置为可控堆地址B
,即满足*(A + 0xe0) = B
_wide_data->_wide_vtable->overflow
设置为地址C
用于劫持RIP
,即满足*(B + 0x18) = C
1 | _IO_wdefault_xsgetn |
总结一下:使用 largebin attack
劫持_IO_list_all
变量
- 将其替换为一个伪造的
IO_FILE
结构体(某个我们可控内容的堆) - IO_FILE的
_wide_data
伪造为可控的堆地址空间,进而控制_wide_data->_wide_vtable
为可控的堆地址空间 - IO_FILE的
vtable
伪造为_IO_wfile_jumps
,这是一个 const 变量, gdb使用p &_IO_wfile_jumps
查看 - 在需要写shellcode时,将C设置为一个写满ROP的堆地址就行。常使用setcontext
house of cat
函数调用链
_IO_wfile_jumps
中的_IO_wfile_seekoff
函数,然后进入到_IO_switch_to_wget_mode
函数中来攻击
1 | __malloc_assert |
并且house of cat在FSOP的情况下也是可行的,只需修改虚表指针的偏移来调用_IO_wfile_seekoff
即可(通常是结合__malloc_assert
,改vtable为_IO_wfile_jumps+0x10
。
1 | off64_t |
在这里调用 _wide_data
里的 vtable的_overflow
,JUMP宏 且没有检查
1 | int |
在_IO_switch_to_wget_mode
调试时发现如下的汇编代码
- rdi 是 fp 指针,是我们可以伪造的一个 IO_FILE。
- 通过 rdi控制 rax,在通过rax控制rdx,也可以过jbe指令。从而最后call 我们指定的shellcode
1 | 0x7f4cae745d30 <_IO_switch_to_wget_mode> endbr64 |
所以最后的伪造如下
- rax1 为上面的rax
- rax2 为下面的rax寄存器
1 | fake_io_addr = heapbase+0xb00 # 伪造的fake_IO结构体的地址 |
house of banana
不是一种攻击IO_FILE的利用手段。程序通过exit退出时,会调用一个名叫 rtld_global
的结构体中的一系列函数来进行诸如恢复寄存器,清除缓冲区等操作。
- 可以任意地址写一个堆地址(通常使用
large bin attack
) - 能够从
main
函数返回或者调用exit
函数 - 可以泄露
libc
地址和堆地址
gdb 常用的指令
- 这是ld.so 文件中的一个地址,因此不能使用libc.sym获得地址
1 | p &(_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next) |
rtld_global
结构体里面装有 _dl_ns
结构体,通过正常 main 函数返回或者调用 exit 退出,触发函数调用链:exit()->_dl_call_fini->(fini_t)array[i]
。
- glibc 2.37 后的源码,对比之前的与那吗,发现主要的变化为
_dl_call_fini(l);
,跟进函数发现除了输出debugging信息函数变了,其余都没变 - link map 使用双向链表连接起来
- nmaps 是
maps[]
中元素个数,也就是GL(dl_ns)[ns]._ns_loaded
- 建议自己随便写个程序,将其中变量打印出来看看。这里加载下面的注释里
1 | // pwndbg> p _rtld_global |
走到 _dl_call_fini
- 存在一个函数调用
((fini_t)array[sz])()
,map为参数,也就是上面的GL(dl_ns)[ns]._ns_loaded
和其 next,next->next…
1 | void _dl_call_fini(void *closure_map) { |
主要是函数调用能攻击一下就行,为了更容易的通过if的条件的,我们一般替换链表最后一个 link_map,也就是打第3个linkmapns_loaded.l_next.l_next.l_netx
- 这是部分的内容,只截取了我们需要的内容
- 伪造l_addr, fini_array->d_un.d_ptr 内容
- DT_FINI_ARRAY 为 26,DT_FINI_ARRAYSZ 为 28
- 因为源码可能比较抽象,不如直接打印出来,这里只截取有用的部分
1 | pwndbg> p **(struct link_map **) 0x7ffff7fbb188 |
伪造,堆地址 A
- l = l->real => A + 0x28 内容放着堆地址
0x28 = distance _rtld_global._dl_ns[0]._ns_loaded &_rtld_global._dl_ns[0]._ns_loaded.l_real
- l->l_init_called 不为0,数字随意,根据版本而异。
distance _rtld_global._dl_ns[0]._ns_loaded &_rtld_global._dl_ns[0]._ns_loaded.l_init_called
。我测的是0x312 map.l_info[26]
不为 0,distance _rtld_global._dl_ns[0]._ns_loaded &_rtld_global._dl_ns[0]._ns_loaded.l_info[26]
map.l_info[28]
+ 8 控制循环次数,一般写成1就行- 控制函数执行流
map->l_addr + fini_array->d_un.d_ptr
。也就是map->l_addr + map->l_info[26]->d_un.d_ptr
- fini_array
map.l_info[26]
偏移是0x110。那么28是0x120
1 | // l = l->real |
pwntools filepointer
其实看pwntools文档可以看出其中对 IO_FILE
也存在很多可以利用的点
1 | from pwnlib.filepointer import * |
- IO_FILE 结构体
_wide_data
就是我们现在常利用的点。- 改变成员也只是需要
fs.flags = 0x123
直接赋值 - 两个 unknown 变量填充结构体
1 | FileStructure(null=0xdeadbeef) |
- house of orange
- io_list_all 地址
- 伪造的 vtable 地址
1 | 0xdeadbeef) fileStr = FileStructure( |
- stdout leak
- 从 addr 泄露 size 大小的数据
1 | fileStr = FileStructure(0xdeadbeef) |
- packing,因为我们需要伪造file结构体,因此我们可以使用如下函数
1 | # 根据 context.arch打包, 类似 p32,p64 函数 |
测试
最好手动调试一下 largebin attack 和 house_of_banana。
house of banana
参考一下 house_of_banana源码分析这一篇文章的demo
- 注意改rtld相关指针和libc的偏移大小
makefile
1 | CC := gcc |
house of banana
- 伪造结构体 l_next 为 0
- l_init_called 一个比较神奇的数字,具体的libc打印
- ubuntu 22.04 LTS 测试一下,在gdb 下可以执行一个命令就会崩溃。
- 高版本libc 没有patch进行测试,但是根据源码可行(理论上)
1 |
|