WWW2Exec - atexit()

Wesprzyj HackTricks

Struktury __atexit

Obecnie jest bardzo dziwne, aby to wykorzystać!

atexit() to funkcja, do której inne funkcje są przekazywane jako parametry. Te funkcje zostaną wykonane podczas wykonywania exit() lub powrotu z main. Jeśli możesz zmodyfikować adres którejkolwiek z tych funkcji, aby wskazywał na shellcode na przykład, zdobędziesz kontrolę nad procesem, ale obecnie jest to bardziej skomplikowane. Obecnie adresy funkcji do wykonania są ukryte za kilkoma strukturami, a ostatecznie adres, do którego wskazują, nie są adresami funkcji, ale są zaszyfrowane za pomocą XOR i przesunięć z losowym kluczem. Obecnie ten wektor ataku nie jest zbyt przydatny przynajmniej na x86 i x64_86. Funkcja szyfrowania to PTR_MANGLE. Inne architektury takie jak m68k, mips32, mips64, aarch64, arm, hppa... nie implementują funkcji szyfrowania, ponieważ zwracają to samo, co otrzymały jako wejście. Dlatego te architektury byłyby podatne na ten wektor ataku.

Możesz znaleźć dogłębne wyjaśnienie, jak to działa na https://m101.github.io/binholic/2017/05/20/notes-on-abusing-exit-handlers.html

Jak wyjaśniono w tym poście, Jeśli program kończy działanie za pomocą return lub exit(), uruchomi __run_exit_handlers(), który wywoła zarejestrowane destruktory.

Jeśli program kończy działanie za pomocą funkcji _exit(), wywoła exit syscall i obsługiwacze wyjścia nie zostaną wykonane. Dlatego, aby potwierdzić wykonanie __run_exit_handlers(), można ustawić punkt przerwania na to.

Ważny kod to (źródło):

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
}

Zauważ, jak map -> l_addr + fini_array -> d_un.d_ptr jest używane do obliczenia pozycji tablicy funkcji do wywołania.

Istnieje kilka opcji:

  • Nadpisz wartość map->l_addr, aby wskazywała na fałszywy fini_array z instrukcjami do wykonania arbitralnego kodu

  • Nadpisz wpisy l_info[DT_FINI_ARRAY] i l_info[DT_FINI_ARRAYSZ] (które są mniej więcej kolejne w pamięci), aby wskazywały na sfałszowaną strukturę Elf64_Dyn, która ponownie sprawi, że array wskazuje na obszar pamięci kontrolowany przez atakującego.

  • Ten opis nadpisuje l_info[DT_FINI_ARRAY] adresem kontrolowanej pamięci w .bss, zawierającym fałszywy fini_array. Ten fałszywy array zawiera najpierw adres one gadget, który zostanie wykonany, a następnie różnicę między adresem tego fałszywego arraya a wartością map->l_addr, aby *array wskazywał na fałszywy array.

  • Zgodnie z głównym postem tej techniki i tym opisem ld.so pozostawia wskaźnik na stosie, który wskazuje na link_map binarnego w ld.so. Dzięki arbitralnemu zapisowi możliwe jest nadpisanie go i spowodowanie, że wskazuje on na fałszywy fini_array kontrolowany przez atakującego z adresem one gadget na przykład.

Po poprzednim kodzie znajdziesz kolejny interesujący fragment kodu:

/* 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));
}

W tym przypadku możliwe byłoby nadpisanie wartości map->l_info[DT_FINI], wskazującej na sfałszowaną strukturę ElfW(Dyn). Znajdź więcej informacji tutaj.

Nadpisanie listy dtor_list w pamięci TLS w __run_exit_handlers

Jak wyjaśniono tutaj, jeśli program kończy działanie za pomocą return lub exit(), zostanie wykonane __run_exit_handlers(), które wywoła zarejestrowane funkcje destrukcyjne.

Kod z _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 ();

Kod z __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);
[...]
}
}

Dla każdej zarejestrowanej funkcji w tls_dtor_list, zdemontuje wskaźnik z cur->func i wywoła go z argumentem cur->obj.

Korzystając z funkcji tls z tego forka GEF, można zobaczyć, że dtor_list jest bardzo blisko stack canary i PTR_MANGLE cookie. Dlatego, przy przepełnieniu go, możliwe byłoby nadpisanie cookie i stack canary. Poprzez nadpisanie PTR_MANGLE cookie, możliwe byłoby obejście funkcji PTR_DEMANLE poprzez ustawienie go na 0x00, co oznaczałoby, że xor użyty do uzyskania rzeczywistego adresu to po prostu skonfigurowany adres. Następnie, poprzez zapisanie w dtor_list, możliwe jest łańczenie kilku funkcji z adresem funkcji i jej argumentem.

Na koniec zauważ, że przechowywany wskaźnik nie tylko będzie xorowany z cookie, ale także obracany o 17 bitów:

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

Więc musisz wziąć to pod uwagę przed dodaniem nowego adresu.

Znajdź przykład w oryginalnym poście.

Inne zmienione wskaźniki w __run_exit_handlers

Ta technika jest wyjaśniona tutaj i zależy ponownie od programu zakończającego działanie za pomocą return lub exit(), więc zostaje wywołane __run_exit_handlers().

Sprawdźmy więcej kodu tej funkcji:

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

Zmienna f wskazuje na strukturę initial i w zależności od wartości f->flavor zostaną wywołane różne funkcje. W zależności od wartości, adres funkcji do wywołania będzie w innym miejscu, ale zawsze będzie rozwiązany.

Ponadto, w opcjach ef_on i ef_cxa można również kontrolować argument.

Możliwe jest sprawdzenie struktury initial w sesji debugowania z uruchomionym GEF za pomocą gef> p initial.

Aby to wykorzystać, należy albo ujawnić lub usunąć ciasteczko PTR_MANGLE a następnie nadpisać wpis cxa w initial na system('/bin/sh'). Przykład tego można znaleźć w oryginalnym wpisie na blogu dotyczącym tej techniki.

Last updated