WWW2Exec - .dtors & .fini_array

Impara l'hacking AWS da zero a eroe con htARTE (Esperto Red Team AWS di HackTricks)!

Altri modi per supportare HackTricks:

.dtors

Oggi è molto strano trovare un binario con una sezione .dtors!

I distruttori sono funzioni che vengono eseguite prima che il programma finisca (dopo che la funzione main ritorna). Gli indirizzi di queste funzioni sono memorizzati all'interno della sezione .dtors del binario e quindi, se riesci a scrivere l'indirizzo di un shellcode in __DTOR_END__, questo verrà eseguito prima che il programma finisca.

Ottieni l'indirizzo di questa sezione con:

objdump -s -j .dtors /exec
rabin -s /exec | grep “__DTOR”

Di solito troverai i marcatori DTOR tra i valori ffffffff e 00000000. Quindi se vedi solo quei valori, significa che non c'è alcuna funzione registrata. Quindi sovrascrivi il 00000000 con l'indirizzo dello shellcode per eseguirlo.

Naturalmente, prima devi trovare un posto dove memorizzare lo shellcode per poterlo chiamare in seguito.

.fini_array

Essenzialmente si tratta di una struttura con funzioni che verranno chiamate prima che il programma finisca, come .dtors. Questo è interessante se puoi chiamare il tuo shellcode saltando a un indirizzo, o nei casi in cui devi tornare di nuovo a main per sfruttare la vulnerabilità una seconda volta.

objdump -s -j .fini_array ./greeting

./greeting:     file format elf32-i386

Contents of section .fini_array:
8049934 a0850408

#Put your address in 0x8049934

Nota che quando una funzione dall'.fini_array viene eseguita, passa alla successiva, quindi non verrà eseguita più volte (prevenendo loop eterni), ma ti darà solo un'esecuzione della funzione posizionata qui.

Nota che le voci in .fini_array vengono chiamate in ordine inverso, quindi probabilmente vorrai iniziare a scrivere dall'ultima.

Loop eterno

Per abusare di .fini_array per ottenere un loop eterno puoi controllare cosa è stato fatto qui: Se hai almeno 2 voci in .fini_array, puoi:

  • Utilizzare il tuo primo write per richiamare di nuovo la funzione di scrittura arbitraria vulnerabile

  • Quindi, calcolare l'indirizzo di ritorno nello stack memorizzato da __libc_csu_fini (la funzione che chiama tutte le funzioni di .fini_array) e mettere lì l'indirizzo di __libc_csu_fini

  • Questo farà sì che __libc_csu_fini si richiami nuovamente eseguendo di nuovo le funzioni di .fini_array che richiameranno la funzione WWW vulnerabile 2 volte: una per la scrittura arbitraria e un'altra per sovrascrivere di nuovo l'indirizzo di ritorno di __libc_csu_fini nello stack per richiamarsi di nuovo.

Nota che con Full RELRO, la sezione .fini_array viene resa sola lettura.

Come spiegato in questo post, se il programma esce utilizzando return o exit() verrà eseguito __run_exit_handlers() che chiamerà i distruttori registrati.

Se il programma esce tramite la funzione _exit(), verrà chiamata la syscall di exit e gli handler di uscita non verranno eseguiti. Quindi, per confermare che __run_exit_handlers() viene eseguito, puoi 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.

  • Questo articolo 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 questo articolo 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 dtor_list di TLS-Storage 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 il dtor_list è molto vicino 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 bypassare la funzione PTR_DEMANLE impostandola su 0x00, ciò significherebbe che lo xor utilizzato per ottenere l'indirizzo reale è proprio l'indirizzo configurato. Quindi, scrivendo sul 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.

Impara l'hacking AWS da zero a esperto con htARTE (HackTricks AWS Red Team Expert)!

Altri modi per supportare HackTricks:

Last updated