Verwendung von Zeigersubtraktion zur Größenbestimmung

Beschreibung

Die Verwendung von Zeigersubtraktion zur Größenbestimmung tritt auf, wenn Code Puffergrößen oder Offsets berechnet, indem Pointer subtrahiert werden, ohne sicherzustellen, dass sie auf dasselbe Array oder dieselbe Speicherallokation verweisen. In C/C++ ist Zeigersubtraktion nur definiert, wenn beide Pointer auf Elemente innerhalb desselben Arrays verweisen (oder eines hinter dem Ende). Das Subtrahieren von Zeigern aus verschiedenen Allokationen erzeugt undefiniertes Verhalten. Selbst innerhalb derselben Allokation kann das Ergebnis falsch verwendet werden, wenn der Programmierer den Rückgabetyp (ptrdiff_t, der vorzeichenbehaftet ist und überlaufen kann) missversteht.

Risiko

Falsche Zeigersubtraktion führt zu undefiniertem Verhalten, Integer-Überläufen und Sicherheitslücken. Wenn Pointer aus verschiedenen Allokationen subtrahiert werden, ist das Ergebnis bedeutungslos und kann Pufferüberläufe verursachen, wenn es für Speicheroperationen verwendet wird. Vorzeichenbehafteter Überlauf in ptrdiff_t kann zu negativen Werten umwickeln, was Logikfehler verursacht oder riesige Größen an Speicherfunktionen übergibt. Angreifer können Speicherlayouts manipulieren, um diese Berechnungen für Pufferüberläufe und Informationspreisgabe auszunutzen.

Lösung

Subtrahieren Sie nur Pointer, die auf Elemente innerhalb desselben Arrays oder allokierten Blocks verweisen. Validieren Sie, dass beide Pointer im erwarteten Bereich liegen, bevor Sie subtrahieren. Verwenden Sie size_t für nicht-negative Größen, aber behandeln Sie die Umwandlung von vorzeichenbehaftet zu vorzeichenlos sorgfältig. Bevorzugen Sie explizite Größenverfolgung gegenüber Zeigerarithmetik wenn möglich. Verwenden Sie Address Sanitizers beim Testen, um ungültige Zeigeroperationen zu erkennen. Erwägen Sie die Verwendung von std::distance() in C++ mit ordnungsgemäßer Iterator-Validierung.

Häufige Auswirkungen

AuswirkungDetails
IntegritätUmfang: Speicherkorruption

Falsche Größenberechnungen führen zu Pufferüberläufen.
VerfügbarkeitUmfang: Absturz/DoS

Undefiniertes Verhalten durch ungültige Subtraktion verursacht Abstürze.
SicherheitUmfang: Ausnutzung

Manipulierte Größen ermöglichen Buffer Overflow-Angriffe.

Beispielcode

Anfälliger Code

// ANFÄLLIG: Subtraktion von Zeigern aus verschiedenen Allokationen
size_t calculate_size_vulnerable(char* start, char* end) {
    // Undefiniert wenn start und end aus verschiedenen Allokationen sind!
    return end - start;
}

void process_vulnerable(char* buf1, char* buf2) {
    // buf1 und buf2 könnten verschiedene Allokationen sein!
    size_t size = calculate_size_vulnerable(buf1, buf2);
    memset(buf1, 0, size);  // Potenziell riesige oder negative Größe!
}

// ANFÄLLIG: Vorzeichenbehafteter Überlauf bei Zeigerdifferenz
void copy_data_vulnerable(int* dest, int* src, int* end) {
    // Wenn end - src riesig ist, könnte ptrdiff_t überlaufen
    ptrdiff_t count = end - src;

    // Wenn count zu negativ übergelaufen ist, ist dies falsch
    if (count > 0) {
        memcpy(dest, src, count * sizeof(int));
    }
}

// ANFÄLLIG: Zeigervergleich ohne Same-Array-Prüfung
int* find_in_range_vulnerable(int* haystack_start, int* haystack_end,
                               int* search_start, int* search_end) {
    // Nimmt an dass alle Pointer vergleichbar sind - nicht garantiert!
    for (int* p = search_start; p < search_end; p++) {
        for (int* h = haystack_start; h < haystack_end; h++) {
            if (*p == *h) {
                // Berechnung des Offsets über verschiedene Arrays!
                size_t offset = h - haystack_start;
                return haystack_start + offset;
            }
        }
    }
    return NULL;
}

// ANFÄLLIG: Verwendung von Zeigerdifferenz als Array-Index
void index_with_diff_vulnerable(int* base, int* current) {
    // Was wenn current vor base ist? Negativer Index!
    int* arr = base;
    arr[current - base] = 42;  // Undefiniert wenn außerhalb der Grenzen!
}

// ANFÄLLIG: Verlassen auf Zeigerreihenfolge
size_t span_size_vulnerable(void* a, void* b) {
    char* start = (char*)a;
    char* end = (char*)b;

    // Nimmt an a < b, aber was wenn b < a?
    return end - start;  // Könnte negativ sein (umwickelt zu riesig)!
}

// ANFÄLLIG: Verbleibender Puffer-Berechnung
size_t remaining_vulnerable(char* current, char* buffer_start, size_t buffer_size) {
    char* buffer_end = buffer_start + buffer_size;

    // Was wenn current außerhalb des Puffers ist?
    return buffer_end - current;  // Könnte negativ/riesig sein!
}

// ANFÄLLIG: Struktur mit separaten Allokationen
struct Message {
    char* data;
    char* current_pos;
};

size_t bytes_read_vulnerable(struct Message* msg) {
    // Nimmt an data und current_pos sind in derselben Allokation
    // Aber was wenn sie unabhängig gesetzt wurden?
    return msg->current_pos - msg->data;
}

// ANFÄLLIG: Stack vs Heap Verwirrung
void mixed_memory_vulnerable() {
    char stack_buffer[100];
    char* heap_buffer = malloc(100);

    // Völlig undefiniertes Verhalten!
    ptrdiff_t diff = heap_buffer - stack_buffer;
    printf("Differenz: %td\n", diff);

    free(heap_buffer);
}
// ANFÄLLIG: C++ Iterator-Subtraktion
class VulnerableContainer {
    std::vector<int> data1;
    std::vector<int> data2;

public:
    size_t distanceBetween_vulnerable(int* p1, int* p2) {
        // Könnte aus verschiedenen Vektoren sein!
        return p2 - p1;  // Undefiniertes Verhalten!
    }

    void process_vulnerable() {
        int* it1 = data1.data();
        int* it2 = data2.data();

        // Subtraktion von Iteratoren aus verschiedenen Containern!
        size_t diff = it2 - it1;  // Undefiniert!
    }
};

// ANFÄLLIG: Template mit unsicherer Subtraktion
template<typename T>
size_t unsafe_distance(T* first, T* last) {
    // Keine Validierung dass Pointer zusammengehören
    return last - first;
}

// ANFÄLLIG: Verwirrung durch benutzerdefinierten Allokator
class VulnerableAllocator {
    char* pool1;
    char* pool2;

public:
    size_t offset_from_pool(char* ptr) {
        // Aus welchem Pool ist ptr?
        // Subtraktion vom falschen Pool ist undefiniert!
        return ptr - pool1;  // Könnte aus pool2 sein!
    }
};

Korrigierter Code

// SICHER: Größe explizit verfolgen, Zeigersubtraktion vermeiden
struct SafeBuffer {
    char* data;
    size_t size;
    size_t used;
};

size_t remaining_safe(struct SafeBuffer* buf) {
    // Keine Zeigersubtraktion erforderlich
    return buf->size - buf->used;
}

// SICHER: Zeigerbereich vor Subtraktion validieren
size_t calculate_size_safe(char* buffer, size_t buffer_size,
                           char* start, char* end) {
    char* buffer_end = buffer + buffer_size;

    // Validieren dass beide Pointer innerhalb des Puffers sind
    if (start < buffer || start > buffer_end) {
        return 0;  // Ungültiger Startzeiger
    }
    if (end < buffer || end > buffer_end) {
        return 0;  // Ungültiger Endzeiger
    }
    if (end < start) {
        return 0;  // Ungültiger Bereich
    }

    return (size_t)(end - start);  // Jetzt sicher
}

// SICHER: Explizite Grenzprüfung
void copy_data_safe(int* dest, size_t dest_size,
                    int* src, size_t src_size) {
    // Explizite Größen anstelle von Zeigersubtraktion verwenden
    size_t count = (src_size < dest_size) ? src_size : dest_size;
    memcpy(dest, src, count * sizeof(int));
}

// SICHER: Dieselbe Allokation prüfen (Debug-Builds)
#include <assert.h>

size_t array_span_safe(int* arr, size_t arr_size, int* p1, int* p2) {
    int* arr_end = arr + arr_size;

    // Validieren dass beide Pointer im selben Array sind
    assert(p1 >= arr && p1 <= arr_end);
    assert(p2 >= arr && p2 <= arr_end);

    // Korrekte Reihenfolge sicherstellen
    if (p2 < p1) {
        int* temp = p1;
        p1 = p2;
        p2 = temp;
    }

    return (size_t)(p2 - p1);
}

// SICHER: Offset anstelle von Zeigersubtraktion verwenden
struct SafeMessage {
    char* data;
    size_t data_size;
    size_t current_offset;  // Offset, kein Pointer!
};

size_t bytes_read_safe(struct SafeMessage* msg) {
    return msg->current_offset;  // Keine Subtraktion erforderlich!
}

char* current_position_safe(struct SafeMessage* msg) {
    return msg->data + msg->current_offset;
}

void advance_safe(struct SafeMessage* msg, size_t bytes) {
    if (msg->current_offset + bytes <= msg->data_size) {
        msg->current_offset += bytes;
    }
}

// SICHER: Begrenzter Puffer mit expliziter Verfolgung
struct BoundedBuffer {
    char* base;
    size_t capacity;
    size_t position;
};

int write_to_buffer_safe(struct BoundedBuffer* buf, const char* data, size_t len) {
    // Verfügbaren Platz prüfen
    size_t remaining = buf->capacity - buf->position;

    if (len > remaining) {
        return -1;  // Nicht genug Platz
    }

    memcpy(buf->base + buf->position, data, len);
    buf->position += len;

    return 0;  // Erfolg
}

// SICHER: Zeigerbereich mit Validierungsfunktion
typedef struct {
    void* start;
    void* end;
    size_t element_size;
} ValidatedRange;

int validate_range(ValidatedRange* range, void* base, size_t count, size_t elem_size) {
    char* base_end = (char*)base + count * elem_size;

    if (range->start < base || range->start > base_end) return 0;
    if (range->end < base || range->end > base_end) return 0;
    if (range->end < range->start) return 0;

    return 1;  // Gültig
}

size_t range_size_bytes(ValidatedRange* range) {
    return (char*)range->end - (char*)range->start;
}
// SICHER: C++ mit ordnungsgemäßer Container-Verwendung
class SafeContainer {
    std::vector<int> data;

public:
    // Iteratoren aus demselben Container verwenden
    size_t distanceSafe(std::vector<int>::iterator first,
                        std::vector<int>::iterator last) {
        // std::distance validiert Iteratoren aus demselben Container (Debug-Modus)
        return std::distance(first, last);
    }

    // Besser: Indizes verwenden
    int& at(size_t index) {
        return data.at(index);  // Grenzgeprüft
    }

    size_t size() const {
        return data.size();  // Keine Zeigermathematik
    }
};

// SICHER: Span mit eingebauten Grenzen
#include <span>

class SafeSpan {
public:
    size_t process(std::span<int> data) {
        // Span kennt seine eigene Größe
        return data.size();  // Keine Zeigersubtraktion
    }

    void iterate(std::span<int> data) {
        for (size_t i = 0; i < data.size(); i++) {
            data[i] = static_cast<int>(i);
        }
    }
};

// SICHER: Benutzerdefinierte Range-Klasse mit Validierung
template<typename T>
class ValidRange {
    T* base_;
    size_t size_;
    size_t start_offset_;
    size_t end_offset_;

public:
    ValidRange(T* base, size_t size, size_t start = 0, size_t end = 0)
        : base_(base), size_(size), start_offset_(start),
          end_offset_(end == 0 ? size : end) {
        if (start_offset_ > size_ || end_offset_ > size_) {
            throw std::out_of_range("Ungültige Range-Offsets");
        }
        if (end_offset_ < start_offset_) {
            throw std::invalid_argument("Ende vor Start");
        }
    }

    size_t length() const {
        return end_offset_ - start_offset_;  // Sicher: dieselbe Allokation, validiert
    }

    T* begin() { return base_ + start_offset_; }
    T* end() { return base_ + end_offset_; }
};

// SICHER: Allokator mit verfolgten Regionen
class SafeAllocator {
    std::vector<std::pair<char*, size_t>> allocations_;

public:
    char* allocate(size_t size) {
        char* ptr = new char[size];
        allocations_.emplace_back(ptr, size);
        return ptr;
    }

    // Validieren dass Pointer zu einer Allokation gehört
    std::optional<size_t> offset_in_allocation(char* ptr) {
        for (const auto& [base, size] : allocations_) {
            if (ptr >= base && ptr < base + size) {
                return ptr - base;  // Sicher: validiert dieselbe Allokation
            }
        }
        return std::nullopt;  // Nicht aus unseren Allokationen
    }

    bool same_allocation(char* p1, char* p2) {
        for (const auto& [base, size] : allocations_) {
            bool p1_in = (p1 >= base && p1 <= base + size);
            bool p2_in = (p2 >= base && p2 <= base + size);
            if (p1_in && p2_in) return true;
            if (p1_in || p2_in) return false;  // Einer drin, einer nicht
        }
        return false;
    }

    ptrdiff_t safe_subtract(char* p1, char* p2) {
        if (!same_allocation(p1, p2)) {
            throw std::invalid_argument("Pointer aus verschiedenen Allokationen");
        }
        return p1 - p2;
    }
};

// SICHER: std::addressof und explizite Größenverfolgung verwenden
template<typename Container>
class SafeIterator {
    Container* container_;
    size_t index_;

public:
    SafeIterator(Container& c, size_t i = 0)
        : container_(&c), index_(i) {}

    size_t operator-(const SafeIterator& other) const {
        if (container_ != other.container_) {
            throw std::invalid_argument("Iteratoren aus verschiedenen Containern");
        }
        return index_ - other.index_;
    }
};

Ausgenutzt in der Praxis

Heap-Spray-Angriffe

Angreifer haben Heap-Layouts manipuliert, um Zeigersubtraktion dazu zu bringen, von Angreifern kontrollierte Werte zurückzugeben, was Heap-Overflow-Exploits ermöglicht.

Integer-Überlauf-Exploits

Große Zeigerdifferenzen, die ptrdiff_t-Überlauf verursachen, wurden ausgenutzt, um Größenprüfungen zu umgehen und Puffer zum Überlaufen zu bringen.

Speicherpreisgabe

Ungültige Zeigersubtraktion wurde verwendet, um Offsets zu berechnen, die Speicherinhalte aus benachbarten Allokationen preisgeben.


Werkzeuge zum Testen/Ausnutzen


CVE-Beispiele

  • CVE-2016-9066 — Firefox Integer-Überlauf bei Zeigerberechnung.

  • CVE-2018-16395 — Ruby Zeigersubtraktions-Schwachstelle.

  • Mehrere CVEs mit Integer-Überlauf in Größenberechnungen aus Zeigerdifferenzen.


Referenzen

  1. MITRE. "CWE-469: Use of Pointer Subtraction to Determine Size." https://cwe.mitre.org/data/definitions/469.html

  2. CERT C. "ARR36-C: Do not subtract or compare two pointers that do not refer to the same array." https://wiki.sei.cmu.edu/confluence/display/c/