Débordement de tampon via une variable d'environnement ou Environment Variable-based Buffer Overflow
Dans les articles précédents on a fait déborder un buffer via l'entrée standard, que ce soit sur la pile ou sur le tas. Ici le vecteur change. Ce n'est plus ce que l'utilisateur tape au clavier qui pose problème, c'est une variable d'environnement, un truc qu'on associe rarement à une entrée dangereuse alors qu'elle l'est tout autant.
Les variables d'environnement
À quoi elles servent
Une variable d'environnement est une paire clé/valeur associée à un processus. PATH indique où chercher les exécutables, HOME pointe vers le répertoire personnel de l'utilisateur, LANG fixe la langue du système. Un processus hérite des variables de son parent au moment où il est lancé et peut les lire, les modifier ou en définir de nouvelles pour ses propres enfants.
Depuis un shell on peut lister ce qu'on a avec env, et en définir une nouvelle avec export.
export HOME=/home/aaron
Rien n'empêche de mettre à peu près n'importe quoi dans cette variable, elle n'est pas validée par le système.
Comment getenv() les récupère
Côté C, un programme récupère une variable d'environnement avec getenv().
char *home = getenv("HOME");
getenv() ne fait pas de copie. Elle renvoie un pointeur directement vers la chaîne stockée dans l'environnement du processus, une zone mémoire qui vit en dehors des buffers qu'on a nous-mêmes alloués. Si la variable n'existe pas, elle renvoie NULL, ce qui est déjà une source de bug classique si on oublie de vérifier avant de s'en servir.
Comment le Buffer Overflow survient
Variable contrôlée par l'utilisateur
N'importe quel utilisateur avec un accès shell peut fixer la valeur d'une variable d'environnement avant de lancer un programme. Il contrôle aussi bien le contenu que la longueur.
export HOME=$(python3 -c "print('A'*200)")
Si le programme fait confiance à cette valeur sans vérifier sa taille, on a exactement le même problème qu'avec une entrée clavier trop longue sauf que là l'utilisateur n'a même pas besoin d'interagir avec le programme au moment de l'exécution.
Copie dans un buffer fixe avec sprintf()
#include <stdio.h>
#include <stdlib.h>
void afficher_home() {
char buffer[32];
char *home = getenv("HOME");
sprintf(buffer, "HOME = %s", home);
printf("%s\n", buffer);
}
int main() {
afficher_home();
return 0;
}
buffer[32] réserve 32 octets sur la pile. Le souci vient de cette ligne :
sprintf(buffer, "HOME = %s", home);
sprintf() écrit dans buffer la chaîne formatée sans jamais vérifier si elle tient dedans. Si HOME fait 200 caractères, sprintf() va quand même tout écrire, bien au-delà des 32 octets prévus.
Dépassement et corruption mémoire
À partir de là, le mécanisme est identique à ce qu'on a vu sur la pile. buffer déborde vers les adresses hautes, écrase le RBP sauvegardé puis l'adresse de retour, et le programme finit par sauter là où il ne devrait pas.
Exemple pratique
Modifier HOME
On compile sans les protections modernes pour observer le mécanisme brut.
gcc -fno-stack-protector -no-pie -o env_overflow env_overflow.c
Puis on fixe une valeur volontairement trop longue pour HOME.
export HOME=$(python3 -c "print('A'*200)")
Provoquer le crash
./env_overflow
Segmentation fault (core dumped)
Le crash confirme que quelque chose d'important a été écrasé.
Observer avec GDB ce qui a été écrasé
gdb ./env_overflow
(gdb) run
Program received signal SIGSEGV, Segmentation fault.
0x0000000000401180 in afficher_home ()
Et si on regarde RIP au moment du crash.
(gdb) info registers rip
rip 0x4141414141414141 0x4141414141414141
Comme sur la pile, 0x41 correspond au A qu'on a placé dans HOME. L'adresse de retour a bien été remplacée par une partie du contenu de la variable d'environnement.
Pourquoi c'est exploitable
Une variable d'environnement peut contenir des données contrôlées
Contrairement à une entrée passée au programme au moment de l'exécution, une variable d'environnement peut être préparée à l'avance, avec exactement le contenu voulu et sans aucune limite naturelle de taille. C'est un vecteur particulièrement pratique quand le programme visé ne lit jamais directement l'entrée de l'attaquant mais fait confiance à son environnement.
Elle peut servir directement au débordement ou à stocker des données utiles à une exploitation
Une variable d'environnement peut jouer deux rôles à la fois. Elle peut simplement servir de source du débordement comme dans l'exemple ci-dessus. Mais elle peut aussi transporter des données qu'un attaquant voudrait retrouver en mémoire au moment de l'exploitation, une charge utile, une adresse ou tout autre contenu utile une fois qu'on contrôle le flot d'exécution par ailleurs. C'est une des raisons pour lesquelles les variables d'environnement ont longtemps été un terrain de choix dans l'histoire de l'exploitation de binaires sur Linux.
Comment s'en protéger
Ne pas faire confiance aux variables d'environnement
Une variable d'environnement vient de l'utilisateur qui lance le programme même si ça ne ressemble pas à une entrée classique. Elle doit être traitée avec la même méfiance qu'une entrée clavier ou qu'un argument de ligne de commande.
Vérifier les tailles
Avant de copier une variable d'environnement dans un buffer fixe, il faut vérifier sa longueur avec strlen(), ou simplement limiter la copie à la taille du buffer de destination.
Éviter les fonctions comme sprintf()
char buffer[32];
char *home = getenv("HOME");
if (home) {
snprintf(buffer, sizeof(buffer), "HOME = %s", home);
}
snprintf() prend la taille du buffer en paramètre et tronque proprement ce qui dépasse au lieu d'écrire à l'infini comme sprintf().
Conclusion
Le mécanisme derrière ce buffer overflow n'a rien de nouveau, c'est le même débordement sur la pile que dans le premier article. Ce qui change, c'est la source. Une variable d'environnement paraît anodine, elle fait presque partie du décor, et c'est justement pour ça qu'on oublie parfois de la traiter comme une entrée à part entière. Le réflexe à garder, c'est que toute donnée qui vient de l'extérieur du programme mérite d'être vérifiée avant d'être copiée quelque part, peu importe la forme sous laquelle elle arrive.