Le Buffer Overflow dans .bss
Dans l'article précédent on a fait déborder un buffer global et écrasé sa voisine. Ici on zoome sur l'endroit précis où vivent la plupart de ces buffers quand ils ne sont pas initialisés à la déclaration, la section .bss.
La section .bss
.bss est la section qui accueille les variables globales et statiques qui n'ont pas de valeur initiale explicite, ou qui sont initialisées à zéro. Comme leur valeur de départ est connue d'avance (zéro), le binaire n'a pas besoin de stocker de vraies données pour elles, juste leur taille. C'est le chargeur du système qui réserve l'espace et le remplit de zéros au lancement du programme.
C'est là que finit un buffer comme celui-ci.
static char buffer[64];
Aucune valeur n'est donnée à la déclaration, donc buffer atterrit dans .bss, avec 64 octets de zéros en attendant d'être remplis à l'exécution.
Comment survient le Buffer Overflow
Le mécanisme ne change pas par rapport à un buffer global classique. Une fonction comme strcpy ou sprintf écrit dans le buffer sans vérifier sa taille, et si les données dépassent l'espace réservé dans .bss, l'écriture continue dans la mémoire qui suit, où que se trouve la prochaine variable placée par le compilateur.
Exemple concret
Deux buffers dans .bss
#include <stdio.h>
#include <string.h>
static char buffer[16];
static char secret[16];
int main(void) {
strcpy(secret, "supersecret");
strcpy(buffer, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
printf("secret = %s\n", secret);
return 0;
}
buffer et secret n'ont pas de valeur à la déclaration, les deux vivent donc dans .bss. secret reçoit sa valeur à l'exécution avec strcpy(secret, "supersecret").
Débordement du premier
Le souci vient de cette ligne :
strcpy(buffer, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
La chaîne fait 32 caractères plus le \0, largement au-delà des 16 octets réservés pour buffer.
Corruption du second
$ ./bss_overflow
secret = AAAAAAAAAAAAAAAA
secret valait "supersecret" avant l'appel à strcpy(buffer, ...), et se retrouve écrasé alors que le code ne l'a jamais touché directement.
Observer la corruption avec GDB
gdb ./bss_overflow
(gdb) break main
(gdb) run
(gdb) print &buffer
$1 = (char (*)[16]) 0x404040 <buffer>
(gdb) print &secret
$2 = (char (*)[16]) 0x404050 <secret>
secret commence 16 octets après buffer, exactement la taille réservée pour buffer. Les deux sont donc voisines, sans rien entre elles.
(gdb) next
(gdb) next
(gdb) print secret
$3 = "AAAAAAAAAAAAAAAA"
Après les deux strcpy, secret ne contient plus "supersecret" mais une partie du contenu écrit dans buffer.
Identifier un buffer .bss dans un binaire
Sans avoir le code source sous les yeux, nm permet de lister les symboles d'un binaire et de voir lesquels vivent dans .bss.
nm ./bss_overflow | grep -iE "buffer|secret"
0000000000404040 b buffer
0000000000404050 b secret
La lettre après l'adresse indique le type de symbole. Un b (ou un B pour un symbole visible depuis d'autres fichiers) signale une variable dans .bss. Un d ou un D indiquerait à l'inverse une variable dans .data, donc initialisée à une valeur non nulle dès la compilation.
objdump donne une vue complémentaire, avec la taille de la section elle-même.
objdump -h ./bss_overflow | grep bss
23 .bss 00000030 0000000000404040 0000000000404040
De quoi confirmer où commence la section et combien d'octets elle occupe au total, avant même de chercher un buffer précis dedans.
Comment l'éviter
Le principe reste identique à ce qu'on a vu jusqu'ici, contrôler la taille de ce qu'on écrit avant de l'écrire.
static char buffer[16];
strncpy(buffer, source, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
Le fait que le buffer vive dans .bss plutôt que sur la pile ou le tas ne change rien à la règle, il n'y a jamais de raison de faire confiance à la taille d'une donnée d'entrée sans la vérifier.
Conclusion
Un débordement dans .bss suit exactement la même logique qu'un débordement dans n'importe quelle autre variable globale ou statique, la seule différence est que le buffer en question n'avait pas de valeur initiale. Ça n'a aucune influence sur la façon dont le bug se produit, mais ça donne un moyen concret de repérer ce type de buffer dans un binaire, avec nm ou objdump.