Abhängigkeit von undefiniertem, unspezifiziertem oder implementierungsdefiniertem Verhalten
Beschreibung
Abhängigkeit von undefiniertem, unspezifiziertem oder implementierungsdefiniertem Verhalten ist eine Schwachstelle, bei der Software eine API-Funktion, Datenstruktur oder andere Programmierentität in einer Weise verwendet, die sich auf Eigenschaften verlässt, die nicht immer für diese Entität garantiert sind. Undefiniertes Verhalten in Sprachen wie C/C++ bedeutet, dass der Standard keine Anforderungen an die Implementierung stellt - alles kann passieren. Implementierungsdefiniertes Verhalten bedeutet Verhalten, das zwischen Plattformen oder Compilern variieren kann. Wenn Code von solchem Verhalten abhängt, kann er auf einem System korrekt funktionieren, aber auf einem anderen katastrophal versagen, was potenziell Sicherheitslücken einführt.
Risiko
Die Abhängigkeit von undefiniertem Verhalten schafft ernsthafte und unvorhersehbare Sicherheitsrisiken. Compiler können Code unter der Annahme optimieren, dass undefiniertes Verhalten nie auftritt, und dabei Sicherheitsprüfungen entfernen, von denen der Entwickler erwartete, dass sie ausgeführt werden. Code, der während des Testens "funktioniert", kann in der Produktion auf verschiedenen Plattformen oder Compiler-Versionen versagen. Häufige Beispiele sind Signed-Integer-Overflow (undefiniert in C), Zugriff auf nicht initialisierten Speicher, Dereferenzierung von Null Pointern und Rückgabe von Zeigern auf Stack-Variablen. Wenn sich dieses Verhalten ändert - bei Compiler-Upgrade, Plattformmigration oder Änderung des Optimierungslevels - kann das Ergebnis Abstürze, Speicherbeschädigung oder Sicherheitslücken sein, die vorher nicht existierten.
Lösung
Vermeiden Sie Code-Muster, die sich auf undefiniertes, unspezifiziertes oder implementierungsdefiniertes Verhalten verlassen. Verwenden Sie Compiler-Warnungen und statische Analysetools, um solche Muster zu erkennen. Befolgen Sie Sprachstandards strikt - nehmen Sie keine Verhaltensweisen an, die nicht garantiert sind. Verwenden Sie explizite Prüfungen vor potenziell undefinierten Operationen (wie Prüfung auf Overflow vor Arithmetik). Geben Sie niemals Pointer auf stack-allokierte Variablen zurück. Initialisieren Sie alle Variablen vor der Verwendung. Vermeiden Sie Annahmen über Speicherlayout, Endianness oder Typgrößen, es sei denn, Sie verwenden plattformspezifischen Code. Testen Sie auf mehreren Plattformen und mit mehreren Compilern.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Integrität | Bereich: Integrität Unerwarteter Zustand - Code kann sich anders als erwartet verhalten, wenn sich das Verhalten ändert. |
| Verfügbarkeit | Bereich: Verfügbarkeit DoS: Absturz, Beenden oder Neustart - Undefiniertes Verhalten führt oft zu Abstürzen. |
| Sonstiges | Bereich: Sonstiges Wartbarkeit reduzieren - Code, der von undefiniertem Verhalten abhängt, ist fragil und schwer zu warten. |
Beispielcode + Lösungscode
Anfälliger Code
// Anfällig: Feste Adresse Funktionszeiger
#include <stdio.h>
int (*functionPtr)(float, char, char) = (void*)0x08040000;
void vulnerable_fixed_address() {
// Anfällig: Nimmt an, dass Funktion an fest codierter Adresse existiert
// Adresse ist möglicherweise nicht gültig auf verschiedenen Systemen/Läufen
int result = (*functionPtr)(12.0f, 'a', 'b');
// Könnte abstürzen, beliebigen Code ausführen, oder unvorhersehbar verhalten
// Angreifer könnte potenziell Code an diese Adresse mappen
}
// Anfällig: Pointer auf Stack-Variable zurückgeben
char* vulnerable_stack_return() {
char name[256]; // Stack-allokiert
// Name ausfüllen
strcpy(name, "LocalData");
// Anfällig: Pointer auf Stack-Speicher zurückgeben
return name;
// Nach Rückkehr der Funktion ist 'name' freigegeben
// Zurückgegebener Pointer baumelt - undefiniertes Verhalten
}
void use_vulnerable() {
char* result = vulnerable_stack_return();
// 'result' zeigt auf freigegebenen Stack-Speicher
printf("%s\n", result); // Undefiniertes Verhalten - kann abstürzen oder Müll ausgeben
}
// Anfällig: Signed Integer Overflow (undefiniert in C)
#include <limits.h>
int vulnerable_overflow_check(int value) {
// Anfällig: Signed Overflow ist undefiniertes Verhalten
// Compiler kann diese Prüfung wegoptimieren!
if (value + 1 < value) {
// Overflow aufgetreten
return -1;
}
return value + 1;
}
// Compiler kann transformieren zu:
int optimized_vulnerable(int value) {
// Compiler: "Signed Overflow ist undefiniert, also passiert er nie"
// Prüfung komplett entfernt!
return value + 1; // Overflow-Prüfung weg
}
// Anfällig: NULL-Pointer-Dereferenzierung nach Prüfung
void vulnerable_null_check(int* ptr) {
int value = *ptr; // Dereferenzierung vor Prüfung
if (ptr == NULL) {
// Anfällig: Compiler kann diese Prüfung entfernen
// Begründung: ptr wurde bereits dereferenziert, also wenn Programm
// hier angekommen ist, darf ptr nicht NULL sein (oder undefiniertes Verhalten)
return;
}
process(value);
}
// Anfällig: Nicht initialisierte Variable
void vulnerable_uninitialized() {
int flag; // Nicht initialisiert
if (some_condition()) {
flag = 1;
}
// Anfällig: flag kann nicht initialisiert sein
if (flag) { // Undefiniertes Verhalten wenn flag nicht gesetzt wurde
do_something();
}
}
// Anfällig: Array außerhalb der Grenzen zugreifen
void vulnerable_bounds(int index) {
int array[10];
// Anfällig: Keine Grenzprüfung
// Wenn index >= 10 oder < 0, undefiniertes Verhalten
array[index] = 42;
}
// Anfällig: Type-Punning durch Union (implementierungsdefiniert)
void vulnerable_type_pun() {
union {
float f;
int i;
} converter;
converter.f = 3.14f;
// Implementierungsdefiniert: Lesen eines anderen Members als geschrieben wurde
int bits = converter.i;
// Kann auf einigen Plattformen funktionieren, auf anderen versagen
}
// Anfällig: Shift um Breite des Typs
void vulnerable_shift(unsigned int value) {
// Anfällig: Shiften um >= Breite des Typs ist undefiniert
unsigned int shifted = value << 32; // Wenn int 32 Bits ist, undefiniert!
// Verschiedene Compiler/Plattformen können:
// - 0 zurückgeben
// - Wert unverändert zurückgeben
// - Müll zurückgeben
// - Abstürzen
}
// Anfällig: String-Literal modifizieren
void vulnerable_string_literal() {
char* str = "Hello"; // Zeigt auf schreibgeschützten Speicher (typischerweise)
// Anfällig: Modifizieren von String-Literal ist undefiniert
str[0] = 'J'; // Kann abstürzen, funktionieren, oder andere Daten beschädigen
}
// Anfällig: Abhängigkeit von Auswertungsreihenfolge
int vulnerable_sequence(int* p) {
// Anfällig: Auswertungsreihenfolge ist unspezifiziert
return (*p++) + (*p++); // Ergebnis variiert je nach Compiler
}
Behobener Code
// Behoben: Funktionszeiger ordentlich verwenden
#include <stdio.h>
typedef int (*FunctionPtr)(float, char, char);
int secure_function(float f, char a, char b) {
return (int)(f + a + b);
}
void secure_function_pointer() {
// Behoben: Auf bekannte, gültige Funktion zeigen
FunctionPtr functionPtr = secure_function;
int result = functionPtr(12.0f, 'a', 'b');
printf("Ergebnis: %d\n", result);
}
// Behoben: Heap-allozierten oder statischen Speicher zurückgeben
char* secure_return_string() {
// Behoben: Auf Heap allokieren
char* name = malloc(256);
if (name == NULL) return NULL;
strcpy(name, "HeapAllocatedData");
return name; // Aufrufer muss freigeben
}
// Alternative: Statischen Puffer verwenden (mit Einschränkungen)
char* secure_static_return() {
static char name[256]; // Statischer Speicher - bleibt bestehen
strcpy(name, "StaticData");
return name; // Gültig aber nicht thread-sicher
}
// Am besten: Aufrufer stellt Puffer bereit
int secure_fill_buffer(char* buffer, size_t size) {
if (buffer == NULL || size < 10) return -1;
strncpy(buffer, "SafeData", size - 1);
buffer[size - 1] = '\0';
return 0;
}
// Behoben: Sichere Overflow-Prüfung mit unsigned oder builtin
#include <limits.h>
#include <stdint.h>
int secure_overflow_check(int value) {
// Behoben: Vor Overflow prüfen
if (value == INT_MAX) {
return -1; // Würde überlaufen
}
return value + 1;
}
// Alternative: Compiler-Builtins verwenden
int secure_overflow_builtin(int value) {
int result;
// GCC/Clang Builtin für geprüfte Arithmetik
if (__builtin_add_overflow(value, 1, &result)) {
return -1; // Overflow aufgetreten
}
return result;
}
// Behoben: NULL vor Dereferenzierung prüfen
void secure_null_check(int* ptr) {
// Behoben: VOR Dereferenzierung prüfen
if (ptr == NULL) {
return;
}
int value = *ptr; // Jetzt sicher
process(value);
}
// Behoben: Alle Variablen initialisieren
void secure_initialized() {
int flag = 0; // Behoben: Initialisieren
if (some_condition()) {
flag = 1;
}
// Jetzt ist flag immer definiert
if (flag) {
do_something();
}
}
// Behoben: Grenzprüfung
void secure_bounds(int index, int* array, size_t array_size) {
// Behoben: Grenzen validieren
if (index < 0 || (size_t)index >= array_size) {
return; // Außerhalb der Grenzen
}
array[index] = 42;
}
// Behoben: Ordentlicher Typzugriff mit memcpy
#include <string.h>
void secure_type_access() {
float f = 3.14f;
int bits;
// Behoben: memcpy ist wohldefiniert für Type-Punning
memcpy(&bits, &f, sizeof(bits));
// Jetzt enthält bits die Bit-Repräsentation von f
}
// Behoben: Sichere Shift-Operationen
void secure_shift(unsigned int value, unsigned int shift_amount) {
// Behoben: Shift-Betrag prüfen
if (shift_amount >= sizeof(value) * 8) {
// Shift zu groß - angemessen behandeln
return;
}
unsigned int shifted = value << shift_amount;
}
// Behoben: Modifizierbares Array anstelle von String-Literal verwenden
void secure_string() {
// Behoben: Array ist modifizierbar
char str[] = "Hello"; // Auf Stack kopiert
str[0] = 'J'; // Sicher zu modifizieren
printf("%s\n", str); // Gibt "Jello" aus
}
// Behoben: Klare Auswertungsreihenfolge
int secure_sequence(int* p) {
// Behoben: Explizite Reihenfolge
int first = *p;
p++;
int second = *p;
p++;
return first + second;
}
CVE-Beispiele
- CVE-2006-1902: Änderung im C-Compiler-Verhalten verursachte Pufferüberläufe in Programmen, die von undefiniertem Verhalten abhingen.
- CVE-2008-1685: Compiler optimierte Sicherheitsprüfung weg, die sich auf undefiniertes Signed-Overflow-Verhalten verließ.
Referenzen
- MITRE Corporation. "CWE-758: Reliance on Undefined, Unspecified, or Implementation-Defined Behavior." https://cwe.mitre.org/data/definitions/758.html
- ISO/IEC 9899 C Language Standard - Undefined Behavior.
- CERT C Coding Standard. "MSC15-C. Do not depend on undefined behavior."