Blog 5 Min. Lesezeit

C++-Code sicherer machen mit nativen Compiler-Laufzeitprüfungen

Diesen Artikel teilen
Make Your C++ Code Safer by Enabling Native Compiler Runtime Checks

Laufzeitprüfungen in C++ sind Mechanismen oder Werkzeuge, die während der Programmausführung Fehler, Schwachstellen oder unerwartetes Verhalten erkennen. Sie werden dynamisch zur Laufzeit statt zur Kompilierzeit ausgeführt und können Probleme aufdecken, die bei statischer Analyse oder Code-Reviews nicht sichtbar werden.

In der Softwareentwicklung bezeichnet der Begriff „Sanitizer“ diese Art von Laufzeitprüfung. Sanitizer instrumentieren den Code mit zusätzlichen Prüfungen oder Überwachungsmechanismen, um typische Programmierfehler und Sicherheitslücken zu erkennen. Wird ein Problem gefunden, liefert der Sanitizer in der Regel Hinweise oder Diagnoseinformationen, die Entwicklern helfen, die Ursache zu verstehen und zu beheben.

Nicht allen C++-Entwicklern ist bewusst, dass nahezu alle C++-Compiler native Laufzeitprüfungen bereitstellen. Clang, GCC und der Microsoft-Compiler bieten leistungsfähige Sanitizer, mit denen sich die Sicherheit von C++-Code verbessern lässt.

Der Microsoft-Compiler aktiviert diese Laufzeitprüfungen im Debug-Modus standardmäßig. Bei Clang und GCC müssen sie dagegen manuell eingeschaltet werden.

Hier einige der bekanntesten Sanitizer:

  1. AddressSanitizer (ASan): AddressSanitizer erkennt Speicherfehler wie Buffer Overflows, Use-after-free und Zugriffe außerhalb gültiger Grenzen. Dazu wird neben dem Programmspeicher ein Shadow-Memory-Bereich angelegt, gegen den Speicherzugriffe geprüft werden.
  2. MemorySanitizer (MSan): MemorySanitizer erkennt die Verwendung nicht initialisierten Speichers, die zu undefiniertem Verhalten und Sicherheitslücken führen kann. Er verfolgt zur Laufzeit den Initialisierungsstatus von Speicherbereichen und meldet Zugriffe auf nicht initialisierten Speicher.
  3. UndefinedBehaviorSanitizer (UBSan): UndefinedBehaviorSanitizer erkennt undefiniertes Verhalten in C- und C++-Code, etwa Integer-Überläufe oder die Dereferenzierung von Nullzeigern. Der Code wird so instrumentiert, dass solche Fälle zur Laufzeit erkannt und gemeldet werden.
  4. ThreadSanitizer (TSan): ThreadSanitizer erkennt Data Races und andere Nebenläufigkeitsfehler in mehrthreadigem C- und C++-Code. Dazu analysiert er die Programmausführung und identifiziert konkurrierende Zugriffe verschiedener Threads auf gemeinsam genutzten Speicher.
  5. DataFlowSanitizer (DFSan): DataFlowSanitizer erkennt Taint-basierte Schwachstellen, indem er den Datenfluss durch das Programm verfolgt und potenziell gefährliche Operationen mit verunreinigten Daten identifiziert, etwa SQL- oder Command-Injection.

Clang

Clang bietet zahlreiche Optionen zur Anpassung der Laufzeitprüfungen, wie hier.

Zum Beispiel:

% clang++ -fsanitize=signed-integer-overflow,null,alignment -fno-sanitize-recover=null -fsanitize-trap=alignment a.cc

Das Programm läuft nach Überläufen vorzeichenbehafteter Ganzzahlen weiter, beendet sich nach der ersten ungültigen Verwendung eines Nullzeigers und löst nach der ersten Verwendung eines falsch ausgerichteten Zeigers einen Trap aus.

Clang-Sanitizer helfen dabei, Fehler und Schwachstellen früh im Entwicklungsprozess zu erkennen, bevor sie sich im Produktivcode als Laufzeitfehler oder Sicherheitslücken bemerkbar machen. Besonders wertvoll sind sie bei schwer diagnostizierbaren Problemen, die herkömmliche Testmethoden möglicherweise nicht erfassen.

Microsoft-C++-Compiler

Auch der Microsoft-Compiler bietet zahlreiche Laufzeitprüfungen, wie hier.

Wenn Sie ein Programm mit aktivierten Laufzeitprüfungen debuggen, wird es bei einem Laufzeitfehler standardmäßig angehalten und an den Debugger übergeben. Dieses Standardverhalten lässt sich für jede Laufzeitprüfung ändern. Weitere Informationen finden Sie unter Ausnahmen mit dem Debugger verwalten.

GCC

GCC unterstützt zahlreiche Kommandozeilenoptionen, mit denen sich zusätzliche Laufzeitinstrumentierung in den erzeugten Code einbauen lässt. Diese kann etwa Profiling-Statistiken zur Ermittlung von Hotspots, zur Code-Coverage-Analyse oder für profilgesteuerte Optimierungen sammeln. Eine weitere Form der Instrumentierung ergänzt Laufzeitprüfungen, um Programmierfehler wie ungültige Zeigerdereferenzierungen oder Arrayzugriffe außerhalb der Grenzen sowie gezielte Angriffe wie Stack Smashing oder C++-VTable-Hijacking zu erkennen. Eine vollständige Liste der Sanitizer finden Sie hier.

Wie wird der Einsatz von C++-Sanitizern zur Gewohnheit?

  1. Sanitizer kennenlernen: Machen Sie sich und Ihr Team mit den verschiedenen C++-Sanitizern wie AddressSanitizer (ASan), UndefinedBehaviorSanitizer (UBSan) und ThreadSanitizer (TSan) vertraut. Verstehen Sie, welche Arten von Fehlern und Schwachstellen jeder Sanitizer erkennt.
  2. Sanitizer in den Build-Prozess integrieren: Aktivieren Sie Sanitizer standardmäßig beim Build Ihrer C++-Projekte. Passen Sie Build-Skripte oder Konfigurationsdateien wie CMakeLists.txt so an, dass die entsprechenden Compiler-Flags gesetzt werden.
  3. Mit einem kleinen Projekt beginnen: Führen Sie Sanitizer zunächst in einem kleinen, nicht kritischen Projekt oder Modul Ihrer Codebasis ein. So können Sie sich mit dem Ablauf vertraut machen, ohne sich oder Ihr Team zu überfordern.
  4. Sanitizer für Tests und Debugging aktivieren: Führen Sie Tests und Debug-Builds regelmäßig mit aktivierten Sanitizern aus. So werden Fehler früh im Entwicklungszyklus erkannt und lassen sich einfacher und kostengünstiger beheben.
  5. Sanitizer-Ausgaben regelmäßig prüfen: Prüfen Sie die von Sanitizern erzeugten Meldungen sorgfältig. Nehmen Sie sich Zeit, erkannte Probleme und deren Ursachen zu verstehen, und nutzen Sie dieses Feedback, um den Code zu verbessern und ähnliche Fehler künftig zu vermeiden.
  6. Sanitizer in Code-Reviews einbeziehen: Ermutigen Sie Teammitglieder, Sanitizer im Rahmen von Code-Reviews einzusetzen. Besprechen Sie gefundene Probleme und beheben Sie sie gemeinsam.
  7. Schulungen und Unterstützung anbieten: Bieten Sie Schulungen oder Workshops an, damit Teammitglieder lernen, Sanitizer effektiv einzusetzen. Unterstützen Sie insbesondere diejenigen, die noch wenig Erfahrung damit haben.
  8. Best Practices dokumentieren: Dokumentieren Sie Best Practices für den Einsatz von Sanitizern in Ihrer Organisation. Halten Sie fest, wann und wie sie aktiviert werden, wie ihre Ausgaben zu interpretieren sind und wie typische gemeldete Probleme behoben werden.
  9. Ziele setzen und Fortschritte verfolgen: Setzen Sie Ziele, um den Einsatz von Sanitizern im Team oder Unternehmen zu steigern. Verfolgen Sie die Fortschritte regelmäßig und würdigen Sie Meilensteine, um die Nutzung dauerhaft zu etablieren.
  10. Kontinuierliche Verbesserung: Bewerten und verbessern Sie Ihre Prozesse für den Sanitizer-Einsatz kontinuierlich. Holen Sie Feedback aus dem Team ein und passen Sie Ihr Vorgehen anhand der gewonnenen Erfahrungen an.

Wenn Sie diese Schritte befolgen und Sanitizer als Standardpraxis im Team etablieren, können Sie C++-Sanitizer dauerhaft und effektiv einsetzen, um Qualität und Sicherheit Ihres Codes zu verbessern.

Diesen Artikel teilen