Nicht übereinstimmende Speicherverwaltungsroutinen
Beschreibung
Nicht übereinstimmende Speicherverwaltungsroutinen ist eine Speichersicherheitsschwachstelle, bei der Software versucht, Speicher an das System zurückzugeben, indem eine Freigabefunktion verwendet wird, die mit der ursprünglich zur Allokation verwendeten Funktion inkompatibel ist. Häufige Beispiele sind: Freigabe von Stack-allokiertem Speicher mit free(), mit malloc() allokierter Speicher, der mit delete freigegeben wird, mit new allokierter Speicher, der mit free() freigegeben wird, und Array-Speicher, der mit new[] allokiert, aber mit skalarem delete statt delete[] freigegeben wird. Diese Nichtübereinstimmungen verursachen undefiniertes Verhalten, da verschiedene Allokationsmechanismen unterschiedliche interne Datenstrukturen und Heap-Layouts verwenden.
Risiko
Nicht übereinstimmende Speicherverwaltungsroutinen verursachen Heap-Korruption und undefiniertes Verhalten. Die Konsequenzen hängen von Implementierungsdetails ab, können aber Abstürze, Speicherkorruption und potenziell beliebige Codeausführung umfassen. Verschiedene Allokatoren verfolgen Metadaten unterschiedlich - ihre Vermischung beschädigt diese Metadaten. Zum Beispiel kann new Objekt-Metadaten hinzufügen, die free() nicht erwartet, wodurch es Speichergrenzen falsch interpretiert. Die C++ new[]/delete-Nichtübereinstimmung ist besonders gefährlich, weil delete[] Array-Größeninformationen benötigt, die skalares delete nicht verarbeitet. Diese Fehler stürzen möglicherweise nicht sofort ab, was sie schwer zu debuggen macht und potenziell ausnutzbare Fenster schafft.
Lösung
Verwenden Sie nur übereinstimmende Paare von Allokations- und Freigabefunktionen: malloc/calloc/realloc mit free(), new mit delete, new[] mit delete[]. In C++ bevorzugen Sie Smart Pointer (unique_ptr, shared_ptr), die automatisch die korrekte Freigabe verwenden. Verwenden Sie RAII-Muster (Resource Acquisition Is Initialization). Erwägen Sie die Verwendung von Sprachen mit automatischer Speicherverwaltung. Aktivieren Sie Laufzeiterkennungswerkzeuge wie AddressSanitizer oder Valgrind während der Entwicklung. Seien Sie besonders vorsichtig beim Mischen von C- und C++-Code, welches Allokationssystem jede Komponente verwendet.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Integrität | Bereich: Integrität Speicher ändern - Nicht übereinstimmende Routinen beschädigen Heap-Metadaten und ermöglichen potenziell Speichermodifikation. |
| Verfügbarkeit | Bereich: Verfügbarkeit DoS: Absturz, Beenden oder Neustart - Heap-Korruption verursacht typischerweise Abstürze. |
| Vertraulichkeit | Bereich: Vertraulichkeit, Integrität, Verfügbarkeit Nicht autorisierten Code oder Befehle ausführen - Heap-Korruption kann für Codeausführung ausgenutzt werden. |
Beispielcode
Verwundbarer Code
// Verwundbar: new allokiert, free() freigegeben
void vulnerable_new_free() {
BarObj* ptr = new BarObj();
// ... Objekt verwenden ...
// Verwundbar: Sollte delete verwenden, nicht free()
free(ptr); // Heap-Korruption!
// free() weiß nichts über C++-Objekt-Metadaten
// Destruktor wird nicht aufgerufen
}
// Verwundbar: malloc allokiert, delete freigegeben
void vulnerable_malloc_delete() {
int* ptr = (int*)malloc(sizeof(int));
// ... Speicher verwenden ...
// Verwundbar: Sollte free() verwenden, nicht delete
delete ptr; // Undefiniertes Verhalten
// delete könnte versuchen, Destruktor auf Nicht-Objekt aufzurufen
}
// Verwundbar: new[] allokiert, delete freigegeben (fehlendes [])
void vulnerable_array_delete() {
int* array = new int[100];
// ... Array verwenden ...
// Verwundbar: Sollte delete[] verwenden, nicht delete
delete array; // Löscht nur erstes Element!
// Speicherleck und potenzielle Korruption
// Für Objekte: nur erster Destruktor aufgerufen
}
// Verwundbar: Stack-Speicher freigegeben
void vulnerable_stack_free() {
int stackArray[10];
// ... Array verwenden ...
// Verwundbar: Stack-Speicher kann nicht freigegeben werden!
free(stackArray); // Katastrophale Heap-Korruption
}
// Verwundbar: Bedingte Allokation mit nicht übereinstimmender Freigabe
void vulnerable_conditional(bool useHeap) {
int localArray[2] = {11, 22};
int* p = localArray; // Zeigt auf Stack
if (useHeap) {
p = new int[2]; // Zeigt jetzt auf Heap
}
// ... p verwenden ...
// Verwundbar: delete[] wird unabhängig vom Allokationstyp verwendet
delete[] p; // Stürzt ab wenn useHeap false ist!
}
// Verwundbar: C-Code, der Allokatoren mischt
#include <stdlib.h>
// Benutzerdefinierter Allokator, der malloc umhüllt
void* my_alloc(size_t size) {
void* ptr = malloc(size + sizeof(size_t));
*(size_t*)ptr = size; // Größe am Anfang speichern
return (char*)ptr + sizeof(size_t);
}
void vulnerable_custom_allocator() {
void* ptr = my_alloc(100);
// ... Speicher verwenden ...
// Verwundbar: free() weiß nichts über Größen-Header
free(ptr); // Freigabe falscher Adresse!
// Sollte passende my_free()-Funktion verwenden
}
// Verwundbar: Mischen von alignierter und regulärer Allokation
void vulnerable_aligned() {
void* ptr = aligned_alloc(64, 1024); // Alignierte Allokation
// ... Speicher verwenden ...
// Kann je nach Implementierung verwundbar sein
// Einige Systeme erfordern spezielle Freigabe für alignierten Speicher
free(ptr); // Kann korrekt sein oder nicht
}
// Verwundbar: Klasse mit nicht übereinstimmendem Allokator
class VulnerableClass {
private:
char* buffer;
public:
VulnerableClass(size_t size) {
// Mit malloc allokieren
buffer = (char*)malloc(size);
}
~VulnerableClass() {
// Verwundbar: Mit delete[] freigeben
delete[] buffer; // Nichtübereinstimmung!
}
};
// Verwundbar: Allokierten Speicher mit falschem Typ zurückgeben
char* vulnerable_return_buffer() {
// Mit new[] allokiert
char* buf = new char[256];
return buf;
}
void use_vulnerable() {
char* str = vulnerable_return_buffer();
// Aufrufer weiß nicht, wie Puffer allokiert wurde
free(str); // Verwundbar wenn sie falsch raten
}
Korrigierter Code
// Korrigiert: Übereinstimmendes new/delete
void fixed_new_delete() {
BarObj* ptr = new BarObj();
// ... Objekt verwenden ...
// Korrigiert: Übereinstimmendes delete für new
delete ptr; // Korrekt: Destruktor aufgerufen, Speicher korrekt freigegeben
}
// Korrigiert: Übereinstimmendes malloc/free
void fixed_malloc_free() {
int* ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) return;
// ... Speicher verwenden ...
// Korrigiert: Übereinstimmendes free für malloc
free(ptr); // Korrekt
}
// Korrigiert: Übereinstimmendes new[]/delete[]
void fixed_array_delete() {
int* array = new int[100];
// ... Array verwenden ...
// Korrigiert: Übereinstimmendes delete[] für new[]
delete[] array; // Korrekt: alle Elemente behandelt
}
// Korrigiert: Stack-Speicher nicht freigeben
void fixed_stack_memory() {
int stackArray[10];
// ... Array verwenden ...
// Korrigiert: Stack-Speicher wird automatisch zurückgewonnen wenn Funktion zurückkehrt
// Keine Freigabe erforderlich
}
// Korrigiert: Allokationstyp verfolgen
void fixed_conditional(bool useHeap) {
int localArray[2] = {11, 22};
int* p = localArray;
if (useHeap) {
p = new int[2];
}
// ... p verwenden ...
// Korrigiert: Nur freigeben wenn wir allokiert haben
if (useHeap) {
delete[] p;
}
// Stack-Speicher braucht keine Freigabe
}
// Korrigiert: Smart Pointer verwenden
#include <memory>
void fixed_smart_pointer() {
// unique_ptr verwendet automatisch delete
std::unique_ptr<BarObj> ptr = std::make_unique<BarObj>();
// ... Objekt über ptr.get() oder *ptr verwenden ...
// Automatische Bereinigung mit korrektem delete wenn ptr den Gültigkeitsbereich verlässt
}
void fixed_array_smart_pointer() {
// unique_ptr für Arrays verwendet automatisch delete[]
std::unique_ptr<int[]> array = std::make_unique<int[]>(100);
// ... Array über array[i] oder array.get() verwenden ...
// Automatische Bereinigung mit korrektem delete[]
}
// Korrigiert: Konsistenter Allokator-Wrapper
class SecureBuffer {
private:
char* buffer;
size_t size;
public:
SecureBuffer(size_t s) : size(s) {
buffer = new char[size]; // Konsistent: new[] verwenden
}
~SecureBuffer() {
delete[] buffer; // Konsistent: delete[] verwenden
}
// Kopieren verhindern um Double-Free zu vermeiden
SecureBuffer(const SecureBuffer&) = delete;
SecureBuffer& operator=(const SecureBuffer&) = delete;
char* data() { return buffer; }
size_t length() { return size; }
};
// Noch besser: std::vector verwenden
#include <vector>
void fixed_with_vector() {
std::vector<char> buffer(256);
// ... buffer.data() verwenden ...
// Automatische korrekte Freigabe
}
// Korrigiert: Benutzerdefinierter Allokator mit passender Freigabe
#include <stdlib.h>
void* my_alloc(size_t size) {
void* ptr = malloc(size + sizeof(size_t));
if (ptr == NULL) return NULL;
*(size_t*)ptr = size;
return (char*)ptr + sizeof(size_t);
}
// Korrigiert: Passende Freigabefunktion
void my_free(void* ptr) {
if (ptr == NULL) return;
// Pointer zurück auf echten Anfang anpassen
void* real_ptr = (char*)ptr - sizeof(size_t);
free(real_ptr);
}
void fixed_custom_allocator() {
void* ptr = my_alloc(100);
if (ptr == NULL) return;
// ... Speicher verwenden ...
// Korrigiert: Passenden Deallocator verwenden
my_free(ptr);
}
CVE-Beispiele
- CVE-2006-1564: Nicht übereinstimmende Speicherverwaltungsroutinen in Media Player verursachten ausnutzbaren Absturz.
- CVE-2015-0821: Verwendung von delete statt delete[] verursachte Speicherkorruption.
Referenzen
- MITRE Corporation. "CWE-762: Mismatched Memory Management Routines." https://cwe.mitre.org/data/definitions/762.html
- CERT C++ Coding Standard. "MEM51-CPP. Properly deallocate dynamically allocated resources."
- C++ Core Guidelines. "R.11: Avoid calling new and delete explicitly."