Wie gefährlich ist der Zugriff auf ein Array außerhalb seiner Grenzen (in C)? Es kann manchmal vorkommen, dass ich von außerhalb des Arrays lese (ich verstehe jetzt, dass ich dann auf den Speicher zugreife, der von einigen anderen Teilen meines Programms oder sogar darüber hinaus verwendet wird) oder ich versuche, einen Wert auf einen Index außerhalb des Arrays zu setzen. Das Programm stürzt manchmal ab, läuft aber manchmal nur und liefert nur unerwartete Ergebnisse.
Was ich jetzt gerne wissen würde ist, wie gefährlich ist das wirklich? Wenn es mein Programm beschädigt, ist es nicht so schlimm. Wenn es andererseits etwas außerhalb meines Programms kaputt macht, weil ich es irgendwie geschafft habe, auf einen völlig unabhängigen Speicher zuzugreifen, dann ist es sehr schlecht, stelle ich mir vor. Ich habe viel gelesen, dass alles passieren kann. "Segmentierung könnte das am wenigsten schlimme Problem sein" , "Ihre Festplatte könnte rosa werden und Einhörner könnten unter Ihrem Fenster singen", was alles schön ist, aber was ist wirklich die Gefahr?
Meine Fragen:
- Kann das Lesen von Werten von außerhalb des Arrays etwas anderes als mein Programm beschädigen? Ich würde mir vorstellen, dass das bloße Betrachten von Dingen nichts ändert, oder würde es beispielsweise das Attribut "Zuletzt geöffnet" einer Datei ändern, die ich zufällig erreicht habe?
- Kann das Einstellen von Werten außerhalb des Arrays etwas anderes als mein Programm beschädigen? Aus dieser Frage zum Stapelüberlauf geht hervor, dass es möglich ist, auf jeden Speicherort zuzugreifen, dass es keine Sicherheitsgarantie gibt.
- Ich führe jetzt meine kleinen Programme in XCode aus. Bietet dies zusätzlichen Schutz für mein Programm, wenn es nicht außerhalb seines eigenen Speichers greifen kann? Kann es XCode schaden?
- Irgendwelche Empfehlungen, wie ich meinen inhärent fehlerhaften Code sicher ausführen kann?
Ich benutze OSX 10.7, Xcode 4.6.
Antworten:
In Bezug auf den ISO C-Standard (die offizielle Definition der Sprache) hat der Zugriff auf ein Array außerhalb seiner Grenzen ein " undefiniertes Verhalten ". Die wörtliche Bedeutung davon ist:
Eine nicht normative Anmerkung erweitert dies:
Das ist also die Theorie. Was ist die Realität?
Im "besten" Fall greifen Sie auf einen Speicher zu, der entweder Ihrem aktuell ausgeführten Programm gehört (was dazu führen kann, dass sich Ihr Programm schlecht verhält) oder nicht von Ihrem aktuell laufenden Programm (die wahrscheinlich Ursache Ihres Programms im Besitz Absturz mit so etwas wie einem Segmentierungsfehler). Oder Sie versuchen möglicherweise, in den Speicher zu schreiben, den Ihr Programm besitzt, der jedoch als schreibgeschützt markiert ist. Dies wird wahrscheinlich auch dazu führen, dass Ihr Programm abstürzt.
Dies setzt voraus, dass Ihr Programm unter einem Betriebssystem ausgeführt wird, das versucht, gleichzeitig ausgeführte Prozesse voreinander zu schützen. Wenn Ihr Code auf dem "Bare Metal" ausgeführt wird, z. B. wenn er Teil eines Betriebssystemkerns oder eines eingebetteten Systems ist, gibt es keinen solchen Schutz. Ihr schlecht benommener Code sollte diesen Schutz bieten. In diesem Fall sind die Schadensmöglichkeiten erheblich größer, einschließlich in einigen Fällen physischer Schäden an der Hardware (oder an Dingen oder Personen in der Nähe).
Selbst in einer geschützten Betriebssystemumgebung ist der Schutz nicht immer 100%. Es gibt Betriebssystemfehler, die es nicht privilegierten Programmen ermöglichen, beispielsweise Root-Zugriff (Administratorzugriff) zu erhalten. Selbst mit normalen Benutzerrechten kann ein fehlerhaftes Programm übermäßige Ressourcen (CPU, Speicher, Festplatte) verbrauchen und möglicherweise das gesamte System zum Erliegen bringen. Viele Malware (Viren usw.) nutzt Pufferüberläufe aus, um unbefugten Zugriff auf das System zu erhalten.
(Ein historisches Beispiel: Ich habe gehört, dass auf einigen alten Systemen mit Kernspeicher der wiederholte Zugriff auf einen einzelnen Speicherort in einer engen Schleife buchstäblich dazu führen kann, dass dieser Speicherblock schmilzt. Andere Möglichkeiten sind das Zerstören einer CRT-Anzeige und das Verschieben des Lesevorgangs / schreibe den Kopf eines Festplattenlaufwerks mit der harmonischen Frequenz des Laufwerksgehäuses, wodurch es über einen Tisch läuft und auf den Boden fällt.)
Und es gibt immer Skynet, um das man sich Sorgen machen muss.
Unterm Strich ist dies: Wenn Sie ein Programm schreiben könnte etwas Schlechtes zu tun bewusst , ist es zumindest theoretisch möglich , dass ein fehlerhaftes Programm könnte das gleiche tun zufällig .
In der Praxis ist es sehr unwahrscheinlich, dass Ihr fehlerhaftes Programm, das auf einem MacOS X-System ausgeführt wird, etwas Schwerwiegenderes als einen Absturz verursacht. Es ist jedoch nicht möglich, fehlerhaften Code vollständig daran zu hindern, wirklich schlechte Dinge zu tun.
quelle
Im Allgemeinen führen Betriebssysteme von heute (die ohnehin beliebten) alle Anwendungen in geschützten Speicherbereichen mit einem virtuellen Speichermanager aus. Es stellt sich heraus, dass es nicht besonders EINFACH ist, einfach zu lesen oder an einen Ort zu schreiben, der sich im REAL-Bereich außerhalb der Region (en) befindet, die Ihrem Prozess zugewiesen / zugewiesen wurden.
Direkte Antworten:
1) Durch das Lesen wird ein anderer Prozess fast nie direkt beschädigt. Es kann jedoch indirekt einen Prozess beschädigen, wenn Sie einen KEY-Wert lesen, der zum Ver-, Entschlüsseln oder Validieren eines Programms / Prozesses verwendet wird. Das Lesen außerhalb der Grenzen kann sich nachteilig / unerwartet auf Ihren Code auswirken, wenn Sie Entscheidungen auf der Grundlage der von Ihnen gelesenen Daten treffen
2) Die einzige Möglichkeit, etwas wirklich zu BESCHÄDIGEN, indem Sie in eine Loaction schreiben, auf die über eine Speicheradresse zugegriffen werden kann, besteht darin, dass diese Speicheradresse, an die Sie schreiben, tatsächlich ein Hardwareregister ist (ein Speicherort, der eigentlich nicht zur Datenspeicherung, sondern zur Steuerung eines Teils dient von Hardware) kein RAM-Speicherort. Tatsächlich beschädigen Sie normalerweise immer noch nichts, es sei denn, Sie schreiben einen einmal programmierbaren Ort, der nicht wiederbeschreibbar ist (oder etwas Ähnliches).
3) Im Allgemeinen wird der Code im Debug-Modus ausgeführt, wenn er im Debugger ausgeführt wird. Das Ausführen im Debug-Modus führt dazu, dass Ihr Code (aber nicht immer) schneller gestoppt wird, wenn Sie etwas getan haben, das als unpraktisch oder geradezu illegal angesehen wird.
4) Verwenden Sie niemals Makros, Datenstrukturen, in die bereits eine Überprüfung der Array-Indexgrenzen integriert ist, usw.
ZUSÄTZLICH Ich sollte hinzufügen, dass die obigen Informationen wirklich nur für Systeme gelten, die ein Betriebssystem mit Speicherschutzfenstern verwenden. Wenn Sie Code für ein eingebettetes System oder sogar ein System schreiben, das ein Betriebssystem (Echtzeit oder ein anderes) verwendet, das keine Speicherschutzfenster (oder virtuell adressierte Fenster) hat, sollten Sie beim Lesen und Schreiben in den Speicher viel mehr Vorsicht walten lassen. Auch in diesen Fällen sollten immer sichere und sichere Codierungspraktiken angewendet werden, um Sicherheitsprobleme zu vermeiden.
quelle
Wenn die Grenzen nicht überprüft werden, kann dies zu hässlichen Nebenwirkungen führen, einschließlich Sicherheitslücken. Eine der hässlichen ist die Ausführung von willkürlichem Code . Im klassischen Beispiel: Wenn Sie ein Array mit fester Größe haben und dort
strcpy()
eine vom Benutzer bereitgestellte Zeichenfolge einfügen, kann der Benutzer Ihnen eine Zeichenfolge geben, die den Puffer überläuft und andere Speicherorte überschreibt, einschließlich der Code-Adresse, an die die CPU bei Ihrer Funktion zurückkehren soll endet.Dies bedeutet, dass Ihr Benutzer Ihnen eine Zeichenfolge senden kann, die dazu führt, dass Ihr Programm im Wesentlichen aufgerufen
exec("/bin/sh")
wird. Dadurch wird es in eine Shell umgewandelt und alles ausgeführt, was er auf Ihrem System möchte, einschließlich der Erfassung aller Ihrer Daten und der Umwandlung Ihres Computers in einen Botnetzknoten.Weitere Informationen dazu finden Sie unter Zerschlagen des Stapels für Spaß und Gewinn .
quelle
foo[0]
durch ,foo[len-1]
nachdem zuvor eine Überprüfung genutzt hat , vonlen
, um gegen die Array - Länge entweder ausführen oder ein Stück Code überspringt, sollte der Compiler frei fühlen , dass die anderen Code auszuführen bedingungslos sogar Wenn die Anwendung den Speicher hinter dem Array besitzt und die Auswirkungen des Lesens harmlos gewesen wären, wäre dies beim Aufrufen des anderen Codes nicht der Fall.Du schreibst:
Sagen wir es so: Laden Sie eine Waffe. Richten Sie es außerhalb des Fensters ohne ein bestimmtes Ziel und Feuer. Was ist die Gefahr?
Das Problem ist, dass Sie nicht wissen. Wenn Ihr Code etwas überschreibt, das Ihr Programm zum Absturz bringt, ist alles in Ordnung, da es es in einen definierten Zustand versetzt. Wenn es jedoch nicht abstürzt, treten die Probleme auf. Welche Ressourcen werden von Ihrem Programm kontrolliert und was könnte es ihnen antun? Welche Ressourcen könnten unter die Kontrolle Ihres Programms geraten und was könnte es ihnen antun? Ich kenne mindestens ein Hauptproblem, das durch einen solchen Überlauf verursacht wurde. Das Problem lag in einer scheinbar bedeutungslosen Statistikfunktion, die eine nicht verwandte Konvertierungstabelle für eine Produktionsdatenbank durcheinander brachte. Das Ergebnis war eine sehr teure Bereinigung danach. Eigentlich wäre es viel billiger und einfacher gewesen, wenn dieses Problem die Festplatten formatiert hätte ... mit anderen Worten: Rosa Einhörner könnten Ihr geringstes Problem sein.
Die Idee, dass Ihr Betriebssystem Sie schützt, ist optimistisch. Vermeiden Sie nach Möglichkeit das Schreiben außerhalb der Grenzen.
quelle
Wenn Sie Ihr Programm nicht als Root oder einen anderen privilegierten Benutzer ausführen, wird Ihr System nicht beschädigt. Daher ist dies im Allgemeinen eine gute Idee.
Wenn Sie Daten an einen zufälligen Speicherort schreiben, "beschädigen" Sie kein anderes Programm, das auf Ihrem Computer ausgeführt wird, direkt, da jeder Prozess in seinem eigenen Speicher ausgeführt wird.
Wenn Sie versuchen, auf Speicher zuzugreifen, der Ihrem Prozess nicht zugeordnet ist, verhindert das Betriebssystem, dass Ihr Programm mit einem Segmentierungsfehler ausgeführt wird.
Es besteht also keine Gefahr, dass Ihr Programm direkt (ohne als Root ausgeführt wird und direkt auf Dateien wie / dev / mem zugreift) andere Programme stört, die auf Ihrem Betriebssystem ausgeführt werden.
Trotzdem - und wahrscheinlich haben Sie in Bezug auf die Gefahr davon gehört - können Sie durch versehentliches blindes Schreiben zufälliger Daten in zufällige Speicherorte sicher alles beschädigen, was Sie beschädigen können.
Beispielsweise möchte Ihr Programm möglicherweise eine bestimmte Datei löschen, die durch einen Dateinamen angegeben wird, der irgendwo in Ihrem Programm gespeichert ist. Wenn Sie versehentlich nur den Speicherort überschreiben, an dem der Dateiname gespeichert ist, können Sie stattdessen eine ganz andere Datei löschen.
quelle
NSArray
s in Objective-C wird ein bestimmter Speicherblock zugewiesen. Das Überschreiten der Grenzen des Arrays bedeutet, dass Sie auf Speicher zugreifen, der dem Array nicht zugewiesen ist. Das heisst:Unter dem Aspekt Ihres Programms möchten Sie immer wissen, wann Ihr Code die Grenzen eines Arrays überschreitet. Dies kann dazu führen, dass unbekannte Werte zurückgegeben werden und Ihre Anwendung abstürzt oder ungültige Daten bereitstellt.
quelle
NSArrays
haben außerhalb der Grenzen Ausnahmen. Und diese Frage scheint sich auf C-Array zu beziehen.Möglicherweise möchten Sie versuchen, das
memcheck
Tool in Valgrind zu verwenden wenn Sie Ihren Code testen. Es werden keine Verstöße gegen einzelne Arraygrenzen innerhalb eines Stapelrahmens abgefangen, aber es sollten viele andere Arten von Speicherproblemen auftreten, einschließlich solcher, die subtile, umfassendere Probleme verursachen würden Probleme außerhalb des Bereichs einer einzelnen Funktion.Aus dem Handbuch:
ETA: Obwohl, wie die Antwort von Kaz sagt, es kein Allheilmittel ist und nicht immer die hilfreichste Ausgabe liefert, insbesondere wenn Sie aufregende Zugriffsmuster verwenden.
quelle
Wenn Sie jemals auf Systemebene oder eingebettete Systeme programmieren, können sehr schlimme Dinge passieren, wenn Sie in zufällige Speicherorte schreiben. Ältere Systeme und viele Mikrocontroller verwenden speicherabgebildete E / A-Vorgänge, sodass das Schreiben an einen Speicherort, der einem Peripherieregister zugeordnet ist, zu Chaos führen kann, insbesondere wenn dies asynchron erfolgt.
Ein Beispiel ist das Programmieren eines Flash-Speichers. Der Programmiermodus auf den Speicherchips wird aktiviert, indem eine bestimmte Folge von Werten an bestimmte Stellen innerhalb des Adressbereichs des Chips geschrieben wird. Wenn ein anderer Prozess währenddessen an eine andere Stelle im Chip schreiben würde, würde der Programmierzyklus fehlschlagen.
In einigen Fällen wird die Hardware Adressen umschließen (die wichtigsten Adressbits / -bytes werden ignoriert), sodass das Schreiben an eine Adresse jenseits des Endes des physischen Adressraums tatsächlich dazu führt, dass Daten mitten im Geschehen geschrieben werden.
Und schließlich können ältere CPUs wie der MC68000 so weit gesperrt werden, dass sie nur durch einen Hardware-Reset wieder in Betrieb genommen werden können. Ich habe seit ein paar Jahrzehnten nicht mehr daran gearbeitet, aber ich glaube, wenn beim Versuch, eine Ausnahme zu behandeln, ein Busfehler (nicht vorhandener Speicher) aufgetreten ist, wurde er einfach angehalten, bis der Hardware-Reset bestätigt wurde.
Meine größte Empfehlung ist ein offensichtlicher Stecker für ein Produkt, aber ich habe kein persönliches Interesse daran und bin in keiner Weise mit ihnen verbunden - sondern basiert auf ein paar Jahrzehnten C-Programmierung und eingebetteten Systemen, bei denen die Zuverlässigkeit entscheidend war, dem PC von Gimpel Lint erkennt nicht nur solche Fehler, sondern macht Sie auch ständig zu einem besseren C / C ++ - Programmierer über schlechte Gewohnheiten belästigt.
Ich würde auch empfehlen, den MISRA C-Codierungsstandard zu lesen, wenn Sie sich eine Kopie von jemandem schnappen können. Ich habe in letzter Zeit keine gesehen, aber in den alten Tagen haben sie eine gute Erklärung gegeben, warum Sie die Dinge tun sollten / nicht tun sollten, die sie abdecken.
Keine Ahnung von Ihnen, aber beim zweiten oder dritten Mal, wenn ich einen Coredump oder Hangup von einer Anwendung bekomme, sinkt meine Meinung zu der Firma, die sie produziert hat, um die Hälfte. Das 4. oder 5. Mal und was auch immer das Paket ist, wird zu Regalware und ich fahre einen Holzpfahl durch die Mitte des Pakets / der Scheibe, in die es gekommen ist, nur um sicherzustellen, dass es nie zurückkommt, um mich zu verfolgen.
quelle
Ich arbeite mit einem Compiler für einen DSP-Chip, der absichtlich Code generiert, der über das Ende eines Arrays hinaus auf einen C-Code zugreift, der dies nicht tut!
Dies liegt daran, dass die Schleifen so strukturiert sind, dass am Ende einer Iteration einige Daten für die nächste Iteration vorab abgerufen werden. Das am Ende der letzten Iteration vorab abgerufene Datum wird also nie tatsächlich verwendet.
Das Schreiben eines solchen C-Codes ruft undefiniertes Verhalten hervor, aber das ist nur eine Formalität aus einem Standarddokument, das sich mit maximaler Portabilität befasst.
Meistens ist ein Programm, das außerhalb der Grenzen zugreift, nicht geschickt optimiert. Es ist einfach fehlerhaft. Der Code abruft einige Müll - Wert und, im Gegensatz zu den optimierten Schleifen der eingangs genannten Compiler, der Code dann verwendet , um den Wert in nachfolgenden Berechnungen, um dadurch korrumpieren theim.
Es lohnt sich, solche Fehler zu erkennen, und es lohnt sich, das Verhalten auch nur aus diesem Grund undefiniert zu machen: Damit die Laufzeit eine Diagnosemeldung wie "Array-Überlauf in Zeile 42 von main.c" erzeugen kann.
Auf Systemen mit virtuellem Speicher kann ein Array so zugewiesen werden, dass sich die folgende Adresse in einem nicht zugeordneten Bereich des virtuellen Speichers befindet. Der Zugriff bombardiert dann das Programm.
Trotzdem ist der Zugriff auf nicht initialisierte oder außerhalb der Grenzen liegende Werte manchmal eine gültige Optimierungstechnik, auch wenn sie nicht maximal portabel ist. Dies ist beispielsweise der Grund, warum das Valgrind-Tool keine Zugriffe auf nicht initialisierte Daten meldet, wenn diese Zugriffe stattfinden, sondern nur, wenn der Wert später auf eine Weise verwendet wird, die das Ergebnis des Programms beeinflussen könnte. Sie erhalten eine Diagnose wie "Bedingter Zweig in xxx: nnn hängt vom nicht initialisierten Wert ab" und es kann manchmal schwierig sein, den Ursprung zu ermitteln. Wenn alle derartigen Zugriffe sofort abgefangen würden, würde es viele falsch positive Ergebnisse geben, die sich aus compileroptimiertem Code sowie korrekt handoptimiertem Code ergeben.
Apropos, ich habe mit einem Codec eines Anbieters gearbeitet, der diese Fehler ausgab, wenn er auf Linux portiert und unter Valgrind ausgeführt wurde. Aber der Verkäufer hat mich davon überzeugt, dass nur einige BitsDer verwendete Wert stammte tatsächlich aus dem nicht initialisierten Speicher, und diese Bits wurden von der Logik sorgfältig vermieden. Es wurden nur die guten Bits des Werts verwendet, und Valgrind kann nicht auf das einzelne Bit zurückgreifen. Das nicht initialisierte Material stammt aus dem Lesen eines Wortes nach dem Ende eines Bitstroms codierter Daten, aber der Code weiß, wie viele Bits sich im Stream befinden, und verwendet nicht mehr Bits als tatsächlich vorhanden sind. Da der Zugriff über das Ende des Bitstrom-Arrays hinaus keinen Schaden für die DSP-Architektur verursacht (es gibt keinen virtuellen Speicher nach dem Array, keine speicherabgebildeten Ports und die Adresse wird nicht umbrochen), handelt es sich um eine gültige Optimierungstechnik.
"Undefiniertes Verhalten" bedeutet nicht wirklich viel, denn laut ISO C sind das einfache Einfügen eines Headers, der nicht im C-Standard definiert ist, oder das Aufrufen einer Funktion, die nicht im Programm selbst oder im C-Standard definiert ist, Beispiele für undefiniertes Verhalten Verhalten. Undefiniertes Verhalten bedeutet nicht "von niemandem auf dem Planeten definiert", sondern nur "nicht von der ISO C-Norm definiert". Aber natürlich manchmal wirklich nicht definiertes Verhalten ist absolut nicht von jedermann definiert.
quelle
Abgesehen von Ihrem eigenen Programm glaube ich nicht, dass Sie irgendetwas kaputt machen werden. Im schlimmsten Fall werden Sie versuchen, von einer Speicheradresse zu lesen oder zu schreiben, die einer Seite entspricht, die der Kernel Ihren Prozessen nicht zugewiesen hat, und die richtige Ausnahme erzeugen und getötet zu werden (ich meine, dein Prozess).
quelle