Statische Codeanalyse Wenn einfache Fehler die Sicherheit gefährden

Autor / Redakteur: Dr. Sebastian Krings * / Michael Eckstein

Ein Programmierfehler in uftpd, einem in C implementierten FTP-Server, kann Safety und Security beeinträchtigen – und unter bestimmten Bedingungen missbraucht werden. Mit einfachen Programmiertechniken und Tools lassen sich solche Sicherheitsrisiken minimieren oder vermeiden.

Firmen zum Thema

Multitalente: Tools für die statische Codeanalyse können funktionale Sicherheitseigenschaften erkennen und potentielle Sicherheitslücken aufdecken.
Multitalente: Tools für die statische Codeanalyse können funktionale Sicherheitseigenschaften erkennen und potentielle Sicherheitslücken aufdecken.
(Bild: Axivion)

Funktionale Sicherheit (Safety) im Sinne von ISO/IEC 61508, ISO 26262 und anderen abgeleiteten Normen bezieht sich auf den Schutz vor Fehlern oder Fehlfunktionen, insbesondere im Hinblick auf Gefahren oder Risiken von Verletzungen, Verlust von Leben oder Eigentum. Es gibt diverse Programmierstandards, die Codierrichtlinien, Sicherheitsbedingungen und geeignete Entwicklungsumgebungen und Entwicklungsvorgehen von Software und Systemen definieren. Ein prominentes, in der Automobilindustrie eingesetztes Beispiel sind die MISRA C-Softwareentwicklungsrichtlinien, die 1998 eingeführt und seitdem mehrmals aktualisiert wurden.

Im Gegensatz zur funktionalen Sicherheit konzentriert sich die Softwaresicherheit (Security) auf vorsätzliche Aktivitäten, die explizit darauf abzielen, Schaden zu verursachen. Anstatt zu prüfen, ob ein System aufgrund einer Fehlfunktion Schaden verursachen kann, befasst sich Security mit der Frage, ob ein System durch einen Angreifer manipuliert werden und dadurch Schaden verursachen kann. Auch hier gibt es verschiedene Programmierrichtlinien zur Vermeidung von Sicherheitslücken, wie etwa den SEI CERT C Coding Standard, der gleichzeitig auf Security, Safety und Zuverlässigkeit abzielt. Mit der ISO/IEC TS 17961:2013 wurde eine internationale Norm für die Entwicklung von sicherheitskritischer Software etabliert.

Wie das folgende einfache Beispiel zeigt, haben Softwarefehler oft sowohl einen Safety- als auch einen Security-Aspekt. Infolgedessen knüpfen die oben genannten Normen an verschiedenen Stellen aneinander an. Für einige von ihnen sind Mappings von einem zum anderen Standard vorgesehen, die Überdeckungen und Alleinstellungsmerkmale offensichtlich machen. Safety-Standards wie MISRA haben auch Security-Aspekte aufgegriffen und umgekehrt.

Sicherheitsrisiko CVE-2020-5204

Unser Problem findet sich als CVE-2020-5204 in gängigen Datenbanken für Sicherheitsrisiken wie vulndb und wurde ursprünglich von Aaron Esau entdeckt. Es befindet sich in dem Teil von uftpd, der für die Verarbeitung des PORT-Befehls des FTPs (File Transfer Protocol) verantwortlich ist. Der PORT-Befehl wird vom Client an den Server gesendet, um den Server zu beauftragen eine Rückwärtsverbindung zum Client zu öffnen. Diese Verbindung kann dann zur Übertragung von Dateien genutzt werden.

Damit sich der Server zu ihnen zurück verbinden kann, müssen die Clients eine IP-Adresse und eine Portnummer angeben. Da FTP ein Klartextprotokoll ist, sendet der Client einfach einen String im Format: „PORT (ip1,ip2,ip3,ip4,port1,port2)“. Der Server verwendet dann ip1 bis ip4 als die vier Gruppen einer IPv4-IP-Adresse und berechnet den Port für die Verbindung als port = (port1 * 256) + port2.

Das Problem

Doch wie extrahiert uftpd die IP-Adresse aus dem angegebenen Benutzerbefehl? Das komplette Kommando liegt als ein String vor, aus dem die einzelnen Felder herausgeschnitten werden müssen. Vor der Problembehebung sah der Code wie folgt aus:

(Bild: Axivion)

Die Parameter ip1,ip2,ip3,ip4,port1, und port2, die aus dem vom Client gesendeten Befehl extrahiert wurden, befinden sich in der Zeichenkette str. INET_ADDRSTRLEN als Angabe der Puffergröße ist 16. Das ist ausreichend groß, um eine IPv4-Adresse zu speichern: 3 Bytes pro Gruppe (die jeweils eine bis drei Ziffern enthalten), 3 Bytes für die Punkte und ein Byte für den Nullterminator. Mit sscanf werden die sechs Zahlen aus str extrahiert und in einzelnen Integervariablen gespeichert. Anschließend werden sie mit sprintf wieder zu einem String für eine korrekte IP-Adresse zusammengesetzt. Allerdings nimmt sprintf keine Überprüfung der Größe des Buffers vor, in den es schreibt: Wenn der Client zu große Zahlen (d.h. Zahlen mit mehr als drei Ziffern) liefert, schreibt sprintf mehr, als in addr gespeichert werden kann. Alles, was über addr auf dem Stack liegt, wird überschrieben – das führt zu einem klassischen Stack-Buffer-Overflow.

Auswirkungen

Um die Auswirkungen des Problems zu untersuchen, haben wir den oben gezeigten Code in eine einfache Beispielanwendung extrahiert. Wenn sie mit Zahlen in gültigen Bereichen ausgeführt wird, verhält sich die Anwendung wie erwartet:

(Bild: Axivion)

Sobald einer der Parameter zu viele Ziffern enthält, überschreibt und ersetzt sprintf Teile des Stacks. Dadurch kann die Ausführung nicht korrekt fortgesetzt werden:

(Bild: Axivion)

Das Programm bricht sofort ab – was für eine sicherheitskritische Komponente inakzeptabel wäre. Ein weiteres Sicherheitsproblem ist, dass der Buffer-Overflow dazu genutzt werden kann, Teile des Stacks zu überschreiben. Insbesondere könnte die Rücksprungadresse überschrieben werden. Letztendlich könnte dies dazu führen, dass ein Angreifer die Kontrolle über den laufenden uftpd-Prozess erlangt und die Ausführung von fremdem Code ermöglicht wird.

Glücklicherweise kann wegen des %d in den Formatstrings von sscanf und sprintf der Puffer und damit der Rest des Stacks nur durch Zeichen im entsprechenden Formatüberschrieben werden, also die Ziffern 0 bis 9 sowie das -. Dadurch kann die Rücksprungadresse nicht willkürlich ersetzt werden, und die möglichen Auswirkungen eines Angriffs werden verringert.

Die Fehlerbehebung

Um dem Buffer-Overflow von vornherein vorzubeugen, wurde sprintf durch die Entwickler durch snprintf ersetzt, das vom Aufrufer die Angabe einer maximal zu schreibender Größe verlangt und damit ein Überschreiben verhindert:

(Bild: Axivion)

Abschwächen und Beheben

Stack-Buffer-Overflows sind ein sehr häufiges Problem: Die Common Weakness Enumeration führt sie in ihrer Liste der Top 25 Most Dangerous Software Weaknesses auf. Buffer-Overflows sind für einige der bekanntesten Probleme verantwortlich, die Beispiele für Auswirkungen reichen von Datenbanken bis hin zu Schiffen der US Navy. Infolgedessen wurden verschiedene Techniken zu ihrer Vermeidung entwickelt.

Stack-Canaries und Randomisierung

Die Idee eines Stack-Canary ist die Platzierung eines zufälligen Integers auf dem Stack kurz oder direkt vor der Rücksprungadresse. Wird ein Buffer-Overflow verwendet, um den Zeiger zu ersetzen und so die Kontrolle über den Prozess zu erlangen, wird auch der Canary überschrieben. Bevor eine Routine tatsächlich zurückkehrt, wird der Canary überprüft. Wurde er verändert, erkennt das System, dass ein Überlauf stattgefunden hat und stoppt die Ausführung.

Bei der Randomisierung wird der von einer Anwendung verwendete Adressraum nicht mehr geordnet, sondern zufällig zugewiesen. Das macht es schwieriger, den Buffer-Overflow tatsächlich in auszunutzen, um Schadcode auszuführen. Während die möglichen Sicherheitsauswirkungen reduziert werden, stürzt die Anwendung dennoch ab.

Statische Analyse

Mit Hilfe von statischer Analyse kann der potenzielle Stack-Buffer-Overflow in unserem Beispiel bereits in der Implementierungsphase erkannt werden, so dass der Entwickler die fehlerhaften Aufrufe durch bessere Alternativen ersetzen kann. Speziell für unser Beispiel bietet die Axivion Suite mehrere Regelsätze, die das Problem aufgedeckt hätten:

  • Ein auf dem SEI CERT Coding Standard basierender Regelsatz, der den Pufferüberlauf als Teil der STR31-C-Regel erkennt: „Guarantee that storage for strings has sufficient space for character data and the null terminator“ („“).
  • Ein Regelsatz, der auf den C Secure Coding-Regeln basiert, wie in ISO/IEC TS 17961:2013 definiert: Hier wird das Problem als Teil der Regel 5.40 erkannt, die den Titel „Using a tainted value to write to an object using a formatted input or output function“ („Verwenden eines veränderten Wertes zum Schreiben in ein Objekt mit einer formatierten Ein- oder Ausgabefunktion“) trägt.

Fazit

Selbst auf den ersten Blick – kleine Programmierfehler können sowohl Safety als auch Security eines Systems gefährden. In manchen Fällen kann bereits mit einfachen Kniffen Abhilfe geschaffen werden, um zum Beispiel Stack-Buffer-Overflows zu vermeiden, die zu einem Absturz des Systems führen oder einen unberechtigten Zugriff auf den Stack gewähren könnten. Ein Höchstmaß an Sicherheit bieten jedoch Tools für die statische Analyse: Mit ihnen kann der Entwickler seinen Code bereits in einem frühen Stadium der Softwareentwicklung entsprechend branchenweit anerkannter Richtlinien prüfen und mögliche Schwachstellen in der Programmierung aufdecken.

* Dr. Sebastian Krings ist Software Engineer im Bereich Professional Services und R&D bei Axivion GmbH, Stuttgart

(ID:47387187)