WWW2Exec - atexit()

Підтримайте HackTricks

__Структури atexit

Сьогодні дуже дивно експлуатувати це!

atexit() - це функція, до якої передаються інші функції в якості параметрів. Ці функції будуть виконані при виконанні exit() або поверненні з головної функції. Якщо ви можете змінити адресу будь-якої з цих функцій, щоб вказувати на shellcode, наприклад, ви отримаєте контроль над процесом, але зараз це складніше. Наразі адреси функцій, які мають бути виконані, приховані за декількома структурами, і, нарешті, адреса, на яку вони вказують, не є адресами функцій, а шифруються за допомогою XOR та зсувів з випадковим ключем. Таким чином, наразі цей вектор атаки не є дуже корисним принаймні на x86 та x64_86. Функція шифрування - PTR_MANGLE. Інші архітектури, такі як m68k, mips32, mips64, aarch64, arm, hppa... не реалізують функцію шифрування, оскільки вона повертає те саме, що отримала на вході. Таким чином, ці архітектури можна атакувати за допомогою цього вектора.

Ви можете знайти докладне пояснення того, як це працює за посиланням https://m101.github.io/binholic/2017/05/20/notes-on-abusing-exit-handlers.html

Як пояснено у цьому пості, якщо програма завершується за допомогою return або exit(), вона виконає __run_exit_handlers(), яка викличе зареєстровані деструктори.

Якщо програма завершується через функцію _exit(), вона викличе системний виклик exit і обробники виходу не будуть виконані. Тому, щоб підтвердити виконання __run_exit_handlers(), ви можете встановити точку зупинки на цій функції.

Важливий код (джерело):

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
}

Зверніть увагу, як map -> l_addr + fini_array -> d_un.d_ptr використовується для обчислення позиції масиву функцій для виклику.

Є кілька варіантів:

  • Перезаписати значення map->l_addr, щоб воно вказувало на фальшивий fini_array з інструкціями для виконання довільного коду

  • Перезаписати записи l_info[DT_FINI_ARRAY] та l_info[DT_FINI_ARRAYSZ] (які приблизно послідовні у пам'яті), щоб вони вказували на підроблену структуру Elf64_Dyn, яка знову зробить array вказівником на пам'ять, яку контролює зловмисник.

  • Цей опис перезаписує l_info[DT_FINI_ARRAY] адресою керованої пам'яті в .bss, що містить фальшивий fini_array. Цей фальшивий масив містить спочатку адресу одного гаджета, яка буде виконана, а потім різницю між адресою цього фальшивого масиву та значенням map->l_addr, щоб *array вказував на фальшивий масив.

  • Згідно з основним повідомленням цієї техніки та цим описом ld.so залишає вказівник на стеку, який вказує на бінарний link_map в ld.so. З допомогою довільного запису можна перезаписати його і зробити його вказівником на фальшивий fini_array, керований зловмисником, з адресою одного гаджета, наприклад.

Після попереднього коду ви можете знайти ще один цікавий розділ з кодом:

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

У цьому випадку можливо перезаписати значення map->l_info[DT_FINI], що вказує на сфальсифіковану структуру ElfW(Dyn). Знайдіть більше інформації тут.

Перезапис dtor_list TLS-сховища в __run_exit_handlers

Як пояснено тут, якщо програма завершується за допомогою return або exit(), вона виконає __run_exit_handlers(), яка викличе будь-які зареєстровані функції деструкторів.

Код з _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 ();

Код з __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);
[...]
}
}

Для кожної зареєстрованої функції в tls_dtor_list, він розшифрує вказівник з cur->func та викличе його з аргументом cur->obj.

Використовуючи функцію tls з цього форку GEF, можна побачити, що dtor_list насправді дуже близько до стекового канарейки та PTR_MANGLE кукі. Таким чином, з переповненням цього буде можливо перезаписати куку та стекову канарейку. Перезаписуючи PTR_MANGLE куку, буде можливо обійти функцію PTR_DEMANLE, встановивши її на 0x00, що означатиме, що xor, використаний для отримання реальної адреси, буде лише адресою, яка налаштована. Потім, записуючи в dtor_list, можливо ланцюжити кілька функцій з адресою функції та її аргументом.

Зауважте, що збережений вказівник не лише буде xor з кукою, але також буде обертатися на 17 біт:

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

Отже, вам потрібно врахувати це перед додаванням нової адреси.

Знайдіть приклад у оригінальному пості.

Інші змінені вказівники в __run_exit_handlers

Ця техніка пояснюється тут і знову залежить від програми виходу, яка викликає return або exit(), тоді викликається __run_exit_handlers().

Давайте перевіримо більше коду цієї функції:

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

Змінна f вказує на структуру initial, і в залежності від значення f->flavor будуть викликані різні функції. Залежно від значення, адреса функції для виклику буде знаходитися в різних місцях, але завжди буде розкодована.

Крім того, у параметрах ef_on та ef_cxa також можна контролювати аргумент.

Можливо перевірити структуру initial під час сеансу налагодження з GEF, запустивши gef> p initial.

Для зловживання цим потрібно або витікати або стерти куку PTR_MANGLE і потім перезаписати запис cxa в initial на system('/bin/sh'). Приклад цього можна знайти в оригінальному дописі в блозі про техніку.

Last updated