sichere C-Programmierung

Lesezeit: 9 Minuten

Mir ist aufgefallen, dass ich mit meinem C-Compiler (gcc) Dinge tun kann wie:

#include <stdio.h>
main(){
    short m[32768];
    short y = -1;
    short z = -1;
    printf("%u\n", y);
    m[y] = 12;
    printf("%d\n%d\n", y, m[z]);
}

Wenn ich es starte, spuckt es aus:

4294967295
12
12

Was mir etwas verwirrend erscheint.

Erstens, ist es sicher für mich, solche Programme auszuführen? Besteht die Möglichkeit, dass ich versehentlich das Betriebssystem überschreibe (ich verwende OS X, falls es relevant ist)?

Außerdem hatte ich zumindest eine Art Segfault-Fehler erwartet, wie ich ihn in der Vergangenheit erlebt habe, aber einen Fehler wie diesen stillschweigend zu ignorieren, macht mir wirklich Angst. Wie kommt es, dass dieses Programm mir keinen Segfault gibt?

Und schließlich, aus Neugier (das könnte die dümmste Frage sein), gibt es eine Methode für den Wahnsinn? Kann ich erwarten, dass alle ANSI-C-Compiler auf diese Weise arbeiten? Wie sieht es mit gcc auf verschiedenen Plattformen aus? Ist das Speicherlayout so definiert, dass es ausnutzbar ist (vielleicht wenn Sie plattformübergreifenden verschleierten Code schreiben würden)?

  • Bin ich der einzige, der den Titel gelesen hat und “Sichere C-Programmierung? Was ist das??” 🙂

    – jb.

    28. Mai 2012 um 2:05 Uhr

  • m[-1] ist gefährlich. TU DAS NIEMALS

    – Cole Tobin

    28. Mai 2012 um 2:10 Uhr

  • Was sehen Sie, wenn Sie mit aktivierten Hinweisen und Warnungen kompilieren (-Wall für gcc, IIRC)?

    – Ken Weiß

    28. Mai 2012 um 2:13 Uhr

  • @ColeJohnson: m[-1] kann sicher sein, wenn m zeigt auf ein anderes Element eines Arrays als das erste Element. Im OP-Code ist dies jedoch nicht der Fall.

    – R.. GitHub HÖR AUF, EIS ZU HELFEN

    28. Mai 2012 um 3:21 Uhr


  • @KenWhite Ich habe -Wall ausprobiert und eine Warnung erhalten, dass main nichts zurückgegeben hat. Nachdem ich “main” in “int main” geändert und ein “return 0;” hinzugefügt hatte Am Ende bekam ich keine Warnungen

    – math4tots

    28. Mai 2012 um 9:28 Uhr

Benutzer-Avatar
Ira Baxter

Die C-Sprache definiert das Verhalten bestimmter Programme als “undefiniert”. Sie können alles tun. Wir nennen solche Programme fehlerhaft.

Eines davon ist ein Programm, das außerhalb der deklarierten/zugewiesenen Grenzen eines Arrays zugreift, auf das Ihr Programm sehr sorgfältig tut.

Ihr Programm ist fehlerhaft; das, was Ihr fehlerhaftes Programm tut, ist das, was Sie sehen :-} Es könnte “das Betriebssystem überschreiben”; In der Praxis hindern Sie die meisten modernen Betriebssysteme daran, aber Sie können kritische Werte in Ihrem Prozessbereich überschreiben, und Ihr Prozess könnte abstürzen, sterben oder hängen bleiben.

Die einfache Antwort lautet: “Schreiben Sie keine fehlerhaften Programme”. Dann ergibt das Verhalten, das Sie sehen, “C”-Sinn.

In diesem speziellen Fall funktioniert die Array-Indizierung mit Ihrem speziellen Compiler “irgendwie”: Sie indizieren außerhalb des Arrays und es wird aufgenommen etwas Wert. Der Platz, der m zugeordnet ist, befindet sich im Stapelrahmen; m[0] befindet sich an einer Stelle im Stack-Frame, ebenso wie “m[-1]” basierend auf Maschinenarithmetik, die die Array-Adresse und den Index kombiniert, sodass kein Segfault auftritt und auf eine Speicherstelle zugegriffen wird. Dadurch kann das kompilierte Programm diese Speicherstelle lesen und schreiben … als fehlerhaftes Programm. Grundsätzlich kompilierte C-Programme Überprüfen Sie nicht, ob Ihr Array-Zugriff außerhalb der Grenzen liegt.

Unser CheckPointer Wenn das Tool auf dieses Programm angewendet wird, wird Ihnen mitgeteilt, dass der Array-Index zur Ausführungszeit illegal ist. Sie können also entweder das Programm selbst beobachten, um zu sehen, ob Sie einen Fehler gemacht haben, oder CheckPointer Ihnen sagen lassen, wenn Sie einen Fehler machen. Ich empfehle Ihnen dringend, auf jeden Fall das Auge zu nehmen.

  • @KenWhite: Eigentlich denke ich, dass der C-Standard Ihr Programm bis zu einer undefinierten Operation nicht als legal definiert. Es sagt einfach, dass das Verhalten des Programms undefiniert ist. Das heißt in gewisser Weise muss es gar nicht laufen :-}

    – Ira Baxter

    28. Mai 2012 um 2:52 Uhr

  • Alle: Ich habe meine Antwort überarbeitet, um zwischen dem Programmcode und dem undefinierten Verhalten fehlerhafter Programme zu unterscheiden.

    – Ira Baxter

    28. Mai 2012 um 2:56 Uhr

  • 🙂 Das war in erster Linie mein Vorschlag. +1, übrigens.

    – Ken Weiß

    28. Mai 2012 um 2:59 Uhr

Benutzer-Avatar
gierig

Sie könnten an INRIAs interessiert sein CompCert C, eine formal, mathematisch überprüfbare und verifizierte Implementierung der C-Sprache. Es sind die gleichen Autoren wie die berühmten Coq-Beweis-Assistent. Es gibt auch noch eine andere Variante Nachweisbar C.

Ich weiß nicht viel darüber, aber ich weiß, dass Flugzeugingenieure in Frankreich damit die kommenden eingebetteten Computer in Flugzeugen programmieren, also ist es zumindest in Frankreich eine offiziell anerkannte Sprache für die Programmierung kritischer Systeme.

Beachten Sie schließlich, dass sich eine formal überprüfbare Sprache von einer sicheren Sprache unterscheidet.

Zum Beispiel soll MISRA C eine sichere C-Sprache sein (obwohl dies umstritten ist), und es gibt sie auch Safe-CMicrosofts Geprüft-C und Zyklonzusammen mit sicheren Bibliotheken, ohne den Compiler zu ändern, wie z Sichere C-Bibliothek und libsrtoder verwenden Sie einfach den Standard-Compiler und die Bibliotheken, aber mit einem Quellcode-Analysator wie z frama-c.

Sichere Sprachen bieten zwar Lösungen für einige Probleme wie Pufferüberläufe, garantieren jedoch keinen konsistenten Logikfluss, wie er für kritische Systeme erforderlich ist. Beispielsweise sollte CompCert C immer denselben Satz von Assembler-Befehlen für dieselben C-Befehle erzeugen. Formal überprüfbare Sprachen wie CompCert C und Ada bieten solche formalen Garantien.

Diese Artikel könnten Sie auch interessieren:

Benutzer-Avatar
John3136

Erstens, ist es sicher für mich, solche Programme auszuführen?

Ihr Beispiel: Nein. Absolut nicht. Warum würdest du es überhaupt versuchen? Was erwartest du davon? Allgemeinere Beispiele mit negativen Indizes – solange sie in das legale Gedächtnis dereferenzieren, ist es in Ordnung.

Außerdem hatte ich zumindest eine Art Segfault-Fehler erwartet, wie ich ihn in der Vergangenheit erlebt habe, aber einen Fehler wie diesen stillschweigend zu ignorieren, macht mir wirklich Angst. Wie kommt es, dass dieses Programm mir keinen Segfault gibt?

Blindes Glück. (eigentlich nicht ganz – wie schön erklärt von Ira Baxter)

Und schließlich, aus Neugier (das könnte die dümmste Frage sein), gibt es eine Methode für den Wahnsinn?

Wenn Sie Zeiger auf Arrays einrichten, funktionieren negative Indizes möglicherweise, aber für andere wäre es ein Albtraum, sie zu verstehen und zu pflegen! – Ich habe es in eingebetteten Systemen gesehen.

Kann ich erwarten, dass alle ANSI-C-Compiler auf diese Weise arbeiten?

Ja.

Wie sieht es mit gcc auf verschiedenen Plattformen aus?

Ja

Ist das Speicherlayout so definiert, dass es ausnutzbar ist (vielleicht wenn Sie plattformübergreifenden verschleierten Code schreiben würden)?

Ja – aber ich bin mir nicht sicher, ob Sie sich wirklich darauf verlassen wollen.

  • Re Layout des Speichers, ist es nicht ASLR speziell entwickelt, um den Adressraum eines Prozesses unvorhersehbar zu machen?

    – josh3736

    28. Mai 2012 um 2:19 Uhr

  • Ein Fall, wo p[-1] scheint natürlich ist wo p ist ein Zeiger auf einige Daten, die Sie analysieren, und Sie benötigen einige Look-Behind-Informationen. Das find ich schöner als *(p-1) – vor allem, wenn ich die Klammern-Notation für Zugriffe vor dem Cursor verwende.

    – Michael Anderson

    28. Mai 2012 um 2:23 Uhr

  • Ernsthaft? eww, nein. syntaktisch verwirrend. Ich würde nicht hinter einem Programmierer her sein wollen, der das getan hat. *p ist viel klarer.

    – matchdav

    28. Mai 2012 um 2:25 Uhr


  • Nein, das Speicherlayout ist nicht gut definiert; darauf konnte man sich nicht verlassen.

    – Jonathan Leffler

    28. Mai 2012 um 4:00 Uhr

  • … Ich glaube, ich hätte schreiben sollen: “T=m[k]; p=&m[-1];”. Das “p=m[1]” ist ein indirekter Zugriff über die Array-Grenze hinaus. (Besser spät als nie?)

    – Ira Baxter

    24. Juli 2013 um 8:08 Uhr


  • „Sicher“, unter dem Gesichtspunkt einer möglichen Beschädigung des Betriebssystems – ja, meistens. Die meisten „modernen“ Betriebssysteme implementieren eine Art „Speicherschutz“, damit ein unberechenbares oder bösartiges Programm dem Betriebssystem oder anderen Programmen nichts anhaben kann. (Aber das „meistens“ zeigt an, dass keine Software länger als 50 Zeilen perfekt ist, und es besteht immer die seltsame Chance, dass Sie einen Riss in der Rüstung finden.)
  • “Sicher” vom Standpunkt Ihres Programms “verhalten” Sie sich richtig? Nunllllllll, nein, meistens. Hier bezieht sich das „meistens“ auf die Tatsache, dass sachkundige (ich werde nicht „clevere“) Programmierer gelegentlich „seltsame“ Dinge tun, wie z. B. die Verwendung eines negativen Array-Index, wenn sie wissen, wie der Compiler und die Laufzeit arbeiten und effizient arbeiten müssen etwas erreichen, das etwas außerhalb der Norm liegt. Aber sie tun dies (hoffentlich) in dem Wissen, dass der “Trick”, den sie verwenden, sehr system-/compilerabhängig ist.

Aber wie @jb angedeutet hat, ist “sichere C-Programmierung” ein Oxymoron.

Zusätzlich zu dem oben Gesagten würde ich vorschlagen, dass Programme, die gegen Valgrind ausgeführt werden, eine viel größere Chance haben, als fehlerhaft erkannt zu werden, als Programme, die allein aufgrund des Compiler- und Betriebssystemvertrauens losgelassen werden. Valgrind wird nicht alles abfangen, leistet aber gute Arbeit bei der Erkennung von Zugriffen auf nicht initialisierten oder außerhalb der Grenzen liegenden Speicher.

  • Ich glaube nicht, dass Valgrind den Fehler in diesem speziellen Programm erkennen kann. Er greift auf eine gültige Stelle in seinem Stapelrahmen zu, was Valgrind für in Ordnung hält. Insbesondere erkennt Valgrind keinen fehlerhaften Zugriff außerhalb einer Variablen (Array, Strukturmitglied, …), wenn sich dieser Zugriff noch innerhalb des zugewiesenen Speichers befindet, der sie enthält (z. B. Stapelrahmen, globale Variable oder Heap-zugewiesenes Element). . [CheckPointer will detect such errors, even in thread local storage]

    – Ira Baxter

    28. Mai 2012 um 11:00 Uhr

Benutzer-Avatar
matchdav

Wenn das Programm eine gültige Adresse im Speicher anfordert (was m[-1] könnte tun, da sein Verhalten undefiniert ist), erhalten Sie keinen Segfault … besonders wenn Sie nach einem kurzen fragen, da er in die meisten Wortlängen passt. Das ist eine wirklich schlechte Idee, Sie könnten leicht etwas auf der Festplatte überschreiben, obwohl der Kernel geschützt ist, es sei denn, Sie führen Ihren Code direkt beim Start aus.

Ich bin mir nicht sicher, warum Sie die Adresse von -1 ausgedruckt haben.

  • Ich glaube nicht, dass Valgrind den Fehler in diesem speziellen Programm erkennen kann. Er greift auf eine gültige Stelle in seinem Stapelrahmen zu, was Valgrind für in Ordnung hält. Insbesondere erkennt Valgrind keinen fehlerhaften Zugriff außerhalb einer Variablen (Array, Strukturmitglied, …), wenn sich dieser Zugriff noch innerhalb des zugewiesenen Speichers befindet, der sie enthält (z. B. Stapelrahmen, globale Variable oder Heap-zugewiesenes Element). . [CheckPointer will detect such errors, even in thread local storage]

    – Ira Baxter

    28. Mai 2012 um 11:00 Uhr

1110050cookie-checksichere C-Programmierung

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy