WWW2Exec - .dtors & .fini_array

Naucz się hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!

Inne sposoby wsparcia HackTricks:

.dtors

Obecnie jest bardzo dziwne znalezienie binarnego pliku z sekcją .dtors!

Destruktory to funkcje, które są wykonywane przed zakończeniem programu (po zakończeniu działania funkcji main). Adresy tych funkcji są przechowywane wewnątrz sekcji .dtors binarnego pliku, dlatego jeśli uda ci się zapisać adres do shellcode w __DTOR_END__, to zostanie wykonany przed zakończeniem programu.

Pobierz adres tej sekcji za pomocą:

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

Zazwyczaj znajdziesz znaczniki DTOR pomiędzy wartościami ffffffff i 00000000. Jeśli widzisz tylko te wartości, oznacza to, że nie ma zarejestrowanej żadnej funkcji. Nadpisz 00000000 adresem shellcode, aby go uruchomić.

Oczywiście najpierw musisz znaleźć miejsce do przechowywania shellcode, aby później móc go wywołać.

.fini_array

W zasadzie jest to struktura zawierająca funkcje, które zostaną wywołane przed zakończeniem programu, podobnie jak .dtors. Jest to interesujące, jeśli możesz wywołać swój shellcode, skacząc do adresu, lub w przypadkach, gdy musisz wrócić do main ponownie, aby wykorzystać podatność po raz drugi.

objdump -s -j .fini_array ./greeting

./greeting:     file format elf32-i386

Contents of section .fini_array:
8049934 a0850408

#Put your address in 0x8049934

Zauważ, że gdy funkcja z .fini_array jest wywoływana, przechodzi do następnej, więc nie będzie wykonywana wielokrotnie (zapobiegając wiecznym pętlom), ale również dostaniesz tylko 1 wywołanie funkcji umieszczonej tutaj.

Zauważ, że wpisy w .fini_array są wywoływane w odwrotnej kolejności, więc prawdopodobnie chcesz zacząć pisanie od ostatniego.

Wieczna pętla

Aby nadużyć .fini_array i uzyskać wieczną pętlę, możesz sprawdzić, co zostało tutaj zrobione: Jeśli masz co najmniej 2 wpisy w .fini_array, możesz:

  • Użyj swojego pierwszego zapisu, aby ponownie wywołać podatną funkcję do dowolnego zapisu

  • Następnie oblicz adres powrotu na stosie przechowywany przez __libc_csu_fini (funkcja wywołująca wszystkie funkcje .fini_array) i umieść tam adres __libc_csu_fini

  • Spowoduje to, że __libc_csu_fini ponownie wywoła siebie, wykonując ponownie funkcje .fini_array, które ponownie wywołają podatną funkcję WWW 2 razy: raz dla dowolnego zapisu i jeszcze raz, aby ponownie nadpisać adres powrotu __libc_csu_fini na stosie, aby ponownie wywołać siebie.

Zauważ, że przy pełnym RELRO, sekcja .fini_array jest ustawiona jako tylko do odczytu.

Jak wyjaśniono w tym poście, jeśli program zakończy działanie za pomocą return lub exit(), zostanie uruchomiona funkcja __run_exit_handlers(), która wywoła zarejestrowane destruktory.

Jeśli program zakończy działanie za pomocą funkcji _exit(), zostanie wywołane wywołanie systemowe exit i obsługiwane nie będą wykonywane. 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 spowoduje, ż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ącą 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 na temat tej techniki i tym opisem ld.so pozostawia wskaźnik na stosie, który wskazuje na binarny link_map w ld.so. Dzięki arbitralnemu zapisowi możliwe jest nadpisanie go i spowodowanie, że wskazuje 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 wykonana funkcja __run_exit_handlers(), która 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, zostanie odszyfrowany wskaźnik z cur->func i zostanie ona wywołana 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 cookie PTR_MANGLE. Dlatego, przy przepełnieniu go, możliwe byłoby nadpisanie cookie i stack canary. Przy nadpisaniu cookie PTR_MANGLE, możliwe byłoby obejście funkcji PTR_DEMANLE, ustawiając ją na 0x00, co oznacza, że xor użyty do uzyskania rzeczywistego adresu to po prostu skonfigurowany adres. Następnie, pisząc na dtor_list, możliwe jest łańcuchowe wywoływanie 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

Należy wziąć to pod uwagę przed dodaniem nowego adresu.

Znajdź przykład w oryginalnym poście.

Inne zmodyfikowane wskaźniki w __run_exit_handlers

Ta technika jest wyjaśniona tutaj i ponownie zależy od programu zakończającego działanie za pomocą return lub exit(), aby zostało 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.

Co więcej, 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 wartością system('/bin/sh'). Przykład tego można znaleźć w oryginalnym wpisie na blogu dotyczącym tej techniki.

Last updated