Doppelte Freigabe
Beschreibung
Doppelte Freigabe tritt auf, wenn ein Programm free() auf dieselbe Speicheradresse zweimal aufruft. Das zweite free() beschädigt die Speicherverwaltungsdatenstrukturen, was zu Programmabstürzen, Heap-Korruption und potenziell zur Ausführung beliebigen Codes führen kann. Wenn Speicher freigegeben wird, fügt der Allokator ihn einer Freiliste hinzu. Eine erneute Freigabe beschädigt diese Liste, und nachfolgende Allokationen können denselben Speicher an verschiedene Teile des Programms zurückgeben (Use-After-Free) oder Angreifern ermöglichen, Heap-Metadaten zur Ausnutzung zu manipulieren.
Risiko
Schwachstellen durch doppelte Freigabe sind hochgradig ausnutzbar und wurden in zahlreichen hochkarätigen Angriffen verwendet. Sie können zur Ausführung beliebigen Codes führen, indem Heap-Metadaten oder Funktionszeiger beschädigt werden. Moderne Heap-Allokatoren enthalten einige Schutzmaßnahmen, aber ausgeklügelte Exploits können diese umgehen. Doppelte Freigaben treten oft in komplexen Codepfaden auf, die Fehlerbehandlung, Ausnahmebehandlung oder Ressourcenbereinigung beinhalten. Sie können während der Code-Review schwer zu erkennen sein und manifestieren sich möglicherweise nur unter bestimmten Bedingungen.
Lösung
Setzen Sie Pointer nach der Freigabe auf NULL (obwohl dies nicht alle Fälle verhindert). Verwenden Sie Smart Pointer in C++ (unique_ptr, shared_ptr), die den Speicher automatisch verwalten. Implementieren Sie klare Besitzsemantik—eine Komponente sollte jede Ressource besitzen und freigeben. Verwenden Sie AddressSanitizer während des Testens, um doppelte Freigaben zu erkennen. Führen Sie gründliche Code-Reviews von Fehlerbehandlungspfaden durch. Verwenden Sie statische Analysetools. Erwägen Sie die Verwendung von Garbage-Collected-Sprachen für sicherheitskritischen Code.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Verfügbarkeit | Umfang: Denial of Service Doppelte Freigaben lassen das Programm typischerweise aufgrund von Heap-Korruption abstürzen. |
| Integrität | Umfang: Codeausführung Beschädigte Heap-Metadaten können für beliebige Codeausführung ausgenutzt werden. |
| Vertraulichkeit | Umfang: Informationsoffenlegung Heap-Korruption kann sensible Daten aus anderen Allokationen offenlegen. |
Beispielcode
Anfälliger Code
// ANFÄLLIG: Einfache doppelte Freigabe
void process_data(char *data) {
char *buffer = malloc(100);
strcpy(buffer, data);
if (strlen(buffer) > 50) {
free(buffer);
return; // Frühe Rückkehr
}
// ... buffer verarbeiten ...
free(buffer); // Doppelte Freigabe wenn frühe Rückkehr genommen wurde!
}
// ANFÄLLIG: Doppelte Freigabe in Fehlerbehandlung
int read_file(const char *filename) {
FILE *fp = NULL;
char *buffer = NULL;
fp = fopen(filename, "r");
if (!fp) goto error;
buffer = malloc(1024);
if (!buffer) goto error;
if (fread(buffer, 1, 1024, fp) == 0) {
free(buffer); // Hier freigeben
goto error; // Und nochmal im Fehlerhandler!
}
// ... verarbeiten ...
free(buffer);
fclose(fp);
return 0;
error:
free(buffer); // Doppelte Freigabe!
if (fp) fclose(fp);
return -1;
}
// ANFÄLLIG: Doppelte Freigabe durch Aliase
void process_item(Item *item) {
Item *temp = item; // Alias auf denselben Speicher
// ... temp verwenden ...
free(temp); // Über Alias freigeben
// Später...
free(item); // Doppelte Freigabe - derselbe Speicher!
}
// ANFÄLLIG: Doppelte Freigabe in Exception-ähnlichem Muster
struct Resource {
char *data;
int size;
};
void cleanup_resource(struct Resource *r) {
free(r->data); // Könnte bereits freigegeben sein
free(r);
}
int create_resource(struct Resource **out) {
struct Resource *r = malloc(sizeof(struct Resource));
if (!r) return -1;
r->data = malloc(100);
if (!r->data) {
free(r);
return -1;
}
if (init_data(r->data) != 0) {
free(r->data);
free(r); // Dieser Pfad ist OK
return -1;
}
*out = r;
return 0;
}
void use_resource(void) {
struct Resource *r;
if (create_resource(&r) != 0) {
// Fehler bereits bereinigt
return;
}
if (process(r) != 0) {
cleanup_resource(r); // Erste Freigabe
}
cleanup_resource(r); // Doppelte Freigabe wenn process() fehlschlug!
}
// ANFÄLLIG: C++ mit rohen Zeigern
class DataProcessor {
int* buffer;
public:
DataProcessor() : buffer(new int[100]) {}
~DataProcessor() {
delete[] buffer; // Erste Löschung
}
void reset() {
delete[] buffer; // Weitere Löschung
buffer = new int[100];
}
void process() {
// Wenn Ausnahme nach reset() geworfen wird, wird Destruktor
// doppelt löschen
reset();
throw std::runtime_error("Fehler");
}
};
Korrigierter Code
// SICHER: Pointer nach Freigabe auf NULL setzen
void process_data_safe(char *data) {
char *buffer = malloc(100);
if (!buffer) return;
strcpy(buffer, data);
if (strlen(buffer) > 50) {
free(buffer);
buffer = NULL; // Doppelte Freigabe verhindern
return;
}
// ... buffer verarbeiten ...
free(buffer);
buffer = NULL;
}
// SICHER: Einzelner Bereinigungspfad
int read_file_safe(const char *filename) {
FILE *fp = NULL;
char *buffer = NULL;
int result = -1;
fp = fopen(filename, "r");
if (!fp) goto cleanup;
buffer = malloc(1024);
if (!buffer) goto cleanup;
if (fread(buffer, 1, 1024, fp) == 0) {
goto cleanup; // Zum einzelnen Bereinigungspunkt gehen
}
// ... verarbeiten ...
result = 0;
cleanup:
// Einzelner Bereinigungspunkt - jede Ressource einmal freigegeben
free(buffer); // free(NULL) ist sicher
if (fp) fclose(fp);
return result;
}
// SICHER: Klarer Besitz
void process_item_safe(Item *item) {
// Klar: diese Funktion übernimmt Besitz und gibt frei
// ... item verwenden ...
free(item);
// item nirgendwo anders hin weitergeben nach Freigabe
}
// SICHER: Ordnungsgemäße Ressourcenverwaltung mit explizitem Besitz
struct Resource {
char *data;
int size;
bool owns_data; // Besitz-Flag
};
void cleanup_resource_safe(struct Resource *r) {
if (r) {
if (r->owns_data) {
free(r->data);
r->data = NULL;
r->owns_data = false;
}
free(r);
}
}
int create_resource_safe(struct Resource **out) {
struct Resource *r = calloc(1, sizeof(struct Resource));
if (!r) return -1;
r->data = malloc(100);
if (!r->data) {
free(r);
return -1;
}
r->owns_data = true;
if (init_data(r->data) != 0) {
cleanup_resource_safe(r);
return -1;
}
*out = r;
return 0;
}
void use_resource_safe(void) {
struct Resource *r = NULL;
if (create_resource_safe(&r) != 0) {
return; // Bereits bereinigt
}
if (process(r) != 0) {
// Hier nicht bereinigen - einmal am Ende tun
}
cleanup_resource_safe(r); // Einzelne Bereinigung
r = NULL;
}
// SICHER: Smart Pointer in C++ verwenden
#include <memory>
#include <vector>
class DataProcessorSafe {
std::unique_ptr<int[]> buffer;
public:
DataProcessorSafe() : buffer(std::make_unique<int[]>(100)) {}
// Destruktor behandelt Bereinigung automatisch
// Keine doppelte Löschung möglich
void reset() {
buffer = std::make_unique<int[]>(100);
// Alter Buffer automatisch gelöscht
}
void process() {
reset();
// Wenn Ausnahme geworfen wird, behandelt unique_ptr Bereinigung
throw std::runtime_error("Fehler");
// Keine doppelte Löschung!
}
};
// SICHER: RAII-Muster für benutzerdefinierte Ressourcen
class FileHandle {
FILE* fp;
public:
explicit FileHandle(const char* filename)
: fp(fopen(filename, "r")) {
if (!fp) throw std::runtime_error("Datei kann nicht geöffnet werden");
}
~FileHandle() {
if (fp) fclose(fp);
}
// Kopieren verhindern
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Verschieben erlauben
FileHandle(FileHandle&& other) noexcept : fp(other.fp) {
other.fp = nullptr;
}
FILE* get() { return fp; }
};
// SICHER: shared_ptr für gemeinsamen Besitz verwenden
std::shared_ptr<Data> createSharedData() {
return std::make_shared<Data>();
}
void useSharedData() {
auto data = createSharedData();
// Mehrere Besitzer OK - freigegeben wenn letzte Referenz stirbt
auto data2 = data; // Gemeinsamer Besitz
// Keine doppelte Freigabe - referenzgezählt
}
Ausgenutzt in der Praxis
WhatsApp Remote Code Execution (2019)
CVE-2019-3568 war eine Schwachstelle durch doppelte Freigabe in WhatsApps VOIP-Stack, die Remote-Codeausführung über speziell gestaltete SRTCP-Pakete ermöglichte und in gezielten Angriffen ausgenutzt wurde.
PHP Doppelte Freigabe (2016)
CVE-2016-5771 war eine doppelte Freigabe in PHPs Garbage Collection, die durch speziell gestaltete serialisierte Daten ausgelöst werden könnte und zu Remote-Codeausführung führte.
Sudo Heap-basierter Buffer Overflow/Doppelte Freigabe (2021)
CVE-2021-3156 (Baron Samedit) beinhaltete Heap-Speicherkorruption in sudo, die für Privilegieneskalation ausgenutzt werden könnte und die schwerwiegenden Auswirkungen von Speicherverwaltungsfehlern demonstrierte.
Tools zum Testen/Ausnutzen
-
AddressSanitizer (ASan) — erkennt doppelte Freigaben zur Laufzeit.
-
Valgrind — Speicherfehlerdetektor einschließlich doppelter Freigaben.
-
Electric Fence — Speicher-Debugger.
-
Heap Exploitation Research — Lernressource für Heap-Ausnutzungstechniken.
CVE-Beispiele
-
CVE-2019-3568 — WhatsApp VOIP doppelte Freigabe RCE.
-
CVE-2016-5771 — PHP Garbage Collection doppelte Freigabe.
-
CVE-2017-9800 — Apache Subversion doppelte Freigabe.
Referenzen
-
MITRE. "CWE-415: Double Free." https://cwe.mitre.org/data/definitions/415.html
-
CERT. "MEM30-C: Do not access freed memory." https://wiki.sei.cmu.edu/confluence/display/c/MEM30-C.+Do+not+access+freed+memory