13 KiB
WWW2Exec - .dtors & .fini_array
Naucz się hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!
Inne sposoby wsparcia HackTricks:
- Jeśli chcesz zobaczyć swoją firmę reklamowaną w HackTricks lub pobrać HackTricks w formacie PDF, sprawdź PLANY SUBSKRYPCYJNE!
- Kup oficjalne gadżety PEASS & HackTricks
- Odkryj Rodzinę PEASS, naszą kolekcję ekskluzywnych NFT
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Podziel się swoimi sztuczkami hakerskimi, przesyłając PR-y do HackTricks i HackTricks Cloud github repos.
.dtors
{% hint style="danger" %} Obecnie jest bardzo dziwne znalezienie binarnego pliku z sekcją .dtors! {% endhint %}
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ć.
{% hint style="warning" %} Oczywiście najpierw musisz znaleźć miejsce do przechowywania shellcode, aby później móc go wywołać. {% endhint %}
.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.
{% hint style="danger" %}
Zauważ, że przy pełnym RELRO, sekcja .fini_array
jest ustawiona jako tylko do odczytu.
{% endhint %}
link_map
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.
{% hint style="danger" %}
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.
{% endhint %}
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łszywyfini_array
z instrukcjami do wykonania arbitralnego kodu - Nadpisz wpisy
l_info[DT_FINI_ARRAY]
il_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, żearray
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łszywyfini_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łszywyfini_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.