bypass _IO_vtable_check 之写 fs:[30]

IO_vtable_check bypass

在看HCTF2018里面的一道叫babyprintf的题时候,本是一道很常规的题,解法也很多,但是我看出题师傅自己的解法的时候,把我难住了。他用了一种我一直忽略了的bypass IO_vtable_check的方法。也是非常少见的一种方法,我是第一次看见,但是我感觉理论上应该是最好的一种方法。下面看我的曲折经历。

void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
  /* Honor the compatibility flag.  */
  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,在这里大多数有两种方法:

  1. rtld_active() == NULL
  2. 利用的原有的vtable来,比如_IO_str_jumps,控制流程不会触发_IO_vtable_check

至于进入_dl_addr的利用可能流程上分析来有点略微麻烦,这里我们不考虑这个点。先看第一种方法,这里rtld_active()实际上是_rtld_local_ro->_dl_init_all_dirs,这里你动态调的时候你发现这个结构在不能写的段上,所以这个点是没法用的。再看第二种,用到是_IO_str_jumps中的_IO_str_finish

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。所以这题的解法也是意料之中的。再往前看:

#ifdef SHARED
  /* Honor the compatibility flag.  */
  void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (flag);
#endif
  if (flag == &_IO_vtable_check)
    return;

我们看这个地方,其实有很多文章中提到过这个点。先看具体的汇编内容

mov    rax,QWORD PTR [rip+0x1354c6]        # 0x7ffff7fce448 <IO_accept_foreign_vtables>
ror    rax,0x11
xor    rax,QWORD PTR fs:0x30
lea    rdx,[rip+0xffffffffffffffe5]        # 0x7ffff7e98f7b <_IO_vtable_check>
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

arch_prctl(ARCH_SET_FS, 0x7fc189ed0740) = 0

这是strace的调试的内容,即在对fs指定地址的时候,实际上是使用arch_prctl这个系统调用。上面文章中尽管讲的是stack_canary的东西,其实在这里是一样的,一个fs:[28h],一个是fs:[30h]而已。canary用的是stack_guard,这里用是pointer_guard,再提一句,这样来看,同一线程里面所有的canary应该都是相同。

typedef struct
{
  void *tcb;		/* Pointer to the TCB.  Not necessarily the
			   thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;		/* Pointer to the thread descriptor.  */
  int multiple_threads;
  int gscope_flag;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  uintptr_t pointer_guard;
  ...
} tcbhead_t;

所以这里和上文一样在arch_prctl系统调用这里下断。我们现在的问题就是在于libc中的link_maptls结构为什么是连续的。先来看第一次断在哪里,backtrace如下

#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 ([email protected]=0x7fffffffe110, [email protected]=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里面

...
const char *lossage = TLS_INIT_TP (tcbp);
...

现在找到了第一次对fs的赋值的时候,但是并不知道tcbp值是从何而来。接着我们再寻找tcbp的值是从何而来。往前回溯。

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

void * weak_function
malloc (size_t n)
{
  if (alloc_end == 0)
    {
      /* Consume any unused space in the last page of our data segment.  */
      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_ptralloc_end这两个指针。有初始化和内存不足时的处理的另外两条处理分支,在分配tls结构的过程中,发现内存是足够分配的,所以是直接分配的。那么这里我们需要去寻找在分配tls结构以前alloc_ptr可能的指向,因为这一段内存都是连续的,如果说我们能找到前面某个在这段内存的结构指向的话,那么是可以计算出tls的位置的。所以这里我们在malloc这里下断。尽量要往前面找。

如何确定是不是属于tls同一段内存上的,只需要确定alloc_end是不是一样的。最终断在一个alloc_end发生改变的malloc上。backtrace如下

#0  malloc (n=1191) at dl-minimal.c:69
#1  0x00007ffff7fedbac in calloc ([email protected]=1191, [email protected]=1) at dl-minimal.c:103
#2  0x00007ffff7fe0b44 in _dl_new_object ([email protected]=0x7ffff7ffed90 "./libc-2.28.so", libname=<optimized out>, [email protected]=0x7fffffffce20 "./libc-2.28.so", [email protected]=1, [email protected]=0x7ffff7ffe190, [email protected]=67108864, [email protected]=0) at dl-object.c:73
#3  0x00007ffff7fdc84b in _dl_map_object_from_fd ([email protected]=0x7fffffffce20 "./libc-2.28.so", [email protected]=0x0, [email protected]=3, [email protected]=0x7fffffffc8d0, realname=0x7ffff7ffed90 "./libc-2.28.so", [email protected]=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, [email protected]=0, mode=67108864, [email protected]=0) at dl-load.c:2466
#5  0x00007ffff7fd7309 in map_doit ([email protected]=0x7fffffffcdd0) at rtld.c:592
#6  0x00007ffff7fee15a in _dl_catch_exception ([email protected]=0x7fffffffcd80, [email protected]=0x7ffff7fd72dd <map_doit>, [email protected]=0x7fffffffcdd0) at dl-error-skeleton.c:196
#7  0x00007ffff7fee1bf in _dl_catch_error ([email protected]=0x7fffffffcdf8, [email protected]=0x7fffffffcdf0, [email protected]=0x7fffffffcdcf, [email protected]=0x7ffff7fd72dd <map_doit>, [email protected]=0x7fffffffcdd0) at dl-error-skeleton.c:215
#8  0x00007ffff7fd729b in do_preload ([email protected]=0x7fffffffce20 "./libc-2.28.so", [email protected]=0x7ffff7ffe190, [email protected]=0x7ffff7ff4ee4 "LD_PRELOAD") at rtld.c:763
#9  0x00007ffff7fd8421 in handle_ld_preload (preloadlist=<optimized out>, [email protected]=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 ([email protected]=0x7fffffffe110, [email protected]=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得到的内存段分布也可以看出来。

/* Allocate a `struct link_map' for a new object being loaded,
   and enter it into the _dl_loaded list.  */
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
  /* We create the map for the executable before we know whether we have
     auditing libraries and if yes, how many.  Assume the worst.  */
  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都是测试通过的。有兴趣的同学可以试试。

2 Likes

不容易,那么忙还有时间写文章:+1:

其实我写了快有10篇文章了,都存着呢!

1 Like

:+1::+1::+1: