Technologien, die im PVS-Studio Code Analyzer verwendet werden, um nach Fehlern und potenziellen Schwachstellen zu suchen

Technologie und Magie

Eine kurze Beschreibung der im PVS-Studio-Tool verwendeten Technologien, mit denen eine große Anzahl von Fehlermustern und potenziellen Schwachstellen effektiv erkannt werden kann. Der Artikel beschreibt die Implementierung des Analysators für C- und C ++ - Code. Die obigen Informationen gelten jedoch auch für die Module, die für die Analyse von C # - und Java-Code verantwortlich sind.

Einführung


Es gibt falsche Vorstellungen, dass statische Codeanalysatoren eher einfache Programme sind, die auf der Suche nach Codemustern unter Verwendung regulärer Ausdrücke basieren. Das ist weit von der Wahrheit entfernt. Darüber hinaus ist es einfach nicht möglich , die überwiegende Mehrheit der Fehler mithilfe regulärer Ausdrücke zu identifizieren.

Der Fehler entstand aufgrund der Erfahrung von Programmierern bei der Arbeit mit einigen Tools, die vor 10 bis 20 Jahren existierten. Bei der Arbeit von Tools ging es oft wirklich darum, gefährliche Codemuster und Funktionen wie strcpy , strcat usw. zu finden. Als Vertreter dieser Werkzeugklasse kann man RATS nennen .

Solche Tools waren zwar nützlich, aber im Allgemeinen dumm und ineffektiv. Aus dieser Zeit haben viele Programmierer immer noch Erinnerungen daran, dass statische Analysatoren sehr nutzlose Werkzeuge sind, die die Arbeit mehr stören als helfen.

Die Zeit verging und statische Analysatoren stellten komplexe Lösungen dar, die eine eingehende Codeanalyse durchführen und Fehler finden, die auch nach einer sorgfältigen Codeüberprüfung im Code verbleiben. Leider halten viele Programmierer die statische Analysemethode aufgrund negativer Erfahrungen in der Vergangenheit immer noch für nutzlos und haben es nicht eilig, sie in den Entwicklungsprozess einzuführen.

In diesem Artikel werde ich versuchen, die Situation ein wenig zu beheben. Ich bitte die Leser, sich 15 Minuten Zeit zu nehmen, um sich mit den Technologien vertraut zu machen, die im statischen Code-Analysator PVS-Studio zur Erkennung von Fehlern verwendet werden. Vielleicht werfen Sie danach einen neuen Blick auf die Werkzeuge der statischen Analyse und möchten sie in Ihrer Arbeit anwenden.

Datenflussanalyse


Durch die Analyse des Datenstroms können Sie eine Vielzahl von Fehlern finden. Darunter: Überschreiten der Grenzen eines Arrays, Speicherlecks, immer wahre / falsche Bedingungen, Dereferenzieren eines Nullzeigers und so weiter.

Die Datenanalyse kann auch verwendet werden, um nach Situationen zu suchen, in denen nicht überprüfte Daten verwendet werden, die von außen an das Programm gesendet wurden. Ein Angreifer kann einen solchen Satz von Eingabedaten vorbereiten, damit das Programm so funktioniert, wie es benötigt wird. Mit anderen Worten, es kann den Fehler einer unzureichenden Eingabesteuerung als Sicherheitsanfälligkeit verwenden. Um nach der Verwendung nicht verifizierter Daten in PVS-Studio zu suchen, wurde die spezialisierte Diagnose V1010 implementiert und wird weiter verbessert.

Die Analyse des Datenflusses (Datenflussanalyse) dient zur Berechnung der möglichen Werte von Variablen an verschiedenen Punkten in einem Computerprogramm. Wenn der Zeiger beispielsweise dereferenziert ist und bekannt ist, dass er zu diesem Zeitpunkt Null sein kann, handelt es sich um einen Fehler, der vom statischen Analysator gemeldet wird.

Schauen wir uns ein praktisches Beispiel für die Verwendung der Datenflussanalyse an, um nach Fehlern zu suchen. Vor uns liegt eine Funktion aus dem Projekt Protocol Buffers (protobuf), mit der die Richtigkeit des Datums überprüft werden soll.

static const int kDaysInMonth[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; bool ValidateDateTime(const DateTime& time) { if (time.year < 1 || time.year > 9999 || time.month < 1 || time.month > 12 || time.day < 1 || time.day > 31 || time.hour < 0 || time.hour > 23 || time.minute < 0 || time.minute > 59 || time.second < 0 || time.second > 59) { return false; } if (time.month == 2 && IsLeapYear(time.year)) { return time.month <= kDaysInMonth[time.month] + 1; } else { return time.month <= kDaysInMonth[time.month]; } } 

Der PVS-Studio-Analysator hat zwei logische Fehler in der Funktion gleichzeitig erkannt und zeigt die folgenden Meldungen an:

  • V547 / CWE-571 Der Ausdruck 'time.month <= kDaysInMonth [time.month] + 1' ist immer wahr. time.cc 83
  • V547 / CWE-571 Der Ausdruck 'time.month <= kDaysInMonth [time.month]' ist immer wahr. time.cc 85

Beachten Sie den Unterausdruck „time.month <1 || time.month> 12 ". Wenn der Monatswert außerhalb des Bereichs [1..12] liegt, beendet die Funktion ihre Arbeit. Der Analysator berücksichtigt dies und weiß, dass der Monatswert genau im Bereich [1..12] liegt, wenn die zweite if-Anweisung ausgeführt wurde. Ebenso kennt er den Bereich anderer Variablen (Jahr, Tag usw.), aber sie sind für uns jetzt nicht interessant.

Schauen wir uns nun zwei identische Operatoren für den Zugriff auf Array-Elemente an: kDaysInMonth [time.month] .

Das Array ist statisch festgelegt und der Analysator kennt die Werte aller seiner Elemente:

 static const int kDaysInMonth[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; 

Da die Monate von 1 nummeriert sind, berücksichtigt der Analysator am Anfang des Arrays nicht 0. Es stellt sich heraus, dass ein Wert im Bereich [28..31] aus dem Array extrahiert werden kann.

Je nachdem, ob das Jahr ein Schaltjahr ist oder nicht, wird 1 zur Anzahl der Tage addiert. Dies ist aber auch für uns jetzt nicht interessant. Die Vergleiche selbst sind wichtig:

 time.month <= kDaysInMonth[time.month] + 1; time.month <= kDaysInMonth[time.month]; 

Der Bereich [1..12] (Monatszahl) wird mit der Anzahl der Tage im Monat verglichen.

Wenn man bedenkt, dass im ersten Fall der Monat immer Februar ist ( time.month == 2 ), werden die folgenden Bereiche verglichen:

  • 2 <= 29
  • [1..12] <= [28..31]

Wie Sie sehen, ist das Ergebnis des Vergleichs immer wahr, wovor der PVS-Studio-Analysator warnt. In der Tat enthält der Code zwei identische Tippfehler. Die linke Seite des Ausdrucks sollte ein Mitglied der Tagesklasse verwenden , überhaupt keinen Monat .

Der richtige Code sollte folgendermaßen aussehen:

 if (time.month == 2 && IsLeapYear(time.year)) { return time.day <= kDaysInMonth[time.month] + 1; } else { return time.day <= kDaysInMonth[time.month]; } 

Der hier diskutierte Fehler wurde auch zuvor im Artikel " 31. Februar " beschrieben.

Symbolische Ausführung


Im vorherigen Abschnitt haben wir eine Methode betrachtet, bei der der Analysator die möglichen Werte von Variablen berechnet. Um jedoch einige Fehler zu finden, müssen die Werte der Variablen nicht bekannt sein. Symbolische Ausführung bedeutet das Lösen von Gleichungen in symbolischer Form.

Ich habe in unserer Fehlerdatenbank keine passende Demo gefunden. Betrachten Sie daher ein Beispiel für einen synthetischen Code.

 int Foo(int A, int B) { if (A == B) return 10 / (A - B); return 1; } 

Der PVS-Studio-Analysator generiert eine Warnung V609 / CWE-369 Teilen durch Null. Nenner 'A - B' == 0. test.cpp 12

Die Werte der Variablen A und B sind dem Analysator unbekannt. Der Analysator weiß jedoch, dass zum Zeitpunkt der Berechnung des Ausdrucks 10 / (A - B) die Variablen A und B gleich sind. Daher erfolgt eine Division durch 0.

Ich sagte, dass die Werte von A und B unbekannt sind. Für den allgemeinen Fall ist dies wahr. Wenn der Analysator jedoch einen Funktionsaufruf mit bestimmten Werten der tatsächlichen Argumente sieht, wird dies berücksichtigt. Betrachten Sie ein Beispiel:

 int Div(int X) { return 10 / X; } void Foo() { for (int i = 0; i < 5; ++i) Div(i); } 

Der PVS-Studio-Analysator warnt vor einer Division durch Null: V609 CWE-628 Division durch Null. Nenner 'X' == 0. Die Funktion 'Div' verarbeitet den Wert '[0..4]'. Überprüfen Sie das erste Argument. Überprüfen Sie die Zeilen: 106, 110. consoleapplication2017.cpp 106

Hier funktioniert bereits eine Mischung von Technologien: Datenflussanalyse, symbolische Ausführung und automatische Methodenanmerkung (wir werden diese Technologie im nächsten Abschnitt diskutieren). Der Analysator sieht, dass die Variable X als Divisor in der Div- Funktion verwendet wird. Auf dieser Grundlage wird automatisch eine spezielle Anmerkung für die Div- Funktion erstellt. Es wird ferner berücksichtigt, dass ein Wertebereich [0..4] als Argument X an die Funktion übergeben wird. Der Analysator kommt zu dem Schluss, dass eine Division durch 0 erfolgen sollte.

Methodenanmerkungen


Unser Team hat Tausende von Funktionen und Klassen kommentiert, die in folgenden Bereichen bereitgestellt werden:

  • Winapi
  • C Standardbibliothek
  • Standard Template Library (STL),
  • glibc (GNU C Bibliothek)
  • Qt
  • Mfc
  • zlib
  • libpng
  • Öffnet
  • usw

Alle Funktionen werden manuell mit Anmerkungen versehen, sodass Sie viele Merkmale festlegen können, die für das Auffinden von Fehlern wichtig sind. Beispielsweise wird angegeben, dass die Größe des an die Fread- Funktion übergebenen Puffers nicht geringer sein sollte als die Anzahl der Bytes, die aus der Datei gelesen werden sollen. Die Beziehung zwischen dem 2. und 3. Argument und dem Wert, den die Funktion zurückgeben kann, wird ebenfalls angegeben. Es sieht alles so aus:

PVS-Studio: Funktionsmarkierung

Dank dieser Anmerkung werden im folgenden Code, der die Fread- Funktion verwendet, sofort zwei Fehler angezeigt .

 void Foo(FILE *f) { char buf[100]; size_t i = fread(buf, sizeof(char), 1000, f); buf[i] = 1; .... } 

PVS-Studio-Warnungen:
  • V512 CWE-119 Ein Aufruf der Funktion 'fread' führt zum Überlaufen des Puffers 'buf'. test.cpp 116
  • V557 CWE-787 Array-Überlauf ist möglich. Der Wert des 'i'-Index könnte 1000 erreichen. Test.cpp 117

Zunächst multiplizierte der Analysator das 2. und 3. tatsächliche Argument und berechnete, dass die Funktion bis zu 1000 Datenbytes lesen kann. In diesem Fall beträgt die Puffergröße nur 100 Byte und kann überlaufen.

Zweitens ist der Bereich möglicher Werte der Variablen i [0..1000], da die Funktion bis zu 1000 Bytes lesen kann. Dementsprechend kann der Zugriff auf das Array am falschen Index erfolgen.

Schauen wir uns ein weiteres einfaches Beispiel für einen Fehler an, dessen Erkennung durch das Markup der Memset- Funktion ermöglicht wurde. Hier ist ein Codefragment des CryEngine V.-Projekts.

 void EnableFloatExceptions(....) { .... CONTEXT ctx; memset(&ctx, sizeof(ctx), 0); .... } 

Der PVS-Studio-Analysator hat einen Tippfehler gefunden: V575 Die Funktion 'memset' verarbeitet '0'-Elemente. Untersuchen Sie das dritte Argument. crythreadutil_win32.h 294

Verwechselt das 2. und 3. Argument der Funktion. Infolgedessen verarbeitet die Funktion 0 Bytes und unternimmt nichts. Der Analysator bemerkt diese Anomalie und warnt Programmierer davor. Wir haben diesen Fehler bereits im Artikel "Die lang erwartete Überprüfung von CryEngine V " beschrieben.

PVS-Studio Analyzer ist nicht auf die manuell festgelegten Anmerkungen beschränkt. Darüber hinaus versucht er unabhängig, Anmerkungen zu erstellen, indem er die Funktionskörper untersucht. Auf diese Weise können Sie Fehler bei unsachgemäßer Verwendung von Funktionen finden. Der Analysator merkt sich beispielsweise, dass eine Funktion nullptr zurückgeben kann. Wenn der von dieser Funktion zurückgegebene Zeiger ohne vorherige Überprüfung verwendet wird, warnt der Analysator davor. Ein Beispiel:

 int GlobalInt; int *Get() { return (rand() % 2) ? nullptr : &GlobalInt; } void Use() { *Get() = 1; } 

Warnung: V522 CWE-690 Möglicherweise wird ein potenzieller Nullzeiger 'Get ()' dereferenziert. test.cpp 129

Hinweis Sie können die Suche nach dem gerade untersuchten Fehler auf die entgegengesetzte Weise durchführen. Erinnern Sie sich an nichts und analysieren Sie jedes Mal, wenn ein Aufruf der Get- Funktion auftritt, diese unter Kenntnis der tatsächlichen Argumente. Ein solcher Algorithmus ermöglicht es Ihnen theoretisch, mehr Fehler zu finden, hat jedoch eine exponentielle Komplexität. Die Zeit für die Programmanalyse wächst hunderttausendfach, und wir betrachten diesen Ansatz aus praktischer Sicht als Sackgasse. In PVS-Studio entwickeln wir die Richtung der automatischen Annotation von Funktionen.

Mustervergleich


Technologie, die mit einem Muster übereinstimmt, scheint auf den ersten Blick wie eine Suche mit regulären Ausdrücken. In der Tat ist dies nicht so und alles ist viel komplizierter.

Erstens sind reguläre Ausdrücke, wie ich bereits sagte , im Allgemeinen wertlos. Zweitens arbeiten Analysatoren nicht mit Textzeilen, sondern mit Syntaxbäumen, die es ermöglichen, komplexere und übergeordnete Fehlermuster zu erkennen.

Betrachten Sie zwei Beispiele, eines einfacher und eines komplexer. Der erste Fehler, den ich beim Überprüfen des Quellcodes für Android gefunden habe.

 void TagMonitor::parseTagsToMonitor(String8 tagNames) { std::lock_guard<std::mutex> lock(mMonitorMutex); if (ssize_t idx = tagNames.find("3a") != -1) { ssize_t end = tagNames.find(",", idx); char* start = tagNames.lockBuffer(tagNames.size()); start[idx] = '\0'; .... } .... } 

Der PVS-Studio-Analysator erkennt das klassische Fehlermuster, das mit dem Missverständnis eines Programmierers über die Priorität von Operationen in C ++ verbunden ist: V593 / CWE-783. Überprüfen Sie den Ausdruck vom Typ 'A = B! = C'. Der Ausdruck wird wie folgt berechnet: 'A = (B! = C)'. TagMonitor.cpp 50

Schauen Sie sich diese Zeile genau an:

 if (ssize_t idx = tagNames.find("3a") != -1) { 

Der Programmierer geht davon aus, dass zu Beginn eine Zuordnung durchgeführt wird und erst dann ein Vergleich mit -1 . In der Tat steht der Vergleich an erster Stelle. Klassisch Dieser Fehler wird ausführlicher in dem Artikel zur Android-Überprüfung beschrieben (siehe Kapitel "Andere Fehler").

Betrachten Sie nun eine übergeordnete Mustervergleichsoption.

 static inline void sha1ProcessChunk(....) { .... quint8 chunkBuffer[64]; .... #ifdef SHA1_WIPE_VARIABLES .... memset(chunkBuffer, 0, 64); #endif } 

PVS-Studio Warnung: V597 CWE-14 Der Compiler könnte den Funktionsaufruf 'memset' löschen, mit dem der 'chunkBuffer'-Puffer geleert wird. Die Funktion RtlSecureZeroMemory () sollte verwendet werden, um die privaten Daten zu löschen. sha1.cpp 189

Das Wesentliche des Problems ist, dass nach dem Füllen eines Puffers mit Nullen mithilfe der Memset- Funktion dieser Puffer nirgendwo verwendet wird. Beim Kompilieren von Code mit Optimierungsflags entscheidet der Compiler, dass dieser Funktionsaufruf redundant ist, und löscht ihn. Er hat das Recht dazu, da der Aufruf einer Funktion aus Sicht der C ++ - Sprache kein beobachtbares Verhalten im Programm hat. Unmittelbar nach dem Ausfüllen des chunkBuffer- Puffers endet die Funktion sha1ProcessChunk . Da der Puffer nach dem Beenden der Funktion auf dem Stapel erstellt wird, kann er nicht mehr verwendet werden. Aus Sicht des Compilers ist es daher nicht sinnvoll, ihn mit Nullen zu füllen.

Infolgedessen bleiben irgendwo auf dem Stapel private Daten, was zu Problemen führen kann. Dieses Thema wird im Artikel " Sichere Bereinigung privater Daten " ausführlicher behandelt.

Dies ist ein Beispiel für eine Musterübereinstimmung auf hoher Ebene. Zunächst sollte der Analysator über das Vorhandensein dieser Sicherheitslücke informiert sein, die gemäß der Common Weakness Enumeration als CWE-14: Compiler-Entfernung von Code zum Löschen von Puffern klassifiziert wurde.

Zweitens muss es im Code alle Stellen finden, an denen der Puffer auf dem Stapel erstellt wird. Er wird mit der Memset- Funktion gelöscht und nirgendwo anders verwendet.

Fazit


Wie Sie sehen können, ist die statische Analyse eine sehr interessante und nützliche Methode. Sie können so früh wie möglich eine große Anzahl von Fehlern und potenziellen Schwachstellen beseitigen (siehe SAST ). Wenn Sie immer noch nicht vollständig von statischen Analysen durchdrungen sind, lade ich Sie ein, unseren Blog zu lesen, in dem wir regelmäßig Fehler analysieren, die mit PVS-Studio in verschiedenen Projekten festgestellt wurden. Sie können einfach nicht gleichgültig bleiben.

Wir freuen uns, Ihr Unternehmen bei unseren Kunden zu sehen und Ihre Anwendungen besser, zuverlässiger und sicherer zu machen.



Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Link zur Übersetzung: Andrey Karpov. Im PVS-Studio-Code-Analysator verwendete Technologien zum Auffinden von Fehlern und potenziellen Schwachstellen .

Source: https://habr.com/ru/post/de430604/


All Articles