Compiler-Optimierung entfernt oder modifiziert sicherheitskritischen Code

Beschreibung

Compiler-Optimierung entfernt oder modifiziert sicherheitskritischen Code ist eine Schwachstelle, bei der Entwickler sicherheitskritische Schutzmechanismen in Software einbauen, aber die Optimierungsdurchläufe des Compilers diesen Code entfernen oder modifizieren, weil er scheinbar keine funktionale Auswirkung auf die Programmausgabe hat. Dies ist besonders häufig bei Operationen zum Löschen sensibler Daten - Compiler können memset()-Aufrufe eliminieren, die Passwörter oder kryptographische Schlüssel nullen, weil der Puffer danach nicht mehr gelesen wird. Der optimierte Code wird ohne die beabsichtigten Sicherheitsschutzmaßnahmen ausgeliefert, was sensible Daten im Speicher hinterlässt, wo sie von Angreifern wiederhergestellt werden können.

Risiko

Diese Schwachstelle schafft ernsthafte Sicherheitsrisiken, weil sicherheitskritischer Code stillschweigend während der Kompilierung entfernt wird. Passwörter und kryptographische Schlüssel, die im Speicher verbleiben, können durch Core-Dumps, Speicherinspektion oder Cold-Boot-Angriffe wiederhergestellt werden. Integer-Overflow-Prüfungen, die wegoptimiert werden, können Buffer-Overflows ermöglichen. Sicherheits-Assertions, die durch Optimierung entfernt werden, erlauben das Auftreten von undefiniertem Verhalten ohne Erkennung. Das Risiko wird verstärkt, weil der Quellcode sicher erscheint - die Schwachstelle existiert nur im kompilierten Binary. Entwickler können glauben, dass Schutzmassnahmen vorhanden sind, obwohl sie es nicht sind, was ein gefährliches falsches Sicherheitsgefühl schafft.

Lösung

Verwenden Sie Compiler-spezifische Mechanismen, um die Optimierung von sicherheitskritischem Code zu verhindern. In C/C++ verwenden Sie volatile-Variablen, Memory-Barriers oder Compiler-spezifische Funktionen wie SecureZeroMemory() unter Windows oder explicit_bzero() unter BSD/Linux. Verwenden Sie statische Analysetools, die erkennen können, wenn Sicherheitscode möglicherweise wegoptimiert wird. Verifizieren Sie sicherheitskritische Operationen in kompilierten Binaries durch Inspektion oder Tests. Erwägen Sie die Verwendung von Compiler-Flags, die spezifische gefährliche Optimierungen für sicherheitssensible Codeabschnitte deaktivieren. Dokumentieren Sie, welche Funktionen nicht optimiert werden dürfen und etablieren Sie Build-Prozesse, die diese Anforderungen durchsetzen.

Häufige Auswirkungen

AuswirkungDetails
ZugriffskontrolleBereich: Zugriffskontrolle, Ändere

Schutzmechanismus umgehen - Sicherheitskontrollen werden umgangen, wenn Optimierung schützenden Code entfernt.
VertraulichkeitBereich: Vertraulichkeit

Anwendungsdaten lesen - Sensible Daten wie Passwörter verbleiben im Speicher, wenn Löschcode wegoptimiert wird.
AndereBereich: Ändere

Ausführungslogik ändern - Beabsichtigte Sicherheitslogik wird in optimierten Builds nicht ausgeführt.

Beispielcode

Verwundbarer Code

// Verwundbar: memset wird wegoptimiert
#include <string.h>
#include <stdio.h>

void vulnerable_password_handling(char *mainframe_addr) {
    char password[64];

    // Passwort vom Benutzer holen
    if (GetPasswordFromUser(password, sizeof(password))) {
        // Passwort für Verbindung verwenden
        if (ConnectToMainframe(mainframe_addr, password)) {
            // ... mit Mainframe interagieren ...
        }
    }

    // Verwundbar: Compiler kann dies als "toten Speicher" entfernen
    // weil password nach diesem Punkt nie gelesen wird
    memset(password, 0, sizeof(password));

    // Passwort bleibt im Speicher, wiederherstellbar über:
    // - Core-Dumps
    // - Speicherinspektion durch Malware
    // - Cold-Boot-Angriffe
    // - Prozessspeicher-Reads
}

// Verwundbar: Integer-Overflow-Prüfung wegoptimiert (CVE-2008-1685 Muster)
void vulnerable_overflow_check(int user_size) {
    // Verwundbar: Compiler kann dies wegoptimieren
    // weil Signed-Overflow undefiniertes Verhalten ist
    // Compiler nimmt an, es kann nie passieren
    if (user_size + 100 < user_size) {
        // Overflow erkannt
        return;
    }

    // Diese Prüfung kann komplett entfernt werden!
    char* buffer = malloc(user_size + 100);
    // Mit entfernter Prüfung wrapt Overflow zu kleinem Wert
    process_data(buffer, user_size);
}

// Verwundbar: Sicherheits-Assertion in Release-Builds entfernt
#ifdef NDEBUG
#define security_assert(x) ((void)0)  // In Release entfernt!
#else
#define security_assert(x) assert(x)
#endif

void vulnerable_assertion(int privilege_level) {
    security_assert(privilege_level >= REQUIRED_LEVEL);
    // In Release-Builds ist diese Prüfung komplett weg!

    perform_privileged_operation();
}
// Verwundbar: Crypto-Key-Löschung wegoptimiert
#include <openssl/evp.h>

void vulnerable_crypto() {
    unsigned char key[32];
    unsigned char iv[16];

    // Schlüsselmaterial generieren
    RAND_bytes(key, sizeof(key));
    RAND_bytes(iv, sizeof(iv));

    // Schlüssel für Verschlüsselung verwenden
    encrypt_data(plaintext, ciphertext, key, iv);

    // Verwundbar: Compiler sieht, dass key/iv danach nicht verwendet werden
    memset(key, 0, sizeof(key));  // Kann wegoptimiert werden
    memset(iv, 0, sizeof(iv));    // Kann wegoptimiert werden

    // Schlüsselmaterial bleibt im Speicher
}

// Verwundbar: Sensitiver Vergleich optimiert
int vulnerable_timing_safe_compare(const char* a, const char* b, size_t len) {
    volatile int result = 0;

    // Diese Schleife könnte auf frühzeitigen Ausstieg optimiert werden
    // was Timing-Angriff-Schutz zunichte macht
    for (size_t i = 0; i < len; i++) {
        result |= a[i] ^ b[i];
    }

    return result == 0;
}

Lösungscode

// Behoben: volatile verwenden um Optimierung zu verhindern
#include <string.h>
#include <stdio.h>

// Behoben: Volatile-Pointer stellt sicher, dass Speicherschreibvorgänge erfolgen
void secure_zero_memory(void* ptr, size_t len) {
    volatile unsigned char* p = (volatile unsigned char*)ptr;
    while (len--) {
        *p++ = 0;
    }
}

void secure_password_handling(char *mainframe_addr) {
    char password[64];

    if (GetPasswordFromUser(password, sizeof(password))) {
        if (ConnectToMainframe(mainframe_addr, password)) {
            // ... mit Mainframe interagieren ...
        }
    }

    // Behoben: Funktion verwenden, die nicht wegoptimiert wird
    secure_zero_memory(password, sizeof(password));
}

// Alternative: Plattformspezifische sichere Funktionen verwenden
#ifdef _WIN32
#include <windows.h>
// SecureZeroMemory wird garantiert nicht wegoptimiert
#define secure_clear(ptr, len) SecureZeroMemory(ptr, len)
#elif defined(__STDC_LIB_EXT1__)
// C11 Annex K
#define secure_clear(ptr, len) memset_s(ptr, len, 0, len)
#else
// BSD/Linux
#include <string.h>
#define secure_clear(ptr, len) explicit_bzero(ptr, len)
#endif
// Behoben: Overflow-Prüfung, die Optimierung überlebt
#include <stdint.h>
#include <limits.h>

// Behoben: Vor der Operation prüfen, unsigned für definiertes Verhalten verwenden
int secure_overflow_check(size_t user_size) {
    // Behoben: Auf Overflow prüfen bevor er passiert
    if (user_size > SIZE_MAX - 100) {
        // Würde überlaufen
        return -1;
    }

    size_t total_size = user_size + 100;
    char* buffer = malloc(total_size);
    if (buffer == NULL) return -1;

    process_data(buffer, user_size);
    free(buffer);
    return 0;
}

// Alternative: Compiler-Builtins verwenden
int secure_with_builtin(int a, int b) {
    int result;
    // GCC/Clang Builtin das Overflow prüft
    if (__builtin_add_overflow(a, b, &result)) {
        // Overflow aufgetreten
        return -1;
    }
    return result;
}

// Behoben: Sicherheitsprüfungen, die in Release bestehen bleiben
void secure_privilege_check(int privilege_level) {
    // Behoben: assert nicht für Sicherheitsprüfungen verwenden
    if (privilege_level < REQUIRED_LEVEL) {
        // Versuch protokollieren
        log_security_violation("Unzureichende Privilegien");
        // Sicher fehlschlagen
        abort();  // Oder Exception werfen, Fehler zurückgeben
    }

    perform_privileged_operation();
}
// Behoben: Crypto-Key-Löschung mit Memory-Barrier
#include <openssl/evp.h>

// Memory-Barrier verhindert Umsortierung/Optimierung
static void memory_barrier(void) {
    __asm__ __volatile__("" ::: "memory");
}

void secure_crypto() {
    unsigned char key[32];
    unsigned char iv[16];

    RAND_bytes(key, sizeof(key));
    RAND_bytes(iv, sizeof(iv));

    encrypt_data(plaintext, ciphertext, key, iv);

    // Behoben: OpenSSLs sichere Löschfunktion verwenden
    OPENSSL_cleanse(key, sizeof(key));
    OPENSSL_cleanse(iv, sizeof(iv));

    // Oder explicit_bzero verwenden (POSIX)
    // explicit_bzero(key, sizeof(key));

    // Oder manueller Ansatz mit Barrier
    // memset(key, 0, sizeof(key));
    // memory_barrier();  // Verhindert Optimierung
}

// Behoben: Timing-sicherer Vergleich, der Optimierung widersteht
int secure_timing_safe_compare(const void* a, const void* b, size_t len) {
    const volatile unsigned char* pa = (const volatile unsigned char*)a;
    const volatile unsigned char* pb = (const volatile unsigned char*)b;
    volatile unsigned char result = 0;

    for (size_t i = 0; i < len; i++) {
        result |= pa[i] ^ pb[i];
    }

    // Memory-Barrier um sicherzustellen, dass alle Iterationen abgeschlossen werden
    __asm__ __volatile__("" ::: "memory");

    return result == 0;
}

// Oder plattformbereitgestellten Timing-sicheren Vergleich verwenden
#include <openssl/crypto.h>
// CRYPTO_memcmp ist Timing-sicher
// Behoben: Compiler-Direktiven um Optimierung zu verhindern
// GCC-spezifisch
__attribute__((optimize("O0")))
void security_critical_function(void* sensitive_data, size_t len) {
    // Diese Funktion wird nicht optimiert
    process_sensitive(sensitive_data);
    memset(sensitive_data, 0, len);  // Wird nicht entfernt
}

// MSVC-spezifisch
#pragma optimize("", off)
void security_critical_msvc(void* data, size_t len) {
    // Keine Optimierung in dieser Funktion
    process(data);
    memset(data, 0, len);
}
#pragma optimize("", on)

CVE-Beispiele

  • CVE-2008-1685: Compiler-Optimierung entfernte Integer-Overflow-Erkennungscode, ermöglichte Speicherkorruption.
  • CVE-2019-1010006: Optimierungskette entfernte Integer-Overflow-Erkennung, ermöglichte Out-of-Bounds-Schreiben.

Referenzen

  1. MITRE Corporation. "CWE-733: Compiler Optimization Removal or Modification of Security-critical Code." https://cwe.mitre.org/data/definitions/733.html
  2. CERT C Coding Standard. "MSC06-C. Beware of compiler optimizations."
  3. "Zeroing memory, compiler optimizations and memset_s" - OpenSSL Wiki.