WWW2Exec - atexit(), stockage TLS et autres pointeurs altérés
Reading time: 9 minutes
tip
Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PRs au HackTricks et HackTricks Cloud dépôts github.
__atexit Structures
caution
De nos jours, il est très bizarre d'exploiter cela !
atexit()
est une fonction à laquelle d'autres fonctions sont passées en paramètres. Ces fonctions seront exécutées lors de l'exécution d'un exit()
ou du retour de la main.
Si vous pouvez modifier l'adresse de l'une de ces fonctions pour pointer vers un shellcode par exemple, vous prenez le contrôle du processus, mais cela est actuellement plus compliqué.
Actuellement, les adresses des fonctions à exécuter sont cachées derrière plusieurs structures et finalement l'adresse à laquelle elles pointent n'est pas l'adresse des fonctions, mais est chiffrée avec XOR et des déplacements avec une clé aléatoire. Donc, actuellement, ce vecteur d'attaque n'est pas très utile, du moins sur x86 et x64_86.
La fonction de chiffrement est PTR_MANGLE
. D'autres architectures telles que m68k, mips32, mips64, aarch64, arm, hppa... n'implémentent pas la fonction de chiffrement car elle retourne la même chose que ce qu'elle a reçu en entrée. Donc, ces architectures seraient attaquables par ce vecteur.
Vous pouvez trouver une explication détaillée sur le fonctionnement de cela dans https://m101.github.io/binholic/2017/05/20/notes-on-abusing-exit-handlers.html
link_map
Comme expliqué dans ce post, si le programme se termine en utilisant return
ou exit()
, il exécutera __run_exit_handlers()
qui appellera les destructeurs enregistrés.
caution
Si le programme se termine via la fonction _exit()
, il appellera le syscall exit
et les gestionnaires de sortie ne seront pas exécutés. Donc, pour confirmer que __run_exit_handlers()
est exécuté, vous pouvez définir un point d'arrêt dessus.
Le code important est (source):
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
}
Notez comment map -> l_addr + fini_array -> d_un.d_ptr
est utilisé pour calculer la position de l'array de fonctions à appeler.
Il y a quelques options :
- Écraser la valeur de
map->l_addr
pour qu'elle pointe vers un fauxfini_array
avec des instructions pour exécuter du code arbitraire. - Écraser les entrées
l_info[DT_FINI_ARRAY]
etl_info[DT_FINI_ARRAYSZ]
(qui sont plus ou moins consécutives en mémoire), pour les faire pointer vers une structureElf64_Dyn
forgée qui fera à nouveau pointerarray
vers une zone mémoire contrôlée par l'attaquant. - Ce rapport écrase
l_info[DT_FINI_ARRAY]
avec l'adresse d'une mémoire contrôlée dans.bss
contenant un fauxfini_array
. Ce faux tableau contient d'abord un one gadget adresse qui sera exécutée et ensuite la différence entre l'adresse de ce faux tableau et la valeur demap->l_addr
afin que*array
pointe vers le faux tableau. - Selon le post principal de cette technique et ce rapport, ld.so laisse un pointeur sur la pile qui pointe vers le
link_map
binaire dans ld.so. Avec une écriture arbitraire, il est possible de l'écraser et de le faire pointer vers un fauxfini_array
contrôlé par l'attaquant avec l'adresse d'un one gadget par exemple.
Suite au code précédent, vous pouvez trouver une autre section intéressante avec le code :
/* 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));
}
Dans ce cas, il serait possible de remplacer la valeur de map->l_info[DT_FINI]
pointant vers une structure ElfW(Dyn)
forgée. Trouvez plus d'informations ici.
Surcharge de dtor_list de TLS-Storage dans __run_exit_handlers
Comme expliqué ici, si un programme se termine via return
ou exit()
, il exécutera __run_exit_handlers()
qui appellera toute fonction destructrice enregistrée.
Code de _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 ();
Code de __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);
[...]
}
}
Pour chaque fonction enregistrée dans tls_dtor_list
, il démanglera le pointeur de cur->func
et l'appellera avec l'argument cur->obj
.
En utilisant la fonction tls
de ce fork de GEF, il est possible de voir que le dtor_list
est en fait très proche du stack canary et du PTR_MANGLE cookie. Ainsi, avec un débordement, il serait possible de surcharger le cookie et le stack canary.
En surchargeant le PTR_MANGLE cookie, il serait possible de contourner la fonction PTR_DEMANLE
en le définissant à 0x00, ce qui signifie que le xor
utilisé pour obtenir l'adresse réelle est simplement l'adresse configurée. Ensuite, en écrivant sur le dtor_list
, il est possible de chaîner plusieurs fonctions avec l'adresse de la fonction et son argument.
Enfin, notez que le pointeur stocké ne sera pas seulement xored avec le cookie mais également tourné de 17 bits :
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
Donc, vous devez en tenir compte avant d'ajouter une nouvelle adresse.
Trouvez un exemple dans le post original.
Autres pointeurs altérés dans __run_exit_handlers
Cette technique est expliquée ici et dépend encore une fois du programme sortant en appelant return
ou exit()
donc __run_exit_handlers()
est appelé.
Vérifions plus de code de cette fonction :
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);
La variable f
pointe vers la structure initial
et en fonction de la valeur de f->flavor
, différentes fonctions seront appelées.
Selon la valeur, l'adresse de la fonction à appeler sera à un endroit différent, mais elle sera toujours démanglée.
De plus, dans les options ef_on
et ef_cxa
, il est également possible de contrôler un argument.
Il est possible de vérifier la structure initial
dans une session de débogage avec GEF en exécutant gef> p initial
.
Pour en abuser, vous devez soit fuiter ou effacer le cookie PTR_MANGLE
, puis écraser une entrée cxa
dans initial avec system('/bin/sh')
.
Vous pouvez trouver un exemple de cela dans le post de blog original sur la technique.
tip
Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PRs au HackTricks et HackTricks Cloud dépôts github.