Datenelement mit Pointer ohne ordnungsgemäßes Kopier-Kontrollelement

Beschreibung

Datenelement mit Pointer ohne ordnungsgemäßes Kopier-Kontrollelement tritt auf, wenn Code ein Datenelement mit einem Pointer enthält, das keinen zugehörigen Kopierkonstruktor oder Zuweisungsoperator hat. In C++ ist dies als Verletzung der "Rule of Three" (oder "Rule of Five" in modernem C++) bekannt: Wenn eine Klasse eine Ressource verwaltet (wie dynamisch allokierten Speicher über einen Pointer), sollte sie einen Destruktor, Kopierkonstruktor und Kopierzuweisungsoperator definieren. Ohne ordnungsgemäße Kopierkontrolle führt das Kopieren von Objekten zu flachen Kopien, bei denen mehrere Objekte den gleichen Pointer teilen, was Double-Free-Fehler, Use-After-Free-Schwachstellen und Speicherkorruption verursacht.

Risiko

Fehlende Kopierkontrolle für Objekte mit Zeigern hat ernsthafte Sicherheitsimplikationen. Flache Kopien führen zu Double-Free-Schwachstellen, wenn beide Objekte zerstört werden. Use-After-Free-Bedingungen treten auf, wenn eine Kopie gelöscht wird und die andere den Pointer weiterhin verwendet. Speicherkorruption durch diese Probleme kann für Code-Ausführung ausgenutzt werden. Datenlecks können auftreten, wenn sensible Daten beim Kopieren nicht ordnungsgemäß verwaltet werden. Ressourcenerschöpfung kann entstehen, wenn Ressourcen nicht ordnungsgemäß freigegeben werden. Das undefinierte Verhalten durch fehlerhaftes Kopieren ist unvorhersehbar und potenziell ausnutzbar.

Lösung

Befolgen Sie die Rule of Three (C++03) oder Rule of Five (C++11): Wenn Sie einen von Destruktor, Kopierkonstruktor oder Kopierzuweisungsoperator definieren, definieren Sie alle. In modernem C++ berücksichtigen Sie auch Move-Konstruktor und Move-Zuweisungsoperator. Verwenden Sie Smart Pointer (unique_ptr, shared_ptr), um Ressourcenverwaltung zu automatisieren. Wenden Sie die Rule of Zero an: Bevorzugen Sie RAII-Wrapper, sodass spezielle Mitglieder nicht benötigt werden. Verwenden Sie statische Analysetools, um Verletzungen zu erkennen. Erwägen Sie, Klassen nicht kopierbar zu machen, wenn Kopieren keinen Sinn macht. Verwenden Sie = delete, um Kopieren explizit zu verhindern, wenn angemessen.

Häufige Auswirkungen

AuswirkungDetails
IntegritätBereich: Integrität

Speicherkorruption - Double-Free und Use-After-Free durch fehlerhafte Kopien.
VerfügbarkeitBereich: Verfügbarkeit

DoS: Absturz - Speicherkorruption führt zu Abstürzen.
VertraulichkeitBereich: Vertraulichkeit

Speicher lesen - Use-After-Free kann sensible Daten preisgeben.

Beispielcode

Anfälliger Code

// Anfällig: Rule of Three-Verletzung
class VulnerableBuffer {
private:
    char* data;
    size_t size;

public:
    VulnerableBuffer(size_t sz) : size(sz) {
        data = new char[size];
        memset(data, 0, size);
    }

    ~VulnerableBuffer() {
        delete[] data;  // Ordnungsgemäße Bereinigung
    }

    // Fehlt: Kopierkonstruktor!
    // Fehlt: Kopierzuweisungsoperator!

    void write(const char* input, size_t len) {
        if (len <= size) {
            memcpy(data, input, len);
        }
    }

    char* getData() { return data; }
};

void vulnerableUsage() {
    VulnerableBuffer buf1(100);
    buf1.write("geheime daten", 13);

    // Anfällig: Flache Kopie - beide zeigen auf gleichen Speicher!
    VulnerableBuffer buf2 = buf1;  // Verwendet Standard-Kopierkonstruktor

    // buf1.data und buf2.data zeigen auf gleichen Speicher!

    // Wenn buf2 zerstört wird, wird data freigegeben
    // Wenn buf1 zerstört wird, wird data ERNEUT freigegeben (Double-Free!)
}

void vulnerableAssignment() {
    VulnerableBuffer buf1(100);
    VulnerableBuffer buf2(50);

    buf2 = buf1;  // Standard-Zuweisung - flache Kopie!
    // buf2s ursprünglicher Speicher ist verloren!
    // buf1 und buf2 teilen den gleichen Pointer!
}
// Anfällig: Klasse mit Pointer auf komplexes Objekt
class VulnerableDocument {
private:
    std::string* content;
    std::string* metadata;
    User* owner;  // Roher Pointer auf externes Objekt

public:
    VulnerableDocument(const std::string& text, User* user) {
        content = new std::string(text);
        metadata = new std::string("{}");
        owner = user;
    }

    ~VulnerableDocument() {
        delete content;
        delete metadata;
        // owner nicht löschen - wird geteilt
    }

    // Kein Kopierkonstruktor definiert!
    // Keine Kopierzuweisung definiert!

    void setContent(const std::string& text) {
        *content = text;
    }
};

void vulnerableDocumentCopy() {
    User user("admin");
    VulnerableDocument doc1("geheim", &user);

    // Flache Kopie - beide Dokumente zeigen auf gleichen Inhalt!
    VulnerableDocument doc2 = doc1;

    doc2.setContent("modifiziert");
    // FEHLER: doc1s Inhalt wurde auch geändert!

    // Wenn Funktion endet:
    // doc2 zerstört - löscht content und metadata
    // doc1 zerstört - Double-Free!
}
// Anfällig: Vektor von Objekten mit Zeigern
class VulnerableResource {
private:
    int* values;
    int count;

public:
    VulnerableResource(int n) : count(n) {
        values = new int[n];
    }

    ~VulnerableResource() {
        delete[] values;
    }
    // Fehlende Kopierkontrolle!
};

void vulnerableVector() {
    std::vector<VulnerableResource> resources;

    VulnerableResource r(10);
    resources.push_back(r);  // Flache Kopie in Vektor!

    // r und resources[0] teilen den gleichen values-Pointer

    // Wenn Vektor realloziiert oder r den Gültigkeitsbereich verlässt:
    // Double-Free oder Use-After-Free!
}

Korrigierter Code

// Korrigiert: Ordnungsgemäße Rule of Three-Implementierung
class FixedBuffer {
private:
    char* data;
    size_t size;

public:
    // Konstruktor
    explicit FixedBuffer(size_t sz) : size(sz), data(nullptr) {
        if (size > 0) {
            data = new char[size];
            memset(data, 0, size);
        }
    }

    // Destruktor
    ~FixedBuffer() {
        delete[] data;
    }

    // Kopierkonstruktor - tiefe Kopie
    FixedBuffer(const FixedBuffer& other) : size(other.size), data(nullptr) {
        if (size > 0) {
            data = new char[size];
            memcpy(data, other.data, size);
        }
    }

    // Kopierzuweisungsoperator - tiefe Kopie mit ordnungsgemäßer Bereinigung
    FixedBuffer& operator=(const FixedBuffer& other) {
        if (this != &other) {  // Selbstzuweisungsprüfung
            // Zuerst neuen Puffer erstellen (Ausnahmesicherheit)
            char* newData = nullptr;
            if (other.size > 0) {
                newData = new char[other.size];
                memcpy(newData, other.data, other.size);
            }

            // Alte Daten bereinigen
            delete[] data;

            // Neue Daten zuweisen
            data = newData;
            size = other.size;
        }
        return *this;
    }

    void write(const char* input, size_t len) {
        if (data && len <= size) {
            memcpy(data, input, len);
        }
    }

    const char* getData() const { return data; }
    size_t getSize() const { return size; }
};
// Korrigiert: Rule of Five (C++11) mit Move-Semantik
class ModernBuffer {
private:
    char* data;
    size_t size;

public:
    // Konstruktor
    explicit ModernBuffer(size_t sz) : size(sz), data(nullptr) {
        if (size > 0) {
            data = new char[size]();  // Wert-initialisiert
        }
    }

    // Destruktor
    ~ModernBuffer() {
        delete[] data;
    }

    // Kopierkonstruktor
    ModernBuffer(const ModernBuffer& other) : size(other.size), data(nullptr) {
        if (size > 0) {
            data = new char[size];
            std::copy(other.data, other.data + size, data);
        }
    }

    // Move-Konstruktor
    ModernBuffer(ModernBuffer&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }

    // Kopierzuweisung
    ModernBuffer& operator=(const ModernBuffer& other) {
        if (this != &other) {
            ModernBuffer temp(other);  // Copy-and-Swap-Idiom
            swap(*this, temp);
        }
        return *this;
    }

    // Move-Zuweisung
    ModernBuffer& operator=(ModernBuffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }

    // Swap-Funktion für Copy-and-Swap
    friend void swap(ModernBuffer& a, ModernBuffer& b) noexcept {
        using std::swap;
        swap(a.data, b.data);
        swap(a.size, b.size);
    }

    // Zugriffsmethoden
    const char* getData() const { return data; }
    size_t getSize() const { return size; }
};
// Korrigiert: Rule of Zero - Smart Pointer verwenden
class SmartBuffer {
private:
    std::unique_ptr<char[]> data;
    size_t size;

public:
    explicit SmartBuffer(size_t sz) : size(sz) {
        if (size > 0) {
            data = std::make_unique<char[]>(size);
        }
    }

    // Kein Destruktor benötigt - unique_ptr übernimmt Bereinigung
    // Kein Kopierkonstruktor benötigt - unique_ptr ist move-only

    // Wenn Kopieren benötigt wird, tiefe Kopie implementieren:
    SmartBuffer(const SmartBuffer& other) : size(other.size) {
        if (size > 0) {
            data = std::make_unique<char[]>(size);
            std::copy(other.data.get(), other.data.get() + size, data.get());
        }
    }

    SmartBuffer& operator=(const SmartBuffer& other) {
        if (this != &other) {
            size = other.size;
            if (size > 0) {
                data = std::make_unique<char[]>(size);
                std::copy(other.data.get(), other.data.get() + size, data.get());
            } else {
                data.reset();
            }
        }
        return *this;
    }

    // Move-Operationen werden automatisch für unique_ptr generiert
    SmartBuffer(SmartBuffer&&) = default;
    SmartBuffer& operator=(SmartBuffer&&) = default;

    const char* getData() const { return data.get(); }
};
// Korrigiert: Nicht kopierbare Klasse, wenn Kopieren keinen Sinn macht
class NonCopyableResource {
private:
    int* handle;

public:
    explicit NonCopyableResource(int id) {
        handle = acquireResource(id);
    }

    ~NonCopyableResource() {
        releaseResource(handle);
    }

    // Kopieroperationen explizit löschen
    NonCopyableResource(const NonCopyableResource&) = delete;
    NonCopyableResource& operator=(const NonCopyableResource&) = delete;

    // Move-Operationen erlauben
    NonCopyableResource(NonCopyableResource&& other) noexcept
        : handle(other.handle) {
        other.handle = nullptr;
    }

    NonCopyableResource& operator=(NonCopyableResource&& other) noexcept {
        if (this != &other) {
            releaseResource(handle);
            handle = other.handle;
            other.handle = nullptr;
        }
        return *this;
    }

    int* getHandle() const { return handle; }
};

// Sichere Verwendung in Vektor mit Move-Semantik
void safeVector() {
    std::vector<NonCopyableResource> resources;

    resources.push_back(NonCopyableResource(1));  // Move in Vektor
    resources.emplace_back(2);  // An Ort und Stelle konstruieren

    // Kein Double-Free - jede Ressource hat eindeutige Eigentümerschaft
}
// Korrigiert: Dokument mit ordnungsgemäßer Kopiersemantik
class FixedDocument {
private:
    std::unique_ptr<std::string> content;
    std::unique_ptr<std::string> metadata;
    std::shared_ptr<User> owner;  // Geteilte Eigentümerschaft

public:
    FixedDocument(const std::string& text, std::shared_ptr<User> user)
        : content(std::make_unique<std::string>(text)),
          metadata(std::make_unique<std::string>("{}")),
          owner(std::move(user)) {}

    // Tiefe Kopie
    FixedDocument(const FixedDocument& other)
        : content(std::make_unique<std::string>(*other.content)),
          metadata(std::make_unique<std::string>(*other.metadata)),
          owner(other.owner) {}  // Shared Pointer - geteilte Eigentümerschaft

    FixedDocument& operator=(const FixedDocument& other) {
        if (this != &other) {
            content = std::make_unique<std::string>(*other.content);
            metadata = std::make_unique<std::string>(*other.metadata);
            owner = other.owner;
        }
        return *this;
    }

    // Standard-Move-Operationen funktionieren korrekt mit Smart Pointern
    FixedDocument(FixedDocument&&) = default;
    FixedDocument& operator=(FixedDocument&&) = default;

    void setContent(const std::string& text) {
        *content = text;
    }

    const std::string& getContent() const { return *content; }
};

void safeDocumentCopy() {
    auto user = std::make_shared<User>("admin");
    FixedDocument doc1("geheim", user);

    // Tiefe Kopie - unabhängiger Inhalt
    FixedDocument doc2 = doc1;

    doc2.setContent("modifiziert");
    // doc1s Inhalt unverändert - "geheim"
    // doc2s Inhalt ist "modifiziert"

    // Beide teilen denselben User durch shared_ptr
    // Keine Double-Free-Probleme
}

CVE-Beispiele

Double-Free- und Use-After-Free-Schwachstellen durch fehlende Kopierkontrolle waren häufige Quellen ausnutzbarer Speicherkorruptionsfehler in C++-Anwendungen.


Verwandte CWEs

  • CWE-1076: Insufficient Adherence to Expected Conventions (Eltern)
  • CWE-415: Double Free (kann resultieren aus)
  • CWE-416: Use After Free (kann resultieren aus)
  • CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer (verwandt)

Referenzen

  1. MITRE Corporation. "CWE-1098: Data Element containing Pointer Item without Proper Copy Control Element." https://cwe.mitre.org/data/definitions/1098.html
  2. Stroustrup, Bjarne. "The C++ Programming Language" - Rule of Three/Five/Zero.
  3. C++ Core Guidelines. C.21: If you define or =delete any default operation, define or =delete them all.