I°) Qu'est-ce qu'un buffer-overflow ?
La traduction littérale suffit à expliquer le terme : c'est un dépassement du buffer, aussi appellé
dépassement de mémoire tampon en français. Cela peut arriver très fréquemment. En effet, les langages de haut-niveau
laissent au programmeur le soin de vérifier la non-corruption des données, entre autres de vérifier que les longueurs
limites des tableaux ne peuvent en aucun cas être dépassées dans le programme. Concrètement, si le dépassement est
petit, il va juste corrompre les variables qui suivent le buffer. S'il est un peu plus grand, il peut changer la
valeur des deux pointeurs SFP et return address, causant bien souvent le crash du programme, puisque ces
pointeurs sont par la suite dénués de sens (par exemple, si le pointeur EIP contient une adresse où il n'y a pas
d'instruction valide, le programme s'interrompt avec le message d'erreur Segmentation Fault ou Illegal
Instruction). Voici un exemple simple d'overflow :
//exemple-overflow.c : Dépassement de mémoire tampon
#include <stdio.h>
void demander_nom() {
char buffer_nom[20];
printf("Entrez votre nom\n");
scanf("%s",&buffer_nom);
printf("Votre nom est %s\n",buffer_nom);
}
int main() {
demander_nom();
return 0;
}
La fonction printf imprime à l'écran un texte et scanf stocke ce qui est entré au clavier dans la variable buffer_nom.
Ces fonctions sont contenues dans la librairie C
stdio.h, c'est pourquoi on a inclut cette librairie au début
du programme. Le caractère '\n' est quand à lui le caractère de retour à la ligne. Voici maintenant des exemples
d'éxécutions de ce programme :
$ gcc -m32 -fno-stack-protector exemple-overflow.c -o exemple-overflow
$ ./exemple-overflow
Entrez votre nom
SeriousHacking
Votre nom est SeriousHacking
$ ./exemple-overflow
Entrez votre nom
AAAAAAAAAAAAAAAAAAAAAAA
Votre nom est AAAAAAAAAAAAAAAAAAAAAAA
$ ./exemple-overflow
Entrez votre nom
AAAAAAAAAAAAAAAAAAAAAAAA
Votre nom est AAAAAAAAAAAAAAAAAAAAAAAA
Instruction illégale
$
Nous avons donc l'exemple classique où l'utilisateur fournit un nom, plus petit que 20 caractères et où tout se passe
bien. Dans le second exemple, on fournit exactement 23 fois le caractère 'A'. Le tableau ayant une taille de 20 octets,
toutes ses cases seront remplies de A, 3 caractères à placer plus le
nul byte. Par conséquent, l'espace du
tableau va être dépassé et le pointeur SFP va être réécrit, en l'occurence remplacé par 0x41414100 (le caractère 'A'
s'écrivant 0x41 en hexadécimal). Nous avons donc un
overflow, mais ici sans incidence sur la suite du programme,
car le pointeur EBP n'a plus d'utilité après la fonction, même s'il devient 0x00414141 qui est une addresse sans
signification, celà n'a pas d'effet.
Dans le troisième cas, on a fournit un caractère de plus, à savoir 24 caractères. On devine rapidement ce qui se passe :
le SFP deviendra cette fois 0x41414141 et le
return address 0x00563412. L'EIP va donc pointer vers 0x12345600 après
le
popping de demander_nom(), où il va trouver une instruction qui ne sera pas valide, d'où le crash du programme.
On s'en doute, l'exploitation des buffer-overflows va consister en l'exploitation de ce type de faille. Cet exemple est
basé sur un overflow dans la pile. L'exploitation associée sera dénommée
stack-based overflow et sera notre premier
"BoF" (enfin !) dans la partie suivante.
Attention please!
Afin de bien comprendre d'où viennent et comment marchent les buffer overflows, j'utilise ici les techniques
historiques d'exploitation, qui sont désormais rarement utilisables à cause des protections mises en place. Il faut les désactiver
pour que les exemples marchent :
- désactiver ASLR (adresses aléatoires). En tant que root :
# echo "0" > /proc/sys/kernel/randomize_va_space
- désactiver la pile non exécutable. Lors de la compilation :
- désactiver le stack protector (cookie avant le SFP). Lors de la compilation :
$ gcc -fno-stack-protector ...
- compilation en mode 32-bits (si vous êtes sur une machine 64-bits). Lors de la compilation :