Les Buffers OverFlow (BoF) ont souvent été à la base de failles dans divers programmes.
Dans cet article, nous allons récapituler les bases d'un BoF pour permettre de comprendre l'exploitation puis la correction de celui-ci. À noter que les codes sont écris en C.
Tout d'abord, il faut savoir qu'un buffer est une partie de la mémoire assignée à l'exécution d'un programme. En C et en C++, rien ne
vérifie la taille d'un buffer avant de l'écrire sur la mémoire. Par conséquent, n'importe qui peut écrire au delà du buffer, comme dans l'exemple suivant:
int main () {
int buffer[10];
buffer[20] = 10;
}
Ici, la plupart des compilateurs n'afficheront aucune erreur et compileront correctement le code puisque, justement, il n'y a pas d'erreur.
Cependant, le programme essaye d'écrire au delà de la mémoire assignée pour le buffer (tampon) ce qui risque d'engendrer un comportement inatendu du programme.
Pour comprendre comment les débordements de tampon (BoF) sont devenus populaire dans le monde de la sécurité informatique, voyons d'abord à quoi ressemble un processus dans la mémoire.
Un processus est un programme en cours d'exécution. Un programme exécutable contient un ensemble d'instructions binaires devant être exécutées par le processeur;
quelques données lecture seule (inaltérables); quelques données globales et statiques qui durent durant l'exécution du programme; et un indicateur de brk qui garde une trace de la manipulation de la mémoire.
Les variables locales des fonctions sont des variables automatiques créées sur la pile lorsque les fonctions s'exécutent, et sont effacées alors que la fonction se termine.

Le schéma ci-dessus montre la disposition de mémoire d'un processus sous GNU/Linux.
Une image de processus commence par le code et les données du programme qui comprennent les instructions du programme et l'initialisation des données statiques et globales.
Au dessus, il y a le tas de mémoire servant au cours de l'exécution, puis la pile d'utilisateur (user stack).
Cette pile est employée à chaque fois qu'il y a un appel de fonction.
Une pile est un bloc adjacent à la mémoire et contenant des données. À chaque fois qu'un appel de fonction est fait, les paramètres de fonction sont poussés de droite à gauche sur la pile.
Puis, ce qui est exécuté après la fonction (nommée adresse de retour) et suivi d'un pointeur FP, est poussé sur la pile.

Ce schéma représente une région de la pile lorsqu'une fonction est exécutée. Remarquez que le pointeur FP est situé entre l'adresse locale et celle de retour.
Par exemple, imaginons le code suivant:
void function (int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
int main() {
function(1,2,3);
}
Dans ce cas, la pile resemble à l'image suivante:

Sur ce schéma, on remarque que le buffer1 prend 8bytes, alors que le buffer2 en prend 12, la mémoire ne pouvant être attribuée que pour une taille multiple de 4.
On remarque aussi qu'un FP est necessaire pour accèder au varibles a, b, c, buffer1 et buffer2. Toutes ces variables sont nettoyées de la pile une fois l'exécution de la fonction terminée. Ces variables ne prennent pas de place sur le disque.
Considérons ce code:
void function (char *str) {
char buffer[16];
strcpy (buffer, str);
}
int main () {
char *str = "Je fais plus de 16 bytes"; // str = 26 bytes
function (str);
}
Dans ce bout de code, il y a un BoF puisque une chaine de 26bytes a été copiée dans un buffer de 16bytes. Vous allez donc déborder sur la mémoire assignée pour le FP, et ainsi de suite.
La fonction strcpy ne vérifie pas la longueur des chaines copiées, alors que si vous utiliser strncpy, vous n'aurez pas écris par dessus la pile. Cet exemple montre clairement qu'un débordement de tampon (BoF)
peut réécrire l'adresse de retour, qui alternativement peut changer le chemin de l'exécution du programme. Rappelez-vous que l'adresse de retour d'une fonction est l'adresse de la prochaine instruction dans la mémoire, qui est exécutée juste après que la fonction retourne.
Étant donné qu'il est si facile d'écrire hors du buffer attribué, lorsqu'il y a une intrusion dans un système en exploitant un BoF, un shell (avec permission root) a été utilisé la plupart du temps. Pour ce faire,
l'adresse de retour a été modifiée pour sauter le chemin d'exécution à un code écrit au préalable, ce qui a permis d'engendrer le shell: c'est le shellcode. Mais s'il n'y a pas de code à exploiter dans le programme? Il faut alors placer le code dans la section du tampon qui a été débordée.
Nous recouvrons alors l'adresse de retour qui se dirige alors de nouveau sur le buffer et exécute le code prévu. Un tel code peut être inséré dans le programme en utilisant des variables d'environnement ou une interaction avec l'utilisateur (I/O). Vous trouverez un exemple de Phrack ici.
Il n'est pas possible d'éviter totalement un BoF. Par contre, on peut faire en sorte qu'il soit très difficile à mettre en œuvre.
- Écrivez un code sécurisé. En effet, la plupart des BoF proviennent d'un bourrage de code dans un buffer n'ayant pas la taille suffisante. Les fonctions du C telles que strcpy(), strcat(), sprintf() et vsprintf() fonctionnent sur des chaines se terminant par un null string et ne vérifient pas la longueur de la chaine. gets() est une fonction qui interagit avec l'utilisateur (et copie cette interaction dans un buffer) grâce à stdin jusqu'à ce qu'une nouvelle ligne(^D) ou EOF soit trouvé. Les fonctions variantes de scanf() peut également avoir comme conséquence des BoF. Par conséquent, la meilleure manière de traiter des problèmes de BoF est de ne pas leur permettre de se produire en premier lieu.
- Le code malveillant étant dans le programme, il réside dans la pile et pas dans le segment de code. Par conséquent, la solution la plus simple est d'infirmer la pile pour exécuter toutes les instructions. N'importe quel code qui essaye d'exécuter un autre code résidant dans la pile causera une violation de segmentation.
- Les compilateurs sont devenus de plus en plus stricts au cours de ces dernières années. Plusieurs compilateurs préviennent lorsqu'il y a une utilisation peu sûre des fonctions pouvant engendrer des BoF. Prennons l'exemple suivant :
int main () {
char *str = (char *)malloc(10); // assigne 10 bytes pour str
gets (str); // copie la chaine entrée par l'utilisateur dans str
}
Lorsque vous compilez ce code avec GCC, il affiche l'avertissement suivant:
/tmp/recap_bof-gcc_test.o: In function "main":
En plus d'afficher un avertissement, certains compilateurs génèrent un code permettant de sécuriser le programme. Pour GCC, vous trouverez un patch ce faisant ici.
/tmp/recap_bof-gcc_test.o(.text+0x1f): the "gets" function is dangerous and should not be used.
Vous devriez maintenant avoir la mémoire fraîche pour aller recompiler vos petits programmes, pour éviter les BoF, ou faire quelques tests de BoF.
Sources :
http://destroy.net/machines/security/P49-14-Aleph-One
http://new.linuxjournal.com/node/6701
http://web.inter.nl.net/hcc/Haj.Ten.Brugge