Die statische Analyse ist bereits seit Jahrzehnten ein Standardwerkzeug in der Software-Entwicklung. Diese Technologie analysiert Quellcode, ohne ihn auszuführen. Sie kann also bereits sehr früh innerhalb des Software Development Lifecycles (SDLC) eingesetzt werden – lange bevor dynamische Testverfahren möglich sind. Mit Hilfe der statischen Analyse lassen sich auf relativ einfachem Weg schwerwiegende Programmierfehler wie Buffer Overruns oder Null Pointer Dereferences aufdecken, die zu Sicherheitslücken führen. Zahlreiche Branchenstandards raten deswegen zum Einsatz der statischen Analyse, bei manchen ist diese Methode vorgeschrieben.
Auch wenn Tools zur statischen Analyse in den vergangenen Jahren eine bemerkenswerte Entwicklung durchlaufen haben, sind sie nicht perfekt. Fälschlicherweise gemeldete Fehler, wo keine sind (False Positives) und übersehene Programmierfehler (False Negatives) sind vor allem bei umfangreichen Programmen noch immer unvermeidbar. Besonders manche Formen von Buffer Overruns sind für die statische Analyse schwer zu entdecken. Allerdings gibt es Ansätze, wie Buffer Overruns dynamisch zur Laufzeit erkannt werden können.
Die Grenzen der Buffer-Overrun-Erkennung
In der einfachsten Form werden bei einem Buffer Overrun Daten nach dem Ende eines Objekts im Speicher geschrieben oder gelesen. Das kann bei verschiedenen Programmiersprachen passieren, darunter die sehr weit verbreiteten Sprachen C und C++. Ein einfacher Buffer Overrun sieht etwa folgendermaßen aus:
Hier wird Speicher für ein Array von zehn Zeichen allokiert, der Zugriff erfolgt dann auf den elften Index des Arrays. Das Ergebnis dieses Zugriffs ist nicht definiert. In den meisten Fällen wird das Zeichen a in irgendeinen Speicherbereich neben dem Puffer geschrieben und der vorherige Inhalt dabei überschrieben. Derartige Fehler lassen sich mit der statischen Analyse einfach aufspüren, denn der Index-Wert ist hart kodiert. Im echten Leben jedoch sind die Indexe meist nicht hart kodiert, sondern stammen von unterschiedlichen Quellen wie Anwendereingaben, Dateien, zufälligen Variablen und dergleichen. In vielen Fällen ist das für die Analyse-Tools nur schwer nachzuvollziehen. Ob etwa ein Buffer Overrun im folgenden Beispiel auftritt, hängt vom Wert des Strings ab, auf den die Variable verweist:
Der Wert dieser Variable kann aus jeder denkbaren Quelle stammen. Das Analyse-Tool kann das oftmals allerdings nicht nachvollziehen. Was ein Tool zur statischen Analyse in diesem Fall jedoch machen kann, ist eine Warnung ausgeben, dass eine bestimmte Variable aus einer verdächtigen Quelle befüllt wird. Das Tool kann weiterhin darauf hinweisen, dass sie nicht hinreichend auf Fehlerfreiheit oder verdächtige Werte hin überprüft wird. Eine verdächtige Quelle ist zum Beispiel die Eingabe eines Benutzers, über die die statische Analyse keine Annahme treffen kann.
Im Gegensatz zur statischen Analyse wird beim Testing der Code ausgeführt und auf seine funktionale Korrektheit überprüft – also ob ein bestimmter Input den erwarteten Effekt nach sich zieht. Dieser Effekt kann sowohl eine Ausgabe sein als auch eine Änderung des Systemzustands. Allerdings geschieht es beim Testing leicht, dass Zustandsverletzungen durch Buffer Overflows unerkannt bleiben. Dafür gibt es zwei Gründe:
Das Problem tritt nur in seltenen Randbereichen des Testings auf
Die Zustandsverletzung als solche bleibt unerkannt
Funktionales Testing
Punkt 1 lässt sich durch intensives Testing lösen, das auch die Auswirkungen von fehlerhaftem Input und ungültigen Werten untersucht. Beim zweiten Aspekt wird es schwieriger. Testing-Werkzeuge sind nicht dafür gedacht, Zustandsverletzungen direkt zu entdecken. Unter Umständen werden auch die einfachsten Buffer Overruns nicht erkannt, solange das Überschreiben des Puffers nicht zu einer falschen Ausgabe oder einem Programmabbruch führt. Um Zustandsverletzungen während der Testphase zu finden, müssen die Speicherallokationen überwacht werden. Auch sollten so genannte „Canaries“ – Kanarienvögel als Anspielung auf die frühere Sitte im Bergbau, den Sauerstoffgehalt der Luft mittels dieser Tiere zu überwachen – um die entsprechenden Speicherbereiche herum gesetzt werden und dergleichen mehr.
Auch für diese Aufgaben gibt es zahlreiche Werkzeuge, jedes Tool hat dabei seine eigenen Vor- und Nachteile. Sie überwachen den Speicherzugriff während der Ausführung und liefern ein gewisses Maß an Informationen, wenn verdächtige Zugriffe erkannt werden. Verbreitete Beispiele für solche Tools sind Valgrind oder AddressSanitizer (ASan). Beide werden zu den Debugging-Tools gerechnet. Sie melden Speicherprobleme, indem sie die betroffene Speicheradresse ausgeben. Diese kann dann durch den Debugger auf einen bestimmten Bereich im Quellcode gemappt werden. Auch GrammaTech hat jüngst mit CodeSonar/X ein Werkzeug zur Speicherüberwachung vorgestellt, dass auf der Teilnahme an der DARPA Cyber Grand Challenge basiert.
Das Beste aus drei Welten
Probleme zu erkennen ist die eine Seite der Medaille, den Entwicklern die richtigen Hilfestellungen zu geben die andere. Heute existieren die drei oben skizzierten Ansätze zur Qualitätssicherung in der Software-Entwicklung in der Regel in eigenen Silos. Die statische Analyse hat den Vorteil, dass sie sehr früh im SDLC eingesetzt werden kann und dem Entwickler sehr genaue Hinweise auf Fehlerursachen gibt. Das Testing und die Speicherüberwachung hingegen bedingen einen lauffähigen Code, ihre Ergebnisse sind in Bezug auf Buffer Overruns nicht sehr aussagekräftig. Es ist für die Entwickler sehr aufwändig, diese Warnungen bis zur eigentlichen Ursache zurückzuverfolgen. Hier entstehen Kosten, zudem geht die Fehlersuche zu Lasten der Time to Market. Es liegt also nahe, die Verfahren soweit als möglich zu kombinieren. Vom Standpunkt der Sicherheit betrachtet ist die Integration der Speicherüberwachung in die statische Analyse am sinnvollsten, da sich hier zum einen Buffer Overruns am besten erkennen lassen und zum anderen die Ergebnisse des dynamischen Testings direkt in die Code-Analyse zurückfließen.
Diese Integration ist GrammaTech für das Tool CodeSonar gelungen. Hier können mit Valgrind und ASan zum einen Tools von Drittherstellern als auch das hauseigene Tool in die statische Analyse integriert werden. Dabei wird die Ausgabe der Speicherüberwachung direkt mit dem Ergebnis der Code-Analyse verknüpft. Das Ergebnis sind signifikant genauere Ergebnisse. Denn grundsätzlich besteht bei allen Qualitätssicherungsmaßnahmen ein Dilemma: Auf der einen Seite muss die Trefferquote beim Auffinden sehr hoch sein, was auch zu vielen False Positives führt. Versuche, die Zahl der False Positives zu senken, können jedoch zu Lasten der Genauigkeit gehen, Fehler bleiben dann unerkannt.
Kombinierte Warnsysteme steigern Qualität
Beim kombinierten Ansatz werden alle Zustandsverletzungen an die statische Analyse übergeben und dort in der Benutzeroberfläche angezeigt. Diese Informationen werden automatisch mit den Warnungen der statischen Analyse verknüpft. In der Praxis hat sich gezeigt, dass durch die Kombination von zwei voneinander unabhängigen Analyse- und Warnsystemen die Qualität der Meldungen signifikant steigt:
Vorhandene Warnhinweise werden bestätigt
False Negatives werden mit hoher Zuverlässigkeit erkannt
Die Bestätigung einer bestehenden Warnung erfolgt als True Positive, wenn der dynamisch erkannte potenzielle Fehler auf die gleiche Codezeile verweist wie der statisch gefundene. Für den Entwickler ist das ein klares Signal, dass ein ernsthaftes Problem vorliegt und hieran mit Hochdruck gearbeitet werden sollte. Die Erkennung von False Positives geschieht ähnlich: Kommt es beim dynamischen Test zu einer Zustandsverletzung, deren Ursache von der statischen Analyse nicht erkannt wurde, gibt das Tool eine Warnung mit hoher Priorität aus. Dabei bekommt der Entwickler den kompletten Ausführungspfad angezeigt, er kann das Problem so fokussiert angehen.
Sicherheitsrisiko Buffer Overrun
Buffer Overruns stellen ein immenses Sicherheitsrisiko dar und können damit hohe Kosten nach sich ziehen. Vor allem in Hinblick auf die immer weiter steigende Kritikalität von Software – nicht zuletzt im Embedded-Bereich – müssen diese schwerwiegenden Fehler so gut als möglich vor der Auslieferung beseitigt werden. Im Extremfall gefährdet ein unsicheres Embedded-Device sogar die Gesundheit und das Leben von Menschen.
Die statische Analyse ist ebenso wenig neu wie dynamisches Testing und Speicherüberwachung. Dennoch öffnet die Kombination dieser Ansätze richtungsweisende Möglichkeiten in der Software-Entwicklung. Umsonst ist diese Sicherheit sicher nicht zu bekommen. Investitionen in angemessene Testinfrastrukturen und durchdachte Testszenarien sind unausweichlich. Auf der anderen Seite jedoch können schwer zu findende Probleme früher und schneller erkannt werden. Und je früher ein Fehler gefunden wird, desto weniger Kosten verursacht die Beseitigung. Von den Kosten, die durch defekte ausgelieferte Produkte entstehen, ganz abgesehen.