web-dev-qa-db-fra.com

À quel point est-il dangereux d'accéder à un tableau en dehors des limites?

Quel est le degré de danger pour accéder à un tableau en dehors de ses limites (en C)? Il peut parfois arriver que je lise de l'extérieur du tableau (je comprends maintenant que j'accède ensuite à la mémoire utilisée par d'autres parties de mon programme ou même au-delà) ou que j'essaie de définir une valeur pour un index situé en dehors du tableau. Le programme se bloque parfois, mais parfois s’exécute, ne donnant que des résultats inattendus.

Maintenant, ce que je voudrais savoir, c’est vraiment dangereux. Si cela endommage mon programme, ce n'est pas si grave. Si par contre cela casse quelque chose en dehors de mon programme, parce que j'ai réussi à accéder à une mémoire totalement indépendante, alors c'est très mauvais, j'imagine. J'ai lu beaucoup de "tout peut arriver", "la segmentation pourrait être le moindre problème" , "votre disque dur peut devenir rose et des licornes chanter sous votre fenêtre", ce qui est bien, mais quel est vraiment le danger?

Mes questions:

  1. La lecture de valeurs hors du tableau peut-elle endommager quoi que ce soit en dehors de mon programme? J'imagine que le simple fait de regarder les choses ne change rien, ou cela changerait-il par exemple l'attribut de "dernière ouverture" d'un fichier que je suis parvenu à atteindre?
  2. La définition de valeurs hors du tableau peut-elle endommager quoi que ce soit en dehors de mon programme? De ceci question de débordement de pile Je suppose qu'il est possible d'accéder à n'importe quel emplacement de mémoire, il n'y a aucune garantie de sécurité.
  3. Je lance maintenant mes petits programmes depuis XCode. Est-ce que cela fournit une protection supplémentaire autour de mon programme lorsqu'il ne peut pas accéder à l'extérieur de sa propre mémoire? Peut-il nuire à XCode?
  4. Des recommandations sur la façon d’exécuter mon code intrinsèquement buggy en toute sécurité?

J'utilise OSX 10.7, Xcode 4.6.

218
ChrisD

En ce qui concerne la norme ISO C (la définition officielle du langage), accéder à un tableau en dehors de ses limites a un comportement " indéfini ". Le sens littéral de ceci est:

comportement, lors de l'utilisation d'une construction de programme non portable ou erronée ou de données erronées, pour lequel la présente Norme internationale n'impose aucune exigence

Une note non normative développe ceci:

Le comportement non défini possible peut aller d’ignorer complètement la situation avec des résultats imprévisibles, de se comporter pendant la traduction ou l’exécution du programme d’une manière documentée caractéristique de l’environnement (avec ou sans émission d’un message de diagnostic), en mettant fin à la traduction ou à l’exécution (avec la publication). d'un message de diagnostic).

Donc c'est la théorie. Quelle est la réalité?

Dans le "meilleur" cas, vous accéderez à une partie de la mémoire appartenant à votre programme en cours d'exécution (ce qui pourrait entraîner un mauvais comportement de votre programme), ou qui n'est pas appartenant à votre programme en cours d'exécution (ce qui entraînera probablement le blocage de votre programme avec quelque chose comme une erreur de segmentation). Ou vous pouvez essayer d'écrire en mémoire que votre programme possède, mais c'est marqué en lecture seule; cela causera probablement aussi le blocage de votre programme.

Cela suppose que votre programme s'exécute sous un système d'exploitation qui tente de protéger les processus simultanément exécutés les uns des autres. Si votre code s'exécute sur le "bare metal", par exemple, s'il fait partie d'un noyau de système d'exploitation ou d'un système intégré, il n'y a pas de telle protection; votre code se comportant mal est ce qui était censé fournir cette protection. Dans ce cas, les possibilités de dommages sont considérablement plus grandes, y compris, dans certains cas, de dommages physiques au matériel (ou à des objets ou à des personnes à proximité).

Même dans un environnement de système d'exploitation protégé, les protections ne sont pas toujours à 100%. Il y a des bogues de système d'exploitation qui permettent aux programmes sans privilèges d'obtenir un accès root (administratif), par exemple. Même avec les privilèges de l'utilisateur ordinaire, un programme qui ne fonctionne pas correctement peut consommer des ressources excessives (CPU, mémoire, disque), voire mettre tout le système en panne. Un grand nombre de logiciels malveillants (virus, etc.) exploitent les dépassements de mémoire tampon pour obtenir un accès non autorisé au système.

(Un exemple historique: j'ai entendu dire que sur certains anciens systèmes dotés de mémoire principale , l'accès répété à un seul emplacement de mémoire dans une boucle serrée risquerait littéralement de faire fondre cette partie de la mémoire. Vous pouvez également détruire un L’affichage à tube cathodique et le déplacement de la tête de lecture/écriture d’un lecteur de disque avec la fréquence harmonique de l’armoire de lecteur, ce qui la fait traverser une table et tomber au sol.)

Et il y a toujours Skynet à s'inquiéter.

La ligne du bas est la suivante: si vous pouviez écrire un programme pour faire quelque chose de mal délibérément , il est au moins théoriquement possible qu'un programme buggy puisse faire la même chose accidentellement .

En pratique, il est très peu probable que votre programme buggy exécuté sur un système MacOS X fasse quelque chose de plus grave que le crash. Mais il n'est pas possible complètement d'empêcher le code buggy de faire de très mauvaises choses.

120
Keith Thompson

En général, les systèmes d'exploitation actuels (les plus répandus en tout cas) exécutent toutes les applications dans des régions de mémoire protégées à l'aide d'un gestionnaire de mémoire virtuelle. Il s'avère qu’il n’est pas extrêmement FACILE (par exemple) de simplement lire ou écrire dans un emplacement qui existe dans un espace REAL en dehors de la ou des régions qui ont été attribuées/attribuées à votre processus.

Réponses directes:

1) La lecture n'endommagera presque jamais directement un autre processus. Toutefois, elle peut indirectement endommager un processus si vous lisez une valeur KEY utilisée pour chiffrer, déchiffrer ou valider un programme/processus. La lecture en dehors des limites peut avoir des effets quelque peu défavorables/inattendus sur votre code si vous prenez des décisions en fonction des données que vous lisez.

2) Le seul moyen de Dommer quelque chose en écrivant dans une position accessible par une adresse mémoire est que si cette adresse mémoire est un registre matériel (un emplacement qui ne sert pas à stocker des données, mais à contrôler certaines du matériel) pas un RAM emplacement. En fait, vous ne pourrez toujours pas endommager quelque chose sauf si vous écrivez un emplacement programmable une fois qui n'est pas réinscriptible (ou quelque chose de ce genre).

3) Généralement, l'exécution à partir du débogueur exécute le code en mode débogage. Le fait de s’exécuter en mode débogage empêche TEND (mais pas toujours) d’arrêter votre code plus rapidement lorsque vous avez fait quelque chose qui n’est pas pratique ou qui est carrément illégal.

4) N'utilisez jamais de macros, utilisez des structures de données qui possèdent déjà une vérification des limites d'indice de tableau intégrée, etc.

ADDITIONNEL J'ajouterai que les informations ci-dessus ne concernent en réalité que les systèmes utilisant un système d'exploitation avec des fenêtres de protection de la mémoire. Si vous écrivez du code pour un système intégré ou même un système utilisant un système d’exploitation (temps réel ou autre) qui n’a pas de fenêtre de protection de la mémoire (ou de fenêtre adressée virtuelle), il convient de faire preuve de beaucoup plus de prudence en lecture et en écriture en mémoire. Dans ces cas également, les pratiques de codage SAFE et SECURE doivent toujours être utilisées pour éviter les problèmes de sécurité.

25
trumpetlicks

Ne pas vérifier les limites peut entraîner des effets secondaires déplaisants, notamment des failles de sécurité. L'un des plus laids est exécution de code arbitraire . Dans l'exemple classique: si vous avez un tableau de taille fixe et utilisez strcpy() pour y placer une chaîne fournie par l'utilisateur, l'utilisateur peut vous donner une chaîne qui dépasse le tampon et écrase d'autres emplacements de mémoire, y compris l'adresse du code où CPU devrait revenir lorsque votre fonction se termine.

Ce qui signifie que votre utilisateur peut vous envoyer une chaîne qui fera en sorte que votre programme appelle essentiellement exec("/bin/sh"), ce qui le transformera en Shell et exécutera tout ce qu'il souhaite sur votre système, y compris la collecte de toutes vos données et la transformation de votre machine. noeud de botnet.

Voir Smashing The Stack For Fun and Profit pour plus de détails sur la manière de procéder.

9
che

Vous écrivez:

J'ai lu beaucoup de "tout peut arriver", "la segmentation pourrait être le moindre problème", "votre disque dur pourrait virer au rose et des licornes chanter sous votre fenêtre", ce qui est tout à fait gentil, mais quel est le danger?

Disons-le ainsi: chargez une arme à feu. Pointez-le hors de la fenêtre sans but particulier ni feu. Quel est le danger?

Le problème est que vous ne savez pas. Si votre code écrase quelque chose qui bloque votre programme, tout va bien, car il l'arrêtera dans un état défini. Cependant, si cela ne se bloque pas, les problèmes commencent à se poser. Quelles ressources sont sous contrôle de votre programme et que pourrait-il leur faire? Quelles ressources pourraient contrôler votre programme et que pourrait-il leur faire? Je connais au moins un problème majeur causé par un tel débordement. Le problème était lié à une fonction statistique apparemment dépourvue de sens, qui a gâché une table de conversion non liée pour une base de données de production. Le résultat était un peu très nettoyage coûteux après. En fait, cela aurait été beaucoup moins cher et plus facile à gérer si ce problème avait formaté les disques durs ... en d'autres termes: les licornes roses pourraient être votre moindre problème.

L'idée que votre système d'exploitation va vous protéger est optimiste. Si possible, essayez d'éviter d'écrire en dehors des limites.

8
Udo Klein

Ne pas exécuter votre programme en tant qu'utilisateur root ou tout autre utilisateur privilégié ne nuira pas à votre système, aussi peut-être une bonne idée.

En écrivant des données dans un emplacement de mémoire aléatoire, vous ne causerez pas directement des dommages à tout autre programme exécuté sur votre ordinateur, chaque processus s'exécutant dans son propre espace mémoire.

Si vous essayez d'accéder à toute mémoire non allouée à votre processus, le système d'exploitation empêchera votre programme de s'exécuter avec une erreur de segmentation.

Donc, directement (sans exécuter en tant que root et accéder directement à des fichiers tels que/dev/mem), il n'y a aucun risque que votre programme interfère avec un autre programme exécuté sur votre système d'exploitation.

Néanmoins - et c'est probablement ce dont vous avez entendu parler en termes de danger - en écrivant aveuglément des données aléatoires dans des emplacements de mémoire aléatoires par accident, vous pouvez certainement endommager tout ce que vous pouvez endommager.

Par exemple, votre programme peut vouloir supprimer un fichier spécifique donné par un nom de fichier stocké quelque part dans votre programme. Si, par accident, vous écrasez simplement l'emplacement où le nom de fichier est stocké, vous risquez de supprimer un fichier très différent.

7
mikyra

Vous voudrez peut-être essayer d'utiliser l'outil memcheck dans Valgrind lorsque vous testez votre code - il n'acceptera pas les violations de limites de tableaux individuelles dans un cadre de pile , mais cela devrait englober bien d’autres types de problèmes de mémoire, y compris des problèmes susceptibles de causer des problèmes plus subtils et plus vastes en dehors du cadre d’une seule fonction.

Du manuel:

Memcheck est un détecteur d’erreur de mémoire. Il peut détecter les problèmes suivants, courants dans les programmes C et C++.

  • Pour accéder à la mémoire, vous ne devriez pas, par exemple. surcharger et sous-exécuter les blocs de tas, surcharger le haut de la pile et accéder à la mémoire après sa libération.
  • Utilisation de valeurs non définies, c’est-à-dire des valeurs qui n’ont pas été initialisées ou qui ont été dérivées d’autres valeurs non définies.
  • Libération incorrecte de la mémoire de segment, telle que la suppression de blocs de segment à double libération, ou l'utilisation incorrecte de malloc/new/new [] versus free/delete/delete []
  • Chevauchement de pointeurs src et dst dans memcpy et fonctions connexes.
  • Fuites de mémoire.

ETA: Toutefois, comme le dit la réponse de Kaz, ce n'est pas une panacée et ne donne pas toujours le résultat le plus utile, en particulier lorsque vous utilisez = excitant modèles d'accès.

4
Aesin

Si vous faites de la programmation au niveau des systèmes ou des systèmes intégrés, de très mauvaises choses peuvent arriver si vous écrivez dans des emplacements de mémoire aléatoires. Les systèmes plus anciens et de nombreux micro-contrôleurs utilisent des E/S mappées en mémoire. L'écriture dans un emplacement mémoire mappé sur un registre de périphérique peut donc causer des dégâts, en particulier si elle est effectuée de manière asynchrone.

Un exemple est la programmation de la mémoire flash. Le mode de programmation sur les puces mémoire est activé en écrivant une séquence spécifique de valeurs dans des emplacements spécifiques à l'intérieur de la plage d'adresses de la puce. Si un autre processus écrit en un autre endroit de la puce pendant son exécution, le cycle de programmation échouera.

Dans certains cas, le matériel encapsule les adresses (la plupart des bits/octets d’adresse sont ignorés). Par conséquent, si vous écrivez sur une adresse au-delà de la fin de l’espace adresse physique, les données seront écrites au beau milieu.

Enfin, les anciens processeurs tels que le MC68000 peuvent être verrouillés au point que seule une réinitialisation matérielle peut les remettre en marche. Je n'y ai pas travaillé depuis plusieurs décennies, mais je crois que lorsque quelqu'un essayait de gérer une exception (une mémoire inexistante) tentait de gérer une exception, il s'arrêtait simplement jusqu'à ce que la réinitialisation matérielle soit confirmée.

Ma plus grande recommandation est une fiche flagrante pour un produit, mais je n'y porte aucun intérêt personnel et je n'y suis en aucune façon affiliée - mais basée sur une vingtaine d'années de programmation C et de systèmes embarqués où la fiabilité était essentielle, le PC de Gimpel Lint ne détectera pas seulement ce type d’erreurs, il fera de vous un meilleur programmeur C/C++ de constamment vous harcelant au sujet des mauvaises habitudes.

Je vous recommande également de lire la norme de codage MISRA C, si vous pouvez en prendre une copie à quelqu'un. Je n’en ai vu aucune récemment mais jadis, ils ont bien expliqué pourquoi vous ne devriez pas/ne devriez pas faire les choses qu’ils couvrent.

Je ne sais pas pour vous, mais à peu près à la deuxième ou à la troisième fois que je reçois une copie ou une suspension de n'importe quelle application, mon opinion sur la société produite a été réduite de moitié. La 4e ou la 5e fois, quel que soit le paquet utilisé, tout devient un article de magasin. Je conduis un piquet en bois au centre du paquet/disque, il est entré juste pour m'assurer qu'il ne reviendra jamais me hanter.

3
Dan Haynes

NSArrays dans Objective-C se voient attribuer un bloc de mémoire spécifique. Si vous dépassez les limites du tableau, vous accéderez à de la mémoire qui n’est pas affectée au tableau. Ça signifie:

  1. Cette mémoire peut avoir n'importe quelle valeur. Il n'y a aucun moyen de savoir si les données sont valides en fonction de votre type de données.
  2. Cette mémoire peut contenir des informations sensibles telles que des clés privées ou d'autres informations d'identification de l'utilisateur.
  3. L'adresse mémoire peut être invalide ou protégée.
  4. La mémoire peut avoir une valeur changeante car elle est accédée par un autre programme ou thread.
  5. D'autres objets utilisent l'espace d'adressage mémoire, tels que les ports mappés en mémoire.
  6. L'écriture de données sur une adresse mémoire inconnue peut planter votre programme, écraser l'espace mémoire du système d'exploitation et provoquer généralement l'implosion de Sun.

En ce qui concerne l'aspect de votre programme, vous voulez toujours savoir quand votre code dépasse les limites d'un tableau. Cela peut entraîner le renvoi de valeurs inconnues, entraînant le blocage de votre application ou la fourniture de données non valides.

3
Richard Brown

Je travaille avec un compilateur pour une puce DSP qui génère délibérément du code qui accède au-delà de la fin d'un tableau en code C, ce qui n'est pas le cas!

En effet, les boucles sont structurées de manière à ce que la fin d'une itération prélève des données pour la prochaine itération. Ainsi, la donnée pré-extraite à la fin de la dernière itération n'est jamais réellement utilisée.

Écrire du code C comme cela appelle un comportement indéfini, mais ce n'est qu'une formalité d'un document de normes qui se préoccupe de la portabilité maximale.

Le plus souvent, un programme dont l'accès est interdit est mal optimisé. C'est simplement un buggy. Le code va chercher une valeur résiduelle et, contrairement aux boucles optimisées du compilateur susmentionné, le code utilise alors la valeur dans les calculs ultérieurs, corrompant ainsi le sim.

Cela vaut la peine d'attraper de tels bogues, il est donc utile de ne pas définir le comportement, ne serait-ce que pour cette raison uniquement: pour que l'exécution puisse générer un message de diagnostic du type "tableau saturé à la ligne 42 de main.c".

Sur les systèmes dotés de mémoire virtuelle, il est possible qu’un tableau soit affecté de telle sorte que l’adresse qui suit se trouve dans une zone non mappée de mémoire virtuelle. L'accès bombardera alors le programme.

Soit dit en passant, en C, nous sommes autorisés à créer un pointeur qui se situe un après la fin d'un tableau. Et ce pointeur doit comparer plus que tout pointeur à l'intérieur d'un tableau. Cela signifie qu'une implémentation C ne peut pas placer un tableau juste à la fin de la mémoire, l'adresse une plus s'afficherait et semblerait plus petite que les autres adresses du tableau.

Néanmoins, l’accès à des valeurs non initialisées ou hors limites est parfois une technique d’optimisation valable, même si elle n’est pas portable au maximum. C’est par exemple pourquoi l’outil Valgrind ne signale pas les accès aux données non initialisées lorsque ces accès se produisent, mais uniquement lorsque la valeur est utilisée ultérieurement d’une manière susceptible d’affecter le résultat du programme. Vous obtenez un diagnostic du type "branche conditionnelle dans xxx: nnn dépend de la valeur non initialisée" et il peut parfois être difficile de localiser son origine. Si tous ces accès étaient immédiatement pris au piège, il y aurait beaucoup de faux positifs résultant du code optimisé pour le compilateur ainsi que du code correctement optimisé manuellement.

En parlant de cela, je travaillais avec un codec d'un fournisseur qui émettait ces erreurs lorsqu'il était porté sur Linux et exécuté sous Valgrind. Mais le vendeur m'a convaincu que seuls plusieurs bits de la valeur utilisée provenaient en réalité d'une mémoire non initialisée et que ces bits étaient soigneusement évités par la logique. Seuls les bons bits de la valeur étaient utilisés et Valgrind n'a pas la capacité de traquer jusqu'au bit individuel. Le matériel non initialisé provient de la lecture d'un mot au-delà de la fin d'un flux de données codées, mais le code sait combien de bits se trouvent dans le flux et n'utilisera pas plus de bits qu'il n'en existe réellement. Étant donné que l'accès au-delà de la fin de la matrice de flux de bits ne nuit pas à l'architecture DSP (il n'y a pas de mémoire virtuelle après la matrice, pas de ports mappés sur la mémoire et l'adresse n'est pas bouclée), il s'agit d'une technique d'optimisation valide.

"Comportement indéfini" n'a pas vraiment de sens, car selon ISO C, l'inclusion d'un en-tête non défini dans le standard C ou l'appel d'une fonction non définie dans le programme lui-même ou dans le standard C sont des exemples d'indéfini. comportement. Un comportement indéfini ne signifie pas "non défini par quiconque sur la planète" mais simplement "non défini par la norme ISO C". Mais bien sûr, un comportement parfois indéfini n’est en aucun cas défini par personne.

2
Kaz

En plus de votre propre programme, je ne pense pas que vous allez casser quoi que ce soit, dans le pire des cas, vous essayerez de lire ou d'écrire à partir d'une adresse mémoire qui correspond à une page que le noyau n'a pas affectée à vos processus, générant l'exception appropriée. et d'être tué (je veux dire, votre processus).

1
jbgs