Code Execution by Faking IO_FILE->vtable in GLIBC 2.36 [0x0]
Since vtables were added to a specific read-only segment in GLIBC and IO_validate_vtable() will verify the vtable of IO_FILE structure, exploitation of IO_FILE->vtable is becoming much more complex than before. Although we have some great exploitation chains such as House of banana or House of apple, some of them require a series of complex structure construction. So I would like to put forward another way to exploit IO_FILE->vtable here which can enable the attacker to gain code execution by faking vtable directly.
vtable validation
When calling the method in vtable, GLIBC will call IO_validate_vtable first
when vtable isn’t located in __libc_IO_vtables, it will then invoke IO_vtable_check() to do further validation.
1 2 3 4 5 6 7 8 9 10 11 12
staticinlineconst 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; constchar *ptr = (constchar *) vtable; uintptr_t offset = ptr - __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) /* 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; }
vtable is actually kind of a implementation of polymorphism, so there may be a chance to modify the vtable pointer to perform custom IO operations. In IO_vtable_check()
/* 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; structlink_map *l; if (!rtld_active () || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0 && l->l_ns != LM_ID_BASE)) return; }
#else/* !SHARED */ /* We cannot perform vtable validation in the static dlopen case because FILE * handles might be passed back and forth across the boundary. Therefore, we disable checking in this case. */ if (__dlopen != NULL) return; #endif
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n"); }
We can notice that it does give us a chance to use another foreign vtable. Now the question becomes how can we bypass the checking here.
First, !rtlf_active() seems a great choice. But the value it returns is based on a value in read-only memory. Then what about _dl_addr (_IO_vtable_check, &di, &l, NULL) != 0? This line is performed to check the ld namespace of a given address. To bypass it we are required to construct a really complex linker-related structure. So let’s check the very first flag checking.
_IO_check_vtable will load IO_accept_foreign_vtables and compare it against with _IO_vtable_check and luckily IO_accept_foreign_vtables is writable! So the problem is how can we defeat the PTR_MANGLE stuff.
Defeat PTR_MAGLE
Pointer guard is used to proctect some critical pointers from being forged. We can use the following pseudocode to compute a PTR_MANGLEd pointer value.
1
rol(ptr ^ pointer_guard, 0x11)
to demangle we can just
1
ror(ptr, 0x11) ^ pointer_guard
Now the problem becomes how can we leak or overwrite the pointer_guard.
Pointer guard is located in TLS structure. Although we do have method to modify the value, but why not ROP directly when we have this kind of primitive. So let’s take a look at how to leak pointer guard.
We have two ways to do so. Directly leak them from TLS or leak a mangled pointer and then calculate the pointer_guard xor key.
Leaking from TLS requires us to leak TLS address first so we will focu on the second method here.
Fortunately we do have a mangled pointer! Consider the following code.
/* As a QoI issue we detect NULL early with an assertion instead of a SIGSEGV at program exit when the handler is run (bug 20544). */ assert (func != NULL);
__libc_lock_lock (__exit_funcs_lock); new = __new_exitfn (listp);
if (new == NULL) { __libc_lock_unlock (__exit_funcs_lock); return-1; }
So leaking _dl_fini in _exit_function means leaking xor key now. Then override IO_accepte_foreign_vtable with a mangled _IO_vtable_check pointer, we are able to use any vtable to completely take over the control flow.
for (int i = 0; i < 0x50-2; ++i) ((u_int64_t *)fake_vtable)[i+2] = &pwn;
return0;
}
Run the example with a simple script
1 2 3 4 5
Fatal error: glibc detected an invalid stdio handle Fatal error: glibc detected an invalid stdio handle Fatal error: glibc detected an invalid stdio handle pwn!!! pwn after 234 attempts
Conclusion
The offset between libc and ld is not a constant value so we need to bruteforce the _dl_fini here since it’s in ld.
With a ld leaking the exploitation can be more reliable. And I will show a much more reliable way to exploit in next chapter.