web-dev-qa-db-fra.com

Conséquences pour la sécurité de la négligence de l'octet supplémentaire pour la terminaison NULL dans les tableaux C / C ++

Veuillez considérer : l'anglais est ma deuxième langue.


Sur le Security Now ! podcast épisode 518 ( HORNET: un correctif pour TOR?), à 27:51, marque Steve Gibson cite un exemple de code vulnérable en C/C++:

"[...] l'un d'eux [problèmes avec le code vulnérable] crée un nouveau tableau d'une certaine taille [...]. Et le correctif est" d'une certaine taille + 1 ". Donc, [...] il [le code vulnérable] n'était qu'un octet trop court. Probablement un terminateur NULL, de sorte que lorsque vous remplissez le tableau avec des objets de taille, vous auriez un octet supplémentaire de NULL qui garantirait la terminaison NULL, et qui empêcherait le dépassement de cette chaîne . Mais ce n'est pas ce que le codeur a fait: il avait oublié le ' + 1' [.. .] "

Je comprends ce qu'il veut dire: lorsque vous créez un tableau, vous devez autoriser un octet supplémentaire pour l'octet de terminaison NULL. Ce que j'aimerais réaliser avec ce post, c'est d'obtenir un pointeur pour de plus amples recherches sur l'impact d'avoir un tableau dont le dernier octet n'est pas le terminateur d'octets; Je ne comprends pas toutes les implications d'une telle négligence et comment cela pourrait conduire à un exploit. Quand il dit qu'avoir la terminaison NULL

"empêcherait le dépassement de cette chaîne",

ma question est "comment est-elle dépassée dans les cas où le caractère de terminaison NULL est négligé?".

Je comprends que c'est un sujet énorme et donc ne pas imposer à la communauté une réponse trop globale. Mais si quelqu'un pouvait avoir la gentillesse de fournir des conseils pour une lecture plus approfondie, je serais très reconnaissant et heureux d'aller faire la recherche moi-même.

21
user82100

Vulnérabilité de terminaison de chaîne


En y réfléchissant davantage, l'utilisation de strncpy() est probablement le moyen le plus courant (auquel je peux penser) qui pourrait créer des erreurs de terminaison nulles. Puisque généralement, les gens pensent que la longueur du tampon n'inclut pas \0. Vous verrez donc quelque chose comme ceci:

strncpy(a, "0123456789abcdef", sizeof(a));

En supposant que a est initialisé avec char a[16] La chaîne a ne sera pas terminée par null. Alors, pourquoi est-ce un problème? Eh bien en mémoire, vous avez maintenant quelque chose comme:

30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66 
e0 f3 3f 5a 9f 1c ff 94 49 8a 9e f5 3a 5b 64 8e

Sans un terminateur nul, les fonctions de chaîne standard ne connaîtront pas la longueur du tampon. Par exemple, strlen(a) continuera de compter jusqu'à ce qu'il atteigne un octet 0x00. Quand est-ce, qui sait? Mais chaque fois qu'il le trouvera, il retournera une longueur beaucoup plus grande que votre tampon; disons 78. Regardons un exemple:

int main(int argc, char **argv) {
    char a[16];

    strncpy(a, "0123456789abcdef", sizeof(a));

    ... lots of code passes, functions are called...
    ... we finally come back to array a ...

    do_something_with_a(a);
}

void do_something_with_a(char *a) {
    int a_len = 0;
    char new_array[16];

    // Don't know what the length of the 'a' string is, but it's a string so lets use strlen()!
    a_len = strlen(a);

    // Gonna munge the 'a' string, so lets copy it first into new_array
    strncpy(new_array, a, a_len);
}

Vous venez d'écrire 78 octets dans une variable qui ne dispose que de 16 octets.

Débordements de tampon


Un débordement de tampon se produit lorsque plus de données sont écrites dans un tampon que ce qui est alloué pour ce tampon. Ce n'est pas différent pour une chaîne, sauf que de nombreuses fonctions string.h S'appuient sur cet octet nul pour signaler la fin d'une chaîne. Comme nous l'avons vu plus haut.

Dans l'exemple, nous avons écrit 78 octets dans un tampon qui n'est alloué que pour 16. Non seulement cela, mais c'est une variable locale. Ce qui signifie que le tampon a été alloué sur la pile. Maintenant, les 66 derniers octets qui ont été écrits, ils ont juste écrasé 66 octets de la pile.

Si vous écrivez suffisamment de données au-delà de la fin de ce tampon, vous écraserez l'autre variable locale a_len (Également pas bonne si vous l'utilisez plus tard), tout pointeur de cadre de pile qui a été enregistré sur la pile, puis le adresse de retour de la fonction. Maintenant, vous êtes vraiment parti et vous avez foiré les choses. Parce que maintenant l'adresse de retour est quelque chose de complètement faux. Lorsque la fin de do_something_with_a() est atteinte, de mauvaises choses se produisent.

Nous pouvons maintenant ajouter un peu plus à l'exemple ci-dessus.

void do_something_with_a(char *a, char *new_a) {
    int a_len = 0;
    char new_array[16];

    // Don't know what the length of the 'a' string is, but it's a string so
    // lets use strlen()!
    a_len = strlen(a);

    // 
    // By the way, copying anything based on a length that's not what you
    // initialized the array with is horrible horrible coding.  But it's
    // just an example.
    //
    // Gonna munge the 'a' string, so lets copy it first into new_array
    strncpy(new_array, a, a_len);

    // 'a_len' was on the stack, that we just blew away by writing 66 extra 
    // bytes to the 'new_array' buffer.  So now the first 4 bytes after 16
    // has now been written into a_len.  This can still be interpreted as
    // a signed int.  So if you use the example memory, a_len is now 0xe0f33f5a
    //
    // ... did some more munging ...
    //
    // Now I want to return the new munged string in the *new_a variable
    strncpy(new_a, new_array, a_len);

    // Everything burns

}

Je pense que mes commentaires expliquent à peu près tout. Mais à la fin, vous avez maintenant écrit une énorme quantité de données dans un tableau en pensant probablement que vous n'écrivez que 16 octets. Selon la façon dont cette vulnérabilité se manifeste, cela pourrait conduire à une exploitation via l'exécution de code à distance.

Il s'agit d'un exemple très artificiel de mauvais codage, mais vous pouvez voir comment les choses peuvent dégénérer rapidement si vous ne faites pas attention lorsque vous travaillez avec de la mémoire et copiez des données. La plupart du temps, la vulnérabilité ne sera pas aussi évidente. Avec de gros programmes, vous avez tellement de choses en cours que la vulnérabilité pourrait ne pas être facile à repérer et pourrait être déclenchée par du code appelant plusieurs fonctions.

Pour en savoir plus sur comment fonctionnent les débordements de tampon .

Et avant que quelqu'un ne le mentionne, j'ai ignoré l'endianisme lors du référencement de la mémoire par souci de simplicité


Lectures complémentaires

Description complète de la vulnérabilité
Entrée de l'énumération des faiblesses communes (CWE)
Présentation des chaînes de codage sécurisées (téléchargement automatique des fichiers PDF)
niversity of Pittsburgh - Secure Coding C/C++: String Vulnerabilities (PDF)

22
RoraΖ

Je risque d'être redondant en ajoutant une autre réponse, mais je pense que les réponses existantes pourraient ne pas répondre pleinement à ce que vous demandez. Dans une vulnérabilité de débordement de tampon traditionnelle (en particulier de la variété basée sur la pile), on essaie d'écraser le pointeur de trame sur la pile afin de faire sauter l'exécution dans le code d'exploitation lorsque la fonction actuelle essaie de revenir.

Évidemment, cela ne fonctionnera pas si la seule chose que vous (l'attaquant) pouvez faire écrire au programme après la fin du tampon est un octet zéro. Vous pouvez potentiellement faire planter le programme de cette manière en le faisant essayer de sauter vers une adresse non valide, mais ce n'est qu'un DoS et non une exécution de code à distance.

Cependant, considérez que vous obtenez le programme pour écrire une chaîne de longueur 16 dans un tampon de 16 octets que nous appellerons "A", de sorte que l'octet nul dépasse. Ensuite, vous faites écraser le programme cet octet nul par quelque chose qui n'est pas\0, donc maintenant la chaîne A n'est pas terminée par null. Si vous obtenez ensuite que le programme vous envoie le contenu de A, il sera lu au-delà de la fin de A, vous donnant potentiellement accès à toutes sortes d'informations secrètes. Heartbleed a utilisé ce type de divulgation d'informations pour voler des clés privées, ce qui est assez grave.

À ce stade, la chaîne A est en fait plus longue que le programmeur ne s'y attendait. Il n'est pas trop difficile d'imaginer que le programmeur s'appuyant sur A soit une chaîne de 16 octets et la copiant ailleurs, débordant potentiellement les autres tampons de bien plus d'un octet. Cela pourrait ensuite être utilisé pour exécuter du code arbitraire.

3
Lexelby

Comme vous l'avez indiqué dans votre réponse, les dépassements de tampon sont une vulnérabilité probable si un programmeur ne termine pas une chaîne de caractères avec un octet NULL. La raison en est que la plupart des fonctions de chaîne le supposent et continueront jusqu'à ce qu'elles rencontrent un zéro. Si vous êtes chanceux, l'erreur est suffisamment grave pour que vous obteniez une erreur de segmentation au début du développement, afin que vous puissiez déboguer et corriger le problème. Cependant, avec de nombreux bogues, un échec, cette évidence ne se produira que dans des conditions spéciales. Souvent, un attaquant peut profiter du comportement du programme et, selon les particularités de la vulnérabilité, l'exploiter afin de lire le contenu de la mémoire qui devrait être caché ou copier des données de l'entrée utilisateur dans des zones de mémoire auxquelles l'utilisateur n'était pas destiné à contrôler, etc. Si le tampon est situé sur la pile, ce dernier exploit peut être utilisé pour injecter du code, et pour écraser l'adresse de retour stockée dans le cadre de pile situé à une adresse plus élevée (sur x86) sur la pile. Certains systèmes d'exploitation ont des protections, comme des segments de mémoire non exécutables, mais c'est certainement quelque chose sur lequel vous, en tant que programmeur, ne voulez pas compter :)

Les programmeurs qui débutent dans la programmation dans des langages comme C peuvent trouver les pratiques pour éviter ces problèmes difficiles ou sujettes aux erreurs, mais finalement cela devient une seconde nature, bien qu'il soit toujours possible de faire une erreur. Entrainez-vous autant que possible, je programme en C depuis environ 7 ans, et j'ai toujours besoin de corriger une erreur de temps en temps.

Une bonne façon de s'exercer est d'allouer un tableau de caractères, de définir l'ensemble du tableau avec des caractères non imprimables ASCII autre que 0, 1 est très bien. Utilisez votre fonction de bibliothèque standard de choix pour copier une chaîne dans le tableau, évidemment, s'il plante, le programme est incorrect. Sinon, utilisez simplement une boucle for de base pour itérer sur chaque élément du tableau et imprimer la valeur numérique, assurez-vous que le 0 est là où il devrait être, si vous utilisez printf, il s'arrêtera à ce point de la chaîne. Je trouve que c'est un bon moyen d'expérimenter pour trouver les différences entre les fonctions, par exemple strcat, strncat, strcpy, strlcpy, strlcpy, strlcat, sprint, etc. Je recommanderais d'utiliser strlcpy et strlcat sur strncpy et strncat pour la plupart des choses, ils sont beaucoup plus faciles et moins sujets aux erreurs.

Autre astuce, s'il vous semble difficile de valider votre algorithme dans votre tête, imaginez que vous faites la même chose sur une entrée extrêmement petite. Pour les chaînes, imaginez que vous effectuez l'opération sur une chaîne avec seulement de la place pour 1 caractère plus l'octet NULL. Cela permet de voir facilement de nombreuses propriétés des chaînes de caractères qui nécessitent autrement plus de travail cérébral. Par exemple, vous devez allouer un tableau avec 2 éléments, même si vous devez stocker un seul caractère. Le deuxième élément, str [1], doit évidemment être 0. strlen indiquera que la longueur de la chaîne est 1. Maintenant, vous pouvez facilement généraliser pour savoir que strlen (str) est toujours l'index de l'octet NULL (en supposant qu'il est NULL terminé bien sûr :), de même strlen (str) - 1 où> 0 est toujours l'index du dernier caractère de la chaîne. La quantité de stockage nécessaire pour la chaîne et l'octet NULL est toujours strlen (str) + 1.

Une dernière chose. Il est important de noter que les chaînes terminées par NULL ne sont qu'une convention. Il y a eu et il existe de nombreuses alternatives possibles. Il n'est nécessaire que si vous utilisez des fonctions qui supposent que l'octet NULL indique le point en mémoire où il doit cesser de faire des choses. C'est le cas des fonctions de chaîne dans libc. Vous pouvez écrire vos propres fonctions de chaîne qui stockent la longueur ajoutée au début de la chaîne. Au prix d'une certaine complexité supplémentaire introduite par le type punning requis pour les chaînes de longueur dépassant 255 caractères et en conservant ce nombre chaque fois que la chaîne est mise à jour, cette approche a l'avantage de trouver la longueur de la chaîne dans O(1) heure au lieu de O (n). Vous pouvez également stocker un pointeur de chaîne et une valeur de longueur dans une structure, bien que cela ne puisse pas être généralisé avec précision aux chaînes ne se trouvant pas sur le tas. La plupart des programmeurs juste vous dire que vous devriez vous en tenir à des représentations de chaînes standard pour la plupart des choses, et elles ont probablement raison. Mais si c'est votre propre code, qui devrait vous dire quoi faire, c'est votre machine de calcul universelle (approximation finie au moins), explorez le paysage du calcul et faites-en votre bac à sable, et amusez-vous!

1
user3259161

Cela flotte dans les réponses ci-dessus, mais je pense que cela devrait être explicite. La gestion des tableaux de caractères de C/C++ présente un certain nombre de risques potentiels de coup par coup ... Exemples:

""  // a zero length string requiring one byte of storage
    // in memory:  00

"Hi."  // a length 3 string requiring four bytes of storage
       // in memory:  48 69 2e 00
"Hi."[3]  // is the 00, the characters in a string and a string array are indexed starting at 0, to wit
"Hi."[0]  // is the 'H'.

char foo[3]  // a length three character array requiring three bytes of storage
     bar[4]  // a length four character array requiring four bytes of storage

strncpy(foo, "Hi.", 3)  // copies three characters from a length three string to a length three character array.  
                        // The result is not a string because the null is not copied.

strcpy(foo, "Hi.")  // copies four characters from a length three string to a length three character array
                    // This causes overrun of the array.
                    // It writes 00 on whatever (if anything) is allocated next in storage.

strcpy(bar, "Hi.")  // copies four characters from a length three string to a length four character array.
                    // This works/is safe (enough).

Donc

  • une chaîne de trois longueurs contient quatre caractères.
  • une chaîne de trois longueurs ne rentre pas dans un tableau de trois caractères de longueur
  • copier trois caractères d'une chaîne de trois longueurs ne copie pas la chaîne
  • si mystring est une chaîne de longueur n, mystring [n] est le 00 de fin. Par conséquent, on peut raisonner brièvement (ou pas du tout) que la copie jusqu'au - n - le e caractère copiera le 00.

Ou, pour résumer, cela est conçu au maximum pour provoquer des erreurs de coup par coup.

1
Eric Towers