WWW2Exec - atexit(), Almacenamiento TLS y Otros Punteros Dañados
Reading time: 9 minutes
tip
Aprende y practica AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a HackTricks y HackTricks Cloud repos de github.
__atexit Estructuras
caution
Hoy en día es muy raro explotar esto!
atexit()
es una función a la que se le pasan otras funciones como parámetros. Estas funciones serán ejecutadas al ejecutar un exit()
o el retorno del main.
Si puedes modificar la dirección de cualquiera de estas funciones para que apunte a un shellcode, por ejemplo, ganarás control del proceso, pero esto es actualmente más complicado.
Actualmente, las direcciones a las funciones que se van a ejecutar están ocultas detrás de varias estructuras y, finalmente, la dirección a la que apuntan no son las direcciones de las funciones, sino que están encriptadas con XOR y desplazamientos con una clave aleatoria. Así que actualmente este vector de ataque no es muy útil al menos en x86 y x64_86.
La función de encriptación es PTR_MANGLE
. Otras arquitecturas como m68k, mips32, mips64, aarch64, arm, hppa... no implementan la función de encriptación porque devuelve lo mismo que recibió como entrada. Así que estas arquitecturas serían atacables por este vector.
Puedes encontrar una explicación detallada sobre cómo funciona esto en https://m101.github.io/binholic/2017/05/20/notes-on-abusing-exit-handlers.html
link_map
Como se explicó en esta publicación, si el programa sale usando return
o exit()
, ejecutará __run_exit_handlers()
que llamará a los destructores registrados.
caution
Si el programa sale a través de la función _exit()
, llamará a la exit
syscall y los manejadores de salida no se ejecutarán. Así que, para confirmar que __run_exit_handlers()
se ejecuta, puedes establecer un punto de interrupción en él.
El código importante es (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
}
Nota cómo map -> l_addr + fini_array -> d_un.d_ptr
se utiliza para calcular la posición del array de funciones a llamar.
Hay un par de opciones:
- Sobrescribir el valor de
map->l_addr
para que apunte a un falsofini_array
con instrucciones para ejecutar código arbitrario. - Sobrescribir las entradas
l_info[DT_FINI_ARRAY]
yl_info[DT_FINI_ARRAYSZ]
(que son más o menos consecutivas en memoria), para hacer que apunten a una estructuraElf64_Dyn
forjada que hará que nuevamentearray
apunte a una zona de memoria controlada por el atacante. - Este informe sobrescribe
l_info[DT_FINI_ARRAY]
con la dirección de una memoria controlada en.bss
que contiene un falsofini_array
. Este array falso contiene primero un one gadget dirección que se ejecutará y luego la diferencia entre la dirección de este array falso y el valor demap->l_addr
para que*array
apunte al array falso. - Según la publicación principal de esta técnica y este informe, ld.so deja un puntero en la pila que apunta al
link_map
binario en ld.so. Con una escritura arbitraria es posible sobrescribirlo y hacer que apunte a un falsofini_array
controlado por el atacante con la dirección a un one gadget por ejemplo.
Siguiendo el código anterior, puedes encontrar otra sección interesante con el código:
/* 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));
}
En este caso, sería posible sobrescribir el valor de map->l_info[DT_FINI]
apuntando a una estructura ElfW(Dyn)
forjada. Encuentra más información aquí.
Sobrescritura de dtor_list de TLS-Storage en __run_exit_handlers
Como se explica aquí, si un programa sale a través de return
o exit()
, ejecutará __run_exit_handlers()
que llamará a cualquier función destructora registrada.
Código de _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 ();
Código de __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);
[...]
}
}
Para cada función registrada en tls_dtor_list
, se descompone el puntero de cur->func
y se llama con el argumento cur->obj
.
Usando la función tls
de este fork de GEF, es posible ver que en realidad la dtor_list
está muy cerca del stack canary y la cookie PTR_MANGLE. Así, con un desbordamiento en ella sería posible sobrescribir la cookie y el stack canary.
Sobrescribiendo la cookie PTR_MANGLE, sería posible eludir la función PTR_DEMANLE
configurándola a 0x00, lo que significará que el xor
utilizado para obtener la dirección real es solo la dirección configurada. Luego, al escribir en la dtor_list
es posible encadenar varias funciones con la dirección de la función y su argumento.
Finalmente, nota que el puntero almacenado no solo se va a xorear con la cookie, sino que también se rotará 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
Así que necesitas tener esto en cuenta antes de agregar una nueva dirección.
Encuentra un ejemplo en el post original.
Otros punteros dañados en __run_exit_handlers
Esta técnica está explicada aquí y depende nuevamente de que el programa salga llamando a return
o exit()
para que se llame a __run_exit_handlers()
.
Veamos más código de esta función:
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);
La variable f
apunta a la estructura initial
y dependiendo del valor de f->flavor
se llamarán diferentes funciones.
Dependiendo del valor, la dirección de la función a llamar estará en un lugar diferente, pero siempre estará demangled.
Además, en las opciones ef_on
y ef_cxa
también es posible controlar un argumento.
Es posible verificar la estructura initial
en una sesión de depuración con GEF ejecutando gef> p initial
.
Para abusar de esto, necesitas leak o borrar la cookie PTR_MANGLE
y luego sobrescribir una entrada cxa
en initial con system('/bin/sh')
.
Puedes encontrar un ejemplo de esto en el blog original sobre la técnica.
tip
Aprende y practica AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a HackTricks y HackTricks Cloud repos de github.