Freigabe von Speicher, der nicht auf dem Heap liegt

Beschreibung

Freigabe von Speicher, der nicht auf dem Heap liegt, tritt auf, wenn Code versucht, Speicher freizugeben, der nicht dynamisch über malloc, calloc, realloc oder äquivalente Heap-Allokationsfunktionen allokiert wurde. Dies umfasst Versuche, Stack-allokierte Variablen, globale Variablen, String-Literale oder Teile von allozierten Blöcken (Interior-Pointer) freizugeben. Das Speicherverwaltungssystem kann solche Anfragen nicht verarbeiten, was zu undefiniertem Verhalten, Heap-Korruption oder Abstürzen führt.

Risiko

Das Freigeben von Nicht-Heap-Speicher korrumpiert die Heap-Verwaltungsstrukturen und führt zu unvorhersehbarem Verhalten. Sofortige Abstürze sind häufig, aber die Korruption kann unentdeckt bleiben, bis nachfolgende Heap-Operationen fehlschlagen. Angreifer können Heap-Korruption potenziell für beliebige Code-Ausführung durch Techniken wie Heap Feng Shui ausnutzen. Der Fehler kann in manchen Umgebungen zu funktionieren scheinen, aber in anderen katastrophal fehlschlagen, was ihn in der Produktion gefährlich macht.

Lösung

Geben Sie nur Speicher frei, der von malloc/calloc/realloc (oder new in C++) erhalten wurde. Verfolgen Sie Allokationsquellen - erwägen Sie Wrapper-Funktionen, die aufzeichnen, ob Speicher heap-allokiert wurde. Geben Sie keine Stack-Adressen, globalen Variablen oder String-Literale frei. Geben Sie niemals Interior-Pointer frei (Pointer auf die Mitte allokierter Blöcke). Verwenden Sie statische Analysewerkzeuge, die Allokations-/Deallokationspaare verfolgen. In C++ bevorzugen Sie Smart-Pointer, die ihren eigenen Speicher verwalten.

Häufige Auswirkungen

AuswirkungDetails
VerfügbarkeitBereich: Absturz

Heap-Korruption durch ungültige Freigabe verursacht sofortige oder verzögerte Abstürze.
IntegritätBereich: Speicherkorruption

Heap-Metadaten-Korruption beeinflusst nachfolgende Allokationen.
SicherheitBereich: Code-Ausführung

Heap-Korruption kann für beliebige Code-Ausführung ausgenutzt werden.

Beispielcode + Lösungscode

Verwundbarer Code

// VERWUNDBAR: Stack-allozierten Speicher freigeben
void free_stack_vulnerable() {
    int local_array[100];
    process(local_array);
    free(local_array);  // FEHLER! Stack-Speicher!
}

// VERWUNDBAR: Globale Variable freigeben
int global_data[50];

void free_global_vulnerable() {
    process(global_data);
    free(global_data);  // FEHLER! Globaler/statischer Speicher!
}

// VERWUNDBAR: String-Literal freigeben
void free_literal_vulnerable() {
    char* str = "Hello, World!";
    process(str);
    free(str);  // FEHLER! String-Literal in schreibgeschütztem Speicher!
}

// VERWUNDBAR: Interior-Pointer freigeben
void free_interior_vulnerable() {
    char* buffer = malloc(100);
    char* middle = buffer + 50;  // Zeigt auf Mitte des Blocks
    // ... middle verwenden ...
    free(middle);  // FEHLER! Nicht der Start der Allokation!
}

// VERWUNDBAR: Bedingte Allokation, unbedingte Freigabe
void conditional_alloc_vulnerable(int use_heap) {
    char* data;

    if (use_heap) {
        data = malloc(256);
    } else {
        char local[256];
        data = local;
    }

    process(data);
    free(data);  // FEHLER wenn use_heap falsch war!
}

// VERWUNDBAR: Stack-Adresse zurückgeben, Aufrufer gibt frei
char* get_data_vulnerable() {
    char buffer[100];
    strcpy(buffer, "data");
    return buffer;  // Gibt Stack-Adresse zurück!
}

void caller_vulnerable() {
    char* data = get_data_vulnerable();  // Stack-Adresse!
    // ... data verwenden ...
    free(data);  // Doppeltes Problem: dangling UND Freigabe von Nicht-Heap!
}

// VERWUNDBAR: Struct mit Stack-Pointer freigeben
struct Container {
    char* data;
    size_t size;
};

void process_container_vulnerable(struct Container* c) {
    char local_buffer[64];
    c->data = local_buffer;  // Stack-Adresse!
    c->size = sizeof(local_buffer);
    // ... Container wird verwendet ...
}

void cleanup_container_vulnerable(struct Container* c) {
    free(c->data);  // FEHLER! Kann Stack-Speicher sein!
    free(c);
}

// VERWUNDBAR: Array-Adresse als Heap behandelt
void array_address_vulnerable(int arr[], int size) {
    if (size > 100) {
        free(arr);  // FEHLER! arr könnte Stack-Array vom Aufrufer sein!
    }
}
// VERWUNDBAR: C++ mit Nicht-Heap delete
void delete_stack_vulnerable() {
    int local = 42;
    int* ptr = &local;
    delete ptr;  // FEHLER! Stack-Variable!
}

// VERWUNDBAR: String-Literal löschen
void delete_literal_vulnerable() {
    const char* str = "constant";
    delete[] str;  // FEHLER! Literal in schreibgeschützter Sektion!
}

// VERWUNDBAR: Gemischte Allokationsquellen
class VulnerableManager {
    int* data;
    bool ownData;

public:
    void setData(int* d, bool heap_allocated) {
        data = d;
        ownData = heap_allocated;  // Eigentum verfolgen
    }

    ~VulnerableManager() {
        delete[] data;  // FEHLER! Ignoriert ownData-Flag!
    }
};

void use_vulnerable() {
    int stack_array[10];
    VulnerableManager mgr;
    mgr.setData(stack_array, false);
    // Destruktor wird Stack-Speicher löschen!
}

// VERWUNDBAR: Placement new dann reguläres delete
void placement_delete_vulnerable() {
    char buffer[sizeof(MyClass)];
    MyClass* obj = new (buffer) MyClass();  // Placement new

    delete obj;  // FEHLER! buffer ist auf Stack!
}

// VERWUNDBAR: Vector-Datenzeiger freigegeben
void vector_data_vulnerable() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    int* data = vec.data();  // Pointer auf internen Speicher

    // Später fälschlicherweise freigegeben
    free(data);  // FEHLER! Vector verwaltet eigenen Speicher!
}

Lösungscode

// SICHER: Nur Heap-allozierten Speicher freigeben
void free_heap_safe() {
    int* heap_array = malloc(100 * sizeof(int));
    if (heap_array == NULL) return;

    process(heap_array);
    free(heap_array);  // Korrekt: Heap-Speicher
}

// SICHER: Globals oder Stack nicht freigeben
int global_data[50];

void use_global_safe() {
    process(global_data);
    // Kein free - es ist global/statisch
}

void use_local_safe() {
    int local_array[100];
    process(local_array);
    // Kein free - es ist Stack
}

// SICHER: String-Literale ordnungsgemäß behandeln
void use_string_safe() {
    // String-Literale müssen nicht freigegeben werden
    const char* literal = "Hello, World!";
    process(literal);

    // Für dynamische Strings allokieren und freigeben
    char* dynamic = malloc(100);
    if (dynamic) {
        strcpy(dynamic, "Dynamischer String");
        process(dynamic);
        free(dynamic);
    }
}

// SICHER: Originalen Allokationszeiger behalten
void interior_pointer_safe() {
    char* buffer = malloc(100);
    if (buffer == NULL) return;

    char* middle = buffer + 50;  // Interior-Pointer zur Verwendung
    process(middle);

    free(buffer);  // Den ORIGINALEN Allokationszeiger freigeben
}

// SICHER: Allokationsquelle verfolgen
struct SafeData {
    char* data;
    size_t size;
    int heap_allocated;  // Flag zur Quellenverfolgung
};

void init_safe_data(struct SafeData* sd, int use_heap) {
    if (use_heap) {
        sd->data = malloc(256);
        sd->heap_allocated = 1;
    } else {
        // Aufrufer muss sicherstellen, dass dies SafeData-Verwendung überlebt
        sd->data = NULL;  // Oder auf statischen Buffer zeigen
        sd->heap_allocated = 0;
    }
}

void cleanup_safe_data(struct SafeData* sd) {
    if (sd->heap_allocated && sd->data != NULL) {
        free(sd->data);
    }
    sd->data = NULL;
}

// SICHER: Heap-allokierte Daten zurückgeben
char* get_data_safe() {
    char* buffer = malloc(100);
    if (buffer) {
        strcpy(buffer, "data");
    }
    return buffer;  // Aufrufer muss freigeben
}

void caller_safe() {
    char* data = get_data_safe();
    if (data) {
        process(data);
        free(data);  // Korrekt: Heap-Speicher
    }
}

// SICHER: Container mit ordnungsgemäßer Eigentumsverfolgung
struct SafeContainer {
    char* data;
    size_t size;
    int owns_data;
};

struct SafeContainer* create_container_heap(size_t size) {
    struct SafeContainer* c = malloc(sizeof(struct SafeContainer));
    if (c == NULL) return NULL;

    c->data = malloc(size);
    if (c->data == NULL) {
        free(c);
        return NULL;
    }

    c->size = size;
    c->owns_data = 1;
    return c;
}

struct SafeContainer create_container_view(char* external_data, size_t size) {
    return (struct SafeContainer){
        .data = external_data,
        .size = size,
        .owns_data = 0  // Besitzt nicht, gibt nicht frei
    };
}

void destroy_container(struct SafeContainer* c) {
    if (c->owns_data && c->data) {
        free(c->data);
    }
    c->data = NULL;
}
// SICHER: Smart-Pointer verwenden
void smart_pointer_safe() {
    auto ptr = std::make_unique<int>(42);
    // Automatisch freigegeben wenn ptr den Scope verlässt
}

// SICHER: Nicht-Heap-Speicher nicht löschen
void no_delete_stack() {
    int local = 42;
    int* ptr = &local;
    // ptr nur verwenden, nicht löschen!
    process(*ptr);
}

// SICHER: Ordnungsgemäße Eigentumsverfolgung
class SafeManager {
    std::unique_ptr<int[]> ownedData;
    int* viewData = nullptr;
    bool hasOwnership = false;

public:
    void setOwnedData(std::unique_ptr<int[]> d) {
        ownedData = std::move(d);
        hasOwnership = true;
    }

    void setViewData(int* d) {
        viewData = d;
        hasOwnership = false;  // Besitzt nicht, löscht nicht
    }

    ~SafeManager() {
        // unique_ptr behandelt besessene Daten
        // viewData wird nicht gelöscht
    }
};

// SICHER: Ordnungsgemäßes Placement new/delete
void placement_new_safe() {
    alignas(MyClass) char buffer[sizeof(MyClass)];
    MyClass* obj = new (buffer) MyClass();  // Placement new

    obj->~MyClass();  // Expliziter Destruktoraufruf, nicht delete!
    // buffer verlässt Scope natürlich
}

// SICHER: Vector-Interna nicht freigeben
void vector_safe() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    // data() für Zugriff verwenden, aber Vector verwaltet Speicher
    const int* data = vec.data();
    for (size_t i = 0; i < vec.size(); i++) {
        process(data[i]);
    }
    // Vector-Destruktor behandelt Bereinigung
}

// SICHER: Span für nicht-besitzende Ansichten
#include <span>

class SafeProcessor {
    std::span<int> view;  // Nicht-besitzende Ansicht

public:
    void setView(std::span<int> v) {
        view = v;
    }

    void process() {
        for (int val : view) {
            doSomething(val);
        }
    }

    // Kein Destruktor nötig - span besitzt keinen Speicher
};

// SICHER: Klare Eigentumssemantik mit observer_ptr (Guideline Support Library)
void clear_ownership_example() {
    std::unique_ptr<Resource> owned = std::make_unique<Resource>();

    // Nicht-besitzender Pointer zum Herumreichen
    Resource* observer = owned.get();
    use(observer);  // observer nicht löschen!

    // owned behandelt Löschung
}

Ausgenutzt in der Praxis

Heap-Korruptions-Exploits

Ungültige Free-Operationen haben Heap-Metadaten korrumpiert und beliebige Schreib-Primitive in Exploits ermöglicht.

Use-After-Free-Ketten

Freigabe von Nicht-Heap-Speicher hat Heap-Korruption ausgelöst, die später durch nachfolgende Allokationen ausgenutzt wurde.

Denial of Service

Anwendungen sind abgestürzt durch Freigabe von Nicht-Heap-Speicher, was Denial-of-Service-Angriffe ermöglichte.


Tools zum Testen/Ausnutzen

  • Valgrind — erkennt ungültige Free-Operationen
  • AddressSanitizer — fängt Freigabe von Nicht-Heap-Speicher ab
  • Electric Fence — Speicher-Debugging-Bibliothek
  • Dr. Memory — Speicherfehler-Detektor

CVE-Beispiele

  • Mehrere CVEs mit Heap-Korruption durch ungültiges Free
  • Browser-Schwachstellen durch Freigabe von Nicht-Heap-Speicher
  • Bibliotheksabstürze durch unsachgemäße Speicherverwaltung

Referenzen

  1. MITRE. "CWE-590: Free of Memory not on the Heap." https://cwe.mitre.org/data/definitions/590.html
  2. CERT C. "MEM34-C: Only free memory allocated dynamically." https://wiki.sei.cmu.edu/confluence/display/c/