Skip to main content

Im Schlaglicht: Schwachstellen in C++

Artikel von:
Aviad Hahami
Aviad Hahami
wordpress-sync/feature-c-vulnerabilities-orange

15. April 2022

0 Min. Lesezeit

Lumos!

Ganz in Hogwarts-Manier wollen wir begleitend zum jüngst in Snyk ergänzten Support für nicht verwaltete Abhängigkeiten in C/C++ Bibliotheken mit diesem Spruch das Licht beschwören – und Ihnen so Klarheit über die größten Risiken im Kontext dieser Programmiersprache vermitteln. Für Neulinge wie Fortgeschrittene gleichermaßen geeignet, zeigen diese Einblicke auf, wie sich Schwachstellen in C und C++ identifizieren lassen, was ihre Folgen sein können und wie sie sich beheben lassen.

Hochrisiko-Schwachstellen und wo sie sich verbergen

C wie auch sein Cousin C++ (den wir in diesem Artikel immer implizit auch bei Erwähnung von C meinen) sind Low-Level-Programmiersprachen. Das Speichermanagement ist also anders geregelt als bei High-Level-Sprachen wie JavaScript oder Python. Bei diesen wird die Zuweisung, Nutzung und Bereitstellung implizit verwaltet, bei C muss dies entwicklerseitig geschehen. Performance und Implementierung lassen sich so zwar äußerst feinstufig optimieren, zugleich ist dieses Programmierkonzept aber auch mit sehr spezifischen Problematiken verbunden.

Buffer-Overflows (CWE-121) und Out-of-Bounds Write (CWE-787)

Im Kontext speicherbezogener Schwachstellen sind Buffer Overflows mithin besonders berüchtigt. Für schädliche Aktivitäten ausnutzen lassen sie sich zwar relativ schwierig, dafür ist ein Überlauf in zugewiesenem Speicher und damit ein Buffer Overflow aber auch umso leichter verursacht. Aus Sicht der Begrifflichkeit beschreibt ein Buffer Overflow in der Regel den Exploit desselben. Da die in der CWE als „Stack-based Buffer Overflow“ bezeichnete Schwachstelle im Prinzip die gleiche Ursache hat wie Out-of-Bonds Write, gehen wir auf die beiden nicht separat ein.

Schwierig gestaltet sich ein entsprechender Exploit deshalb, weil es selbst bei einem Überlauf des Speichers nicht immer ohne Weiteres möglich ist, Code in den Stack (bzw. Heap) zu schreiben. Der Schutz davor ist freilich dennoch wichtig, und so wurden im Laufe der Zeit immer wieder entsprechende Ansätze und Mechanismen zur codeseitigen Absicherung und Mitigierung entwickelt. Je nach dem jeweils zugrunde liegenden Betriebssystem, verwendeten Kernel-Features und Compilern sowie diversen weiteren Faktoren sind hier etwa Address Space Layout Randomization (ASLR) und Stack Canaries oder auch Data Execution Prevention (DEP) zu nennen. Der Schutz vor speicherseitigen Fehlern bzw. Angriffen wie Buffer Overflows erfolgt dabei nach einem recht einfachen Prinzip: Schlägt der jeweilige Mechanismus fehl, beendet das Betriebssystem die Ausführung der Laufzeit infolge einer Schutzverletzung,Segfault bzw. Segmentation Fault genannt. Dies wiederum erschwert weitere Aktivitäten im Zusammenhang mit dem Exploit der Schwachstelle.

Ein Beispiel dafür, wie sich diese in C ausdrückt und wie sie ausgenutzt werden kann, zeigt der folgende Programmcode:

1#include <stdlib.h>
2#include <unistd.h>
3#include <stdio.h>
4
5int main(int argc, char **argv)
6  volatile int modified;
7  char buffer[64];
8
9  modified = 0;
10  gets(buffer);
11
12  if(modified != 0) {
13    printf("you have changed the 'modified' variable\n");
14  } else {
15    printf("Try again?\n");
16  }
17}

Code-Beispiel mit freundlicher Genehmigung von 0xRick,entnommen aus diesem Blog.

Zunächst einmal haben wir hier für Buffer eine Zeichenlänge von 64, außerdem erkennen wir mit modified einen Wert vom Typ Integer. Auch ohne spezifische C-Kenntnisse dürfte beides direkt klar sein.

Ebenfalls erkennen wir beim Blick auf modified, dass dafür der Wert 0 definiert ist. Über die kurz darauf folgende IF-Bedingung wiederum wird geprüft, ob der Wert 0 ist oder nicht. Was wir also erreichen wollen? Richtig, dass eben dies nicht erfüllt wird. Über die Funktion gets wird dabei die nutzerseitige Eingabe (in C „stdin“ bzw. „standard input“ genannt) für die Variable buffer abgerufen. Zur Erläuterung: Mit gets wird eine Nutzereingabe so lange in den jeweils definierten Buffer eingelesen, bis über \\n ein Zeilenumbruch bestimmt, dass eine neue Zeichenfolge beginnt.

Der Stack vergrößert sich auf einer niedrigeren Speicheradresse, außerdem ist modified aufgrund der Datenstruktur des Stacks quasi unter dem Buffer verortet. (Im oben verlinkten YouTube-Video zum Thema Speichermanagement wird dies näher erläutert.) Sobald unsere Eingabe in den Buffer nun also 64 Zeichen überschreitet, überschreiben wir den Wert von modified. Et voilà: Das ist dann der Buffer Overrun.

Im vorliegenden Beispiel ist dies weniger bedenklich. Gravierender wird es aber etwa, wenn so ein Wert einer Passwort-Variable oder die URL überschrieben wird, auf die das System verweist. Gegenmaßnahmen hierfür lassen sich allerdings leicht implementieren, dies am besten über die Funktion fgets. Denn damit wird nicht nur geprüft, ob ein „End Sequence Character“ vorhanden ist, sondern auch die Länge der Eingabe.

Use After Free (CWE-416)

Der Name verrät eigentlich schon alles. Hierbei geht es darum, dass eine Referenz auf eine Variable nach ihrer Freigabe weiterhin genutzt wird. Die damit verbundene Anfälligkeit hängt mit einem Fehler im Zusammenhang mit dem Speichermanagement im Software-Ablauf zusammen: Aus der Nutzung der Variable nach ihrer Freigabe resultierten bei der betroffenen Anwendung unerwartete Aktionen oder andere unerwünschte Effekte.

Äußerst anschaulich wird diese Schwachstelle in diesem Video vom Whitehat-Hacker LiveOverflow erläutert, in dem er im Rahmen einer Challenge einen Exploit auf sie fährt.

Integer Overflow/Underflow (CWE-190 und CWE-191)

Diese beiden Fehler und die daraus resultierenden Schwachstellen stehen im Zusammenhang damit, wie Zahlenwerte für Computer verständlich dargestellt werden.

Die zugehörigen Variablentypen und die zugrunde liegenden technischen Konzepte lassen wir hier einmal außen vor. Im vorliegenden Kontext wichtig sind ohnehin nur die Methoden signed und unsigned zur Definition des Wertebereichs. Als signed deklarierte Variablen können negative und positive Zahlen umfassen, unsigned wiederum beschränkt den Wertebereich auf positive Zahlen.

Kommt es nun zu einem Integer-Overflow, wird der Computer zur Speicherung einer Zahl angewiesen, die größer ist als der maximal mögliche Wert. Bei einem Underflow verhält es sich analog: Eine als „unsigned“ deklarierten Integer zu speichernde Zahl ist kleiner als der mögliche Minimalwert, es wird also beispielsweise für einen als „unsigned“ deklarierten Integer eine negative Zahl verwendet.

In beiden Fällen wirkt sich dies in gleicher Weise auf den jeweiligen Wert aus: Er wird „umgekehrt“, also auf den Anfang bzw. Ende des entsprechenden Wertbereichs gesetzt. So erhält er bei einem Overflow einen Wert beginnend bei 0, beim Underflow wird auf einen der maximal zur Speicherung möglichen Werte gesetzt (also im Falle eines 8-Bit Unsigned-Integer auf den Dezimalwert 256).

Nachfolgend ein Diagramm zur Veranschaulichung der für die verschiedenen Variablentypen jeweils möglichen Werte.

wordpress-sync/blog-cworld-variabletypes

Ein Exploit, bei dem über einen Integer-Overflow ein Buffer-Overflow ausgelöst wird, wurde etwa in Version 3.3 des OpenSSH-Package aufgedeckt (CVE-2002-0639).

Hierzu ein Blick auf folgenden Code:

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

Angenommen, der Wert für „nresp“ lautet 1073741824 und für den Pointer sizeof(char\*) werden die für diesen üblichen 4 verwendet, resultiert die Variable nresp*sizeof(char*) in einem Overflow: Sie wird umgekehrt und so auf den Wert 0 gesetzt und für xmalloc() in der Folge ein Buffer mit der Größe von 0 Byte zugewiesen. Dies verursacht eine Schleife mit der Folge eines Overflows des Heap-Buffers, da ein Schreibvorgang in einen nicht zugewiesenen Speicherbereich erfolgt. Entsprechend kann hier also jeder beliebige Code etwa durch einen Angreifer ausgeführt werden.

Null Pointer Dereference (CWE-467)

Als Dereferenzierung wird die Ausführung einer Aktion auf einen Wert einer bestimmten Adresse bezeichnet. Die zugehörige Schwachstelle erschließt sich am besten anhand des folgenden Beispiels:

1#include <stddef.h>
2
3void main(){
4        int *x = NULL;
5        *x = 1;
6}

Standardmäßig reagiert C bei Ausführung dieses Codes mit einem Fehler aufgrund nicht definierten Verhaltens. Bei den meisten Implementierungen der Programmiersprache ist die Reaktion jedoch heftiger: Sie gehen von einem SEGFAULT aus, also einer Schutzverletzung aufgrund des Zugriffs auf einen nicht genehmigten Speicherbereich. Dementsprechend wird die Ausführung der Software vom Betriebssystem beendet.

Out-of-Bounds Read (CWE-125)

Hierbei besteht die Angreifbarkeit darin, dass ein Lesevorgang außerhalb des vorgesehenen Ziels bzw. Buffers möglich ist. Im besten Fall resultiert dies in einem Absturz des Systems, genauso könnten dadurch aber auch Daten wie Passwörter anderer Nutzer aus der entsprechenden Anwendung ausgelesen werden.

Nachvollziehbar wird dies etwa am Beispiel des nachfolgenden Code-Auszugs aus der Anwendung PureFTPd. Angenommen, in Zeile 17 übersteigt die Länge der Variable s1 die von s2, resultiert dies in folgendem Szenario: Da über die Anweisung in Zeile 8 die Länge von s1 iteriert wird, liegt das Ergebnis in Zeile 10 außerhalb der Grenzen von s2 und stellt somit einen Out-of-Bounds Read dar.

1int pure_memcmp(const void *const b1_, const void *const b2_, size_t len)
2  {
3   const unsigned char *b1 = (const unsigned char *) b1_;
4   const unsigned char *b2 = (const unsigned char *) b2_;
5   size_t i;
6   unsigned char d = (unsigned char) 0 U;
7   for (i = 0 U; i < len; i++)
8   {
9     d |= b1[i] ^ b2[i];
10   }
11   return (int)((1 &((d - 1) >> 8)) - 1);
12}
13
14int pure_strcmp(const char *const s1, const char *const s2)
15{
16  return pure_memcmp(s1, s2, strlen(s1) + 1 U);
17}

Erfasst wurde der Fehler in CVE-2020-9365, den zugehörigen Bericht finden Sie hier.

Fazit und nächste Schritte

Sicher haben wir das Thema hier nur kurz angerissen. An welchen Stellen Ihr in C bzw. C++ geschriebener Code jedoch Schwachstellen aufweisen könnte und wie diese sich ausdrücken, dafür sind Sie damit bereits recht gut gewappnet. Auch mögen diese Problematiken zunächst eher banal erscheinen. Sie zu verstehen bedeutet jedoch, die Prozesse innerhalb einer Software in ganzer Tiefe durchblicken und so kritische Fehler vermeiden bzw. ihnen vorbeugen zu können.

Denn wie aus den erläuterten Integer-Overflows ersichtlich ist, kann eine ganze Kette entsprechender Schwachstellen entstehen, die schließlich zum Einfallstor für böswillige Akteure wird.

Dieser Kurzabriss war jedoch nur der Anfang: Im Zuge der nun in Snyk Open Source verfügbaren Unterstützung für C und C++ veröffentlichen wir künftig regelmäßig weiteren Content dazu, wie Sie damit verbundene Schwachstellen erkennen, wie sie von Angreifern ausgenutzt werden und wie Sie sie beheben.