WWW2Exec - atexit()

Unterstützen Sie HackTricks

__atexit-Strukturen

Heutzutage ist es sehr seltsam, dies auszunutzen!

atexit() ist eine Funktion, der andere Funktionen als Parameter übergeben werden. Diese Funktionen werden ausgeführt, wenn ein exit() ausgeführt wird oder das Hauptprogramm beendet wird. Wenn Sie die Adresse einer dieser Funktionen so ändern können, dass sie beispielsweise auf einen Shellcode zeigt, erhalten Sie die Kontrolle über den Prozess, aber dies ist derzeit komplizierter. Derzeit sind die Adressen der auszuführenden Funktionen hinter mehreren Strukturen versteckt und schließlich sind die Adressen, auf die sie zeigen, nicht die Adressen der Funktionen, sondern sind verschlüsselt mit XOR und Verschiebungen mit einem zufälligen Schlüssel. Daher ist dieser Angriffsvektor derzeit nicht sehr nützlich, zumindest auf x86 und x64_86. Die Verschlüsselungsfunktion ist PTR_MANGLE. Andere Architekturen wie m68k, mips32, mips64, aarch64, arm, hppa... implementieren die Verschlüsselungsfunktion nicht, weil sie das gleiche zurückgeben wie sie als Eingabe erhalten haben. Daher wären diese Architekturen durch diesen Vektor angreifbar.

Eine ausführliche Erklärung, wie dies funktioniert, finden Sie unter https://m101.github.io/binholic/2017/05/20/notes-on-abusing-exit-handlers.html

Wie in diesem Beitrag erklärt, Wenn das Programm mit return oder exit() beendet wird, wird __run_exit_handlers() ausgeführt, das registrierte Destruktoren aufruft.

Wenn das Programm über die Funktion _exit() beendet wird, wird der exit-Syscall aufgerufen und die Exit-Handler werden nicht ausgeführt. Um zu bestätigen, dass __run_exit_handlers() ausgeführt wird, können Sie einen Breakpoint darauf setzen.

Der wichtige Code ist (Quelle):

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
}

Beachten Sie, wie map -> l_addr + fini_array -> d_un.d_ptr verwendet wird, um die Position des Arrays von aufzurufenden Funktionen zu berechnen.

Es gibt ein paar Optionen:

  • Überschreiben Sie den Wert von map->l_addr, um ihn auf ein gefälschtes fini_array mit Anweisungen zur Ausführung beliebigen Codes zu zeigen.

  • Überschreiben Sie die Einträge l_info[DT_FINI_ARRAY] und l_info[DT_FINI_ARRAYSZ] (die im Speicher mehr oder weniger aufeinander folgen), um sie auf eine gefälschte Elf64_Dyn-Struktur zeigen zu lassen, die erneut dazu führt, dass array auf einen vom Angreifer kontrollierten Speicherbereich zeigt.

  • Dieser Bericht überschreibt l_info[DT_FINI_ARRAY] mit der Adresse eines vom Angreifer kontrollierten Speichers im .bss, der ein gefälschtes fini_array enthält. Dieses gefälschte Array enthält zuerst eine One-Gadget-Adresse, die ausgeführt wird, und dann die Differenz zwischen der Adresse dieses gefälschten Arrays und dem Wert von map->l_addr, sodass *array auf das gefälschte Array zeigt.

  • Laut Hauptbeitrag dieser Technik und diesem Bericht hinterlässt ld.so einen Zeiger auf dem Stapel, der auf den binären link_map in ld.so zeigt. Mit einem beliebigen Schreibvorgang ist es möglich, ihn zu überschreiben und auf ein vom Angreifer kontrolliertes gefälschtes fini_array zeigen zu lassen, das die Adresse zu einem One-Gadget beispielsweise enthält.

Nach dem vorherigen Code finden Sie einen weiteren interessanten Abschnitt mit dem 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 diesem Fall wäre es möglich, den Wert von map->l_info[DT_FINI] zu überschreiben, der auf eine gefälschte ElfW(Dyn)-Struktur zeigt. Finde hier weitere Informationen.

Überschreiben der TLS-Speicher dtor_list in __run_exit_handlers

Wie hier erklärt, wenn ein Programm über return oder exit() beendet wird, wird es __run_exit_handlers() ausführen, das alle registrierten Destruktoren aufrufen wird.

Code aus _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 von __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);
[...]
}
}

Für jede registrierte Funktion in tls_dtor_list wird der Zeiger aus cur->func demangled und mit dem Argument cur->obj aufgerufen.

Mit der tls Funktion aus diesem Fork von GEF ist es möglich zu sehen, dass tatsächlich die dtor_list sehr nah am Stack-Canary und am PTR_MANGLE-Cookie liegt. Daher wäre es bei einem Überlauf möglich, das Cookie und den Stack-Canary zu überschreiben. Durch das Überschreiben des PTR_MANGLE-Cookies wäre es möglich, die PTR_DEMANLE-Funktion zu umgehen, indem sie auf 0x00 gesetzt wird, was bedeutet, dass das xor, das verwendet wird, um die tatsächliche Adresse zu erhalten, einfach die konfigurierte Adresse ist. Dann ist es durch Schreiben in die dtor_list möglich, mehrere Funktionen mit der Funktions-Adresse und ihrem Argument zu verketten.

Schließlich ist zu beachten, dass der gespeicherte Zeiger nicht nur mit dem Cookie xor-verknüpft wird, sondern auch um 17 Bits rotiert wird:

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 müssen Sie dies berücksichtigen, bevor Sie eine neue Adresse hinzufügen.

Finden Sie ein Beispiel im Originalbeitrag.

Andere veränderte Zeiger in __run_exit_handlers

Diese Technik wird hier erklärt und hängt erneut davon ab, dass das Programm beendet wird, indem return oder exit() aufgerufen wird, sodass __run_exit_handlers() aufgerufen wird.

Lassen Sie uns mehr Code dieser Funktion überprüfen:

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);

Die Variable f zeigt auf die initial Struktur und je nach Wert von f->flavor werden unterschiedliche Funktionen aufgerufen. Abhängig vom Wert befindet sich die Adresse der aufzurufenden Funktion an einem anderen Ort, aber sie wird immer entmangelt sein.

Darüber hinaus ist es in den Optionen ef_on und ef_cxa auch möglich, ein Argument zu steuern.

Es ist möglich, die initial Struktur in einer Debugging-Sitzung mit GEF auszuchecken, indem man gef> p initial ausführt.

Um dies auszunutzen, müssen Sie entweder das PTR_MANGLE-Cookie leaken oder löschen und dann einen cxa-Eintrag in initial mit system('/bin/sh') überschreiben. Ein Beispiel hierfür finden Sie im ursprünglichen Blog-Beitrag über die Technik.

Last updated