1 Punkte von GN⁺ 2024-04-05 | 1 Kommentare | Auf WhatsApp teilen
  • -fanalyzer in GCC 14 verfolgt mehrere Ausführungspfade von C-Code per symbolischer Ausführung und konzentriert sich darauf, Fehler bereits zur Compile-Zeit zu finden, die vor dem Lauf schwer zu entdecken sind
  • Die Verbesserungen gliedern sich diesmal in Erkennung endloser Schleifen, Visualisierung von Buffer Overflows, Nachverfolgung von C-Strings und standardmäßig aktivierte Taint-Analyse; der Fokus liegt darauf, Diagnoseergebnisse leichter verständlich zu machen
  • -Wanalyzer-out-of-bounds geht über eine einfache Warnung hinaus und zeigt mit einem Textdiagramm die Beziehung zwischen Buffer und Schreibposition; bei UTF-8-Strings wird auch angezeigt, in welches Zeichen hinein der Overflow erfolgt
  • Das neue Attribut null_terminated_string_arg(PARAM_IDX) teilt dem Analyzer und Code-Lesern mit, dass eine API nullterminierte Strings erwartet, und macht Übergabepfade mit nicht nullterminierten Buffern besser sichtbar
  • Die Taint-Analyse wird bei Auswahl von -fanalyzer standardmäßig aktiviert, wodurch sich Flüsse leichter erkennen lassen, in denen von Angreifern kontrollierte Werte ohne Grenzprüfung für Größen, Indizes, Offsets usw. verwendet werden

Welchen Problemumfang -fanalyzer abdeckt

  • -fanalyzer ist ein statischer Analyse-Pass von GCC, der mehrere Ausführungspfade von C-Quellcode per symbolischer Ausführung verfolgt, um Fehler zur Compile-Zeit zu finden
  • Die Verbesserungen in GCC 14 wurden im Vorfeld des offiziellen Releases im April 2024 zusammengefasst; zum Zeitpunkt der Veröffentlichung des Artikels wurde GCC 14.1 im Laufe des April 2024 erwartet
  • Das Pre-Release von GCC 14.0 wird bereits in Fedora 40 Beta verwendet
  • Die Beispiele lassen sich im Compiler Explorer zusammen mit den neuen Compiler-Optionen ausprobieren

Erkennung einfacher Endlosschleifen

  • GCC 14 ergänzt die neue Warnung -Wanalyzer-infinite-loop
  • Der Beispielcode zeigt in verschachtelten for-Schleifen, dass die Bedingung der zweiten Schleife j < n lautet, der Inkrementausdruck aber fälschlich i++ geblieben ist
    • Das entspricht dem Fehler, nach dem Kopieren der ersten for-Schleife das i im Inkrementausdruck nicht in j geändert zu haben
    • Der Analyzer verfolgt auf diesem Pfad fortlaufend den True-Zweig von j < n und warnt vor der Schleifenwiederholung
  • Die aktuelle Diagnoseausgabe lässt sich am leichtesten verstehen, wenn man die Ereignisnummern (1) bis (5) der Reihe nach liest
  • Für GCC 15 bleibt als Wunsch offen, die Lesbarkeit mit einer Art ASCII-Art zur Hervorhebung des Kontrollfluss-Pfads zu verbessern
  • Der entsprechende Code kann im Compiler-Explorer-Beispiel ausprobiert werden

Buffer Overflow als Text visualisieren

  • In GCC 13 begann der Analyzer mit -Wanalyzer-out-of-bounds, Prüfungen auf Grenzverletzungen zu unterstützen
  • In GCC 14 kann die räumliche Beziehung eines vorhergesagten Buffer Overflows als textbasiertes Diagramm ausgegeben werden
  • Im Beispiel mit char buf[10], auf das erst strcpy(buf, "hello") angewendet und danach strcat(buf, " world!") aufgerufen wird, wird ein stackbasierter Buffer Overflow erkannt
    • Die bisherige Meldung zeigte an, dass buf eine Kapazität von 10 Byte hat und ein Schreibzugriff von Byte 10 bis Byte 12 außerhalb des zulässigen Bereichs erfolgt
    • Das Diagramm in GCC 14 zeigt zusätzlich den von strcpy gefüllten Ziel-Buffer und das vorhandene terminierende NUL-Byte, an dem strcat ansetzt
  • Auch im Beispiel mit Nicht-ASCII-Strings wird die Overflow-Position entlang der UTF-8-Darstellung angezeigt
    • Als Beispiel dient Code, der "サツキ" in char buf[11] kopiert und anschließend "メイ" anhängt
    • Das Diagramm zeigt, dass der Overflow mitten im Zeichen , also bei U+30E1, auftritt
  • Der zugehörige Code ist im ASCII-String-Beispiel und im Nicht-ASCII-String-Beispiel zu sehen

Verbesserte Nachverfolgung von C-String-Operationen

  • Der Analyzer in GCC 14 verbessert die Nachverfolgung von C-String-Operationen und simuliert APIs, die beim Scannen eines Buffers nach dem nullterminierenden Byte suchen
  • Wenn ein Zeiger auf einen nicht nullterminierten Buffer zeigt und ein Pfad existiert, auf dem er an eine solche API übergeben wird, erfolgt eine Warnung
  • Das neue Funktionsattribut null_terminated_string_arg(PARAM_IDX) signalisiert dem Analyzer und Code-Lesern, dass ein bestimmter Parameter ein nullterminierter String sein muss
  • Im Beispiel ist example_fn(const char *p) mit den Attributen null_terminated_string_arg(1) und nonnull versehen
    • char str[3] = "abc"; hat keinen Platz für ein nullterminierendes Byte
    • Beim Aufruf von example_fn(str) warnt der Analyzer vor einem stackbasierten Buffer Over-Read, bei dem 1 Byte hinter dem Ende von str gelesen wird
    • Die Diagnose enthält außerdem eine Note, dass das erste Argument von example_fn ein Zeiger auf einen nullterminierten String sein muss
  • Dieses Beispiel lässt sich im Compiler Explorer ausprobieren

Taint-Analyse standardmäßig aktiviert

Kernel-Analyse am Beispiel von CVE-2011-2210

  • Ein aus dem Linux-Kernel entnommenes Beispiel zu CVE-2011-2210 wird als Fall für die Taint-Analyse verwendet
  • Durch das Hinzufügen von __attribute__((tainted_args)) zum Makro __SYSCALL_DEFINEx wird dem Analyzer mitgeteilt, dass die Argumente von osf_getsysinfo Werte sind, die eine Vertrauensgrenze überschritten haben und als tainted values behandelt werden müssen
  • Der Analyzer in GCC 14 warnt bei copy_to_user(buffer, hwrpb, nbytes), dass der von Angreifern kontrollierte Wert nbytes ohne obere Grenzprüfung als Größe verwendet wird
    • Die Diagnose zeigt, dass nbytes in if (nbytes < sizeof(*hwrpb)) nur eine untere Grenzprüfung erhalten hat
    • Der dritte Parameter von copy_to_user ist über das Attribut access(write_only, 1, 3) als Größenparameter gekennzeichnet
  • Das Problem liegt darin, dass die Sanitizing-Bedingung als if (nbytes < sizeof(*hwrpb)) geschrieben wurde
    • Die beabsichtigte Bedingung war von der Form if (nbytes > sizeof(*hwrpb))
    • In der korrigierten Version warnt der Analyzer nicht
  • Die Arbeit, den Analyzer im Kernel auszuführen, Schwachstellen zu finden und False Positives des Analyzers zu beheben, wird fortgesetzt

1 Kommentare

 
GN⁺ 2024-04-05
Hacker-News-Kommentare
  • Für mich ist fanalyzer eines der Killer-Features von GCC im Vergleich zu Clang. Weil er Fehler erklärt, wird C-Programmierung viel einfacher, und die Fehlermeldungen wirken entwicklerfreundlich, fast schon ähnlich wie bei Rust.

    • Mir ist klar, dass Rust besonders auf HN wegen Speichersicherheit und guter Abstraktionen viel Aufmerksamkeit bekommt, aber ich frage mich, wie viel von Rusts Beliebtheit auf die Fehlermeldungen zurückgeht.
      Einer der größten Gründe, warum Leute beim Lernen von Technik aufgeben, sind oft frustrierende oder unklare Fehler. Das ist etwas off topic, aber ich meinte: Ich mag C, und mit Fehlermeldungen auf Rust-Niveau würde es mir noch besser gefallen.
    • Clang hat auch ein ähnliches Tool, den Clang Static Analyzer: https://clang-analyzer.llvm.org/
    • Ich habe genau die gegenteilige Erfahrung gemacht. Clang zeigt oft deutlich bessere Fehlermeldungen als GCC, einige Warnungs- oder Fehlerimplementierungen erfassen mehr Fälle, und clang-tidy liefert eine viel bessere statische Analyse.
    • Das erinnert mich an einen der Gründe, warum ich C++ so gehasst habe. Statt error: missing semicolon bekam man oft über 1000 Zeilen Fehlermeldungen zur Template-Instanziierung.
    • Das klingt ziemlich überraschend. Ich frage mich, was der GCC-Analyzer zusätzlich findet, was der statische Analyzer von Clang nicht ohnehin schon meldet.
      Ich habe den GCC-Analyzer ein paarmal benutzt, aber keinen vernünftigen Frontend gefunden, der die Ausgabe gut lesbar macht. Bei Clang gibt es mehrere brauchbare Optionen wie recht ordentliche HTML-Ausgabe, CodeChecker oder Xcode-Integration. Mich würde interessieren, wie man die Ausgabe auf GCC-Seite eigentlich lesen soll. Außerdem scheint GCC viel mehr False Positives als Clang zu erzeugen.
  • In einem anderen Thread gibt es 36 weitere Kommentare: https://news.ycombinator.com/item?id=39918278
    „GCC 14 Boasts Nice ASCII Art for Visualizing Buffer Overflows (phoronix.com)“, vor 2 Stunden gepostet.

  • Vor ein paar Monaten habe ich ein kleines Linux-Utility gebaut. Es war ein Drop-in-Shim als Ersatz für eine beliebige ausführbare Datei: Beim Aufruf tat es so, als wäre es das eigentliche Programm, forkte dann das Original und verband sich mit stdout/stderr.
    Die Fehlerausgabe wurde an einen angepassten GPT-Assistenten geschickt, der den Kontext des Programms kannte. Der Assistent wandelte den ursprünglichen Fehler in eine für Menschen besser lesbare Form um und schrieb ihn dann auf stderr des Shims aus. Ich nutzte das, weil ich die verschachtelten Compiler-Dumps von GCC/Clang zu Concepts/Templates leid war, aber theoretisch hätte man es für jedes Programm verwenden können. Es funktionierte gut, aber ich wurde schwer krank und konnte nicht weiter daran arbeiten. Wenn jemand das noch einmal richtig und allgemeiner bauen würde, könnte das ein gutes Projekt sein.

  • Ich hätte gern ein besseres Ausgabeformat für die Analyseergebnisse. So wie es jetzt ist, ist es die Hölle für Screenreader.

    • Zur Info: In GCC 13 wurde SARIF-Ausgabe implementiert, und man kann sie zum Beispiel in VS Code per Plugin ansehen. Die ASCII-Art ist allerdings nicht enthalten.
      Ein Beispiel für die Ausgabe gibt es hier: https://godbolt.org/z/aan6Kfxds
      Es ist einfach das erste Beispiel aus dem Artikel mit der zusätzlichen Kommandozeilenoption -fdiagnostics-format=sarif-stderr. Es gab auch Experimente, Diagramme als SVG auszugeben, aber das wurde nicht weit genug ausgearbeitet, um es in GCC 14 aufzunehmen.
  • Der ursprüngliche Code gab -1 zurück, wenn nbytes < sizeof(*hwrpb) war, und -2, wenn copy_to_user(buffer, hwrpb, nbytes) fehlschlug. Der eingespielte Fix prüfte nbytes > sizeof(*hwrpb), aber ich halte copy_to_user(buffer, hwrpb, sizeof(*hwrpb)) für die richtige Korrektur.
    Es ergibt keinen Sinn, aus einem hwrpb-Pointer etwas mit einer anderen Größe als sizeof(*hwrpb) herauszukopieren.

    • Wenn der Aufrufer nbytes = 4 übergibt und sizeof(hwrpb) 16 Byte ist, kann man 12 Byte zu viel kopieren und Speicher lesen, der dem Aufrufer nicht gehört. Das sollte man vermeiden.
      Die bessere Lösung ist, nur die minimale Bytezahl zu kopieren, die sowohl Aufrufer als auch Aufgerufener unterstützen. Etwa so: nbytes = MIN(nbytes, sizeof(hwrpb));. Unter der Annahme, dass die Versionsinformation in hwrpb->size erhalten bleibt, kann man damit Abwärts- und Aufwärtskompatibilität gewährleisten, selbst wenn Teile der Struktur nicht initialisiert sind.
    • Stimmt. Aber da die Puffergröße vorgegeben ist, ergibt es genauso wenig Sinn, über das Ende des Aufruferpuffers hinauszuschreiben, also darf man auch keinen Wert über nbytes hinaus übergeben.
  • Wirklich hervorragend. Der Arbeitsaufwand dahinter scheint enorm zu sein. Vom Schwierigkeitsgrad her wirkt es vergleichbar mit der Einführung von fat pointers/Array-Views in Standardbibliothek und C-Standard.

  • -Wstringop-overflow ist eine Warnung, die ich als Erstes abschalte, weil sie viel zu viele False Positives hat. Ich bezweifle, dass eine Analyzer-Variante davon besser sein wird.

    • Ist das nicht ein bisschen so, als würde man bei einem Kohlenmonoxidmelder die Batterie herausnehmen, weil das ständige Piepen Kopfschmerzen macht und müde macht?
  • Sehr cool. Ich entwickle heute nicht mehr viel in C, daher frage ich mich, wie oft strcpy und strcat noch verwendet werden. Als ich zuletzt nachgesehen habe, galten sie fast als ähnlich tabu wie goto.
    Natürlich weiß ich, dass goto in der Kernel-Entwicklung oft bevorzugt wird. Ich frage mich nur, wie hilfreich C-String-Analyse in der Praxis wirklich ist.

    • In manchen Kontexten ist die Verwendung von goto eindeutig korrekt und elegant. Wenn man es um jeden Preis vermeiden will, kann das zu schwer wartbarem, hässlichem und verschachteltem Code führen. Es ist nicht häufig, aber es hat legitime Anwendungsfälle.
      Funktionen wie strcpy sind zwar weniger empfehlenswert, aber es gibt Situationen mit stärkeren Invarianten, etwa sprachseitigen Invarianten, in denen garantiert ist, dass sie korrekt sind, solange diese Invarianten nicht verletzt werden. Wenn sie doch verletzt sind, hat man ohnehin schon ein viel größeres Problem. Seltene Fälle, aber man kann argumentieren, dass eine nominell sicherere Alternative ohne Nutzen etwas weniger effizient sein könnte.
    • Solange man goto logisch äquivalent zu strukturierten Programmierkonstrukten verwendet, die C nicht hat, ist eine gewisse Nutzung von goto in C immer noch idiomatisch. Man muss vorsichtig sein, aber es ist eben C.
      longjmp mag ich allerdings überhaupt nicht.
    • Mit einfachem goto ist nichts verkehrt. Die strxcpy-Familie dagegen ist kompletter Müll und sollte aus keinem Grund verwendet werden. Dass sie im Kernel benutzt wird, ist erschreckend.
      Diese Funktionen und alle gescheiterten Versuche, sie „zu reparieren“, hätte man in die Umlaufbahn schießen sollen.
    • goto ist in Ordnung, wenn man es bedacht einsetzt. strcpy und strcat sind auch in dem Sinne „in Ordnung“, dass man weiß, dass der Code korrekt ist und im Fehlerfall etwas Großes schiefläuft. Leider trifft diese Beschreibung auf den Großteil von C zu.
    • Ich würde nicht sagen, dass goto so tabu ist wie strcat und strcpy. goto ist okay, aber strcat und strcpy, wenn sie ohne ein gleichsinnig korrekt dimensioniertes malloc im selben Scope verwendet werden, sind eher ein Code Smell.
  • Sehr gut. Schön, dass überall detaillierte Berichte beigefügt sind, die erklären, was genau falsch gelaufen ist.