Parasoft-Logo Suche

Entdecken Sie das TÜV-zertifizierte GoogleTest mit Agentic AI für C/C++-Tests!
Details ansehen »

Parasoft-Blog

So verhindern Sie Pufferüberlauf und andere Fehler bei der Speicherverwaltung

By Artur Hicken November 28, 2023 10 min gelesen
November 28, 2023 | 10 min gelesen
By Artur Hicken
Text links: So verhindern Sie Pufferüberlauf und andere Fehler bei der Speicherverwaltung. Rechts ist ein Bild von Lichtstrahlen zu sehen, die von einem hellen Licht ausgehen.

Wenn die Datenmenge die Speicherkapazität des Speicherpuffers überschreitet, kommt es zu einem Pufferüberlauf. Sehen Sie sich an, wie die Static Analysis Checker-Funktion in der Parasoft C/C++-Lösung Ihnen helfen kann, Fehler aufgrund von Pufferüberläufen zu verwalten.

Speichermanagement ist mit vielen Gefahren behaftet, insbesondere in C und C++. Tatsächlich machen Fehler, die auf Schwächen im Speichermanagement zurückzuführen sind, einen beträchtlichen Teil der CWE Top 25 aus. Acht der Top 25 hängen direkt mit Pufferüberläufen , fehlerhaften Zeigern und dem Speichermanagement zusammen.

Die mit Abstand größte Software-Schwachstelle ist CWE-119, „Unzulässige Einschränkung von Operationen innerhalb der Grenzen eines Speicherpuffers“. Diese Art von Fehlern spielt eine wichtige Rolle bei Sicherheitsproblemen in allen Arten von Software, einschließlich sicherheitskritischer Anwendungen in Automobilen, medizinischen Geräten und der Avionik.

Hier sind die Speicherfehler im Zusammenhang mit allgemeinen Schwachstellenaufzählungen aus den CWE Top 25:

RangIDName
[1].CWE-787Außerhalb der Grenzen schreiben
[4].CWE-416Verwenden Sie nach Frei
[7].CWE-125Außerhalb der Grenzen lesen
[12].CWE-476NULL-Zeiger-Dereferenzierung
[14].CWE-190Integer Overflow oder Wraparound
[17].CWE-119Unsachgemäße Einschränkung von Operationen innerhalb der Grenzen eines Speicherpuffers

Obwohl diese Fehler C, C ++ und andere Sprachen seit Jahrzehnten plagen, treten sie heute immer häufiger auf. Sie sind gefährliche Fehler in Bezug auf Konsequenzen für Qualität, Sicherheit und Zuverlässigkeit, und ihre Anwesenheit ist eine der Hauptursachen für Sicherheitslücken.

Was sind Pufferüberläufe?

Ein Pufferüberlauf tritt auf, wenn mehr Daten in einen Speicherabschnitt oder Puffer geschrieben werden, als dieser aufnehmen kann, beispielsweise wenn Sie versuchen, 12 Buchstaben in ein Feld einzufügen, das nur 10 Buchstaben enthält. Dies kann zum Überschreiben des angrenzenden Speichers führen Leerzeichen, was zu unvorhersehbarem Verhalten in einem Programm führt.

Pufferüberlauffehler können die Qualität, Sicherheit und Zuverlässigkeit von Software erheblich beeinträchtigen. Aus Sicherheitsgründen können böswillige Akteure Pufferüberlauffehler ausnutzen, um beliebigen Code auszuführen oder den Betrieb eines Systems zu stören. Dies liegt daran, dass ein Angreifer bei einem Pufferüberlauf möglicherweise steuern kann, welche Daten über den Puffer hinaus geschrieben werden, und so möglicherweise den Ausführungsfluss des Programms ändern kann.

Daher ist es für Entwickler unerlässlich, eine robuste Datenverarbeitung zu implementieren und ihre Software ordnungsgemäß zu testen, um Pufferüberlauffehler zu vermeiden und die Integrität und Sicherheit ihrer Software zu gewährleisten.

Hauptursache für Sicherheitslücken

Microsoft stellte fest , dass in den letzten zwölf Jahren über 70 % der Sicherheitslücken in ihren Produkten auf Speicherprobleme zurückzuführen waren . Diese Art von Fehlern stellt die größte Angriffsfläche für ihre Anwendungen dar, und Hacker nutzen sie aus. Laut ihrer Untersuchung sind die häufigsten Ursachen für Sicherheitsangriffe Heap-Out-of-Bounds-Schwachstellen, Use-After-Free-Schwachstellen und nicht initialisierte Speichernutzung. Wie Microsoft betont, existieren diese Schwachstellen seit mindestens 20 Jahren und sind nach wie vor weit verbreitet.

In ähnlicher Weise hat Google herausgefunden, dass 70 % der Sicherheitslücken im Chromium-Projekt, der Open-Source-Basis für den Chrome-Browser, auf dieselben Speicherverwaltungsprobleme zurückzuführen sind. Ihre Hauptursache war ebenfalls „Use-after-free“, gefolgt von anderer unsicherer Speicherverwaltung.

Angesichts dieser Beispiele realer Erkenntnisse ist es wichtig, dass Softwareteams diese Art von Fehlern ernst nehmen. Glücklicherweise gibt es Möglichkeiten, diese Art von Problemen mit einer effektiven und effizienten statischen Analyse zu verhindern und zu erkennen.

Wie Speicherverwaltungsfehler zu Sicherheitslücken führen

In den meisten Fällen sind Speicherverwaltungsfehler das Ergebnis schlechter Programmierpraktiken bei Verwendung von Zeigern in C / C ++ und direktem Zugriff auf den Speicher. In anderen Fällen hängt es damit zusammen, dass schlechte Annahmen über die Länge und den Inhalt von Daten getroffen werden.

Diese Software-Schwachstellen werden meist mit verunreinigten Daten ausgenutzt – Daten von außerhalb der Anwendung, die weder auf Länge noch auf Format geprüft wurden. Die berüchtigte Heartbleed- Schwachstelle ist ein Beispiel für die Ausnutzung eines Pufferüberlaufs. Genauer gesagt handelt es sich um einen Pufferüberlauf. Wie wir bereits in unserem Blogbeitrag über SQL-Injections erläutert haben , stellt die Verwendung ungeprüfter und unbeschränkter Eingaben ein Sicherheitsrisiko dar.

Betrachten wir einige der wichtigsten Kategorien von Schwächen in Speicherverwaltungssoftware. Die übergreifende Schwäche ist CWE-119 : Unzureichende Beschränkung von Operationen innerhalb der Grenzen eines Speicherpuffers.

Pufferüberlauf

Programmiersprachen, meist C und C++, die direkten Speicherzugriff ermöglichen und die Gültigkeit der Speicheradressen nicht automatisch überprüfen, sind anfällig für Speicherbeschädigungsfehler . Diese Beschädigungen können sowohl in Daten- als auch in Codebereichen des Speichers auftreten und sensible Informationen offenlegen, unbeabsichtigte Codeausführung verursachen oder zum Absturz einer Anwendung führen.

Das folgende Beispiel zeigt einen klassischen Fall eines Pufferüberlaufs aus CWE-120 :

char last_name[20];
printf ("Enter your last name: ");
scanf ("%s", last_name);

In diesem Fall gibt es keine Beschränkung für die Benutzereingabe durch `scanf()` , die maximale Länge von `last_name` beträgt jedoch 20 Zeichen. Die Eingabe eines Nachnamens mit mehr als 20 Zeichen führt dazu, dass die Benutzereingabe über die Grenzen des Puffers `last_name` hinaus in den Speicher kopiert wird . Hier ist ein subtileres Beispiel aus CWE-119:

void host_lookup(char *user_supplied_addr){
struct hostent *hp;
in_addr_t *addr;
char hostname[64];
in_addr_t inet_addr(const char *cp);

/*routine that ensures user_supplied_addr is in the right format for conversion */

validate_addr_form(user_supplied_addr);
addr = inet_addr(user_supplied_addr);
hp = gethostbyaddr( addr, sizeof(struct in_addr), AF_INET);
strcpy(hostname, hp->h_name);
}

Diese Funktion verwendet eine vom Benutzer bereitgestellte Zeichenfolge, die eine IP-Adresse enthält, beispielsweise 127.0.0.1, und ruft den Hostnamen dafür ab.

Die Funktion validiert die Benutzereingabe (gut!), prüft aber nicht die Ausgabe von `gethostbyaddr()` (schlecht!). In diesem Fall reicht ein langer Hostname aus, um den auf 64 Zeichen begrenzten Hostnamenpuffer zu überlaufen. Beachten Sie, dass auch ein Nullzeiger-Dereferenzierungsfehler auftritt, wenn `gethostaddr() ` `null` zurückgibt, weil kein Hostname gefunden werden kann!

Use-After-Free-Fehler

Interessanterweise stellte Microsoft in seiner Studie fest, dass Use-After-Free-Fehler die häufigsten Probleme bei der Speicherverwaltung waren. Wie der Name schon sagt, bezieht sich der Fehler auf die Verwendung von Zeigern im Fall von C/C++, die auf zuvor freigegebenen Speicher zugreifen. C und C++ verlassen sich normalerweise darauf, dass der Entwickler die Speicherzuweisung verwaltet, was oft schwierig zu erledigen ist. Wie das folgende Beispiel aus CWE-416 zeigt, kann man oft leicht davon ausgehen, dass ein Zeiger noch gültig ist:

char* ptr = (char*)malloc (SIZE);
if (err) {
abrt = 1;
free(ptr);
}

if (abrt) {
logError("operation aborted before commit", ptr);
}

Im obigen Beispiel ist der Zeiger `ptr` frei, wenn `an err` wahr ist. Er wird jedoch später, nachdem er freigegeben wurde, dereferenziert, wenn `abrt` wahr ist, was wiederum auf `true` gesetzt wird, wenn `err` wahr ist. Dies mag konstruiert wirken, ist aber bei viel Code zwischen diesen beiden Codeabschnitten leicht zu übersehen. Außerdem tritt dies möglicherweise nur in einem Fehlerfall auf, der nicht ausreichend geprüft wird.

NULL-Zeiger-Dereferenzierung

Eine weitere häufige Software-Schwachstelle ist die Verwendung von Zeigern oder Objekten in C++ und Java, die zwar gültig sein sollten, aber NULL sind. Obwohl diese Dereferenzierungen in Sprachen wie Java als Ausnahmen abgefangen werden, können sie zum Anhalten, Beenden oder Absturz einer Anwendung führen. Betrachten Sie das folgende Beispiel in Java aus CWE-476 :

String cmd = System.getProperty("cmd");
cmd = cmd.trim();

Dies erscheint harmlos, da der Entwickler annehmen könnte, dass die Methode `getProperty()` immer einen Wert zurückgibt. Tatsächlich wird jedoch `NULL` zurückgegeben, wenn die Eigenschaft „cmd“ nicht existiert, was bei ihrer Verwendung zu einer Nullzeiger-Ausnahme führt. Obwohl dies zunächst unbedenklich klingt, kann es schwerwiegende Folgen haben.

In seltenen Fällen ist das Schreiben oder Lesen von Speicher möglich, wenn NULL der 0x0-Speicheradresse entspricht und privilegierter Code darauf zugreifen kann, was zur Codeausführung führen kann.

Effektive Minderungsstrategien

Es gibt verschiedene Abhilfemaßnahmen, die Entwickler implementieren sollten. In erster Linie müssen Entwickler sicherstellen, dass Zeiger für Sprachen wie C und C ++ gültig sind, und zwar mit verifizierter Logik und gründlicher Überprüfung.

Für alle Sprachen ist es unbedingt erforderlich, dass Code oder Bibliotheken, die den Speicher manipulieren, Eingabeparameter validieren, um einen Zugriff außerhalb der Grenzen zu verhindern. Im Folgenden finden Sie einige verfügbare Optionen zur Schadensbegrenzung. Entwickler sollten sich jedoch nicht darauf verlassen, dass sie schlechte Programmierpraktiken ausgleichen.

Wahl der Programmiersprache

Einige Sprachen bieten integrierten Schutz vor Überläufen wie Ada und C #.

Verwendung sicherer Bibliotheken

Es gibt Bibliotheken wie die Safe C String Library, die integrierte Prüfungen zur Vermeidung von Speicherfehlern bieten. Allerdings sind nicht alle Pufferüberläufe auf Stringmanipulationen zurückzuführen. Andernfalls sollten Programmierer stets Funktionen verwenden, die die Pufferlänge als Argumente entgegennehmen, beispielsweise ` strncpy()` anstelle von `strcpy()`.

Kompilierung und Laufzeithärtung

Dieser Ansatz verwendet Kompilierungsoptionen, die der Anwendung Code hinzufügen, um die Verwendung von Zeigern zu überwachen. Dieser hinzugefügte Code kann verhindern, dass zur Laufzeit Überlauffehler auftreten.

Härten der Ausführungsumgebung

Betriebssysteme verfügen über Optionen, um die Ausführung von Code in Datenbereichen einer Anwendung zu verhindern, z. B. einen Stapelüberlauf mit Code-Injection. Es gibt auch Optionen zum zufälligen Anordnen der Speicherzuordnung, um zu verhindern, dass Hacker vorhersagen, wo sich ausnutzbarer Code befinden könnte.

Trotz dieser Abschwächungen gibt es keinen Ersatz für geeignete Codierungspraktiken, um Pufferüberläufe überhaupt zu vermeiden. Daher sind Erkennung und Vorbeugung von entscheidender Bedeutung, um das Risiko dieser Softwareschwächen zu verringern.

Verschieben Sie die Erkennung und Beseitigung von Pufferüberläufen

Die Anwendung eines DevSecOps-Ansatzes in der Softwareentwicklung bedeutet, Sicherheit in alle Aspekte der DevOps-Pipeline zu integrieren. Genau wie Qualitätsprozesse wie Codeanalyse und Unit-Tests so früh wie möglich im Softwareentwicklungszyklus (SDLC) vorangetrieben werden , gilt dies auch für die Sicherheit.

Pufferüberläufe und andere Speicherverwaltungsfehler könnten der Vergangenheit angehören, wenn die Entwicklungsteams einen solchen Ansatz allgemeiner anwenden würden. Wie Untersuchungen von Google und Microsoft zeigen, machen diese Fehler immer noch 70% ihrer Sicherheitslücken aus. Lassen Sie uns unabhängig davon einen Ansatz skizzieren, der sie so früh wie möglich verhindert.

Das Auffinden und Beheben von Speicherverwaltungsfehlern zahlt sich im Vergleich zum Patchen einer freigegebenen Anwendung aus. Der unten beschriebene Ansatz zum Erkennen und Verhindern basiert auf der Verlagerung der Minderung von Pufferüberläufen nach links in die frühesten Entwicklungsstadien. Und dies durch Erkennung durch statische Code-Analyse zu verstärken.

Detection

Das Erkennen von Speicherverwaltungsfehlern beruht auf einer statischen Analyse, um diese Arten von Schwachstellen im Quellcode zu finden. Die Erkennung erfolgt auf dem Desktop des Entwicklers und im Build-System. Es kann vorhandenen Code, Legacy-Code und Code von Drittanbietern enthalten.

Durch die kontinuierliche Erkennung von Sicherheitsproblemen wird sichergestellt, dass alle Probleme gefunden werden, die:

  • Entwickler in der IDE verpasst.
  • Bestehen Sie in Code, der älter ist als Ihr neuer Ansatz zum Erkennen und Verhindern.

Der empfohlene Ansatz ist ein Trust-But-Verify-Modell. Die Sicherheitsanalyse wird auf IDE-Ebene durchgeführt, wobei Entwickler anhand der erhaltenen Berichte Entscheidungen in Echtzeit treffen. Überprüfen Sie als Nächstes auf Build-Ebene. Im Idealfall besteht das Ziel auf Build-Ebene nicht darin, Schwachstellen zu finden. Es soll überprüft werden, ob das System sauber ist.

Parasoft C/C++test beinhaltet statische Analyseprüfungen für diese Arten von Speicherverwaltungsfehlern, einschließlich Pufferüberläufen. Betrachten Sie das folgende Beispiel aus C/C++test.

Screenshot des Parasoft C/C++-Tests, der die Erkennung eines Speicherzugriffs außerhalb der Grenzen zeigt.

Parasoft C/C++-Testbeispiel, das die Erkennung eines außerhalb der Grenzen liegenden Speicherzugriffs zeigt.

Bei genauerer Betrachtung der Details erkennt die Funktion printMessage() den Fehler:

Parasoft-C / C ++ - Test Beispiel für einen Fehler

Der Parasoft C / C ++ - Test bietet auch Ablaufverfolgungsinformationen darüber, wie das Tool zu dieser Warnung gelangt ist:

Parasoft C / C ++ - Test Beispiel für Trace-Informationen

In der Seitenleiste werden Details zum Beheben dieser Sicherheitsanfälligkeit sowie entsprechende Verweise angezeigt:

Screenshot mit Details zur Behebung einer CWE Top 25-Schwachstelle sowie entsprechenden Referenzen.

Eine genaue Erkennung sowie unterstützende Informationen und Korrekturempfehlungen sind entscheidend, damit statische Analysen und die frühzeitige Erkennung dieser Sicherheitsanfälligkeiten für Entwickler nützlich und sofort umsetzbar sind.

Verhindern von Pufferüberläufen und anderen Speicherverwaltungsfehlern

Die ideale Zeit und der ideale Ort, um Pufferüberläufe zu verhindern, ist, wenn Entwickler Code in ihre IDE schreiben. Teams, die sichere Codierungsstandards wie SEI CERT C für C und C ++ und OWASP Top 10 für Java und .NET oder CWE Top 25 anwenden, haben Richtlinien, die vor Speicherverwaltungsfehlern warnen.

CERT C enthält beispielsweise die folgenden Regeln für die Speicherverwaltung:

Screenshot mit CERT CERT C-Regeln für die Speicherverwaltung (MEM) – MEM30-C bis MEM36-C.

Zu diesen Regeln gehören präventive Codierungstechniken, die Speicherverwaltungsfehler von vornherein vermeiden. Jeder Regelsatz umfasst eine Risikobewertung zusammen mit den Abhilfekosten, sodass Softwareteams die Richtlinien wie folgt priorisieren können:

Tabelle einer zusammenfassenden Risikobewertung, in der die MEM30-C- bis MEM36-C-Regeln, der Schweregrad, die Wahrscheinlichkeit, die Sanierungskosten, die Priorität und die Stufe aufgeführt sind.

In der CERT C-Verwendung des Wortes „Regel“ führt ein Verstoß gegen eine Regel höchstwahrscheinlich zu einem Fehler, und die Einhaltung sollte automatisch oder manuell durch Überprüfung des Codes erfolgen. Regeln gelten als verbindlich. Ausnahmen bei Regelverstößen sind zu dokumentieren.

Andererseits bietet eine Empfehlung eine Anleitung, deren Befolgung die Sicherheit, Zuverlässigkeit und Sicherheit verbessern sollte. Ein Verstoß gegen eine Empfehlung bedeutet jedoch nicht zwangsläufig, dass ein Fehler im Code vorliegt. Empfehlungen sind nicht verpflichtend.

CERT C hat die folgenden Empfehlungen für die Speicherverwaltung:

Screenshot mit CERT CERT C-Regeln für die Speicherverwaltung (MEM) – MEM000-C bis MEM12-C.

Die zugehörige Risikobewertung für diese Empfehlungen lautet wie folgt:

Tabelle einer zusammenfassenden Risikobewertung, in der die MEM00-C- bis MEM12-C-Regeln, der Schweregrad, die Wahrscheinlichkeit, die Sanierungskosten, die Priorität und die Stufe aufgeführt sind.

Eine wichtige Präventionsstrategie besteht darin, einen Codierungsstandard zu übernehmen, der an Branchenrichtlinien wie SEI CERT angepasst ist, und ihn bei der zukünftigen Codierung durchzusetzen. Die Vermeidung dieser Schwachstellen durch bessere Codierungspraktiken ist kostengünstiger, weniger riskant und bietet die höchste Kapitalrendite.

Das Ausführen einer statischen Analyse des neu erstellten Codes ist schnell und einfach. Es ist für Teams einfach, sich sowohl in die Desktop-IDE als auch in den CI / CD-Prozess zu integrieren. Um zu verhindern, dass dieser Code jemals in den Build aufgenommen wird, sollten Sie zu diesem Zeitpunkt alle Sicherheitswarnungen und unsicheren Codierungspraktiken untersuchen.

Screenshot des Parasoft C/C++-Tests mit statischen Analyseergebnissen im Hintergrund und Vergrößerung der CWE Top 25-Standardreferenzen.

Screenshot der Parasoft-Integration in die Eclipse-IDE, mit der Schwachstellen auf dem Desktop des Entwicklers verhindert und erkannt werden.

Ein ebenso wichtiger Aspekt beim Aufspüren schlechter Programmierpraktiken ist die Nützlichkeit der Berichte. Um Fehler in der statischen Codeanalyse schnell und effizient zu beheben, ist es entscheidend, die Ursache zu verstehen. Hier spielen kommerzielle Tools wie Parasofts C/C++test , dotTEST und Jtest ihre Stärken aus.

Die automatisierten Testtools von Parasoft bieten vollständige Spuren für Warnungen, veranschaulichen diese in der IDE und sammeln kontinuierlich Build- und andere Informationen. Diese gesammelten Daten bieten neben Testergebnissen und Metriken einen umfassenden Überblick über die Einhaltung des Codierungsstandards des Teams sowie über den allgemeinen Qualitäts- und Sicherheitsstatus.

Entwickler können Ergebnisse basierend auf anderen Kontextinformationen wie Metadaten zum Projekt, dem Alter des Codes und dem für den Code verantwortlichen Entwickler oder Team weiter filtern. Tools wie Parasoft mit künstlicher Intelligenz (KI) und maschinellem Lernen (ML) verwenden diese Informationen, um die kritischsten Probleme genauer zu bestimmen.

Die Dashboards und Berichte enthalten die Risikomodelle, die Teil der von OWASP, CERT und CWE bereitgestellten Informationen sind. Auf diese Weise verstehen Entwickler besser, welche Auswirkungen die vom Tool gemeldeten potenziellen Schwachstellen haben und welche dieser Schwachstellen priorisiert werden müssen. Alle auf IDE-Ebene generierten Daten korrelieren mit den oben beschriebenen nachgelagerten Aktivitäten.

Fazit: Schützen Sie Ihren Code vor Pufferüberläufen

Pufferüberläufe und andere Speicherverwaltungsfehler plagen weiterhin Anwendungen. Sie zählen nach wie vor zu den Hauptursachen von Sicherheitslücken. Trotz des Wissens um ihre Funktionsweise und Ausnutzung sind sie weiterhin weit verbreitet. Aktuelle Beispiele finden Sie in der IoT Hall of Shame .

Als Ergänzung zu aktiven Sicherheitstests schlagen wir einen Präventions- und Erkennungsansatz vor, der Pufferüberläufe so früh wie möglich im SDLC verhindert, bevor sie in den Code geschrieben werden. Das Verhindern solcher Speicherverwaltungsfehler in der IDE und deren Erkennung in der CI/CD-Pipeline ist der Schlüssel zum Entfernen dieser Fehler aus Ihrer Software.

Intelligente Softwareteams können Fehler bei der Speicherverwaltung minimieren. Mit den richtigen Prozessen, Tools und Automatisierung in ihren bestehenden Arbeitsabläufen können sie Einfluss auf Qualität und Sicherheit nehmen.

Auswählen und Implementieren des richtigen sicheren Codierungsstandards
Jetzt das Whitepaper herunterladen