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
| Auswirkung | Details |
|---|---|
| Integrität | Umfang: Speicherkorruption Schreiben an falsch berechnete Adressen korrumpiert Speicher. |
| Vertraulichkeit | Umfang: Informationspreisgabe Lesen falscher Speicherorte legt benachbarte Daten offen. |
| Verfügbarkeit | Umfang: 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
-
MITRE. "CWE-468: Incorrect Pointer Scaling." https://cwe.mitre.org/data/definitions/468.html
-
CERT C. "ARR39-C: Do not add or subtract a scaled integer to a pointer." https://wiki.sei.cmu.edu/confluence/display/c/