-
-
[原创]无路远征——GLIBC2.37后时代的IO攻击之道(零)
-
发表于: 2023-2-4 17:13 21016
-
昨天,突然有一个师傅突然提醒我GLIBC-2.37已经发布,就重新看了一下,瞬间感觉背后发凉,largebin
没有什么修改,IO修改很大,针对性强的一逼,以前一些利用方式已无法使用,比较庆幸的是还有有一些新的利用方式可以继续挖掘。而且我认为GNU下一步会按2.37的思路不断修改IO,以前很多方法都会渐渐失效,在此我也决定将几个2.37以后失效的攻击链公布出来。本篇的内容主要是开篇,简单介绍一下IO,老手就可以无视。
既然是IO操作,那么就有必要知道计算机到底是怎么读写硬盘或者其他设备的,此处主要以块设备为主。以硬盘为例,要想和硬盘交互,说白了也是和硬盘上的芯片+系统交互,在x86的体系下,其实就是和硬盘上的寄存器进行交互,为了让这个过程变的简单,硬盘上的寄存器被映射成了端口,硬盘的端口说明如下表。而能够交互端口的操作就是汇编指令中的in out
,这些指令是特权指令,三环无法使用,像我们的常说的底层read
或write
函数其实也是靠操作系统执行in,out
指令来实现外部设备读写的。具体的执行细节不是我们的重点,不再细写。需要知道的是,对于LBA硬盘来说,读写数据都必须一块一块的读,这也就是常说的块设备。
对于LBA硬盘来说,读写数据都必须一块一块的读,如果我们每次执行read,write
时都是操作很少的数据,则对系统消耗非常大,因此,C库就想了一个好办法——缓冲区。所以,就比较好理解了,缓冲区是为了减少3坏操作外部硬件时的消耗产生的,一切都是以外部硬件为服务对象。
1.从外部硬件读取时。为了减少消耗,会一次从外部硬件读取一“块”数据,并放入缓冲区,然后当target
需要时,再从头部慢慢读取,只到读完才再次从硬件读取。这个缓冲区叫输入缓冲区。
2.向外部硬件写入时。为了减少消耗,不会一有东西就写入,而是先将内容从source
写入缓冲区,当缓冲区满了时候再将内存一起写入硬件。这个缓冲区叫输出缓冲区。
为了更好的定位,对每个操作我们肯定至少要有3个基础数据。首先,以从外部硬件读取为例,我们要有输入缓冲区开始(base)、结尾(end)和当前(ptr)已经用了多少的指针。很明显当ptr == end
时,说明输入缓冲区里的东西已经全部读完,需要重新从硬件读入。
同样,对于向外部硬件写入为例,我们要有输出缓冲区开始(base)、结尾(end)和当前(ptr)已经写了多少的指针。很明显当ptr == end
时,说明输出缓冲区已经写满,可以向硬件写入了。
上面的内容看似非常清楚,但这里其实有一些比较容易混乱的地方。因为缓冲区内存储的是数据,输入、输出两者数据流动方向不同,但保护主体都一样,都是外部设备,所以有用的数据部分就有所不相同。
虽然,输入、输出缓冲区作用不同,但原理上都是一块内存。一块外部设备可能既可以写入也可以读取,为了节省空间,我们可以定义一块缓冲区,需要输入的时候就做输入缓冲区,需要输出就做输出缓冲区。那么我们就有了8个指针。
那么到现在,基本思路理清了,其他就方便了.
从文件中读取 程序是从fd
中读取一批数据到缓冲区中(_IO_buf_base
至 _IO_buf_end
),_IO_read_ptr
指向已向target
中写完的位置,既 _IO_read_ptr
至 _IO_read_end
为还没有写入target
中的数据。当_IO_read_ptr == _IO_read_end
时,说明输入缓冲区内已经没有可用数据,需要再次从文件中读入数据。
向文件输出 程序是先将source
中的数据写入到缓冲区中,_IO_write_ptr
指向已从source
中写到的位置,既 _IO_write_ptr
至 _IO_write_pend
为还剩余的空间。当_IO_write_ptr == _IO_buf_end
时,再全部写入fd
中。
既然有了数据结构,我们就可以简单定义一些操作来进行操作。
这个逻辑前面已经说的非常清楚,简单逻辑如下。
同理,操作逻辑如下。
上面的操作中,我们还忘了一个基本的问题:缓冲区从哪里来?其实缓冲区就是一块内存,可以在栈上、堆上、libc
中,甚至随便mmap
一块内存都可以,但不论怎么来,我们都需要这样一块区域,在此,我们借用glibc
中在malloc
的方法来申请缓冲区。那么我们还需要第三个操作。
这个操作非常简单。
申请一块缓冲区,并设置_IO_buf_base
为开头,_IO_buf_end
为结尾。
到此为止,IO的所有基本操作就已经算是完成了。当然,操作中还需要一些安全检测,例如判断缓冲区是否存在、malloc
是否成功等内容,这里就不再赘述。下面,我以glibc
中_IO_file_jumps
为例,梳理一下函数操作的意图。
说明顺序根据_IO_file_jumps
中的操作顺序来,因为里面的互相调用还是挺多的,就不说具体写过程,主要说明操作的意图。
这个看名字就非常简单,是文件结束的操作,所以它的操作如下
这个函数意图比较简单,主要是处理当输出缓冲区用完时,向硬盘写入数据。当然,其实这个函数内部非常复杂,加入了一些检测。例如,如果缓冲区不存在则要初始化缓冲区。并且,这个函数的参数中有一个标志位。
这个函数与_IO_new_file_overflow
差不多,主要是用于从硬盘中读取数据,每次读取都是_IO_buf_base 至 _IO_buf_end
。为了防止硬盘中没有这么多数据,设置_IO_read_end
为读取的总数。如果,缓冲区不存在则要初始化缓冲区。程序返回_IO_read_ptr
指针。
这个函数就是调用_IO_new_file_underflow
,并简单做了一些检测。
设置存储的函数,暂不重要。
这个函数是主要目的是将数据从source
放入输出输出缓冲区。显然,放入过程中还有几种情况。
说明:我们平时的输出函数主要就是调用此函数。
这个函数是主要目的是将数据从输入缓冲区放入target
。显然放入过程中还有几种情况。
说明:我们平时的输入函数主要就是调用此函数。
设置偏移函数,就是设置我们所说的ptr
指针。
就是调用_IO_new_file_seekoff
。
这个函数也比较简单,看名字就知道是设置缓冲区的,作用就是初始化各个缓冲区
同步函数,负责与硬盘和缓冲区之间进行同步。
这个就是申请缓冲区的函数,申请完之后还要把输入、输出缓冲区初始化。
这个是输入的最终函数,它将syscall_read
进行了一定的封装。
这个是输出的最终函数,它将syscall_write
进行了一定的封装。
调用__lseek64
。
就和名字一样,关闭文件。
获取文件描述符的状态。调用__fxstat64
。
此函数没用,返回-1。
此函数没用。
清空缓冲区,将输出缓冲区清空。
_IO_setg
_IO_setp
等等
我认为这是IO里面最让人头疼的地方,它的初始化形式使用大量宏来操作,为了说明问题,我专门找了一个不常用虚表(wfile
)。
其中,带default
的都是共用的函数,大都在genops.c
里面;new_file
和file
的大都在fileops.c
里面;wdefault
是宽字符共用的函数,大都在wgenops.c
里面;只有wfile
的才是自己单独定义的函数,在wfileops.c
里面。从上面可以看出wfile
单独定义的操作只有5个。
通过源码可以看出,_IO_FILE
结构体经过了很多次的完善。
在调试中可以看到全部信息。
可以看出 fflush
函数在参数为空时,清空(_IO_flush_all_lockp => _IO_OVERFLOW
)全部文件;不为空时,同步(sync
)指定文件,两种情况执行步骤不同。
FSOP执行是靠_IO_flush_all_lockp
,该函数的功能是刷新所有FILE结构体的输出缓冲区,执行这个程序的时候会沿着fp->chain
执行overflow
程序。
_IO_flush_all_lockp
调用函数的时机包括:
首先是abort
函数的流程,利用的double free
漏洞触发,栈回溯为:
exit 函数,栈回溯为:
程序正常退出,栈回溯为:
从上面可以看出,很多函数并没有用,但为什么还要设置这些呢?以下是我的猜想。
对这些清楚了之后,我们就可以看看其他的house
到底是干什么的了。
虚表检测是2.24之后加入的内容,IO_validate_vtable
检测如果虚表超出范围就进入_IO_vtable_check
函数。各路大神找到的house
很多都不是打file
的跳表,而是其他处理跳表,但都差不太多。简要梳理如下。
虚表位置判断主要在IO_validate_vtable
函数,2.37以前判断区间为_IO_helper_jumps - _IO_str_jumps
之间的区域 0xd60,里面有以下虚表。
在IO_validate_vtable
函数检查如果虚表超出范围,会进入_IO_vtable_check
函数,
这里就很有意思,也就是说GNU其实也同意你能够外部重构vtable
,只是要满足一定条件。那么我们还是可以绕过虚表检测的。
但无论如何我们都需要有ld文件。
check_stdfiles_vtables
函数是设置外置虚表的函数,如果能执行这个函数,也可以绕过虚表检测。
将宽字符函数调用单独拿出来主要是因为,目前(2.36及以前,2.37也没有修订)宽字符跳表的引用没有加入保护,house_of_apple house_of_cat
都是利用这一点。
以2.36为例,目前,涉及到宽字符跳转的函数一共有19个,也就是说跳表中的都定义了。
但实际上有引用的仅为以下4个
其中,_IO_WSETBUF
仅用在_IO_setbuffer
中,也就是我们经常用的setbuf
歌舞为谁演、拨珠为谁算,执戟儿郎为谁站,流血为谁干。
苏女庆生平,卫商秦问鼎,八千江东子弟兵,明总建大清。
歌舞为谁演、拨珠为谁算,执戟儿郎为谁站,流血为谁干。
苏女庆生平,卫商秦问鼎,八千江东子弟兵,明总建大清。
I/O地址 | 读(主机从硬盘读数据) | 写(主机数据写入硬盘) |
---|---|---|
1F0H | 数据寄存器 | 数据寄存器 |
1F1H | 错误寄存器(只读寄存器) | 特征寄存器 |
1F2H | 扇区计数寄存器 | 扇区计数寄存器 |
1F3H | 扇区号寄存器或 LBA 块地址 0~7 | 扇区号或 LBA 块地址 0~7 |
1F4H | 磁道数低 8 位或 LBA 块地址 8~15 | 磁道数低 8 位或 LBA 块地址 8~15 |
1F5H | 磁道数高 8 位或 LBA 块地址 16~23 | 磁道数高 8 位或 LBA 块地址 16~23 |
1F6H | 驱动器/磁头或 LBA 块地址 24~27 | 驱动器/磁头或 LBA 块地址 24~27 |
1F7H | 命令寄存器或状态寄存器 | 命令寄存器 |
char
*
_IO_buf_base;
/
/
缓冲区的基地址
char
*
_IO_buf_end;
/
/
缓冲区的结束地址
char
*
_IO_read_base;
/
/
输入缓冲区基地址
char
*
_IO_read_ptr;
/
/
输入当前位置
char
*
_IO_read_end;
/
/
输入缓冲区结尾地址
char
*
_IO_write_base;
/
/
输出缓冲区基地址
char
*
_IO_write_ptr;
/
/
输出当前位置
char
*
_IO_write_end;
/
/
输出缓冲区结尾地址
char
*
_IO_buf_base;
/
/
缓冲区的基地址
char
*
_IO_buf_end;
/
/
缓冲区的结束地址
char
*
_IO_read_base;
/
/
输入缓冲区基地址
char
*
_IO_read_ptr;
/
/
输入当前位置
char
*
_IO_read_end;
/
/
输入缓冲区结尾地址
char
*
_IO_write_base;
/
/
输出缓冲区基地址
char
*
_IO_write_ptr;
/
/
输出当前位置
char
*
_IO_write_end;
/
/
输出缓冲区结尾地址
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000
const struct _IO_jump_t _IO_wfile_jumps libio_vtable
=
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_new_file_finish),
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
JUMP_INIT(xsputn, _IO_wfile_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_wfile_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
JUMP_INIT(doallocate, _IO_wfile_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
libc_hidden_data_def (_IO_wfile_jumps)
const struct _IO_jump_t _IO_wfile_jumps libio_vtable
=
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_new_file_finish),
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
JUMP_INIT(xsputn, _IO_wfile_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_wfile_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
JUMP_INIT(doallocate, _IO_wfile_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
libc_hidden_data_def (_IO_wfile_jumps)
struct _IO_FILE
{
int
_flags;
/
*
High
-
order word
is
_IO_MAGIC; rest
is
flags.
*
/
/
*
The following pointers correspond to the C
+
+
streambuf protocol.
*
/
char
*
_IO_read_ptr;
/
*
Current read pointer
*
/
char
*
_IO_read_end;
/
*
End of get area.
*
/
char
*
_IO_read_base;
/
*
Start of putback
+
get area.
*
/
char
*
_IO_write_base;
/
*
Start of put area.
*
/
char
*
_IO_write_ptr;
/
*
Current put pointer.
*
/
char
*
_IO_write_end;
/
*
End of put area.
*
/
char
*
_IO_buf_base;
/
*
Start of reserve area.
*
/
char
*
_IO_buf_end;
/
*
End of reserve area.
*
/
/
*
The following fields are used to support backing up
and
undo.
*
/
char
*
_IO_save_base;
/
*
Pointer to start of non
-
current get area.
*
/
char
*
_IO_backup_base;
/
*
Pointer to first valid character of backup area
*
/
char
*
_IO_save_end;
/
*
Pointer to end of non
-
current get area.
*
/
struct _IO_marker
*
_markers;
struct _IO_FILE
*
_chain;
int
_fileno;
int
_flags2;
__off_t _old_offset;
/
*
This used to be _offset but it's too small.
*
/
/
*
1
+
column number of pbase();
0
is
unknown.
*
/
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[
1
];
_IO_lock_t
*
_lock;
#ifdef _IO_USE_OLD_IO_FILE // 可以看出如果使用旧的 _IO_FILE ,那我们经常说的IO就是 _IO_FILE_complete
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
__off64_t _offset;
/
*
Wide character stream stuff.
*
/
struct _IO_codecvt
*
_codecvt;
struct _IO_wide_data
*
_wide_data;
struct _IO_FILE
*
_freeres_list;
void
*
_freeres_buf;
size_t __pad5;
int
_mode;
/
*
Make sure we don't get into trouble again.
*
/
char _unused2[
15
*
sizeof (
int
)
-
4
*
sizeof (void
*
)
-
sizeof (size_t)];
};
struct _IO_FILE_complete_plus
{
struct _IO_FILE_complete
file
;
const struct _IO_jump_t
*
vtable;
};
struct _IO_FILE
{
int
_flags;
/
*
High
-
order word
is
_IO_MAGIC; rest
is
flags.
*
/
/
*
The following pointers correspond to the C
+
+
streambuf protocol.
*
/
char
*
_IO_read_ptr;
/
*
Current read pointer
*
/
char
*
_IO_read_end;
/
*
End of get area.
*
/
char
*
_IO_read_base;
/
*
Start of putback
+
get area.
*
/
char
*
_IO_write_base;
/
*
Start of put area.
*
/
char
*
_IO_write_ptr;
/
*
Current put pointer.
*
/
char
*
_IO_write_end;
/
*
End of put area.
*
/
char
*
_IO_buf_base;
/
*
Start of reserve area.
*
/
char
*
_IO_buf_end;
/
*
End of reserve area.
*
/
/
*
The following fields are used to support backing up
and
undo.
*
/
char
*
_IO_save_base;
/
*
Pointer to start of non
-
current get area.
*
/
char
*
_IO_backup_base;
/
*
Pointer to first valid character of backup area
*
/
char
*
_IO_save_end;
/
*
Pointer to end of non
-
current get area.
*
/
struct _IO_marker
*
_markers;
struct _IO_FILE
*
_chain;
int
_fileno;
int
_flags2;
__off_t _old_offset;
/
*
This used to be _offset but it's too small.
*
/
/
*
1
+
column number of pbase();
0
is
unknown.
*
/
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[
1
];
_IO_lock_t
*
_lock;
#ifdef _IO_USE_OLD_IO_FILE // 可以看出如果使用旧的 _IO_FILE ,那我们经常说的IO就是 _IO_FILE_complete
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
__off64_t _offset;
/
*
Wide character stream stuff.
*
/
struct _IO_codecvt
*
_codecvt;
struct _IO_wide_data
*
_wide_data;
struct _IO_FILE
*
_freeres_list;
void
*
_freeres_buf;
size_t __pad5;
int
_mode;
/
*
Make sure we don't get into trouble again.
*
/
char _unused2[
15
*
sizeof (
int
)
-
4
*
sizeof (void
*
)
-
sizeof (size_t)];
};
struct _IO_FILE_complete_plus
{
struct _IO_FILE_complete
file
;
const struct _IO_jump_t
*
vtable;
};
pwndbg> ptype stdout
type
=
struct _IO_FILE {
int
_flags;
char
*
_IO_read_ptr;
char
*
_IO_read_end;
char
*
_IO_read_base;
char
*
_IO_write_base;
# 本质上是通过修改这个结构题泄露
char
*
_IO_write_ptr;
# 这两个指针地址之间的内容
char
*
_IO_write_end;
# 输出内容的结尾
char
*
_IO_buf_base;
# 缓冲区的基地址
char
*
_IO_buf_end;
# 缓冲区的结束地址
char
*
_IO_save_base;
char
*
_IO_backup_base;
char
*
_IO_save_end;
struct _IO_marker
*
_markers;
struct _IO_FILE
*
_chain;
int
_fileno;
int
_flags2;
__off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[
1
];
_IO_lock_t
*
_lock;
__off64_t _offset;
struct _IO_codecvt
*
_codecvt;
struct _IO_wide_data
*
_wide_data;
struct _IO_FILE
*
_freeres_list;
void
*
_freeres_buf;
size_t __pad5;
int
_mode;
char _unused2[
20
];
}
*
pwndbg> ptype stdout
type
=
struct _IO_FILE {
int
_flags;
char
*
_IO_read_ptr;
char
*
_IO_read_end;
char
*
_IO_read_base;
char
*
_IO_write_base;
# 本质上是通过修改这个结构题泄露
char
*
_IO_write_ptr;
# 这两个指针地址之间的内容
char
*
_IO_write_end;
# 输出内容的结尾
char
*
_IO_buf_base;
# 缓冲区的基地址
char
*
_IO_buf_end;
# 缓冲区的结束地址
char
*
_IO_save_base;
char
*
_IO_backup_base;
char
*
_IO_save_end;
struct _IO_marker
*
_markers;
struct _IO_FILE
*
_chain;
int
_fileno;
int
_flags2;
__off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[
1
];
_IO_lock_t
*
_lock;
__off64_t _offset;
struct _IO_codecvt
*
_codecvt;
struct _IO_wide_data
*
_wide_data;
struct _IO_FILE
*
_freeres_list;
void
*
_freeres_buf;
size_t __pad5;
int
_mode;
char _unused2[
20
];
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
赞赏
- [原创]反序列化的前生今世 9081
- [原创]gdb在逆向爆破中的应用 3037
- [原创]EOP编程 8860
- [原创]格式化字符串打出没有回头路(下)——回头望月 45079
- [原创]格式化字符串打出没有回头路(上) 16527