在看HCTF2018里面的一道叫babyprintf的题时候,本是一道很常规的题,解法也很多,但是我看出题师傅自己的解法的时候,把我难住了。他用了一种我一直忽略了的bypass IO_vtable_check
的方法。也是非常少见的一种方法,我是第一次看见,但是我感觉理论上应该是最好的一种方法。下面看我的曲折经历。
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 void attribute_hidden_IO_vtable_check (void ) { #ifdef SHARED void (*flag) (void ) = atomic_load_relaxed (&IO_accept_foreign_vtables); #ifdef PTR_DEMANGLE PTR_DEMANGLE (flag); #endif if (flag == &_IO_vtable_check) return ; { Dl_info di; struct link_map *l ; if (!rtld_active () || (_dl_addr (_IO_vtable_check, &di, &l, NULL ) != 0 && l->l_ns != LM_ID_BASE)) return ; } ... __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n" ); }
在这个地方如果vtable的位置不是在指定的范围内,就会进入IO_vtable_check
,在这里大多数有两种方法:
rtld_active() = NULL
利用的原有的vtable来,比如_IO_str_jumps
,控制流程不会触发_IO_vtable_check
至于进入_dl_addr
的利用可能流程上分析来有点略微麻烦,这里我们不考虑这个点。先看第一种方法,这里rtld_active()
实际上是_rtld_local_ro->_dl_init_all_dirs
,这里你动态调的时候你发现这个结构在不能写的段上,所以这个点是没法用的。再看第二种,用到是_IO_str_jumps
中的_IO_str_finish
1 2 3 4 5 6 7 8 9 10 void _IO_str_finish (FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) free (fp->_IO_buf_base); fp->_IO_buf_base = NULL ; _IO_default_finish (fp, 0 ); }
这里并不像libc-2.24中的那样,这里已经不存在可以写的函数指针了,直接用的是free
。所以这题的解法也是意料之中的。再往前看:
1 2 3 4 5 6 7 8 #ifdef SHARED void (*flag) (void ) = atomic_load_relaxed (&IO_accept_foreign_vtables); #ifdef PTR_DEMANGLE PTR_DEMANGLE (flag); #endif if (flag == &_IO_vtable_check) return ;
我们看这个地方,其实有很多文章中提到过这个点。先看具体的汇编内容
1 2 3 4 5 6 mov rax,QWORD PTR [rip+0x1354c6 ] ror rax,0x11 xor rax,QWORD PTR fs:0x30 lea rdx,[rip+0xffffffffffffffe5 ] cmp rax,rdx je 0x7ffff7e98fec <_IO_vtable_check+113 >
很多文章里面仅仅是提到过,但是都是以这里涉及到关于fs
的段寄存器的读写,最后下的结论都是很难利用。我第一次看见的也是这么认为的,所以我忽略了这种利用方式。在前面HCTF中babyprintf出题人的exp中,虽然exp里面写的不是很详细,一些偏移计算不太理解,但是我知道这还是一种bypass _IO_vtable_check
的方法。最好我把它锁定在上面汇编处,但是无法理解,exp出现了tls
的地址,并且加上了一段偏移就直接指向了fs:[30]
的地址上,这让我实在无法理解,遂去联系了出题人为什么会这样做,因为时间太长,他也无法解释得通,得到的回答是用了一个随便指向tls
的指针,如果想深究,他建议我去看glibc的源码。经过一段时间对glibc的探索,终于得知了个所以然。网上的资料比较少,过程较为曲折。
在第一面对这个问题时候,我首先想的是fs
段指向的是哪里?这个问题很简单能得知,在glibc用fs
寄存器当做pthead
的储存地址,即tls
的结构。所以这里我想的是,如果想知道fs
具体的指向,就要看第一次对fs
的使用。这是思路的开始。
在看exp中tls的地址是如何拿到的。文中tls地址值,在调试过程中发现是libc中GOT[1]的内容,后知后觉其实是link_map的地址。这里可以肯定是tls的位置和link_map的位置是连续的。
关于如何寻找对第一次对fs的使用,终于一篇文章给我了思路 https://unix.stackexchange.com/questions/453749/what-sets-fs0x28-stack-canary
1 arch_prctl(ARCH_SET_FS, 0x7fc189ed0740 ) = 0
这是strace的调试的内容,即在对fs指定地址的时候,实际上是使用arch_prctl
这个系统调用。上面文章中尽管讲的是stack_canary的东西,其实在这里是一样的,一个fs:[28h]
,一个是fs:[30h]
而已。canary用的是stack_guard
,这里用是pointer_guard
,再提一句,这样来看,同一线程里面所有的canary应该都是相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct { void *tcb; dtv_t *dtv; void *self; int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; ... } tcbhead_t ;
所以这里和上文一样在arch_prctl
系统调用这里下断。我们现在的问题就是在于libc中的link_map
和 tls
结构为什么是连续的。先来看第一次断在哪里,backtrace如下
1 2 3 4 5 6 #0 init_tls () at rtld.c:741 #1 0x00007ffff7fdaa86 in dl_main (phdr=<optimized out>, phnum=<optimized out>, user_entry=<optimized out>, auxv=<optimized out>) at rtld.c:1808 #2 0x00007ffff7fed3d8 in _dl_sysdep_start (start_argptr=start_argptr@entry=0x7fffffffe110, dl_main=dl_main@entry=0x7ffff7fd8439 <dl_main>) at ../elf/dl-sysdep.c:253 #3 0x00007ffff7fd8080 in _dl_start_final (arg=0x7fffffffe110) at rtld.c:415 #4 _dl_start (arg=0x7fffffffe110) at rtld.c:522 #5 0x00007ffff7fd7098 in _start () from /root/heap_libc_debug/lib/ld-2.28.so
在init_tls
里面
1 2 3 ... const char *lossage = TLS_INIT_TP (tcbp);...
现在找到了第一次对fs
的赋值的时候,但是并不知道tcbp
值是从何而来。接着我们再寻找tcbp
的值是从何而来。往前回溯。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void *tcbp = _dl_allocate_tls_storage ();void *_dl_allocate_tls_storage (void ) { void *result; size_t size = GL(dl_tls_static_size); ... size_t alignment = GL(dl_tls_static_align); void *allocated = malloc (size + alignment + sizeof (void *)); if (__glibc_unlikely (allocated == NULL )) return NULL ; ... void *aligned = (void *) roundup ((uintptr_t ) allocated, alignment); result = aligned + size - TLS_TCB_SIZE; memset (result, '\0' , TLS_TCB_SIZE); ... result = allocate_dtv (result); if (result == NULL ) free (allocated); return result; }
用的是malloc
来获取地址,这里的malloc
并不是堆管理那个指针,这里libc.so
都还没映射,是一个临时用来管理申请内存的弱类型函数。跟进malloc
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 void * weak_functionmalloc (size_t n){ if (alloc_end == 0 ) { extern int _end attribute_hidden; alloc_ptr = &_end; alloc_end = (void *) 0 + (((alloc_ptr - (void *) 0 ) + GLRO(dl_pagesize) - 1 ) & ~(GLRO(dl_pagesize) - 1 )); } ... if (alloc_ptr + n >= alloc_end || n >= -(uintptr_t ) alloc_ptr) { caddr_t page; size_t nup = (n + GLRO(dl_pagesize) - 1 ) & ~(GLRO(dl_pagesize) - 1 ); if (__glibc_unlikely (nup == 0 && n != 0 )) return NULL ; nup += GLRO(dl_pagesize); page = __mmap (0 , nup, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1 , 0 ); if (page == MAP_FAILED) return NULL ; if (page != alloc_end) alloc_ptr = page; alloc_end = page + nup; } alloc_last_block = (void *) alloc_ptr; alloc_ptr += n; return alloc_last_block; }
这里可以看到用来管理内存的是alloc_ptr
和alloc_end
这两个指针。有初始化和内存不足时的处理的另外两条处理分支,在分配tls
结构的过程中,发现内存是足够分配的,所以是直接分配的。那么这里我们需要去寻找在分配tls
结构以前是alloc_ptr
可能的指向,因为这一段内存都是连续的,如果说我们能找到前面某个在这段内存的结构指向的话,那么是可以计算出tls
的位置的。所以这里我们在malloc
这里下断。尽量要往前面找。
如何确定是不是属于tls
同一段内存上的,只需要确定alloc_end
是不是一样的。最终断在一个alloc_end
发生改变的malloc
上。backtrace如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #0 malloc (n=1191) at dl-minimal.c:69 #1 0x00007ffff7fedbac in calloc (nmemb=nmemb@entry=1191, size=size@entry=1) at dl-minimal.c:103 #2 0x00007ffff7fe0b44 in _dl_new_object (realname=realname@entry=0x7ffff7ffed90 "./libc-2.28.so", libname=<optimized out>, libname@entry=0x7fffffffce20 "./libc-2.28.so", type=type@entry=1, loader=loader@entry=0x7ffff7ffe190, mode=mode@entry=67108864, nsid=nsid@entry=0) at dl-object.c:73 #3 0x00007ffff7fdc84b in _dl_map_object_from_fd (name=name@entry=0x7fffffffce20 "./libc-2.28.so", origname=origname@entry=0x0, fd=fd@entry=3, fbp=fbp@entry=0x7fffffffc8d0, realname=0x7ffff7ffed90 "./libc-2.28.so", loader=loader@entry=0x7ffff7ffe190, l_type=1, mode=67108864, stack_endp=0x7fffffffc8c0, nsid=0) at dl-load.c:1001 #4 0x00007ffff7fde557 in _dl_map_object (loader=0x7ffff7ffe190, name=0x7fffffffce20 "./libc-2.28.so", type=1, trace_mode=trace_mode@entry=0, mode=67108864, nsid=nsid@entry=0) at dl-load.c:2466 #5 0x00007ffff7fd7309 in map_doit (a=a@entry=0x7fffffffcdd0) at rtld.c:592 #6 0x00007ffff7fee15a in _dl_catch_exception (exception=exception@entry=0x7fffffffcd80, operate=operate@entry=0x7ffff7fd72dd <map_doit>, args=args@entry=0x7fffffffcdd0) at dl-error-skeleton.c:196 #7 0x00007ffff7fee1bf in _dl_catch_error (objname=objname@entry=0x7fffffffcdf8, errstring=errstring@entry=0x7fffffffcdf0, mallocedp=mallocedp@entry=0x7fffffffcdcf, operate=operate@entry=0x7ffff7fd72dd <map_doit>, args=args@entry=0x7fffffffcdd0) at dl-error-skeleton.c:215 #8 0x00007ffff7fd729b in do_preload (fname=fname@entry=0x7fffffffce20 "./libc-2.28.so", main_map=main_map@entry=0x7ffff7ffe190, where=where@entry=0x7ffff7ff4ee4 "LD_PRELOAD") at rtld.c:763 #9 0x00007ffff7fd8421 in handle_ld_preload (preloadlist=<optimized out>, main_map=main_map@entry=0x7ffff7ffe190) at rtld.c:861 #10 0x00007ffff7fda7af in dl_main (phdr=<optimized out>, phnum=<optimized out>, user_entry=<optimized out>, auxv=<optimized out>) at rtld.c:1625 #11 0x00007ffff7fed3d8 in _dl_sysdep_start (start_argptr=start_argptr@entry=0x7fffffffe110, dl_main=dl_main@entry=0x7ffff7fd8439 <dl_main>) at ../elf/dl-sysdep.c:253 #12 0x00007ffff7fd8080 in _dl_start_final (arg=0x7fffffffe110) at rtld.c:415 #13 _dl_start (arg=0x7fffffffe110) at rtld.c:522 #14 0x00007ffff7fd7098 in _start () from /root/heap_libc_debug/lib/ld-2.28.so
会发现刚好是tls所在段刚好用mmap分配的时候。那么这个返回的地址是否可以在某一处应用到呢?往前回溯你会发现这是这个地方申请的内存恰好是link_map的结构。且刚好是在读取libc.so的之前,从mmap得到的内存段分布也可以看出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 struct link_map *_dl_new_object (char *realname , const char *libname , int type , struct link_map *loader , int mode , Lmid_t nsid ) { size_t libname_len = strlen (libname) + 1 ; struct link_map *new ; struct libname_list *newname ; #ifdef SHARED unsigned int naudit = GLRO(dl_naudit) ?: ((mode & __RTLD_OPENEXEC) ? DL_NNS : 0 ); size_t audit_space = naudit * sizeof (new ->l_audit[0 ]); #else # define audit_space 0 #endif new = (struct link_map *) calloc (sizeof (*new ) + audit_space + sizeof (struct link_map *) + sizeof (*newname) + libname_len, 1 );
确实是如此,这个地址的指向是libc.so中的link_map的指针,到这里也解决了为什么link_map和tls在地址上是连续的,他们处于同一个mmap申请0x2000的内存中,并且是固定偏移。所以这里exp中泄露并不是tls的地址,而是link_map的地址,通过计算得到tls的地址。所以这里只要我们确定了libc里面GOT[1]的值,就可以确定tls的结构,至于是否存在其他的对于这段内存的引用,有兴趣的同学,可以接着分析接下来的几个malloc,到分配tls的结构,这中间还是存在一些malloc的过程的。
至此,我们如果能控制fs:[30]的内容,并且控制IO_accept_foreign_vtables的值,那么是完全可以绕过_IO_vtable_check,而且应该是一种更通用的方法。这里在2.27和2.28都是测试通过的。有兴趣的同学可以试试。