15 KiB
WWW2Exec - .dtors & .fini_array
Learn AWS hacking from zero to hero with htARTE (HackTricks AWS Red Team Expert)!
Other ways to support HackTricks:
- If you want to see your company advertised in HackTricks or download HackTricks in PDF Check the SUBSCRIPTION PLANS!
- Get the official PEASS & HackTricks swag
- Discover The PEASS Family, our collection of exclusive NFTs
- Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
- Share your hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.
.dtors
{% hint style="danger" %} Nowadays is very weird to find a binary with a .dtors section! {% endhint %}
The destructors are functions that are executed before program finishes (after the main
function returns).
The addresses to these functions are stored inside the .dtors
section of the binary and therefore, if you manage to write the address to a shellcode in __DTOR_END__
, that will be executed before the programs ends.
Get the address of this section with:
objdump -s -j .dtors /exec
rabin -s /exec | grep “__DTOR”
Usually you will find the DTOR markers between the values ffffffff
and 00000000
. So if you just see those values, it means that there isn't any function registered. So overwrite the 00000000
with the address to the shellcode to execute it.
{% hint style="warning" %} Ofc, you first need to find a place to store the shellcode in order to later call it. {% endhint %}
.fini_array
Essentially this is a structure with functions that will be called before the program finishes, like .dtors
. This is interesting if you can call your shellcode just jumping to an address, or in cases where you need to go back to main
again to exploit the vulnerability a second time.
objdump -s -j .fini_array ./greeting
./greeting: file format elf32-i386
Contents of section .fini_array:
8049934 a0850408
#Put your address in 0x8049934
Note that when a function from the .fini_array
is executed it moves to the next one, so it won't be executed several time (preventing eternal loops), but also it'll only give you 1 execution of the function placed here.
Note that entries in .fini_array
are called in reverse order, so you probably wants to start writing from the last one.
Eternal loop
In order to abuse .fini_array
to get an eternal loop you can check what was done here: If you have at least 2 entries in .fini_array
, you can:
- Use your first write to call the vulnerable arbitrary write function again
- Then, calculate the return address in the stack stored by
__libc_csu_fini
(the function that it calling all the.fini_array
functions) and put there the address of__libc_csu_fini
- This will make
__libc_csu_fini
call himself again executing the.fini_array
functions again which will call the vulnerable WWW function 2 times: one for arbitrary write and another one to overwrite again the return address of__libc_csu_fini
on the stack to call itself again.
- This will make
{% hint style="danger" %}
Note that with Full RELRO, the section .fini_array
is made read-only.
{% endhint %}
link_map
As explained in this post, If the program exist using return
or exit()
it'll run __run_exit_handlers()
which will call registered destructors.
{% hint style="danger" %}
If the program exits via _exit()
function, it'll call the exit
syscall and the exit handlers will not be executed. So, to confirm __run_exit_handlers()
is executed you can set a breakpoint on it.
{% endhint %}
The important code is (source):
ElfW(Dyn) *fini_array = map->l_info[DT_FINI_ARRAY];
if (fini_array != NULL)
{
ElfW(Addr) *array = (ElfW(Addr) *) (map->l_addr + fini_array->d_un.d_ptr);
size_t sz = (map->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr)));
while (sz-- > 0)
((fini_t) array[sz]) ();
}
[...]
// This is the d_un structure
ptype l->l_info[DT_FINI_ARRAY]->d_un
type = union {
Elf64_Xword d_val; // address of function that will be called, we put our onegadget here
Elf64_Addr d_ptr; // offset from l->l_addr of our structure
}
Note how map -> l_addr + fini_array -> d_un.d_ptr
is used to calculate the position of the array of functions to call.
There are a couple of options:
- Overwrite the value of
map->l_addr
to make it point to a fakefini_array
with instructions to execute arbitrary code - Overwrite
l_info[DT_FINI_ARRAY]
andl_info[DT_FINI_ARRAYSZ]
entries (which are more or less consecutive in memory) , to make them points to a forgedElf64_Dyn
structure that will make againarray
points to a memory zone the attacker controlled.- This writeup overwrites
l_info[DT_FINI_ARRAY]
with the address of a controlled memory in.bss
containing a fakefini_array
. This fake array contains first a one gadget address which will be executed and then the difference between in the address of this fake array and the value ofmap->l_addr
so*array
will point to the fake array. - According to main post of this technique and this writeup ld.so leave a pointer on the stack that points to the binary
link_map
in ld.so. With an arbitrary write it's possible to overwrite it and make it point to a fakefini_array
controlled by the attacker with the address to a one gadget for example.
- This writeup overwrites
Following the previous code you can find another interesting section with the code:
/* Next try the old-style destructor. */
ElfW(Dyn) *fini = map->l_info[DT_FINI];
if (fini != NULL)
DL_CALL_DT_FINI (map, ((void *) map->l_addr + fini->d_un.d_ptr));
}
In this case it would be possible to overwrite the value of map->l_info[DT_FINI]
pointing to a forged ElfW(Dyn)
structure. Find more information here.
TLS-Storage dtor_list overwrite in __run_exit_handlers
As explained here, if a program exits via return
or exit()
, it'll execute __run_exit_handlers()
which will call any destructors function registered.
Code from _run_exit_handlers()
:
/* Call all functions registered with `atexit' and `on_exit',
in the reverse of the order in which they were registered
perform stdio cleanup, and terminate program execution with STATUS. */
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit, bool run_dtors)
{
/* First, call the TLS destructors. */
#ifndef SHARED
if (&__call_tls_dtors != NULL)
#endif
if (run_dtors)
__call_tls_dtors ();
Code from __call_tls_dtors()
:
typedef void (*dtor_func) (void *);
struct dtor_list //struct added
{
dtor_func func;
void *obj;
struct link_map *map;
struct dtor_list *next;
};
[...]
/* Call the destructors. This is called either when a thread returns from the
initial function or when the process exits via the exit function. */
void
__call_tls_dtors (void)
{
while (tls_dtor_list) // parse the dtor_list chained structures
{
struct dtor_list *cur = tls_dtor_list; // cur point to tls-storage dtor_list
dtor_func func = cur->func;
PTR_DEMANGLE (func); // demangle the function ptr
tls_dtor_list = tls_dtor_list->next; // next dtor_list structure
func (cur->obj);
[...]
}
}
For each registered function in tls_dtor_list
, it'll demangle the pointer from cur->func
and call it with the argument cur->obj
.
Using the tls
function from this fork of GEF, it's possible to see that actually the dtor_list
is very close to the stack canary and PTR_MANGLE cookie. So, with an overflow on it's it would be possible to overwrite the cookie and the stack canary.
Overwriting the PTR_MANGLE cookie, it would be possible to bypass the PTR_DEMANLE
function as setting it to 0x00, will mean that the xor
used to get the real address is just the address configured. Then, writing on the dtor_list
it's possible chain several functions with the function address and it's argument.
Finally notice that the stored pointer is not only going to be xored with the cookie but also rotated 17 bits:
0x00007fc390444dd4 <+36>: mov rax,QWORD PTR [rbx] --> mangled ptr
0x00007fc390444dd7 <+39>: ror rax,0x11 --> rotate of 17 bits
0x00007fc390444ddb <+43>: xor rax,QWORD PTR fs:0x30 --> xor with PTR_MANGLE
So you need to take this into account before adding a new address.
Find an example in the original post.
Other mangled pointers in __run_exit_handlers
This technique is explained here and depends again on the program exiting calling return
or exit()
so __run_exit_handlers()
is called.
Let's check more code of this function:
while (true)
{
struct exit_function_list *cur;
restart:
cur = *listp;
if (cur == NULL)
{
/* Exit processing complete. We will not allow any more
atexit/on_exit registrations. */
__exit_funcs_done = true;
break;
}
while (cur->idx > 0)
{
struct exit_function *const f = &cur->fns[--cur->idx];
const uint64_t new_exitfn_called = __new_exitfn_called;
switch (f->flavor)
{
void (*atfct) (void);
void (*onfct) (int status, void *arg);
void (*cxafct) (void *arg, int status);
void *arg;
case ef_free:
case ef_us:
break;
case ef_on:
onfct = f->func.on.fn;
arg = f->func.on.arg;
PTR_DEMANGLE (onfct);
/* Unlock the list while we call a foreign function. */
__libc_lock_unlock (__exit_funcs_lock);
onfct (status, arg);
__libc_lock_lock (__exit_funcs_lock);
break;
case ef_at:
atfct = f->func.at;
PTR_DEMANGLE (atfct);
/* Unlock the list while we call a foreign function. */
__libc_lock_unlock (__exit_funcs_lock);
atfct ();
__libc_lock_lock (__exit_funcs_lock);
break;
case ef_cxa:
/* To avoid dlclose/exit race calling cxafct twice (BZ 22180),
we must mark this function as ef_free. */
f->flavor = ef_free;
cxafct = f->func.cxa.fn;
arg = f->func.cxa.arg;
PTR_DEMANGLE (cxafct);
/* Unlock the list while we call a foreign function. */
__libc_lock_unlock (__exit_funcs_lock);
cxafct (arg, status);
__libc_lock_lock (__exit_funcs_lock);
break;
}
if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called))
/* The last exit function, or another thread, has registered
more exit functions. Start the loop over. */
goto restart;
}
*listp = cur->next;
if (*listp != NULL)
/* Don't free the last element in the chain, this is the statically
allocate element. */
free (cur);
}
__libc_lock_unlock (__exit_funcs_lock);
The variable f
points to the initial
structure and depending on the value of f->flavor
different functions will be called.
Depending on the value, the address of the function to call will be in a different place, but it'll always be demangled.
Moreover, in the options ef_on
and ef_cxa
it's also possible to control an argument.
It's possible to check the initial
structure in a debugging session with GEF running gef> p initial
.
To abuse this you need either to leak or erase the PTR_MANGLE
cookie and then overwrite a cxa
entry in initial with system('/bin/sh')
.
You can find an example of this in the original blog post about the technique.
Learn AWS hacking from zero to hero with htARTE (HackTricks AWS Red Team Expert)!
Other ways to support HackTricks:
- If you want to see your company advertised in HackTricks or download HackTricks in PDF Check the SUBSCRIPTION PLANS!
- Get the official PEASS & HackTricks swag
- Discover The PEASS Family, our collection of exclusive NFTs
- Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
- Share your hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.