WWW2Exec - atexit()

Sostieni HackTricks

Strutture __atexit

Oggi è molto strano sfruttare questo!

atexit() è una funzione a cui vengono passate altre funzioni come parametri. Queste funzioni verranno eseguite quando si esegue un exit() o il ritorno del main. Se riesci a modificare l'indirizzo di una di queste funzioni in modo che punti a un codice shell ad esempio, otterrai il controllo del processo, ma attualmente è più complicato. Attualmente gli indirizzi delle funzioni da eseguire sono nascosti dietro diverse strutture e infine l'indirizzo a cui puntano non sono gli indirizzi delle funzioni, ma sono criptati con XOR e spostamenti con una chiave casuale. Quindi attualmente questo vettore di attacco non è molto utile almeno su x86 e x64_86. La funzione di crittografia è PTR_MANGLE. Altre architetture come m68k, mips32, mips64, aarch64, arm, hppa... non implementano la funzione di crittografia perché restituisce lo stesso che ha ricevuto in input. Quindi queste architetture sarebbero attaccabili da questo vettore.

Puoi trovare una spiegazione dettagliata su come funziona questo in https://m101.github.io/binholic/2017/05/20/notes-on-abusing-exit-handlers.html

Come spiegato in questo post, Se il programma esce usando return o exit() eseguirà __run_exit_handlers() che chiamerà i distruttori registrati.

Se il programma esce tramite la funzione _exit(), chiamerà la syscall di exit e i gestori di uscita non verranno eseguiti. Quindi, per confermare che __run_exit_handlers() viene eseguito, è possibile impostare un breakpoint su di esso.

Il codice importante è (fonte):

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 come map -> l_addr + fini_array -> d_un.d_ptr viene utilizzato per calcolare la posizione dell'array di funzioni da chiamare.

Ci sono un paio di opzioni:

  • Sovrascrivere il valore di map->l_addr in modo che punti a un falso fini_array con istruzioni per eseguire codice arbitrario

  • Sovrascrivere le voci l_info[DT_FINI_ARRAY] e l_info[DT_FINI_ARRAYSZ] (che sono più o meno consecutive in memoria), in modo che puntino a una struttura Elf64_Dyn falsificata che farà di nuovo sì che array punti a una zona di memoria controllata dall'attaccante.

  • Questa spiegazione sovrascrive l_info[DT_FINI_ARRAY] con l'indirizzo di una memoria controllata in .bss contenente un falso fini_array. Questo array falso contiene prima un indirizzo one gadget che verrà eseguito e poi la differenza tra l'indirizzo di questo array falso e il valore di map->l_addr in modo che *array punti all'array falso.

  • Secondo il post principale di questa tecnica e questa spiegazione ld.so lascia un puntatore nello stack che punta al link_map binario in ld.so. Con una scrittura arbitraria è possibile sovrascriverlo e farlo puntare a un falso fini_array controllato dall'attaccante con l'indirizzo di un one gadget ad esempio.

Segue il codice precedente un'altra sezione interessante con il codice:

/* 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 questo caso sarebbe possibile sovrascrivere il valore di map->l_info[DT_FINI] puntando a una struttura ElfW(Dyn) forgiata. Trova ulteriori informazioni qui.

Sovrascrittura di TLS-Storage dtor_list in __run_exit_handlers

Come spiegato qui, se un programma esce tramite return o exit(), eseguirà __run_exit_handlers() che chiamerà qualsiasi funzione di distruttore registrata.

Codice da _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 ();

Codice da __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);
[...]
}
}

Per ogni funzione registrata in tls_dtor_list, demanglerà il puntatore da cur->func e lo chiamerà con l'argomento cur->obj.

Utilizzando la funzione tls da questo fork di GEF, è possibile vedere che effettivamente la dtor_list è molto vicina al canary dello stack e al cookie PTR_MANGLE. Quindi, con un overflow su di esso sarebbe possibile sovrascrivere il cookie e il canary dello stack. Sovrascrivendo il cookie PTR_MANGLE, sarebbe possibile aggirare la funzione PTR_DEMANLE impostandola su 0x00, ciò significherebbe che lo xor usato per ottenere l'indirizzo reale è proprio l'indirizzo configurato. Quindi, scrivendo sulla dtor_list è possibile concatenare diverse funzioni con l'indirizzo della funzione e il suo argomento.

Infine notare che il puntatore memorizzato non solo verrà sottoposto a xor con il cookie ma verrà anche ruotato di 17 bit:

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

Quindi è necessario tenere conto di questo prima di aggiungere un nuovo indirizzo.

Trova un esempio nel post originale.

Altri puntatori modificati in __run_exit_handlers

Questa tecnica è spiegata qui e dipende nuovamente dal programma che esce chiamando return o exit() quindi viene chiamato __run_exit_handlers().

Controlliamo più codice di questa funzione:

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 variabile f punta alla struttura initial e a seconda del valore di f->flavor verranno chiamate diverse funzioni. A seconda del valore, l'indirizzo della funzione da chiamare sarà in un posto diverso, ma sarà sempre demangled.

Inoltre, nelle opzioni ef_on e ef_cxa è anche possibile controllare un argomento.

È possibile controllare la struttura initial in una sessione di debug con GEF eseguendo gef> p initial.

Per sfruttare ciò è necessario leak o cancellare il cookie PTR_MANGLE e sovrascrivere una voce cxa in initial con system('/bin/sh'). È possibile trovare un esempio di ciò nel post originale del blog sulla tecnica.

Last updated