C-Programmierung: Sicherheit

Wenn man einmal die Grundlagen der C-Programmierung verstanden hat, sollte man mal eine kleine Pause machen. Denn an diesem Punkt werden Sie sicher ihre ersten Programme schreiben wollen, die nicht nur dem Erlernen der Sprache C dienen, sondern Sie wollen für sich und vielleicht auch für andere Werkzeuge erstellen, mit denen sich die Arbeit erleichtern lässt. Doch Vorsicht, bis jetzt wurden die Programme von ihnen immer nur so genutzt, wie Sie es dachten.

Wenn Sie so genannten Produktivcode schreiben wollen, sollten Sie davon ausgehen, dass dies nicht länger der Fall sein wird. Es wird immer mal einen Benutzer geben, der nicht das eingibt, was Sie dachten oder der versucht, eine längere Zeichenkette zu verarbeiten, als Sie es bei ihrer Überlegung angenommen haben. Deshalb sollten Sie spätestens jetzt ihr Programm durch eine Reihe von Verhaltensmustern schützen, so gut es geht.

Der Compiler ist dein Freund Bearbeiten

Viele ignorieren die Warnungen, die der Compiler ausgibt, oder haben sie gar nicht angeschaltet. Frei nach dem Motto "solange es kein Fehler ist". Dies ist mehr als kurzsichtig. Mit Warnungen will der Compiler uns mitteilen, dass wir gerade auf dem Weg in die Katastrophe sind. Also gleich von Beginn an den Warnungen nachgehen und dafür sorgen, dass diese nicht mehr erscheinen. Wenn sich die Warnungen in einem ganz speziellen Fall nicht beseitigen lassen, ist es selbstverständlich, dass man dem Projekt eine Erklärung beilegt, die ganz genau erklärt, woher die Warnung kommt, warum man diese nicht umgehen kann und es ist zu beweisen, dass die Warnung unter keinen Umständen zu einem Programmversagen führen wird. Also im Klartext: "Ist halt so" ist keine Begründung.

Wenn Sie ihre Programme mit dem GNU C Compiler schreiben, sollten Sie dem Compiler mindestens diese Argumente mitgeben, um viele sinnvolle Warnungen zu sehen:

gcc -Wall -W -Wstrict-prototypes -O

Auch viele andere Compiler können sinnvolle Warnungen ausgeben, wenn Sie ihnen die entsprechenden Argumente mitgeben.

Zeiger und der Speicher Bearbeiten

Zeiger sind in C ohne Zweifel eine mächtige Waffe, aber Achtung! Es gibt eine Menge Programme, bei denen es zu sogenannten    Pufferüberläufen (Buffer Overflows) gekommen ist, weil der Programmierer sich nicht der Gefahr von Zeigern bewusst war. Wenn Sie also mit Zeigern hantieren, nutzen Sie die Kontrollmöglichkeiten. malloc() oder fopen() geben im Fehlerfall z.B. NULL zurück. Testen Sie also, ob das Ergebnis NULL ist und/oder nutzen Sie andere Kontrollen, um zu überprüfen, ob Ihre Zeiger auf gültige Inhalte zeigen.

Strings in C Bearbeiten

Wie Sie vielleicht wissen, sind Strings in C nichts anderes als ein Array von char. Das hat zur Konsequenz, dass es bei Stringoperationen besonders oft zu Pufferüberläufen kommt, weil der Programmierer einfach nicht mit überlangen Strings gerechnet hat. Vermeiden Sie dies, indem Sie nur die Funktionen verwenden, welche die Länge des Zielstrings überwachen:

  • snprintf statt sprintf
  • bei scanf/sscanf den width-Spezifizierer benutzen

Lesen Sie sich unbedingt die Dokumentation durch, die zusammen mit diesen Funktionen ausgeliefert wird. Überlegen Sie sich auch, was im Falle von zu langen Strings passieren soll. Falls der String nämlich später benutzt wird, um eine Datei zu löschen, könnte es leicht passieren, dass eine falsche Datei gelöscht wird.

Das Problem der Reellen Zahlen (Floating Points) Bearbeiten

Auch wenn es im C-Standard die Typen "float" und "double" gibt, so sind diese nur bedingt einsatzfähig. Durch die interne Darstellung einer Floatingpointzahl auf eine fest definierte Anzahl von Bytes in Exponentialschreibweise, kann es bei diesen Datentypen schnell zu Rundungsfehlern kommen, insbesondere sind davon Gleichheitsoperationen (==,!=,<=,>=) betroffen, die Ergebnisse sind dabei oft überraschend. Deshalb sollten Sie in ihren Projekten überlegen ob Sie nicht die Float-Berechnungen durch Integerdatentypen ersetzen können, um eine bessere Genauigkeit zu erhalten. So kann beispielsweise bei finanzmathematischen Programmen, welche cent- oder zehntelcentgenau rechnen, oft der größtmögliche Integerdatentyp (C89: long int/unsigned long int; C99 intmax_t/uintmax_t) benutzt werden. Auch hierbei sind aber Überläufe/Unterläufe zu beachten und auszuschließen.

Die Eingabe von Werten Bearbeiten

Falls Sie eine Eingabe erwarten, gehen Sie immer vom Schlimmsten aus. Vermeiden Sie, einen Wert vom Benutzer ohne Überprüfung zu verwenden. Denn wenn Sie zum Beispiel eine Zahl erwarten, und der Benutzer gibt einen Buchstaben ein, sind meist Ihre daraus folgenden Berechnungen Blödsinn. Also besser erst als Zeichenkette einlesen, dann auf Gültigkeit prüfen und erst dann in den benötigen Typ umwandeln. Auch das Lesen von Strings sollten Sie überdenken: Zum Beispiel prüft der folgende Aufruf die Länge nicht!

char str[10];
scanf("%s",str);

Wenn jetzt der Bereich str nicht lang genug für die Eingabe ist, haben Sie einen Pufferüberlauf. Abhilfe schafft hier die Verwendung des width-Spezifizierers:

char str[10];
scanf("%9s",str);

Hier werden maximal 9 Zeichen eingelesen, da am Ende noch das Null-Zeichen angehängt werden muss.

Magic Numbers sind böse Bearbeiten

Wenn Sie ein Programm schreiben und dort Berechnungen anstellen oder Register setzen, sollten Sie es vermeiden, dort direkt mit Zahlen zu arbeiten. Nutzen Sie besser die Möglichkeiten von Defines oder Konstanten, die mit sinnvollen Namen ausgestattet sind. Denn nach ein paar Monaten können selbst Sie nicht mehr sagen, was die Zahl in Ihrer Formel sollte. Hierzu ein kleines Beispiel:

x=z*9.81;    // schlecht: man kann vielleicht ahnen was der Programmierer will
F=m*9.81;    /* besser: wir können jetzt an der Formel vielleicht schon
                        erkennen: es geht um Kraftberechnung */
#define GRAVITY 9.81
F=m*GRAVITY; // am besten: jeder kann jetzt sofort sagen worum es geht

Auch wenn Sie Register haben, die mit ihren Bits irgendwelche Hardware steuern, sollten Sie statt den Magic Numbers einfach einen Header schreiben, welcher über defines den einzelnen Bits eine Bedeutung gibt, und dann über das binäre ODER eine Maske schaffen die ihre Ansteuerung enthält, hierzu ein Beispiel:

counters= 0x74;  // Schlecht
counters= COUNTER1 | BIN_COUNTER | COUNTDOWN | RATE_GEN ; // Besser

Beide Zeilen machen auf einem fiktiven Mikrocontroller das gleiche, aber für den Code in Zeile 1 müsste ein Programmierer erstmal die Dokumentation des Projekts, wahrscheinlich sogar die des Mikrocontroller lesen, um die Zählrichtung zu ändern. In der Zeile 2 weiß jeder, dass das COUNTDOWN geändert werden muss, und wenn der Entwickler des Headers gut gearbeitet hat, ist auch ein COUNTUP bereits definiert.

Die Zufallszahlen Bearbeiten

„Gott würfelt nicht“ soll Einstein gesagt haben; vielleicht hatte er recht, aber sicher ist, der Computer würfelt auch nicht. Ein Computer erzeugt Zufallszahlen, indem ein Algorithmus Zahlen ausrechnet, die - mehr oder weniger - zufällig verteilt (d.h. zufällig groß) sind. Diese nennt man Pseudozufallszahlen. Die Funktion rand() aus der stdlib.h ist ein Beispiel dafür. Für einfache Anwendungen mag rand() ausreichen, allerdings ist der verwendete Algorithmus nicht besonders gut, so dass die hiermit erzeugten Zufallszahlen einige schlechte statistische Eigenschaften aufweisen. Eine Anwendung ist etwa in Kryptografie oder Monte-Carlo-Simulationen nicht vertretbar. Hier sollten bessere Zufallszahlengeneratoren eingesetzt werden. Passende Algorithmen finden sich in der GNU scientific library [1] oder in Numerical Recipes [2] (C Version frei zugänglich [3]).

Undefiniertes Verhalten Bearbeiten

Es gibt einige Funktionen, die in gewissen Situationen ein undefiniertes Verhalten an den Tag legen. Das heißt, Sie wissen in der Praxis dann nicht, was passieren wird: Es kann passieren, dass das Programm bis in alle Ewigkeit läuft – oder auch nicht. Meiden Sie undefiniertes Verhalten! Sie begeben sich sonst in die Hand des Compilers und was dieser daraus macht. Auch ein "bei mir läuft das aber" ist keine Erlaubnis, mit diesen Schmutzeffekten zu arbeiten. Das undefinierte Verhalten zu nutzen grenzt an Sabotage.

return-Statement fehlt Bearbeiten

Wenn für eine Funktion zwar ein Rückgabewert angegeben wurde, jedoch ohne return-Statement endet, gibt der Compiler bei Standardeinstellung keinen Fehler aus. Problematisch an diesem Zustand ist, dass eine solche Funktion in diesem Fall eine zufällige, nicht festgelegte Zahl zurück gibt. Abhilfe schafft nur ein höheres Warning-Level (siehe #Der Compiler ist dein Freund]) bzw. explizit diese Warnungen mit dem Parameter -Wreturn-type einzuschalten.

Wartung des Codes Bearbeiten

Ein Programm ist ein technisches Produkt, und wie alle anderen technischen Produkte sollte es wartungsfreundlich sein. So dass Sie oder Ihr Nachfolger in der Lage sind, sich schnell wieder in das Progamm einzuarbeiten. Um das zu erreichen, sollten Sie sich einen einfach zu verstehenden Programmierstil für das Projekt suchen und sich selbst dann an den Stil halten, wenn ein anderer ihn verbrochen hat. Beim Linux-Kernel werden auch gute Patches abgelehnt, weil sie sich z.B. nicht an die Einrücktiefe gehalten haben.

Wartung der Kommentare Bearbeiten

Auch wenn es trivial erscheinen mag, wenn Sie ein Quellcode ändern, vergessen Sie nicht den Kommentar. Man könnte argumentieren, dass der Kommentar ein Teil Ihres Programms ist und so auch einer Wartung unterzogen werden sollte, wie der Code selbst. Aber die Wahrheit ist eigentlich viel einfacher; ein Kommentar, der von der Programmierung abweicht, sorgt bei dem Nächsten, der das Programm ändern muss, erstmal für große Fragezeichen im Kopf. Denn wie wir im Kapitel Programmierstil besprochen haben, soll der Kommentar helfen, die Inhalte des so genannten Fachkonzeptes zu verstehen und dieser Prozess dauert dann viel länger, als mit den richtigen Kommentaren.

Weitere Informationen Bearbeiten

Ausführlich werden die Fallstricke in C und die dadurch möglichen Sicherheitsprobleme im CERT C Secure Coding Standard dargestellt [4]. Er besteht aus einem Satz von Regeln und Empfehlungen, die bei der Programmierung beachtet werden sollten.