Freigabe eines Zeigers, der nicht auf den Pufferanfang zeigt

Beschreibung

Freigabe eines Zeigers, der nicht auf den Pufferanfang zeigt, ist eine Speicherverwaltungsschwachstelle, die auftritt, wenn Software free() auf einen Pointer zu Heap-allokiertem Speicher aufruft, aber der Pointer nicht auf den Anfang des ursprünglich allokierten Puffers verweist. Dies geschieht typischerweise nachdem Zeigerarithmetik den Pointer auf eine innere Position oder hinter das Ende des Puffers verschoben hat. Der Heap-Speicherallokator erwartet, genau den Pointer zu erhalten, der von malloc(), calloc() oder realloc() zurückgegeben wurde. Die Übergabe einer anderen Adresse verursacht Heap-Korruption, was potenziell zu Abstürzen, Speicherkorruption oder ausnutzbaren Zuständen führt.

Risiko

Die Freigabe eines Zeigers, der nicht auf den Pufferanfang zeigt, beschädigt die internen Datenstrukturen des Heaps. Dies kann sofortige Abstürze verursachen, aber gefährlicher ist, dass es zu ausnutzbaren Zuständen führen kann. Angreifer können möglicherweise Heap-Metadaten manipulieren, um beliebige Schreibfähigkeiten zu erlangen, was potenziell zur Codeausführung führt. Die Schwachstelle ist besonders gefährlich, weil sie möglicherweise nicht sofort abstürzt - die Heap-Korruption könnte sich erst später manifestieren, was das Debugging erschwert und Time-of-Check-to-Time-of-Use-Fenster schafft. In einigen Fällen können Angreifer sorgfältig Bedingungen gestalten, um beschädigte Heap-Metadaten für Privilegienerweiterung auszunutzen.

Lösung

Bewahren Sie immer den ursprünglichen Pointer auf, der von Allokationsfunktionen zurückgegeben wurde. Verwenden Sie eine separate Variable oder einen Index zum Durchlaufen des Puffers, während Sie den ursprünglichen Pointer für die Freigabe aufbewahren. In C++ verwenden Sie Smart Pointer (unique_ptr, shared_ptr), die automatisch den Speicher verwalten. Bei der Verwendung von Funktionen wie strtok(), die Pointer modifizieren, denken Sie daran, dass die zurückgegebenen Token in den ursprünglichen Puffer zeigen - geben Sie sie nicht einzeln frei. Erwägen Sie die Verwendung von Container-Klassen oder speichersicheren Sprachen, wo möglich. Verwenden Sie Laufzeitwerkzeuge wie AddressSanitizer oder Valgrind während der Entwicklung, um diese Probleme zu erkennen.

Häufige Auswirkungen

AuswirkungDetails
IntegritätBereich: Integrität

Speicher ändern - Freigabe falscher Adressen beschädigt Heap-Metadaten und ermöglicht potenziell Speichermodifikation.
VerfügbarkeitBereich: Verfügbarkeit

DoS: Absturz, Beenden oder Neustart - Heap-Korruption verursacht typischerweise Abstürze.
VertraulichkeitBereich: Vertraulichkeit, Integrität, Verfügbarkeit

Nicht autorisierten Code oder Befehle ausführen - In einigen Fällen kann Heap-Korruption für Codeausführung ausgenutzt werden.

Beispielcode

Verwundbarer Code

// Verwundbar: Freigabe eines verschobenen Zeigers nach String-Suche
#include <stdlib.h>
#include <string.h>

char searchChar(char c) {
    char* str = malloc(20 * sizeof(char));
    if (str == NULL) return -1;

    strcpy(str, "Search Me!");

    // Durchlaufen des Strings mit Zeigerarithmetik
    while (*str != '\0') {
        if (*str == c) {
            // Verwundbar: str wurde inkrementiert!
            free(str);  // Freigabe eines inneren Zeigers
            return 1;   // Gefunden
        }
        str = str + 1;  // Pointer entfernt sich von der ursprünglichen Allokation
    }

    // Verwundbar: str zeigt jetzt hinter die ursprüngliche Allokation
    free(str);  // Freigabe falscher Adresse
    return 0;   // Nicht gefunden
}

// Verwundbar: Freigabe des strtok-Ergebnisses
void vulnerable_tokenize(char* input_copy) {
    char* input = malloc(strlen(input_copy) + 1);
    strcpy(input, input_copy);

    char* tok = strtok(input, " \t");

    while (tok != NULL) {
        if (is_malformed(tok)) {
            // Verwundbar: tok zeigt INNERHALB des input-Puffers
            free(tok);  // Heap-Korruption!
        }
        tok = strtok(NULL, " \t");
    }

    // Ursprünglicher 'input'-Pointer ist verloren, wenn wir versucht haben, Token freizugeben
}

// Verwundbar: Arithmetik vor free
void vulnerable_arithmetic() {
    int* array = malloc(10 * sizeof(int));

    // Array füllen
    for (int i = 0; i < 10; i++) {
        array[i] = i;
    }

    // Verarbeitung mit Zeigerarithmetik
    int* ptr = array;
    ptr += 5;  // Zeigt jetzt auf die Mitte des Arrays

    // ... später vergisst der Programmierer, dass ptr verschoben wurde
    free(ptr);  // Verwundbar: Freigabe der Mitte des Puffers
}
// Verwundbar: Inkrementieren des Zeigers in Schleife, dann Freigabe
char* vulnerable_skip_whitespace(char* original) {
    char* str = malloc(100);
    strcpy(str, original);

    // Führende Leerzeichen überspringen
    while (*str == ' ' || *str == '\t') {
        str++;  // Pointer bewegt sich vorwärts
    }

    // ... str verwenden ...
    process(str);

    // Verwundbar: str zeigt nicht mehr auf den Allokationsanfang
    free(str);  // Heap-Korruption

    return NULL;
}

// Verwundbar: Funktion gibt inneren Pointer zurück
char* vulnerable_find_substring(char* haystack, char* needle) {
    char* buffer = malloc(strlen(haystack) + 1);
    strcpy(buffer, haystack);

    char* found = strstr(buffer, needle);

    // Verwundbar: Rückgabe eines Zeigers in den Puffer
    // Aufrufer könnte versuchen, diesen Pointer freizugeben
    return found;  // Zeigt in den Puffer, nicht an den Anfang
}

void use_vulnerable() {
    char* result = vulnerable_find_substring("Hello World", "World");
    if (result) {
        printf("Found: %s\n", result);
        free(result);  // ABSTURZ: Freigabe eines inneren Zeigers
    }
}

Korrigierter Code

// Korrigiert: Index statt Zeigerarithmetik verwenden
#include <stdlib.h>
#include <string.h>

char searchChar_fixed(char c) {
    char* str = malloc(20 * sizeof(char));
    if (str == NULL) return -1;

    strcpy(str, "Search Me!");

    // Korrigiert: Index verwenden, ursprünglichen Pointer beibehalten
    int i = 0;
    while (i < strlen(str)) {
        if (str[i] == c) {
            free(str);  // Korrigiert: Ursprünglicher Pointer unverändert
            return 1;
        }
        i = i + 1;  // Index wird inkrementiert, nicht Pointer
    }

    free(str);  // Korrigiert: Immer noch der ursprüngliche Pointer
    return 0;
}

// Korrigiert: Token kopieren statt sie freizugeben
void fixed_tokenize(char* input_copy) {
    char* input = malloc(strlen(input_copy) + 1);
    if (input == NULL) return;

    strcpy(input, input_copy);

    char* tok = strtok(input, " \t");

    while (tok != NULL) {
        if (!is_malformed(tok)) {
            // Korrigiert: Token in neue Allokation kopieren
            char* command = malloc((strlen(tok) + 1) * sizeof(char));
            if (command != NULL) {
                strcpy(command, tok);
                add_to_command_queue(command);  // Übernimmt Besitz
            }
        }
        tok = strtok(NULL, " \t");
    }

    // Korrigiert: Ursprünglichen Puffer einmal am Ende freigeben
    free(input);
}

// Korrigiert: Ursprünglichen Pointer aufbewahren
void fixed_arithmetic() {
    int* array = malloc(10 * sizeof(int));
    if (array == NULL) return;

    // Array füllen
    for (int i = 0; i < 10; i++) {
        array[i] = i;
    }

    // Korrigiert: Separaten Pointer zum Durchlaufen verwenden
    int* ptr = array + 5;  // Temporärer Pointer für Zugriff

    // ... ptr zum Lesen verwenden ...
    process(ptr);

    // Korrigiert: Ursprünglichen Pointer freigeben
    free(array);  // Korrekt: ursprüngliche Allokation freigeben
}
// Korrigiert: Ursprünglichen Pointer für Aufräumen aufbewahren
char* fixed_skip_whitespace(char* original, char** out_start) {
    char* str = malloc(100);
    if (str == NULL) return NULL;

    strcpy(str, original);

    // Korrigiert: Separaten Pointer zum Durchlaufen verwenden
    char* current = str;

    // Führende Leerzeichen überspringen
    while (*current == ' ' || *current == '\t') {
        current++;
    }

    // Beide Pointer zurückgeben
    *out_start = current;  // Wo der Inhalt beginnt
    return str;            // Ursprüngliche Allokation (zum Freigeben)
}

void use_fixed() {
    char* content_start;
    char* allocation = fixed_skip_whitespace("  Hello  ", &content_start);

    if (allocation) {
        process(content_start);  // Inhalt verwenden
        free(allocation);        // Ursprünglichen Pointer freigeben
    }
}

// Korrigiert: Offset oder Kopie zurückgeben statt inneren Pointer
typedef struct {
    char* buffer;
    size_t offset;
} SubstringResult;

SubstringResult fixed_find_substring(char* haystack, char* needle) {
    SubstringResult result = {NULL, 0};

    char* buffer = malloc(strlen(haystack) + 1);
    if (buffer == NULL) return result;

    strcpy(buffer, haystack);
    char* found = strstr(buffer, needle);

    result.buffer = buffer;  // Ursprüngliche Allokation

    if (found) {
        result.offset = found - buffer;  // Offset zurückgeben, nicht Pointer
    } else {
        result.offset = (size_t)-1;  // Nicht-gefunden-Indikator
    }

    return result;
}

void use_fixed_substring() {
    SubstringResult result = fixed_find_substring("Hello World", "World");

    if (result.buffer && result.offset != (size_t)-1) {
        printf("Found at offset %zu: %s\n",
               result.offset, result.buffer + result.offset);
    }

    free(result.buffer);  // Korrigiert: Ursprüngliche Allokation freigeben
}
// Korrigiert: C++ mit Smart Pointern
#include <memory>
#include <string>
#include <vector>

class SecureStringProcessor {
public:
    bool searchChar(char c) {
        // Korrigiert: unique_ptr verwaltet Speicher automatisch
        auto str = std::make_unique<char[]>(20);
        strcpy(str.get(), "Search Me!");

        // String sicher verwenden
        for (int i = 0; str[i] != '\0'; i++) {
            if (str[i] == c) {
                return true;
            }
        }
        return false;
        // Speicher wird automatisch freigegeben, wenn str den Gültigkeitsbereich verlässt
    }

    // Noch besser: std::string verwenden
    bool searchCharString(char c) {
        std::string str = "Search Me!";

        for (char ch : str) {
            if (ch == c) {
                return true;
            }
        }
        return false;
        // Keine manuelle Speicherverwaltung erforderlich
    }
};

CVE-Beispiele

  • CVE-2019-11930: Funktion rief intern calloc auf und gab einen Pointer an einem Index innerhalb des allokierten Puffers zurück, was zu ungültiger Speicherfreigabe führte.
  • CVE-2010-2547: Double-Free-Schwachstelle durch Freigabe eines inneren Zeigers verursacht.

Referenzen

  1. MITRE Corporation. "CWE-761: Free of Pointer not at Start of Buffer." https://cwe.mitre.org/data/definitions/761.html
  2. CERT C Coding Standard. "MEM34-C. Only free memory allocated dynamically."
  3. CERT C Coding Standard. "MEM31-C. Free dynamically allocated memory when no longer needed."