Failles de sécurité

Débordements de tampons

int save() {
    FILE* file = NULL;
    char filename[256];

    do {
        printf("Entrez le nom de la sauvegarde : ");
        fflush(stdout);
    } while (gets(filename) == NULL);
    file = fopen(filename, "rw");
    ...
}

On suppose ici que le nom de fichier est au plus 256 caractères. Or en réalité rien n'empêche l'utilisateur d'entrer un chaîne de caractères avec plus de 256 caractères.

Le risque est gets aille écrire au delà de la zone réservée pour filename. Elle peut en particulier écrire sur l'adresse de retour de la fonction save.

Pire, un attaquant pourrait se débrouiller que cette adresse de retour soit le contenu du fichier file dans lequel il aura écrit les actions qu'il désire.

Débordement sur le tas

C'est la même idée mais maintenant, on va écrire au delà d'un bloc allouée. On peut alors toucher des informations de gestion de la mémoire (la liste doublement chaînée de la mémoire).

String-format vulnerabilities

int i;
printf("blip%n", &i);

D'habitude printf ne fait que écrire dans la console mais ne modifie pas de variables. En fait, si, avec %n on écrit dans un entier le nombre de caractères déjà écrit sur la console.

Supposons qu'un attaquant contrôle la chaîne de caractères de formatage. Soit trivialement avec :

int i;
printf(stringControléeParLeMéchant, &i);

soit parce qu'il a déjà écrit dans la mémoire à l'endroit de la chaîne de formatage.

Dans ce cas, il peut faire :

int i;
printf("%c %d %n %n %n %n", c, x);

Avec les %n surnuméraires, il écrit dans le contexte de la fonction appelante ou au delà dans la pile.

Conseils :

  • éviter gets, strcat, strcpy, sprintf
  • utiliser printf("%s", string) au lieu de printf(string) !
  • faire du fuzzing, c'est-à-dire du test avec des entrées aléatoires, à compris tester les fonctions accessibles à l'utilisateur par exemple avec une chaîne de caractères et un entier sensé être sa longueur, mais différente de sa longueur, etc.

Race conditions

Une race condition ou condition de course désigne l'examen d'un contexte puis la prise de la décision en fonction de cet examen. Par exemple : je regarde si j'ai les droits sur un fichier, si oui, j'écris dedans.

Le problème est que le contexte peutchanger entre l'examen et le moment où on agit. Ce problème arrive souvent avec plusieurs threads. Mais il arrive aussi en séquentiel, avec des fichiers notamment.

if(access(filename, W_OK) == 0) {
    if((fd = open(filename, O_WRONLY)) == NULL) {
        perror(filename);
        return 0;
    }
    /** écrire dans le fichier */
}

Débordements sur les nombres

longueur = ...;//qui dépend de l'utilisateur
char* buf = (char*) malloc(longueur*2);
if(buf == NULL) return -1;
...

Quel est le problème du code ci-dessus ?

Il se peut que longueur * 2 ne tiennent pas dans un size_t.

Supposons que size_t soit un unsigned char, dans ce cas, longueur = 129, que se passe-t-il ?

longueur * 2 vaut 2.

Résultat : on écrit en dehors de la zone allouée, et catastrophe. Avec OpenSSH <=3.3, un problème de ce type permettait à un attaquant d'être administrateur.

Remarque : calloc permet d'allouer n objets de taille m, et ça vérifie que (m \times n) ne déborde pas.

int nombre;

if(nombre < 1000) {
    char* buf = malloc(nombre * sizeof(char));
    ...
}

Est-ce que vous voyez un problème ?

nombre peut être négatif. On convertit en size_t non signé, on allouera souvent une place très grande. On peut alors écrire un très gros fichier, ou envoyer plein de données sur Internet...

Bibliographie

Programmation avancée en C. Sébastien Varette et Nicolas Bernard.