io_vtable_check-of-pwn

在看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
/* 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

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
/* 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;

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

1
2
3
4
5
6
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

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; /* 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如下

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_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如下

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
/* 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都是测试通过的。有兴趣的同学可以试试。