web-dev-qa-db-fra.com

Pourquoi les programmes écrits en C et C ++ sont-ils si fréquemment vulnérables aux attaques par débordement?

Quand je regarde les exploits des dernières années liés aux implémentations, je vois que beaucoup d'entre eux sont en C ou C++, et beaucoup sont des attaques par débordement.

  • Heartbleed était un débordement de tampon dans OpenSSL;
  • Récemment, un bogue dans la glibc a été trouvé qui permettait des débordements de tampon pendant la résolution DNS;

c'est juste ceux auxquels je peux penser en ce moment, mais je doute que ce soient les seuls que A) sont pour les logiciels écrits en C ou C++ et B) sont basés sur un débordement de tampon.

En particulier concernant le bug de la glibc, j'ai lu un commentaire qui déclare que si cela s'était produit en JavaScript plutôt qu'en C, il n'y aurait pas eu de problème. Même si le code était juste compilé en Javascript, cela n'aurait pas été un problème.

Pourquoi le C et le C++ sont-ils si vulnérables aux attaques par débordement?

133
Nzall

C et C++, contrairement à la plupart des autres langages, ne vérifient traditionnellement pas les débordements. Si le code source dit de mettre 120 octets dans un tampon de 85 octets, le CPU le fera avec plaisir. Ceci est lié au fait que si C et C++ ont une notion de array, cette notion est uniquement à la compilation. Au moment de l'exécution, il n'y a que des pointeurs, il n'y a donc pas de méthode d'exécution pour vérifier un accès au tableau en ce qui concerne la longueur conceptuelle de ce tableau.

En revanche, la plupart des autres langages ont une notion de tableau qui survit au moment de l'exécution, de sorte que tous les accès au tableau peuvent être systématiquement vérifiés par le système d'exécution. Cela n'élimine pas les débordements: si le code source demande quelque chose de non-sens comme écrire 120 octets dans un tableau de longueur 85, cela n'a toujours aucun sens. Cependant, cela déclenche automatiquement une condition d'erreur interne (souvent une "exception", par exemple un ArrayIndexOutOfBoundException en Java) qui interrompt l'exécution normale et ne laisse pas le code continuer. Cela perturbe l'exécution et implique souvent l'arrêt du traitement complet (le thread meurt), mais il empêche normalement l'exploitation au-delà d'un simple déni de service.

Fondamentalement, les exploits de débordement de tampon nécessitent que le code fasse le débordement (lecture ou écriture au-delà des limites du tampon accédé) et pour continuer à faire des choses au-delà de ce débordement. La plupart des langages modernes, contrairement au C et C++ (et à quelques autres tels que Forth ou Assembly), ne permettent pas vraiment au débordement de se produire et de tirer sur le contrevenant. Du point de vue de la sécurité, c'est beaucoup mieux.

172
Thomas Pornin

Notez qu'il y a une certaine quantité de raisonnement circulaire impliqué: Les problèmes de sécurité sont souvent liés à C et C++. Mais dans quelle mesure cela est-il dû aux faiblesses inhérentes à ces langues, et dans quelle mesure parce que ce sont simplement les langues dans lesquelles la plupart des infrastructures informatiques sont écrites in?


C est destiné à être "un pas en avant de l'assembleur". Il n'y a pas de vérification de limites autre que ce que vous avez vous-même implémenté pour extraire le dernier cycle d'horloge de votre système.

C++ offre diverses améliorations par rapport à C, la plus pertinente pour la sécurité étant ses classes de conteneur (par exemple <vector> et <string>), et depuis C++ 11, des pointeurs intelligents, qui vous permettent de gérer des données sans avoir à gérer manuellement la mémoire également. Cependant, en raison d'être évolution de C au lieu d'un langage complètement nouveau, il aussi fournit toujours la mécanique de gestion de la mémoire manuelle de C, donc si vous insistez pour vous tirer dessus le pied, C++ ne fait rien pour vous en empêcher.


Alors pourquoi des choses comme SSL, bind ou noyaux OS sont-elles toujours écrites dans ces langues?

Parce que ces langages peuvent modifier directement la mémoire, ce qui les rend particulièrement adaptés à un certain type d'application de bas niveau hautes performances (comme le chiffrement, les recherches de table DNS, les pilotes matériels ... ou Java VM, d'ailleurs ;-)).

Donc, si un logiciel lié à la sécurité est violé, le chance qu'il soit écrit en C ou C++ est élevé, tout simplement parce que la plupart des logiciels liés à la sécurité est écrit en C ou C++, généralement pour des raisons historiques et/ou de performances. Et s'il est écrit en C/C++, le vecteur d'attaque principal est le dépassement de tampon.

S'il s'agissait d'un langage différent, ce serait un vecteur d'attaque différent, mais je suis sûr qu'il y aurait tout aussi bien des failles de sécurité.


L'exploitation d'un logiciel C/C++ est plus facile que l'exploitation, disons Java logiciel. La même manière que l'exploitation d'un système Windows est plus facile que l'exploitation un système Linux: le premier est omniprésent, bien compris (c.-à-d. vecteurs d'attaque bien connus, comment les trouver et comment les exploiter), et beaucoup de gens recherchent à la recherche des exploits où la récompense/l'effort le rapport est élevé.

Cela ne signifie pas que ce dernier est intrinsèquement sûr (saf er, peut-être, mais pas safe). Cela signifie que - étant la cible la plus difficile avec des avantages moindres - les Bad Boys n'y perdent pas autant de temps pour le moment.

58
DevSolar

En fait, "heartbleed" n'était pas vraiment un débordement de tampon. Pour rendre les choses plus "efficaces", ils ont mis de nombreux petits tampons dans un seul grand tampon. Le grand tampon contenait des données de divers clients. Le bogue lisait des octets qu'il n'était pas censé lire, mais il ne lisait pas réellement les données en dehors de ce grand tampon. Une langue qui aurait vérifié les débordements de tampon n'aurait pas empêché cela, car quelqu'un s'est mis en travers ou a empêché de telles vérifications de trouver le problème.

37
gnasher729

Premièrement, comme d'autres l'ont mentionné, le C/C++ est parfois caractérisé comme un assembleur de macros glorifié: il est censé être "proche du fer", comme langage de programmation au niveau du système.

Ainsi, par exemple, le langage me permet de déclarer un tableau de longueur nulle comme espace réservé alors qu'en fait, il peut représenter une section de longueur variable dans un paquet de données ou le début d'une région de longueur variable en mémoire qui est utilisée pour communiquer avec un matériel.

Malheureusement, cela signifie également que C/C++ est dangereux entre de mauvaises mains; si un programmeur déclare un tableau de 10 éléments et écrit ensuite à l'élément 101, le compilateur le compilera avec plaisir, le code s'exécutera avec joie, mettant à la poubelle tout ce qui se trouve à cet emplacement de mémoire (code, données, pile, qui sait.)

Deuxièmement, C/C++ est idiosyncrasique. Un bon exemple est les chaînes, qui sont essentiellement des tableaux de caractères. Mais chaque constante de chaîne porte un caractère de fin invisible supplémentaire. Cela a été la cause d'innombrables erreurs car (en particulier, mais pas exclusivement) les programmeurs débutants ne parviennent souvent pas à allouer cet octet supplémentaire nécessaire pour le null final.

Troisièmement, C/C++ est en fait assez ancien. Le langage a vu le jour à une époque où les attaques externes contre un système logiciel étaient pratiquement inexistantes. Les utilisateurs étaient censés être dignes de confiance et coopératifs, pas hostiles, car leur objectif était de faire fonctionner le programme, et non de le planter.

C'est pourquoi la bibliothèque C/C++ standard contient de nombreuses fonctions intrinsèquement dangereuses. Prenez strcpy (), par exemple. Il copiera n'importe quoi jusqu'à un caractère nul final. S'il ne trouve pas de caractère nul final, il continuera à copier jusqu'à ce que l'enfer se bloque, ou plus probablement, jusqu'à ce qu'il écrase quelque chose de vital et que le programme se bloque. Cela ne posait pas de problème au bon vieux temps, lorsqu'un utilisateur n'était pas censé entrer dans un champ réservé, disons, à un code postal, 16 000 caractères inutiles suivis d'un ensemble d'octets spécialement conçu pour être exécuté. après que la pile a été mise à la poubelle et que le processeur a repris l'exécution à la mauvaise adresse.

Juste pour être sûr, C/C++ n'est pas le seul langage idiosyncrasique. D'autres systèmes ont un comportement idiosyncratique différent, mais cela peut être tout aussi mauvais. Prenez les langages de programmation back-end comme PHP, et combien il est facile d'écrire du code qui permet l'injection SQL.

En fin de compte, si nous donnons aux programmeurs les outils puissants dont ils ont besoin pour faire leur travail, mais sans une formation adéquate et une sensibilisation à l'environnement de sécurité, de mauvaises choses se produiront quel que soit le langage de programmation utilisé.

25
Viktor Toth

Je vais probablement aborder certaines choses que certaines des autres réponses ont déjà énoncées ... mais ... Je trouve que la question elle-même est erronée et "vulnérable".

Comme demandé, la question suppose beaucoup sans comprendre les problèmes sous-jacents. C/C++ ne sont pas "plus vulnérables" que les autres langages. Au contraire, ils placent autant de la puissance des appareils informatiques et la responsabilité d'utiliser cette puissance, directement dans les mains du programmeur. Ainsi, la réalité de la situation est que de nombreux programmeurs écrivent du code vulnérable à l'exploitation, et comme C/C++ ne va pas très loin pour se protéger le programmeur d'eux-mêmes comme le font certains langages, leur code est plus vulnérable. Ce n'est pas un problème C/C++, car les programmes écrits en langage assembleur auraient les mêmes problèmes, par exemple.

La raison pour laquelle une telle programmation de bas niveau peut être si vulnérable est que faire des choses comme la vérification des limites de tableau/tampon peut devenir coûteux en calcul, et est très souvent inutile lors de la programmation défensive. Imaginez, par exemple, que vous écrivez du code pour un moteur de recherche majeur, qui doit traiter des milliards d'enregistrements de base de données en un clin d'œil, afin que l'utilisateur final ne s'ennuie pas ou ne soit pas frustré lors du "chargement de la page ... est affiché. Vous ne voulez pas que votre code continue de vérifier les limites des tableaux/tampons à chaque fois dans la boucle; Bien que cela puisse prendre des nanosecondes pour effectuer une telle vérification, ce qui est trivial si vous ne traitez que dix enregistrements, cela peut prendre jusqu'à plusieurs secondes ou minutes lorsque vous parcourez des milliards ou des milliards d'enregistrements.

Donc, au lieu de cela, vous "faites confiance" à ce que la source de données (par exemple, le "robot Web" qui analyse les sites Web et place les données dans la base de données) ait déjà vérifié les données. Cela ne devrait pas être une hypothèse déraisonnable; Pour un programme typique, vous voulez vérifier les données sur entrée, pour que le code qui traite les données puisse fonctionner à la vitesse maximale. De nombreuses bibliothèques de code adoptent également cette approche. Certains documentent même qu'ils s'attendent à ce que le programmeur ait déjà vérifié les données avant d'appeler les fonctions de bibliothèque pour agir sur les données.

Malheureusement, cependant, de nombreux programmeurs ne programment pas de manière défensive et supposent simplement que les données doivent être valides et dans des limites/paramètres sûrs. Et c'est ce qui est exploité par les attaquants.

Certains langages de programmation sont conçus de telle sorte qu'ils essaient de protéger le programmeur de ces mauvaises pratiques de programmation en insérant automatiquement des vérifications supplémentaires dans le programme généré, que le programmeur n'a pas explicitement écrites dans leur code. Encore une fois, cela ne pose aucun problème lorsque vous ne parcourez le code que quelques centaines de fois ou moins. Mais lorsque vous passez par des milliards ou des milliards d'itérations, cela entraîne de longs retards dans le traitement des données, qui peuvent devenir inacceptables. C'est donc un compromis lors du choix de la langue à utiliser pour un morceau de code particulier, et à quelle fréquence et où vous vérifiez les conditions potentiellement dangereuses/exploitables dans les données.

4
C. M.

Fondamentalement, les programmeurs sont des gens paresseux (y compris moi-même). Ils font des choses comme utiliser gets () au lieu de fgets () et définir des tampons d'E/S sur la pile et ne pas chercher suffisamment de façons dont la mémoire pourrait être écrasée involontairement (enfin involontairement pour le programmeur, intentionnellement pour le pirate :).

2
Bing Bang

Il existe une grande quantité de code C existant qui effectue une écriture non contrôlée dans les tampons. Une partie de cela se trouve dans les bibliothèques. Ce code est dangereusement exploitable si un état externe peut changer la longueur écrite, et seulement très dangereux sinon.

Il existe une plus grande quantité de code C existant qui effectue une écriture limitée dans les tampons. Si l'utilisateur dudit code fait une erreur mathématique et laisse plus d'écriture qu'il ne devrait, c'est aussi exploitable que ci-dessus. Il n'y a aucune garantie à la compilation que les calculs sont bien faits.

Il existe également une grande quantité de code C existant qui lit les décalages basés sur la mémoire. Si le décalage n'est pas vérifié comme étant valide, cela peut entraîner une fuite d'informations.

Le code C++ est souvent utilisé comme langage de haut niveau pour interopérer avec C, donc de nombreuses conceptions C sont suivies, et les bogues de communication avec les API C sont courants.

Il existe des styles de programmation C++ qui empêchent de tels dépassements, mais il ne faut qu'une erreur pour les autoriser.

De plus, le problème des pointeurs pendants, où les ressources mémoire sont recyclées et le pointeur pointe maintenant vers la mémoire avec une durée de vie/structure différente de celle à l'origine, permet certains types d'exploits et de fuites d'informations.

Ce type d'erreurs - erreurs "fencepost", erreurs "pointeur pendant" - est si commun et si difficile à éliminer complètement que de nombreux langages ont été développés avec des systèmes conçus explicitement pour les empêcher de se produire .

Sans surprise, dans les langues conçues pour éliminer ces erreurs, ces erreurs ne se produisent pas aussi souvent. Ils se produisent encore parfois: soit le moteur exécutant le langage a le problème, soit une situation manuelle est configurée qui correspond à l'environnement du cas C/C++ (réutilisation des objets dans un pool, en utilisant un grand tampon commun commun subdivisé par le consommateur, etc. ). Mais parce que ces utilisations sont plus rares, le problème se produit moins souvent.

Chaque allocation dynamique, chaque utilisation de tampon en C/C++ présente ces risques. Et être parfait n'est pas réalisable.

1
Yakk

Les langages les plus couramment utilisés (Java et Ruby, par exemple) se compilent en code qui s'exécute dans une machine virtuelle. Le VM est conçu pour séparer le code machine, les données et généralement la pile. Cela signifie que les opérations de langage régulières ne peuvent pas changer le code ou rediriger le flux de contrôle (il existe parfois des API spéciales qui peuvent faire par exemple pour le débogage).

C et C++ sont généralement compilés directement dans le langage machine natif du CPU - cela offre des avantages de performances et de flexibilité, mais signifie qu'un code erroné peut écraser la mémoire ou la pile du programme et ainsi exécuter des instructions qui ne sont pas dans le programme d'origine.

Cela se produit généralement lorsqu'un tampon est (peut-être délibérément) dépassé en C++. Dans Java ou Ruby, en revanche, un dépassement de tampon provoquera immédiatement une exception et ne peut pas (sauf VM bugs) écraser le code ou modifier le flux de contrôle).

0
Rich