samedi 20 mai 2017

Today we'll talk about abusing exit handlers in order to hijack the control flow.

This research stemmed from Google Project Zero article about heap overflow
NULL byte poisoning where they described using __exit_funcs or tls_dtor_list
to achieve code execution.
The issue I had was to find a way to resolve reliably these
non-exported symbols and access them.

The exit handlers are quite interesting as it is an easy version to do ROP
as they all take one parameter.
Functions such as setuid(), system() or other functions needing 1 parameter
can thus be easily called.

Pointer mangling is a mitigation implemented in order to thwart
direct function pointer corruption.
I'll show in this post how it can be bypassed.

We'll first analyze the code leading to the execution of these exit handlers
and then show how to trigger them.
There will be a lot of pasted listing ahead, these will be explained as we go.

Where is the code leading to executing these exit handlers?

About exit ()

Whenever we call libc exit(), it calls all the handlers we registered
with atexit() and on_exit() before calling the _exit() syscall.

We can see that "__run_exit_handlers()" does use pointer demangling by using
PTR_DEMANGLE() before dereferencing the function pointers and calling
the pointed code.
We will thus need to analyze how the mangling and demangling is done in order
to bypass it.

We first see that it tries to call "__call_tls_dtors()", this is interesting
as this called function is used to call destructors in tls_dtor_list,
we'll come back to it.

Each handler can have 5 flavors : ef_free, ef_us, ef_on, ef_at and ef_cxa.
Depending on the flavor of the exit handler, we'll have a function pointer,
argument and/or dso handle.
The function list can store at most 32 handlers and a linked list is created
if more is needed.
idx is the total number of functions and is 1-based (not 0-based as usually).

And our PTR_MANGLE() and PTR_DEMANGLE() definitions in "sysdeps/unix/sysv/linux/x86_64/sysdep.h".

About Thread Control Block

Like we saw in PTR_MANGLE() and PTR_DEMANGLE(), it all has to do with
the structure "tcbhead_t".
This structure is what's stored at FS, which correspond to the per thread data
(TCB probably for Thread Control Block).

So at fs:0x30 we get the pointer_guard.

It's the pointer guard as defined in "sysdeps/x86_64/nptl/tls.h" in the
structure "tcbhead_t".

We could go look the code at "_dl_setup_pointer_guard()" but research was not
done there.

We still need to determine where we can hit and overwrite these handlers.
Let's start with __exit_funcs.

About atexit() and finding __exit_funcs

The "atexit()" code is located in "cxa_atexit.c"

/* Register a function to be called by exit or when a shared library
is unloaded. This function is only called from code generated by
the C++ compiler. */
int
__cxa_atexit (void (*func) (void *), void *arg, void *d)
{
return __internal_atexit (func, arg, d, &__exit_funcs);
}
libc_hidden_def (__cxa_atexit)

What's interesting is "__exit_funcs" being used.
"__exit_funcs" is an un-exported function but we can resolve it by disassembling
that piece of assembly with capstone and retrieving the needed VA.
"__cxa_atexit()" is an exported symbol so we can retrieve the VA easily using
pwntools.elf.ELF.
You can see at VA 0x3a28a that it calculates the address of "__exit_funcs".

You can see at VA 0x3a5c6 that it dereferences the pointer to tls_dtor_list.
So we can disassemble that function and find that offset using capstone.
"__call_tls_dtors" is exported so the address can be easily parsed out
using pwntools.elf.ELF.

I didn't write code for it but the idea is the same as for __exit_funcs,
this is left as an exercise to the reader.

Bypassing pointer mangling

While playing with a binary challenge, I happened to see that _dl_fini()
is often registered in the __exit_funcs array, so we can recalculate
the pointer_guard value and thus bypass pointer mangling.

The issue with "_dl_fini()" is that it seems to be an un-exported symbol.
I've found the address while digging in gdb.
An elf parser probably has to be written to find "_dl_fini()" address.

A vulnerability that allows you to leak an encoded pointer in __exit_funcs
is also necessary.
Here we use _dl_fini encoded pointer.

The formula to compute the pointer_guard assuming that "_dl_fini()"
is used is as follow:

ptr_guard = ror (ptr_encoded, 0x11, 64) ^ _dl_fini

Here the code you've been waiting for. We re-use "get_exit_funcs()" that
was showed earlier.

Other (untested) ideas to get the pointer_guard?

There probably is another way to get that pointer_guard given you've got
an arbitrary infoleak. This may be possible through a pointer corruption
or a UAF or Type Confusion or something else.
If the attacker somehow manage to find where 'struct tcbhead' is located
in memory, he may be able to just read the value out of it.

Last idea is probably far fetched but let's look at it.
Let's say you got an oracle : crash or not crash and that your process
is respawned through a fork().
You could probably use techniques similar as those used for blind rop
to guess the pointer guard.
More research can be done there but we don't need it for now.

About glibc ptmalloc hooks

It may come a time where you somehow can't manage to exit a program running
as it may run in a infinite loop for example.

In order to use our previous technique, the process has to call
the libc exit() function.
This happens when the process prepare to exit.

We may be able to trigger that function before reaching the end of the program
by using glibc ptmalloc hooks.
In each glibc ptmalloc functions, there is a function pointer that is called
given it's not NULL.
By over-writing one of these hooks with glibc exit() function
and triggering the corresponding malloc(), free() or realloc() call,
we'll trigger the execution of our payload written in __exit_funcs.

These functions hook are all exported symbols that you can easily get with
pwntools.elf.ELF : __free_hook, __malloc_hook, __realloc_hook and __memalign_hook.

Conclusion

Full mitigations bypass is still possible nowadays on the latest
Linux distribution given the proper vulnerabilities and binary. Every technique
is applicable on a case-by-case basis.
Pointer mangling was implemented in order to make destructors corruption
exploitation harder, but as can be seen it's not impossible.

This technique is particularly useful when you don't know where the stack is
and you have full RELRO activated.
It allows you to do an easy version of ROP.