WWW2Exec - atexit(), TLS Storage & Altri puntatori danneggiati

Reading time: 8 minutes

tip

Impara e pratica l'Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica l'Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)

Supporta HackTricks

__atexit Structures

caution

Oggigiorno è molto strano sfruttare questo!

atexit() è una funzione a cui altre funzioni vengono passate come parametri. Queste funzioni verranno eseguite quando si esegue un exit() o il ritorno del main.
Se puoi modificare l'indirizzo di una di queste funzioni per puntare a un shellcode, ad esempio, guadagnerai 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 è l'indirizzo delle funzioni, ma è crittografato 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 come input. Quindi queste architetture sarebbero attaccabili tramite questo vettore.

Puoi trovare una spiegazione approfondita su come funziona 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() verrà eseguito __run_exit_handlers() che chiamerà i distruttori registrati.

caution

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

Il codice importante è (source):

c
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 venga utilizzato per calcolare la posizione dell'array di funzioni da chiamare.

Ci sono un paio di opzioni:

  • Sovrascrivere il valore di map->l_addr per farlo puntare 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), per farle puntare a una struttura Elf64_Dyn contraffatta che farà nuovamente puntare array a una zona di memoria controllata dall'attaccante.
  • Questo writeup sovrascrive l_info[DT_FINI_ARRAY] con l'indirizzo di una memoria controllata in .bss contenente un falso fini_array. Questo falso array contiene prima un one gadget indirizzo che verrà eseguito e poi la differenza tra l'indirizzo di questo falso array e il valore di map->l_addr in modo che *array punti al falso array.
  • Secondo il post principale di questa tecnica e questo writeup 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 per esempio.

Seguendo il codice precedente puoi trovare un'altra sezione interessante con il codice:

c
/* 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) contraffatta. Trova ulteriori informazioni qui.

Sovrascrittura di dtor_list in __run_exit_handlers

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

Codice da _run_exit_handlers():

c
/* 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():

c
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, verrà demanglato il puntatore da cur->func e chiamato con l'argomento cur->obj.

Utilizzando la funzione tls da questo fork di GEF, è possibile vedere che in realtà la dtor_list è molto vicina al stack canary e al PTR_MANGLE cookie. Quindi, con un overflow su di essa sarebbe possibile sovrascrivere il cookie e lo stack canary.
Sovrascrivendo il PTR_MANGLE cookie, sarebbe possibile bypassare la funzione PTR_DEMANLE impostandola a 0x00, il che significa che il xor utilizzato per ottenere l'indirizzo reale è semplicemente l'indirizzo configurato. Poi, scrivendo sulla dtor_list è possibile collegare diverse funzioni con l'indirizzo della funzione e il suo argomento.

Infine, nota che il puntatore memorizzato non verrà solo xored con il cookie ma anche ruotato di 17 bit:

armasm
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 devi tenere conto di questo prima di aggiungere un nuovo indirizzo.

Trova un esempio nel post originale.

Altri puntatori danneggiati in __run_exit_handlers

Questa tecnica è spiegata qui e dipende ancora una volta dal programma che termina chiamando return o exit() così __run_exit_handlers() viene chiamato.

Controlliamo più codice di questa funzione:

c
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 in esecuzione gef> p initial.

Per abusare di questo è necessario leakare o cancellare il PTR_MANGLEcookie e poi sovrascrivere un'entrata cxa in initial con system('/bin/sh').
Puoi trovare un esempio di questo nel post originale del blog sulla tecnica.

tip

Impara e pratica l'Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica l'Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)

Supporta HackTricks