WWW2Exec - atexit(), TLS Storage & Inne zniekształcone wskaźniki

Reading time: 8 minutes

tip

Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Wsparcie HackTricks

__atexit Struktury

caution

Obecnie jest bardzo dziwne, aby to wykorzystać!

atexit() to funkcja, do której przekazywane są inne funkcje jako parametry. Te funkcje będą wykonywane 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, zyskasz kontrolę nad procesem, ale obecnie jest to bardziej skomplikowane.
Obecnie adresy funkcji do wykonania są ukryte za kilkoma strukturami, a ostatecznie adres, na który wskazują, nie jest adresem funkcji, lecz jest szyfrowany za pomocą XOR i przesunięć z losowym kluczem. Tak więc obecnie ten wektor ataku jest niezbyt użyteczny przynajmniej na x86 i x64_86.
Funkcja szyfrująca to PTR_MANGLE. Inne architektury takie jak m68k, mips32, mips64, aarch64, arm, hppa... nie implementują funkcji szyfrującej, ponieważ zwraca to samo, co otrzymała jako wejście. Tak więc te architektury byłyby atakowalne przez ten wektor.

Możesz znaleźć szczegółowe wyjaśnienie, jak to działa w 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 się za pomocą return lub exit(), uruchomi __run_exit_handlers(), który wywoła zarejestrowane destruktory.

caution

Jeśli program kończy się za pomocą funkcji _exit(), wywoła exit syscall i procedury końcowe nie zostaną wykonane. Aby potwierdzić, że __run_exit_handlers() jest wykonywane, możesz ustawić punkt przerwania na nim.

Ważny kod to (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
}

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:

  • Nadpisanie wartości map->l_addr, aby wskazywała na fałszywy fini_array z instrukcjami do wykonania dowolnego kodu
  • Nadpisanie wpisów l_info[DT_FINI_ARRAY] i l_info[DT_FINI_ARRAYSZ] (które są mniej więcej kolejno w pamięci), aby sprawić, że wskazują na sfałszowaną strukturę Elf64_Dyn, która ponownie sprawi, że array będzie wskazywać na strefę pamięci kontrolowaną przez atakującego.
  • Ten opis nadpisuje l_info[DT_FINI_ARRAY] adresem kontrolowanej pamięci w .bss, zawierającej fałszywy fini_array. Ta fałszywa tablica zawiera najpierw adres one gadget, który zostanie wykonany, a następnie różnicę między adresem tej fałszywej tablicy a wartością map->l_addr, aby *array wskazywało na fałszywą tablicę.
  • Zgodnie z głównym postem tej techniki i tym opisem ld.so zostawia wskaźnik na stosie, który wskazuje na binarny link_map w ld.so. Dzięki dowolnemu zapisowi możliwe jest nadpisanie go i sprawienie, aby wskazywał na fałszywy fini_array kontrolowany przez atakującego z adresem do one gadget, na przykład.

Po poprzednim kodzie można znaleźć kolejny interesujący fragment z kodem:

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

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 dtor_list w TLS-Storage w __run_exit_handlers

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

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

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

Dla każdej zarejestrowanej funkcji w tls_dtor_list, zostanie zdemanglowany wskaźnik z cur->func i wywołana z argumentem cur->obj.

Używając funkcji tls z tej forka GEF, można zobaczyć, że tak naprawdę dtor_list jest bardzo blisko stack canary i PTR_MANGLE cookie. Tak więc, przy przepełnieniu możliwe byłoby nadpisanie cookie i stack canary.
Nadpisując PTR_MANGLE cookie, możliwe byłoby obejście funkcji PTR_DEMANLE ustawiając ją na 0x00, co oznacza, że xor użyte do uzyskania rzeczywistego adresu to po prostu skonfigurowany adres. Następnie, pisząc na dtor_list, możliwe jest połączenie kilku funkcji z adresem funkcji i jej argumentem.

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

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

Musisz wziąć to pod uwagę przed dodaniem nowego adresu.

Znajdź przykład w oryginalnym poście.

Inne zniekształcone wskaźniki w __run_exit_handlers

Ta technika jest wyjaśniona tutaj i ponownie zależy od tego, że program kończy działanie, wywołując return lub exit(), więc __run_exit_handlers() jest wywoływane.

Sprawdźmy więcej kodu tej funkcji:

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

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

Ponadto, w opcjach ef_on i ef_cxa możliwe jest również kontrolowanie argumentu.

Można sprawdzić strukturę initial w sesji debugowania z uruchomionym GEF za pomocą gef> p initial.

Aby to wykorzystać, musisz albo leak lub usunąć cookie PTR_MANGLE, a następnie nadpisać wpis cxa w initial za pomocą system('/bin/sh').
Możesz znaleźć przykład tego w oryginalnym poście na blogu o technice.

tip

Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Wsparcie HackTricks