Pourquoi est-il si difficile de rendre le C moins sujet aux débordements de tampon?


23

Je fais un cours à l'université, où l'un des laboratoires est d'effectuer des exploits de débordement de tampon sur le code qu'ils nous donnent. Cela va de simples exploits comme changer l'adresse de retour d'une fonction sur une pile pour revenir à une fonction différente, jusqu'au code qui modifie un registre de programmes / un état de mémoire mais revient ensuite à la fonction que vous avez appelée, ce qui signifie que le la fonction que vous avez appelée est complètement inconsciente de l'exploit.

J'ai fait des recherches à ce sujet, et ces types d'exploits sont utilisés à peu près partout, même maintenant, dans des choses comme l' exécution de homebrew sur la Wii et le jailbreak sans connexion pour iOS 4.3.1

Ma question est pourquoi ce problème est-il si difficile à résoudre? Il est évident que c'est un exploit majeur utilisé pour pirater des centaines de choses, mais il semble qu'il serait assez facile à corriger en tronquant simplement toute entrée au-delà de la longueur autorisée et en purifiant simplement toutes les entrées que vous prenez.

EDIT: Une autre perspective que j'aimerais que les réponses prennent en compte - pourquoi les créateurs de C ne résolvent-ils pas ces problèmes en réimplémentant les bibliothèques?

Réponses:


35

Ils ont réparé les bibliothèques.

Toute bibliothèque standard moderne C contient plus sûres variantes strcpy, strcat, sprintfet ainsi de suite.

Sur les systèmes C99 - qui sont la plupart des Unix - vous les trouverez avec des noms comme strncatet snprintf, le "n" indiquant qu'il faut un argument de la taille d'un tampon ou d'un nombre maximal d'éléments à copier.

Ces fonctions peuvent être utilisées pour gérer de nombreuses opérations de manière plus sécurisée, mais rétrospectivement leur facilité d'utilisation n'est pas excellente. Par exemple, certaines snprintfimplémentations ne garantissent pas que le tampon se termine par null. strncatprend un certain nombre d'éléments à copier, mais de nombreuses personnes dépassent par erreur la taille du tampon dest.

Sous Windows, on trouve souvent le strcat_s, sprintf_sle suffixe "_s" indiquant "sûr". Celles-ci ont également trouvé leur chemin dans la bibliothèque standard C en C11, et offrent plus de contrôle sur ce qui se passe en cas de débordement (troncature vs assert par exemple).

De nombreux fournisseurs proposent encore plus d'alternatives non standard comme asprintfdans la bibliothèque GNU, qui allouera automatiquement un tampon de la taille appropriée.

L'idée que vous pouvez "simplement corriger C" est un malentendu. La correction de C n'est pas le problème - et a déjà été effectuée. Le problème consiste à corriger des décennies de code C écrit par des programmeurs ignorants, fatigués ou pressés, ou du code qui a été transféré de contextes où la sécurité importait peu à des contextes où la sécurité l'est. Aucune modification de la bibliothèque standard ne peut corriger ce code, bien que la migration vers des compilateurs et des bibliothèques standard plus récents puisse souvent aider à identifier automatiquement les problèmes.


11
+1 pour viser le problème sur les programmeurs, pas sur la langue.
Nicol Bolas

8
@Nicol: Dire "le problème [est] les programmeurs" est injustement réductionniste. Le problème est que pendant des années (des décennies) C a facilité l'écriture de code non sécurisé que le code sécurisé, d'autant plus que notre définition de "sûr" a évolué plus rapidement que n'importe quelle norme de langage, et que ce code est toujours là. Si vous voulez essayer de réduire cela à un seul nom, le problème est "libc 1970-1999", pas "les programmeurs".

1
Il incombe toujours aux programmeurs d'utiliser les outils dont ils disposent actuellement pour résoudre ces problèmes. Prenez une demi-journée environ et faites quelques recherches dans le code source pour ces choses.
Nicol Bolas

1
@Nicol: Bien que trivial pour détecter un débordement de tampon potentiel, il n'est souvent pas trivial d'être certain qu'il s'agit d'une menace réelle, et moins trivial de déterminer ce qui devrait se produire si le tampon venait à déborder. La gestion des erreurs n'est / n'a souvent pas été envisagée, il n'est pas possible d'implémenter "rapidement" une amélioration car vous pouvez modifier le comportement d'un module de manière inattendue. Nous venons de le faire dans une base de code héritée de plusieurs millions de lignes, et bien qu'un exercice valable en ait coûté beaucoup de temps (et d'argent).
mattnz

4
@NicolBolas: Je ne sais pas dans quel type de magasin vous travaillez, mais le dernier endroit où j'ai écrit C pour la production a nécessité de modifier le document de conception détaillée, de le réviser, de changer le code, de modifier le plan de test, de revoir le plan de test, d'effectuer une analyse complète test du système, examen des résultats du test, puis recertification du système sur le site du client. Il s'agit d'un système de télécommunications sur un autre continent écrit pour une entreprise qui n'existe plus. Pour la dernière fois que je savais, la source était dans une archive RCS sur une bande QIC qui devrait toujours être lisible, si vous pouvez trouver un lecteur de bande approprié.
TMN

19

Il n'est pas vraiment inexact de dire que C est en fait "sujet aux erreurs" de par sa conception . Mis à part quelques erreurs graves comme gets, le langage C ne peut pas vraiment être autrement sans perdre la fonctionnalité principale qui attire les gens vers C en premier lieu.

C a été conçu comme un langage système pour agir comme une sorte d '«assemblage portable». Une caractéristique majeure du langage C est que, contrairement aux langages de niveau supérieur, le code C correspond souvent très étroitement au code machine réel. En d'autres termes, ce ++in'est généralement qu'une incinstruction, et vous pouvez souvent avoir une idée générale de ce que le processeur fera au moment de l'exécution en consultant le code C.

Mais l'ajout de la vérification implicite des limites ajoute beaucoup de surcharge supplémentaire - surcharge que le programmeur n'a pas demandée et pourrait ne pas vouloir. Cette surcharge va bien au-delà du stockage supplémentaire requis pour stocker la longueur de chaque baie, ou des instructions supplémentaires pour vérifier les limites de la baie à chaque accès à la baie. Et l'arithmétique des pointeurs? Ou si vous avez une fonction qui prend un pointeur? L'environnement d'exécution n'a aucun moyen de savoir si ce pointeur tombe dans les limites d'un bloc de mémoire légitimement alloué. Afin de garder une trace de cela, vous auriez besoin d'une architecture d'exécution sérieuse qui puisse comparer chaque pointeur par rapport à une table de blocs de mémoire actuellement alloués, moment auquel nous entrons déjà dans le territoire d'exécution géré de style Java / C #.


12
Honnêtement, quand les gens demandent pourquoi C n'est pas "sûr", je me demande s'ils se plaindraient que l'assemblage n'est pas "sûr".
Ben Brocka

5
Le langage C ressemble beaucoup à un assemblage portable sur une machine PDP-11 de Digital Equipment Corporation. En même temps , les machines Burroughs avaient des limites du tableau vérifier dans la CPU, donc ils étaient vraiment faciles à obtenir des programmes en plein contrôle Array dans la vie matérielle dans le matériel Rockwell Collins. ( La plupart du temps utilisé dans l' aviation.)
Tim Williscroft

15

Je pense que le vrai problème n'est pas que ces types de bugs sont difficiles à corriger, mais qu'ils sont si faciles à faire: si vous utilisez strcpy, sprintfet vos amis de la manière (apparemment) la plus simple qui puisse fonctionner, alors vous avez probablement ouvert la porte pour un débordement de tampon. Et personne ne le remarquera jusqu'à ce que quelqu'un l'exploite (sauf si vous avez de très bonnes critiques de code). Ajoutez maintenant le fait qu'il existe de nombreux programmeurs médiocres et qu'ils sont sous la pression du temps la plupart du temps - et vous avez une recette de code qui est tellement criblée de débordements de tampon qu'il sera difficile de les corriger tous simplement parce qu'il y a tant d'entre eux et ils se cachent si bien.


3
Vous n'avez pas vraiment besoin de "très bonnes revues de code". Vous avez juste besoin d'interdire sprintf, ou de redéfinir sprintf en quelque chose qui utilise sizeof () et des erreurs sur la taille d'un pointeur, ou etc. Vous n'avez même pas besoin de révisions de code, vous pouvez faire ce genre de choses avec la validation SCM crochets et grep.

1
@JoeWreschnig: sizeof(ptr)vaut 4 ou 8, en général. C'est une autre limitation en C: il n'y a aucun moyen de déterminer la longueur d'un tableau, étant donné juste le pointeur vers celui-ci.
MSalters

@MSalters: Oui, un tableau d'int [1] ou de char [4] ou tout ce qui peut être un faux positif, mais en pratique, vous ne manipulez jamais de tampons de cette taille avec ces fonctions. (Je ne parle pas théoriquement ici - j'ai travaillé sur une grande base de code C pendant quatre ans qui a utilisé cette approche. Je n'ai jamais atteint la limitation du sprintfing dans un caractère [4].)

5
@BlackJack: La plupart des programmeurs ne sont pas stupides - si vous les forcez à passer la taille, ils passeront la bonne. C'est juste que la taille ne passera pas à moins d'être forcée. Vous pouvez écrire une macro qui renverra la longueur d'un tableau s'il est statique ou dimensionné automatiquement, mais des erreurs si un pointeur lui est attribué. Ensuite, vous redéfinissez sprintf pour appeler snprintf avec cette macro donnant la taille. Vous avez maintenant une version de sprintf qui ne fonctionne que sur des tableaux de tailles connues et oblige le programmeur à appeler snprintf avec une taille spécifiée manuellement sinon.

1
Un exemple simple d'une telle macro serait de #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / (sizeof(a) != sizeof(void *))déclencher une division par zéro au moment de la compilation. Un autre astucieux que j'ai vu pour la première fois dans Chromium est celui #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / !(sizeof(a) % sizeof((a)[0]))qui échange la poignée de faux positifs pour certains faux négatifs - malheureusement, il est inutile pour char []. Vous pouvez utiliser diverses extensions du compilateur pour le rendre encore plus fiable, par exemple blogs.msdn.com/b/ce_base/archive/2007/05/08/… .

7

Il est difficile de corriger les dépassements de tampon car C ne fournit pratiquement aucun outil utile pour résoudre le problème. C'est un défaut de langage fondamental que les tampons natifs n'offrent aucune protection et il est pratiquement, sinon complètement, impossible de les remplacer par un produit supérieur, comme C ++ l'a fait avec std::vectoret std::array, et il est difficile, même en mode débogage, de trouver des débordements de tampon.


13
"Défaut de langue" est une affirmation terriblement biaisée. Le fait que les bibliothèques n'aient pas fourni de vérification des limites était un défaut; que la langue n'a pas été un choix conscient pour éviter les frais généraux. Ce choix fait partie de ce qui permet aux constructions de niveau supérieur comme std::vectord'être mises en œuvre efficacement. Et vector::operator[]fait le même choix pour la vitesse sur la sécurité. La sécurité vectorréside dans le fait de faciliter le cadrage autour de la taille, ce qui est la même approche que les bibliothèques C modernes.

1
@Charles: "C ne fournit simplement aucune sorte de tampons à expansion dynamique dans le cadre de la bibliothèque standard." Non, cela n'a rien à voir avec ça. Tout d'abord, C les fournit via realloc(C99 vous permet également de dimensionner les tableaux de pile en utilisant une taille déterminée par l'exécution mais constante via n'importe quelle variable automatique, presque toujours préférable à char buf[1024]). Deuxièmement, le problème n'a rien à voir avec l'expansion des tampons, il a à voir avec le fait que les tampons portent ou non la taille avec eux et vérifient cette taille lorsque vous y accédez.

5
@Joe: Le problème n'est pas tant que les tableaux natifs sont cassés. C'est qu'ils sont impossibles à remplacer. Pour commencer, vector::operator[]vérifie les limites en mode débogage - quelque chose que les tableaux natifs ne peuvent pas faire - et deuxièmement, il n'y a aucun moyen en C d'échanger le type de tableau natif avec un qui peut faire la vérification des limites, car il n'y a pas de modèles et aucun opérateur surcharge. En C ++, si vous souhaitez passer de T[]à std::array, vous pouvez pratiquement échanger un typedef. En C, il n'y a aucun moyen d'y parvenir, et aucun moyen d'écrire une classe avec des fonctionnalités équivalentes, et encore moins une interface.
DeadMG

3
@Joe: Sauf qu'il ne peut jamais être de taille statique et que vous ne pouvez jamais le rendre générique. Il est impossible d'écrire une bibliothèque C qui accomplit le même rôle que std::vector<T>et std::array<T, N>faire en C ++. Il n'y aurait aucun moyen de concevoir et de spécifier une bibliothèque, pas même une bibliothèque standard, qui pourrait le faire.
DeadMG

1
Je ne suis pas sûr de ce que vous entendez par «il ne peut jamais être statiquement dimensionné». Comme j'utiliserais ce terme, std::vectorne peut également jamais être de taille statique. Quant au générique, vous pouvez le rendre aussi générique que le bon C en a besoin - un petit nombre d'opérations fondamentales sur void * (ajouter, supprimer, redimensionner) et tout le reste écrit spécifiquement. Si vous allez vous plaindre que C n'a pas de génériques de style C ++, c'est bien en dehors de la portée de la gestion sécurisée des tampons.

7

Le problème est pas avec la C langue .

OMI, le seul obstacle majeur à surmonter est que le C est tout simplement mal enseigné . Des décennies de mauvaises pratiques et d'informations erronées ont été institutionnalisées dans les manuels de référence et les notes de cours, empoisonnant dès le départ l'esprit de chaque nouvelle génération de programmeurs. Les étudiants reçoivent une brève description des fonctions d'E / S «faciles» comme gets1 ou scanfpuis sont laissés à eux-mêmes. On ne leur dit pas où ni comment ces outils peuvent échouer, ni comment prévenir ces échecs. On ne leur dit pas d'utiliser fgetsetstrtol/strtodparce que ceux-ci sont considérés comme des outils "avancés". Ensuite, ils se déchaînent sur le monde professionnel pour faire des ravages. Ce n'est pas que beaucoup des programmeurs les plus expérimentés connaissent mieux, car ils ont reçu la même éducation cérébrale. C'est exaspérant. Je vois tellement de questions ici et sur Stack Overflow et sur d'autres sites où il est clair que la personne qui pose la question est enseignée par quelqu'un qui ne sait tout simplement pas de quoi il parle , et bien sûr, vous ne pouvez pas simplement dire "votre professeur a tort", car il est professeur et vous n'êtes qu'un gars sur Internet.

Et puis vous avez la foule qui dédaigne toute réponse commençant par, "bien, selon la norme de langue ..." parce qu'ils travaillent dans le monde réel et selon eux, la norme ne s'applique pas au monde réel . Je peux traiter avec quelqu'un qui a juste une mauvaise éducation, mais quiconque insiste pour être ignorant n'est qu'un fléau pour l'industrie.

Il n'y aurait pas de problèmes de dépassement de tampon si la langue était enseignée correctement en mettant l'accent sur l'écriture de code sécurisé. Ce n'est pas "dur", ce n'est pas "avancé", c'est juste d'être prudent.

Oui, cela a été une diatribe.


1 Ce qui, heureusement, a finalement été retiré de la spécification du langage, bien qu'il se cache à jamais dans 40 ans de code hérité.


1
Bien que je sois principalement d'accord avec vous, je pense que vous êtes toujours un peu injuste. Ce que nous considérons comme "sûr" est également une fonction du temps (et je vois que vous êtes un développeur de logiciels professionnel beaucoup plus longtemps que moi, donc je suis sûr que vous le connaissez). Dans dix ans, quelqu'un aura cette même conversation sur la raison pour laquelle tout le monde en 2012 a utilisé des implémentations de table de hachage compatibles DoS, ne savions-nous rien sur la sécurité? S'il y a un problème dans l'enseignement, c'est que nous nous concentrons trop sur l'enseignement des "meilleures" pratiques, et non que les meilleures pratiques elles-mêmes évoluent.

1
Et soyons honnêtes. Vous pouvez écrire du code sûr avec juste sprintf, mais cela ne signifie pas que le langage n'était pas défectueux. C était défectueux et est défectueux - comme n'importe quel langage - et il est important que nous admettions ces défauts afin que nous puissions continuer à les corriger.

@JoeWreschnig - Bien que je sois d'accord avec le point le plus important, je pense qu'il y a une différence qualitative entre les implémentations de table de hachage compatibles DoS et les dépassements de tampon. Le premier peut être attribué aux circonstances qui évoluent autour de vous, mais le second n'a aucune excuse; les dépassements de tampon sont des erreurs de codage, point final. Oui, C n'a pas de protège-lame et vous coupera si vous êtes négligent; on peut se demander si c'est une faille dans la langue ou pas. C'est orthogonal au fait que sont donnés très peu d' élèves toute instruction de sécurité quand ils apprennent la langue.
John Bode

5

Le problème tient autant à la myopie managériale qu'à l'incompétence des programmeurs. N'oubliez pas qu'une application de 90 000 lignes n'a besoin que d' une seule opération non sécurisée pour être totalement non sécurisée. Il est presque hors de portée que toute application écrite au-dessus d'une gestion de chaîne fondamentalement non sécurisée soit 100% parfaite - ce qui signifie qu'elle sera non sécurisée.

Le problème est que les coûts liés à l'insécurité ne sont pas facturés au bon destinataire (la société qui vend l'application ne devra presque jamais rembourser le prix d'achat), ou ne sont pas clairement visibles au moment où les décisions sont prises ("Nous devons expédier en mars quoi qu'il arrive! "). Je suis à peu près certain que si vous preniez en compte les coûts à long terme et les coûts pour vos utilisateurs plutôt que pour le profit de votre entreprise, l'écriture en C ou dans des langues apparentées serait beaucoup plus chère, probablement si chère que ce n'est clairement pas le bon choix dans de nombreux pays. des domaines où de nos jours la sagesse conventionnelle dit que c'est une nécessité. Mais cela ne changera que si une responsabilité logicielle beaucoup plus stricte est introduite - ce que personne dans l'industrie ne veut.


-1: Blâmer la gestion comme la racine de tout mal n'est pas particulièrement constructif. Ignorer l'histoire un peu moins. La réponse est presque rachetée par la dernière phrase.
mattnz

Une responsabilité logicielle plus stricte pourrait être introduite par les utilisateurs intéressés par la sécurité et prêts à payer pour cela. On pourrait sans doute l'introduire en imposant des sanctions sévères pour les atteintes à la sécurité. Une solution basée sur le marché fonctionnerait si les utilisateurs étaient prêts à payer pour la sécurité, mais ce n'est pas le cas.
David Thornley

4

L'un des grands pouvoirs de l'utilisation de C est qu'il vous permet de manipuler la mémoire comme bon vous semble.

L'une des grandes faiblesses de l'utilisation de C est qu'elle vous permet de manipuler la mémoire comme bon vous semble.

Il existe des versions sûres de toutes les fonctions dangereuses. Cependant, les programmeurs et le compilateur n'appliquent pas strictement leur utilisation.


2

pourquoi les créateurs de C ne résolvent-ils pas ces problèmes en réimplémentant les bibliothèques?

Probablement parce que C ++ l'a déjà fait et est rétrocompatible avec le code C. Donc, si vous voulez un type de chaîne sûr dans votre code C, vous utilisez simplement std :: string et écrivez votre code C à l'aide d'un compilateur C ++.

Le sous-système de mémoire sous-jacent peut aider à empêcher les débordements de tampon en introduisant des blocs de garde et en vérifiant leur validité - de sorte que toutes les allocations ont 4 octets de `` fefefefe '' ajoutés, lorsque ces blocs sont écrits, le système peut lancer un wobbler. Ce n'est pas garanti d'empêcher une écriture en mémoire, mais cela montrera que quelque chose s'est mal passé et doit être corrigé.

Je pense que le problème est que les anciennes routines strcpy etc. sont toujours présentes. S'ils étaient supprimés en faveur de strncpy, etc., cela aiderait.


1
Supprimer complètement strcpy etc. rendrait les chemins de mise à niveau incrémentielle encore plus difficiles, ce qui entraînerait à son tour une absence de mise à niveau. La façon dont cela se fait maintenant, vous pouvez basculer vers un compilateur C11, puis commencer à utiliser les variantes _s, puis interdire les variantes non _s, puis corriger l'utilisation existante, quelle que soit la période de temps pratiquement viable.

-2

Il est simple de comprendre pourquoi le problème de débordement n'est pas résolu. C était défectueux dans quelques domaines. À l'époque, ces défauts étaient considérés comme tolérables ou même comme une caractéristique. Maintenant, des décennies plus tard, ces défauts ne peuvent pas être corrigés.

Certaines parties de la communauté de programmation ne veulent pas que ces trous soient bouchés. Il suffit de regarder toutes les guerres de flammes qui recommencent sur les chaînes, les tableaux, les pointeurs, la collecte des ordures ...


5
LOL, réponse terrible et mal dirigée.
Heath Hunnicutt

1
Pour expliquer pourquoi c'est une mauvaise réponse: C a en effet de nombreux défauts, mais autoriser les débordements de tampon, etc. a très peu à voir avec eux, mais avec les exigences de base du langage. Il ne serait pas possible de concevoir un langage pour faire le travail de C et de ne pas autoriser les débordements de tampon. Certaines parties de la communauté ne veulent pas renoncer aux capacités que C leur permet, souvent avec raison. Il existe également des désaccords sur la façon d'éviter certains de ces problèmes, montrant que nous n'avons pas une compréhension complète de la conception du langage de programmation, rien de plus.
David Thornley

1
@DavidThornley: On pourrait concevoir un langage pour faire le travail de C mais faire en sorte que les façons idiomatiques normales de faire permettent au moins à un compilateur de vérifier les débordements de tampon de manière raisonnablement efficace, si le compilateur choisit de le faire. Il y a une énorme différence entre la memcpy()disponibilité et la seule utilisation standard de la copie efficace d'un segment de tableau.
supercat
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.