Skip to main content

Ein unkomplizierter Einstieg in die dunkle Kunst der C/C++-Schwachstellen

Artikel von
Headshot of Aviad Hahami

Aviad Hahami

feature vulnerabilities orange

15. April 2022

0 Min. Lesezeit

Lumos!

Da Snyk die Unterstützung für nicht verwaltete Abhängigkeiten (vor allem C/C++-Bibliotheken) angekündigt hat, möchten wir der Community außerhalb von C einige häufige, schwerwiegende Gefahren vorstellen, die in der C-Welt lauern (verstanden?). Betrachten Sie dies als einen „Leitfaden für Einsteiger“ zu Schwachstellen in C und C++, ihrem Erscheinungsbild, möglichen Problemen und ihrer Behebung.

Großartige Schwachstellen und wo sie zu finden sind

C und C++ (im Rest dieses Beitrags verwenden wir „C“ als Sammelbegriff für beide Sprachen) gelten als Low-Level-Programmiersprachen. Vergleicht man Low-Level-Sprachen mit High-Level-Sprachen (JS, Python usw.), ist der Umgang des Computers mit dem Speicher der wesentliche Unterschied. Während High-Level-Sprachen die Speicherzuweisung, -nutzung und -freigabe verwalten, überlässt C diese Verantwortung den Entwicklern. So können Entwickler die Performance ihrer Routinen und Implementierungsoptimierungen präzise steuern. Gleichzeitig können jedoch verschiedene Probleme auftreten, die für diese Sprachkategorie typisch sind.

Pufferüberläufe (CWE-121) und Out-of-Bounds-Schreibzugriffe (CWE-787)

Pufferüberläufe sind wohl die berüchtigtsten speicherbezogenen Schwachstellen überhaupt. Ihre Ausnutzung kann zwar kompliziert sein, die Schwachstelle selbst ist jedoch einfach: Sie schreiben mehr Daten in den zugewiesenen Puffer, als dieser aufnehmen kann. Genau genommen bezeichnet „Pufferüberlauf“ meist die tatsächliche Ausnutzung der Schwachstelle. „Stack-basierter Pufferüberlauf“ und „Out-of-Bounds-Schreibzugriff“ beschreiben jedoch im Wesentlichen dieselbe Schwachstelle (und werden daher gemeinsam behandelt).

Die Schwachstelle auszunutzen, kann schwierig sein, denn selbst bei einem Pufferüberlauf kann man nicht immer einfach „Code“ in den Stack (oder Heap) schreiben. Im Lauf der Zeit wurden Versuche unternommen, dieses Problem durch verschiedene Schutzmechanismen einzudämmen. Welche Mechanismen zum Einsatz kommen, hängt von vielen Faktoren ab, etwa vom Betriebssystem, den Kernel-Funktionen und dem Compiler. Dazu zählen beispielsweise ASLR (Address Space Layout Randomization), Stack-Canaries und DEP (Data Execution Prevention). Sie alle sollen Speicherbeschädigungsfehler wie Pufferüberläufe verhindern. Wird einer dieser Mechanismen zur Laufzeit ausgelöst, beendet das Betriebssystem die Ausführung und wirft einen SEGFAULT. Dadurch wird die Ausnutzung deutlich erschwert.

Sehen wir uns ein Beispiel für eine solche Schwachstelle und ihre Ausnutzung an. Betrachten Sie das folgende C-Programm:

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char **argv)
  volatile int modified;
  char buffer[64];

  modified = 0;
  gets(buffer);

  if(modified != 0) {
    printf("you have changed the 'modified' variable\n");
  } else {
    printf("Try again?\n");
  }
}

Beispiel mit freundlicher Genehmigung von 0xRick, wie in seinem Blog zu sehen.

Anhand des Codes erkennen wir (auch ohne C-Kenntnisse), dass buffer als Puffer mit einer Länge von 64 Zeichen angelegt wird und modified eine Ganzzahl ist. Prima.

In Zeile 9 sehen wir außerdem, dass modified auf den Wert 0 gesetzt wird. In Zeile 12 wird überprüft, ob der Wert 0 ist. Falls Sie es noch nicht erraten haben: Unser Ziel ist es, den Wert auf eine andere Zahl zu ändern. ? In Zeile 10 verwenden wir die Funktion gets , um aus stdin (= Benutzereingabe) in die Variable buffer zu lesen. Für alle, die gets nicht kennen: Die Funktion liest aus stdin in einen angegebenen Puffer, bis ein Zeilenumbruchzeichen (\n) gelesen wird.

Wir wissen (weil Sie sich das oben verlinkte YouTube-Video angesehen haben), dass der Stack in Richtung einer niedrigeren Adresse wächst und dass modified aufgrund der Stack-Datenstruktur im Speicher „unter“ buffer liegt. Schreiben wir mehr als 64 Zeichen in buffer, überschreiben wir also nach und nach den Wert von modified! Und das ist der magische Pufferüberlauf!

Dieses konkrete Beispiel ist nicht besonders gefährlich (es handelt sich schließlich um eine Demo). Stellen Sie sich jedoch vor, was passieren könnte, wenn der Wert einer Passwortvariable oder der URL, die der Computer ansteuert, überschrieben würde. Die Abhilfe ist einfach: Verwenden Sie die Funktion fgets. Sie überprüft auch die Länge der Eingabe und nicht nur, ob das „Endzeichen“ vorhanden ist.

Use-after-Free (CWE-416)

Use-after-Free-Schwachstellen erklären sich im Grunde selbst: Sie treten auf, wenn eine Variablenreferenz verwendet wird, nachdem der Speicher freigegeben wurde. Ursache ist ein Fehler in der Speicherverwaltung des Programmablaufs: Die Variable wird nachdem sie gelöscht wurde, erneut verwendet. Das kann eine unerwartete Aktion oder einen unvorhergesehenen Nebeneffekt in der Anwendung auslösen.

Um die Schwachstelle und ihre Ausnutzung in der Praxis zu sehen, empfehle ich das Video von LiveOverflow, in dem er eine UAF-Schwachstelle in einer Challenge ausnutzt.

Integer-Überlauf/-Unterlauf (CWE-190 und CWE-191)

Integer-Überläufe und -Unterläufe sind zwei ähnliche Arten von Fehlern (und späteren Schwachstellen), die durch die Zahlendarstellung in Computern entstehen.

Ohne näher auf Variablentypen und die genaue Darstellung von Zahlen in Computern einzugehen, sei erwähnt, dass es zwei Hauptmethoden gibt: vorzeichenbehaftet und vorzeichenlos. Vorzeichenbehaftete Variablen können negative und positive Zahlen darstellen, während vorzeichenlose Variablen nur positive Zahlen speichern.

Bei einem Integer-Überlauf ist der Wert, den der Computer speichern soll, größer als der maximal speicherbare Wert. Bei einem Integer-Unterlauf soll der Computer einen Wert speichern, der kleiner als der Mindestwert ist (zum Beispiel, wenn eine vorzeichenlose Ganzzahl eine negative Zahl speichern soll).

In beiden Fällen ist das Ergebnis ähnlich: Der Wert läuft über (beginnt also wieder am Anfang oder Ende des speicherbaren Bereichs) und ändert sich dadurch. Bei einem Überlauf beginnt der Umlauf bei 0. Bei einem Unterlauf beginnt er beim maximal speicherbaren Wert (eine vorzeichenlose 8-Bit-Ganzzahl läuft also auf 256 [dezimal] zurück).

Dieses Diagramm zeigt die Variablentypen und die Werte, die sie speichern können:

Diagramm mit sechs roten horizontalen Balken unterschiedlicher Länge auf einer gemeinsamen vertikalen Skala, jeweils mit kreisförmigen Endpunkten.

Ein Beispiel für einen Integer-Überlauf, der zu einem Pufferüberlauf führte, wurde in OpenSSH v3.3 entdeckt (CVE-2002-0639).

Betrachten Sie den folgenden Codeausschnitt:

nresp = packet_get_int();
if (nresp > 0) {
 response = xmalloc(nresp*sizeof(char*));
 for (i = 0; i < nresp; i++)
  response[i] = packet_get_string(NULL);
}

Angenommen, nresp ist 1073741824 und sizeof(char*) ist 4 (die typische Zeigergröße). Dann führt nresp*sizeof(char*) zu einem Überlauf (der Wert läuft über und wird zu 0). Daher erhält xmalloc() einen 0-Byte-Puffer und weist ihn zu. Die folgende Schleife verursacht einen Heap-Pufferüberlauf, da wir in einen nicht zugewiesenen Speicherbereich schreiben. Ein Angreifer kann dies wiederum nutzen, um beliebigen Code auszuführen.

Dereferenzierung eines Nullzeigers (CWE-467)

Bei einer Dereferenzierung führen wir eine Aktion auf einem Wert an einer Adresse aus. Um diese Schwachstelle besser zu erklären, sehen wir uns ein Beispiel an:

#include <stddef.h>

void main(){
        int *x = NULL;
        *x = 1;
}

Laut C-Standard kann die Ausführung des obigen Codes zu einem „undefinierten Verhalten“ führen. Die meisten Implementierungen brechen jedoch mit einem SEGFAULT ab. Das bedeutet, dass die Software versucht hat, auf einen geschützten Speicherbereich zuzugreifen und damit eine Speicherzugriffsverletzung verursacht hat. In der Folge beendet das Betriebssystem die Software.

Out-of-Bounds-Lesezugriff (CWE-125)

Ein Out-of-Bounds-Lesezugriff tritt auf, wenn „außerhalb des vorgesehenen Bereichs bzw. Puffers gelesen wird“. Eine solche Schwachstelle kann im besten Fall zum Absturz des Systems und im schlimmsten Fall zur Offenlegung von Informationen aus Ihrer Anwendung führen (z. B. Passwörter anderer Benutzer) – beides ist problematisch.

Als Beispiel für eine solche Schwachstelle sehen wir uns einen Codeausschnitt aus der Anwendung PureFTPd an. Betrachten Sie Zeile 17. Ist die Länge von s1 größer als die Länge von s2, überschreiten wir die Grenzen von s2, da die Schleife in Zeile 8 die Länge von s1 durchläuft. Die in Zeile 10 ausgelesenen Informationen liegen dann außerhalb des zulässigen Bereichs.

int pure_memcmp(const void *const b1_, const void *const b2_, size_t len)
  {
   const unsigned char *b1 = (const unsigned char *) b1_;
   const unsigned char *b2 = (const unsigned char *) b2_;
   size_t i;
   unsigned char d = (unsigned char) 0 U;
   for (i = 0 U; i < len; i++)
   {
     d |= b1[i] ^ b2[i];
   }
   return (int)((1 &((d - 1) >> 8)) - 1);
}

int pure_strcmp(const char *const s1, const char *const s2)
{
  return pure_memcmp(s1, s2, strlen(s1) + 1 U);
}

Dieser Fehler erhielt die Kennung CVE-2020-9365. Den Bericht können Sie hier lesen.

Fazit und nächste Schritte

Wir hoffen, Sie haben nun eine (grobe) Vorstellung davon, wie C/C++-Schwachstellen aussehen, wo sie typischerweise auftreten und welche Formen sie annehmen. Auch wenn manche dieser Angriffe zunächst nicht trivial erscheinen mögen: Ein tieferes Verständnis verbessert Ihr Wissen über die internen Abläufe der Software und kann Ihnen helfen, kritische Fehler zu vermeiden und zu verhindern.

Wie oben anhand des Integer-Überlaufs gezeigt, können solche Schwachstellen miteinander verkettet werden. Dadurch entsteht eine schwache Angriffskette, die sich für böswillige Zwecke ausnutzen lässt.

Jetzt, da Snyk Open Source C/C++ unterstützt, veröffentlichen wir weitere Inhalte dazu, wie Sie Schwachstellen in C und C++ finden, ausnutzen und beheben können.

Sichern Sie Ihre Open-Source-Abhängigkeiten

Die entwicklerorientierten Tools von Snyk erstellen mit einem Klick Fix-Pull-Requests für anfällige Open-Source-Abhängigkeiten und deren transitive Abhängigkeiten.