Falsche Zeigerskalierung

Beschreibung

Falsche Zeigerskalierung tritt auf, wenn Zeigerarithmetik ohne ordnungsgemäße Berücksichtigung der Elementgröße durchgeführt wird. In C und C++ skaliert Zeigerarithmetik automatisch um die Größe des referenzierten Typs—das Inkrementieren eines int* rückt um sizeof(int) Bytes vor, nicht um 1 Byte. Fehler treten auf, wenn Programmierer manuell mit Elementgröße multiplizieren (Doppelskalierung), Pointer auf verschiedene Typen casten ohne die Arithmetik anzupassen, oder Byte-Offsets mit typisierten Zeigern verwenden. Dies führt zum Zugriff auf falsche Speicherorte.

Risiko

Falsche Zeigerskalierung verursacht Out-of-Bounds-Speicherzugriff, Pufferüberläufe und Datenkorruption. Doppelskalierung (Multiplizieren mit Größe wenn der Compiler dies bereits tut) greift auf Speicher weit jenseits beabsichtigter Orte zu. Unterskalierung (Vergessen der Typgröße beim Arbeiten mit Byte-Offsets) greift auf falsche Array-Elemente zu. Diese Fehler können zu Informationspreisgabe durch Lesen benachbarten Speichers führen, Denial-of-Service durch Abstürze, und möglicherweise Code-Ausführung wenn Angreifer-kontrollierte Daten an der falsch berechneten Adresse sind.

Lösung

Verstehen Sie, dass C/C++ Zeigerarithmetik automatisch nach Elementgröße skaliert. Multiplizieren Sie Offsets niemals manuell wenn Sie typisierte Pointer verwenden—lassen Sie den Compiler die Skalierung handhaben. Beim Arbeiten mit rohen Byte-Offsets casten Sie zuerst zu char* oder unsigned char*, dann zurück zum Zieltyp. Verwenden Sie Array-Indizierung (arr[i]) anstelle von Zeigerarithmetik wo möglich. Setzen Sie statische Analysewerkzeuge ein, die Zeigerskalierungs-Probleme erkennen. Bevorzugen Sie moderne C++-Container, die Speicherzugriff abstrahieren.

Häufige Auswirkungen

AuswirkungDetails
IntegritätUmfang: Speicherkorruption

Schreiben an falsch berechnete Adressen korrumpiert Speicher.
VertraulichkeitUmfang: Informationspreisgabe

Lesen falscher Speicherorte legt benachbarte Daten offen.
VerfügbarkeitUmfang: Absturz

Zugriff auf ungültigen Speicher verursacht Segmentierungsfehler.

Beispielcode

Anfälliger Code

// ANFÄLLIG: Doppelskalierung - Compiler multipliziert bereits mit sizeof(int)
void process_array_vulnerable(int* arr, int count) {
    for (int i = 0; i < count; i++) {
        // Falsch! Zeigerarithmetik skaliert bereits nach sizeof(int)
        // Dies greift auf arr[i * sizeof(int)] zu anstelle von arr[i]!
        int* ptr = arr + (i * sizeof(int));  // Doppelt skaliert!
        *ptr = i;
    }
}

// ANFÄLLIG: Manuelle Offset-Berechnungsfehler
int* get_element_vulnerable(int* base, int index) {
    // Falsch! Fügt Byte-Offset zu int-Pointer hinzu
    // Compiler multipliziert index mit sizeof(int), also ist dies 4x zu weit
    return base + (index * sizeof(int));
}

// ANFÄLLIG: Typverwirrung bei Zeigerarithmetik
void copy_data_vulnerable(void* dest, void* src, size_t count) {
    // Falsch! void*-Arithmetik ist in Standard-C undefiniert
    // GCC-Erweiterung behandelt es als char*, aber Absicht ist unklar
    for (size_t i = 0; i < count; i++) {
        *((int*)dest + i * sizeof(int)) = *((int*)src + i * sizeof(int));
    }
}

// ANFÄLLIG: Struct-Array-Zugriff
struct Record {
    int id;
    char name[32];
    double value;
};

struct Record* get_record_vulnerable(struct Record* base, int index) {
    // Falsch! Doppelskalierung nach Strukturgröße
    return (struct Record*)((char*)base + index * sizeof(struct Record) * sizeof(struct Record));
}

// ANFÄLLIG: Puffergrenzen-Berechnung
void check_bounds_vulnerable(int* arr, size_t size, int* ptr) {
    // Falsch! Vergleicht Byte-Offset mit Zeigerarithmetik-Ergebnis
    int* end = arr + size * sizeof(int);  // Doppelt skaliert!

    if (ptr < end) {
        *ptr = 0;  // Kann weit über tatsächlichen Puffer hinaus schreiben!
    }
}

// ANFÄLLIG: Mischen von Zeigertypen
void convert_data_vulnerable(short* shorts, int* ints, int count) {
    for (int i = 0; i < count; i++) {
        // Falsch! Verwendet int-Index mit short-Pointer während in Bytes gedacht
        shorts[i * sizeof(int)] = (short)ints[i];
        // Überspringt 3 short-Elemente jede Iteration!
    }
}

// ANFÄLLIG: Pointer-Subtraktionsfehler
size_t calculate_offset_vulnerable(int* base, int* current) {
    // Gibt Elementdifferenz zurück, aber dann als Byte-Offset missbraucht
    size_t offset = current - base;

    // Später: falsche Byte-Berechnung
    char* byte_ptr = (char*)base + offset;  // Sollte offset * sizeof(int) sein
    return offset;
}

// ANFÄLLIG: memset mit falscher Größenberechnung
void clear_array_vulnerable(int* arr, int count) {
    // Falsch! memset nimmt Byte-Anzahl, aber Zeigerarithmetik fügt Skalierung hinzu
    memset(arr, 0, (arr + count) - arr);  // Löscht nur 'count' Bytes
    // Sollte count * sizeof(int) Bytes löschen!
}
// ANFÄLLIG: C++ mit denselben Zeigerskalierungs-Problemen
template<typename T>
T* offset_pointer_vulnerable(T* base, size_t byte_offset) {
    // Falsch! Dies skaliert byte_offset nach sizeof(T)
    return base + byte_offset;
}

// ANFÄLLIG: Iterator-Arithmetik
class VulnerableArray {
    int* data;
    size_t size;

public:
    int* at_byte_offset_vulnerable(size_t offset) {
        // Falsch! Als Byte-Offset behandelt aber nach sizeof(int) skaliert
        return data + offset;  // Gibt Element bei Index 'offset' zurück, nicht Byte-Offset
    }
};

// ANFÄLLIG: Reinterpret-Cast mit Arithmetik
void process_bytes_vulnerable(uint32_t* values, size_t count) {
    uint8_t* bytes = reinterpret_cast<uint8_t*>(values);

    for (size_t i = 0; i < count; i++) {
        // Falsch! Greift auf Bytes bei i*4 zu wenn wir jedes Byte wollen
        uint8_t byte = *(bytes + i * sizeof(uint32_t));
    }
}

// ANFÄLLIG: Placement new mit falschem Offset
class VulnerablePool {
    char* buffer;
    size_t objectSize;

public:
    template<typename T>
    T* allocateAt_vulnerable(size_t index) {
        // Falsch wenn objectSize != sizeof(T*)
        T* ptr = reinterpret_cast<T*>(buffer) + index * objectSize;
        return new (ptr) T();  // Falsche Adresse!
    }
};

Korrigierter Code

// SICHER: Compiler Skalierung handhaben lassen
void process_array_safe(int* arr, int count) {
    for (int i = 0; i < count; i++) {
        // Korrekt! Zeigerarithmetik skaliert automatisch
        int* ptr = arr + i;  // Äquivalent zu &arr[i]
        *ptr = i;
    }
}

// SICHER: Array-Indizierung (klarer, keine manuelle Arithmetik)
void process_array_safe_v2(int* arr, int count) {
    for (int i = 0; i < count; i++) {
        arr[i] = i;  // Lesbarste Form
    }
}

// SICHER: Einfacher Pointer-Offset
int* get_element_safe(int* base, int index) {
    // Korrekt! Keine manuelle Größenmultiplikation nötig
    return base + index;
}

// SICHER: Arbeiten mit Byte-Offsets - zuerst zu char* casten
void* byte_offset_safe(void* base, size_t byte_offset) {
    // Zu char* für Byte-Arithmetik casten, dann zurück
    return (char*)base + byte_offset;
}

int* element_from_byte_offset_safe(int* base, size_t byte_offset) {
    return (int*)((char*)base + byte_offset);
}

// SICHER: Struct-Array-Zugriff
struct Record* get_record_safe(struct Record* base, int index) {
    // Korrekt! Zeigerarithmetik behandelt Strukturgröße
    return base + index;
}

// Oder mit Byte-Offset wenn nötig
struct Record* get_record_byte_offset_safe(struct Record* base, size_t byte_offset) {
    return (struct Record*)((char*)base + byte_offset);
}

// SICHER: Puffergrenzen-Berechnung
void check_bounds_safe(int* arr, size_t size, int* ptr) {
    // Korrekt! Einfach Elementanzahl addieren
    int* end = arr + size;

    if (ptr >= arr && ptr < end) {
        *ptr = 0;
    }
}

// SICHER: Konvertieren zwischen Zeigertypen
void convert_data_safe(short* shorts, int* ints, int count) {
    for (int i = 0; i < count; i++) {
        shorts[i] = (short)ints[i];  // Einfache Array-Indizierung
    }
}

// SICHER: Zeigerdifferenz und Byte-Berechnung
size_t calculate_byte_offset_safe(int* base, int* current) {
    // Zeigerdifferenz gibt Elementanzahl
    ptrdiff_t element_diff = current - base;

    // Explizit in Bytes konvertieren
    size_t byte_offset = element_diff * sizeof(int);

    return byte_offset;
}

// SICHER: memset mit korrekter Größe
void clear_array_safe(int* arr, int count) {
    // Korrekt! count * sizeof(element) für Byte-Anzahl verwenden
    memset(arr, 0, count * sizeof(*arr));
}

// Oder explizite Größe verwenden
void clear_array_safe_v2(int* arr, int count) {
    memset(arr, 0, count * sizeof(int));
}

// SICHER: Generische Byte-weise Kopie
void copy_bytes_safe(void* dest, const void* src, size_t byte_count) {
    char* d = (char*)dest;
    const char* s = (const char*)src;

    for (size_t i = 0; i < byte_count; i++) {
        d[i] = s[i];
    }
}

// SICHER: Element-weise Kopie mit ordnungsgemäßen Typen
void copy_ints_safe(int* dest, const int* src, size_t count) {
    for (size_t i = 0; i < count; i++) {
        dest[i] = src[i];  // Array-Indizierung, keine Pointer-Mathematik nötig
    }
}

// Oder mit memcpy
void copy_ints_memcpy_safe(int* dest, const int* src, size_t count) {
    memcpy(dest, src, count * sizeof(int));
}
// SICHER: C++ mit ordnungsgemäßer Pointer-Behandlung

// SICHER: Template mit expliziter Byte-Offset-Behandlung
template<typename T>
T* byte_offset_pointer_safe(T* base, size_t byte_offset) {
    // Explizit Byte-Offset behandeln
    return reinterpret_cast<T*>(
        reinterpret_cast<char*>(base) + byte_offset
    );
}

// SICHER: Element-Offset (Standard-Zeigerarithmetik)
template<typename T>
T* element_offset_safe(T* base, size_t element_index) {
    return base + element_index;  // Compiler behandelt Skalierung
}

// SICHER: Array-Klasse mit ordnungsgemäßem Zugriff
class SafeArray {
    int* data;
    size_t size;

public:
    // Element-Zugriff
    int& at(size_t index) {
        if (index >= size) throw std::out_of_range("Index außerhalb der Grenzen");
        return data[index];  // Einfacher Array-Zugriff
    }

    // Wenn Byte-Offset wirklich benötigt wird
    int* at_byte_offset(size_t byte_offset) {
        if (byte_offset % sizeof(int) != 0) {
            throw std::invalid_argument("Byte-Offset nicht ausgerichtet");
        }
        size_t index = byte_offset / sizeof(int);
        if (index >= size) throw std::out_of_range("Offset außerhalb der Grenzen");
        return reinterpret_cast<int*>(
            reinterpret_cast<char*>(data) + byte_offset
        );
    }
};

// SICHER: Bytes eines größeren Typs verarbeiten
void process_bytes_safe(uint32_t* values, size_t count) {
    uint8_t* bytes = reinterpret_cast<uint8_t*>(values);
    size_t total_bytes = count * sizeof(uint32_t);

    for (size_t i = 0; i < total_bytes; i++) {
        uint8_t byte = bytes[i];  // Korrekter Byte-Zugriff
        // Byte verarbeiten...
    }
}

// SICHER: Speicher-Pool mit ordnungsgemäßer Ausrichtung
class SafePool {
    alignas(std::max_align_t) char* buffer;
    size_t objectSize;
    size_t capacity;

public:
    template<typename T>
    T* allocateAt(size_t index) {
        static_assert(alignof(T) <= alignof(std::max_align_t),
                     "Typ-Ausrichtung zu groß für Pool");

        size_t byte_offset = index * sizeof(T);
        if (byte_offset + sizeof(T) > capacity) {
            throw std::bad_alloc();
        }

        void* ptr = buffer + byte_offset;
        return new (ptr) T();
    }
};

// SICHER: std::span verwenden (C++20) oder gsl::span
#include <span>

void process_span_safe(std::span<int> arr) {
    for (size_t i = 0; i < arr.size(); i++) {
        arr[i] = static_cast<int>(i);  // Grenzen-geprüft mit .at()
    }
}

// SICHER: Iteratoren verwenden
void process_iterators_safe(std::vector<int>& vec) {
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        *it = 0;  // Keine manuelle Zeigerarithmetik
    }
}

Ausgenutzt in der Praxis

Browser-Speicherkorruption

Browser-Engines hatten Zeigerskalierungs-Bugs beim Verarbeiten komplexer Datenstrukturen, die zu Speicherkorruption und potenzieller Code-Ausführung führten.

Kernel-Treiber-Schwachstellen

Kernel-Treiber wurden durch Zeigerskalierungs-Fehler beim Kopieren von Daten zwischen User- und Kernel-Space ausgenutzt.

Media-Parser-Exploits

Media-Datei-Parser enthielten Zeigerarithmetik-Bugs, die für Code-Ausführung durch fehlerhafte Dateien ausgenutzt wurden.


Werkzeuge zum Testen/Ausnutzen

  • Coverity — erkennt Zeigerskalierungs-Probleme.

  • PVS-Studio — warnt vor verdächtiger Zeigerarithmetik.

  • AddressSanitizer — erkennt Out-of-Bounds-Zugriff durch Skalierungsfehler.

  • Polyspace — formale Verifikation von Pointer-Operationen.


CVE-Beispiele

  • CVE-2014-1266 — Apple SSL "goto fail" mit Pointer-Problemen.

  • CVE-2018-8781 — Linux-Kernel udl_fb Zeigerskalierung.

  • Verschiedene Buffer Overflow-CVEs mit Wurzel in Zeigerarithmetik-Fehlern.


Referenzen

  1. MITRE. "CWE-468: Incorrect Pointer Scaling." https://cwe.mitre.org/data/definitions/468.html

  2. CERT C. "ARR39-C: Do not add or subtract a scaled integer to a pointer." https://wiki.sei.cmu.edu/confluence/display/c/