Unsachgemäße Kontrolle einer Ressource über ihren Lebenszyklus

Beschreibung

Unsachgemäße Kontrolle einer Ressource über ihren Lebenszyklus tritt auf, wenn eine Anwendung den vollständigen Lebenszyklus einer Ressource von der Zuweisung bis zur Freigabe nicht ordnungsgemäß verwaltet. Dies umfasst das Versäumnis, Ressourcen zu initialisieren, die Verwendung von Ressourcen nach ihrer Freigabe, die mehrfache Freigabe von Ressourcen oder das Nie-Freigeben von Ressourcen. Ressourcen können Speicher, Dateihandles, Datenbankverbindungen, Netzwerk-Sockets und andere Systemobjekte umfassen.

Risiko

Speicherlecks verursachen Ressourcenerschöpfung und Denial of Service. Use-after-free-Schwachstellen ermöglichen Codeausführung. Double-free-Schwachstellen korrumpieren Speicherzuweiser. Nicht initialisierte Ressourcen enthalten sensible Daten aus vorherigen Operationen. Ressourcenlecks in langlebigen Diensten verursachen schrittweise Degradierung. Dateideskriptor-Erschöpfung verhindert neue Verbindungen.

Lösung

Befolgen Sie RAII (Resource Acquisition Is Initialization)-Muster. Verwenden Sie Smart Pointer und automatisches Ressourcenmanagement. Implementieren Sie ordnungsgemäße Bereinigung in allen Codepfaden einschließlich Exceptions. Verwenden Sie try-with-resources oder ähnliche Konstrukte. Verfolgen Sie den Ressourcenstatus explizit. Implementieren Sie Ressourcenpools mit ordnungsgemäßem Lebenszyklus-Management.

Häufige Auswirkungen

AuswirkungDetails
VerfügbarkeitBereich: Ressourcenerschöpfung

Geleakte Ressourcen verursachen Dienst-Degradierung.
VertraulichkeitBereich: Informationspreisgabe

Nicht initialisierte Ressourcen geben sensible Daten preis.
IntegritätBereich: Speicherkorruption

Double-free und Use-after-free korrumpieren Speicher.

Beispielcode und Lösung

Verwundbarer Code

// VERWUNDBAR: Unsachgemäßes Ressourcen-Lebenszyklus-Management
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// VERWUNDBAR: Speicherleck im Fehlerpfad
char* read_file_vulnerable(const char* filename) {
    FILE* file = fopen(filename, "r");
    if (!file) {
        return NULL;
    }

    char* buffer = malloc(1024);
    if (!buffer) {
        // VERWUNDBAR: Dateihandle geleakt!
        return NULL;
    }

    if (fread(buffer, 1, 1024, file) == 0) {
        // VERWUNDBAR: Datei und Buffer geleakt!
        return NULL;
    }

    fclose(file);
    return buffer;  // Aufrufer muss freigeben, aber kein Hinweis darauf
}

// VERWUNDBAR: Use-after-free
void use_after_free_vulnerable() {
    char* data = malloc(100);
    strcpy(data, "sensitive");

    free(data);

    // VERWUNDBAR: Verwendet freigegebenen Speicher
    printf("Data: %s\n", data);  // Use-after-free!
}

// VERWUNDBAR: Double-free
void double_free_vulnerable(int condition) {
    char* ptr = malloc(100);

    if (condition) {
        free(ptr);
    }

    // VERWUNDBAR: Kann bereits freigegebenen Speicher freigeben
    free(ptr);  // Double-free wenn condition wahr war!
}

// VERWUNDBAR: Verwendung von nicht initialisiertem Speicher
void uninitialized_memory_vulnerable() {
    char buffer[256];  // VERWUNDBAR: Nicht initialisiert

    // VERWUNDBAR: Liest nicht initialisierte Daten
    if (buffer[0] == 'A') {  // Undefiniertes Verhalten
        do_something();
    }

    // VERWUNDBAR: Sendet nicht initialisierte Daten
    send(socket, buffer, sizeof(buffer), 0);  // Leakt Stack-Daten
}

// VERWUNDBAR: Ressource wird nie freigegeben
void resource_leak_vulnerable() {
    while (1) {
        int* data = malloc(sizeof(int) * 1000);
        // VERWUNDBAR: Wird nie freigegeben - Speichererschöpfung
        process_data(data);
    }
}

// VERWUNDBAR: Dateideskriptor-Leck
void fd_leak_vulnerable(const char** filenames, int count) {
    for (int i = 0; i < count; i++) {
        int fd = open(filenames[i], O_RDONLY);
        if (fd < 0) continue;

        process_file(fd);
        // VERWUNDBAR: fd wird nie geschlossen - Deskriptor-Erschöpfung
    }
}
# VERWUNDBAR: Python-Ressourcen-Lebenszyklus-Probleme
class VulnerableResourceManager:

    def __init__(self):
        self.connection = None

    def connect(self):
        # VERWUNDBAR: Alte Verbindung nicht geschlossen vor Erstellung einer neuen
        self.connection = create_database_connection()

    def execute(self, query):
        # VERWUNDBAR: Keine Prüfung ob Verbindung existiert
        return self.connection.execute(query)

    # VERWUNDBAR: Keine automatisch aufgerufene Bereinigungsmethode

# VERWUNDBAR: Dateihandle-Lecks
def read_files_vulnerable(filenames):
    results = []
    for filename in filenames:
        f = open(filename, 'r')
        # VERWUNDBAR: Dateien werden nie geschlossen
        results.append(f.read())
    return results

# VERWUNDBAR: Zirkulare Referenzen verursachen Speicherlecks
class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

def create_cycle_vulnerable():
    a = Node("A")
    b = Node("B")
    # VERWUNDBAR: Zirkulare Referenz - potenzielles Speicherleck
    a.next = b
    b.next = a
    # Wenn Funktion zurückkehrt, kann zirkulare Referenz Sammlung verhindern

# VERWUNDBAR: Exception verursacht Ressourcenleck
def process_file_vulnerable(filename):
    f = open(filename, 'r')
    data = f.read()
    process(data)  # Wenn dies eine Exception auslöst, wird Datei nie geschlossen!
    f.close()
// VERWUNDBAR: Java-Ressourcen-Lebenszyklus-Probleme
public class VulnerableResourceManagement {

    // VERWUNDBAR: Verbindungsleck bei Exception
    public String queryDatabase(String sql) throws SQLException {
        Connection conn = DriverManager.getConnection(url);
        Statement stmt = conn.createStatement();

        // VERWUNDBAR: Bei Exception werden conn und stmt nie geschlossen
        ResultSet rs = stmt.executeQuery(sql);
        String result = rs.getString(1);

        conn.close();  // Nur erreicht wenn keine Exception
        return result;
    }

    // VERWUNDBAR: Stream nicht geschlossen
    public String readFile(String path) throws IOException {
        FileInputStream fis = new FileInputStream(path);
        byte[] data = new byte[1024];
        fis.read(data);
        // VERWUNDBAR: fis wird nie geschlossen
        return new String(data);
    }

    // VERWUNDBAR: Lock wird nie freigegeben
    private final ReentrantLock lock = new ReentrantLock();

    public void processWithLock() {
        lock.lock();
        doProcessing();  // Bei Exception wird Lock nie freigegeben!
        lock.unlock();
    }

    // VERWUNDBAR: Thread-Pool wird nie heruntergefahren
    private ExecutorService executor = Executors.newFixedThreadPool(10);

    public void submitTasks(List<Runnable> tasks) {
        for (Runnable task : tasks) {
            executor.submit(task);
        }
        // VERWUNDBAR: executor.shutdown() wird nie aufgerufen
    }
}
// VERWUNDBAR: Node.js-Ressourcen-Lebenszyklus-Probleme
class VulnerableResourceManager {

    // VERWUNDBAR: Event-Listener-Leck
    setupListeners() {
        // VERWUNDBAR: Listener akkumulieren, werden nie entfernt
        this.emitter.on('data', this.handleData);
        this.emitter.on('error', this.handleError);
        // Mehrfach ohne Bereinigung aufgerufen = Speicherleck
    }

    // VERWUNDBAR: Timer-Leck
    startPolling() {
        // VERWUNDBAR: Intervall wird nie gelöscht
        setInterval(() => {
            this.poll();
        }, 1000);
    }

    // VERWUNDBAR: Stream nicht ordnungsgemäß gehandhabt
    async readFile(path) {
        const stream = fs.createReadStream(path);
        const chunks = [];

        // VERWUNDBAR: Bei Fehler wird Stream möglicherweise nicht geschlossen
        for await (const chunk of stream) {
            chunks.push(chunk);
        }

        return Buffer.concat(chunks);
    }

    // VERWUNDBAR: Datenbankverbindungs-Leck
    async query(sql) {
        const client = await pool.connect();
        // VERWUNDBAR: Wenn Abfrage Exception wirft, wird Client nie freigegeben
        const result = await client.query(sql);
        client.release();
        return result;
    }

    // VERWUNDBAR: Kind-Prozess-Leck
    spawnProcess(command) {
        const child = spawn(command);
        // VERWUNDBAR: Kind-Prozess wird beim Herunterfahren nie beendet
        return child;
    }
}

Sichere Lösung

// SICHER: Ordnungsgemäßes Ressourcen-Lebenszyklus-Management
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// SICHER: Ordnungsgemäße Bereinigung in allen Pfaden
char* read_file_safe(const char* filename) {
    FILE* file = NULL;
    char* buffer = NULL;

    file = fopen(filename, "r");
    if (!file) {
        goto cleanup;
    }

    buffer = malloc(1024);
    if (!buffer) {
        goto cleanup;
    }

    if (fread(buffer, 1, 1024, file) == 0) {
        free(buffer);
        buffer = NULL;
        goto cleanup;
    }

cleanup:
    if (file) {
        fclose(file);
    }
    return buffer;  // Aufrufer muss Nicht-NULL-Rückgabe freigeben
}

// SICHER: Pointer nach free bereinigen
void safe_free_pattern() {
    char* data = malloc(100);
    if (!data) return;

    strcpy(data, "sensitive");

    // SICHER: Bereinigen und nullen
    memset(data, 0, 100);  // Sensible Daten löschen
    free(data);
    data = NULL;  // Use-after-free verhindern

    // Sicher: NULL-Prüfung verhindert Probleme
    if (data) {
        printf("Data: %s\n", data);
    }
}

// SICHER: Zustand verfolgen um Double-free zu verhindern
typedef struct {
    char* data;
    int freed;
} ManagedBuffer;

void safe_release(ManagedBuffer* buf) {
    // SICHER: Zustand vor Freigabe prüfen
    if (buf && buf->data && !buf->freed) {
        free(buf->data);
        buf->data = NULL;
        buf->freed = 1;
    }
}

// SICHER: Gesamten Speicher initialisieren
void initialized_memory_safe() {
    char buffer[256] = {0};  // SICHER: Null-initialisiert

    // Oder memset verwenden
    char buffer2[256];
    memset(buffer2, 0, sizeof(buffer2));

    // Oder calloc für Heap
    char* heap_buf = calloc(256, 1);  // SICHER: Null-initialisiert
    if (heap_buf) {
        use_buffer(heap_buf);
        free(heap_buf);
    }
}

// SICHER: Ressourcen immer freigeben
void resource_managed_safe() {
    int* data = NULL;

    while (should_continue()) {
        data = malloc(sizeof(int) * 1000);
        if (!data) break;

        process_data(data);

        // SICHER: Nach jeder Verwendung freigeben
        free(data);
        data = NULL;
    }
}

// SICHER: Dateideskriptoren schließen
void fd_managed_safe(const char** filenames, int count) {
    for (int i = 0; i < count; i++) {
        int fd = open(filenames[i], O_RDONLY);
        if (fd < 0) continue;

        process_file(fd);

        // SICHER: Immer schließen
        close(fd);
    }
}
# SICHER: Python-Ressourcen-Lebenszyklus-Management
class SafeResourceManager:

    def __init__(self):
        self.connection = None

    def connect(self):
        # SICHER: Alte Verbindung schließen bevor neue erstellt wird
        if self.connection:
            self.connection.close()
        self.connection = create_database_connection()

    def execute(self, query):
        if not self.connection:
            raise RuntimeError("Not connected")
        return self.connection.execute(query)

    # SICHER: Context-Manager-Protokoll für automatische Bereinigung
    def __enter__(self):
        self.connect()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.connection:
            self.connection.close()
            self.connection = None
        return False

# SICHER: Context-Manager verwenden
def read_files_safe(filenames):
    results = []
    for filename in filenames:
        # SICHER: Context-Manager stellt sicher, dass Datei geschlossen wird
        with open(filename, 'r') as f:
            results.append(f.read())
    return results

# SICHER: weakref verwenden um Zyklen zu brechen
import weakref

class SafeNode:
    def __init__(self, data):
        self.data = data
        self._next = None

    @property
    def next(self):
        return self._next() if self._next else None

    @next.setter
    def next(self, node):
        # SICHER: Schwache Referenz bricht Zyklus
        self._next = weakref.ref(node) if node else None

# SICHER: Exception-sichere Dateiverarbeitung
def process_file_safe(filename):
    with open(filename, 'r') as f:
        data = f.read()
        process(data)  # Datei wird auch bei Exception geschlossen
// SICHER: Java-Ressourcen-Lebenszyklus mit try-with-resources
public class SafeResourceManagement {

    // SICHER: Try-with-resources stellt Bereinigung sicher
    public String queryDatabase(String sql) throws SQLException {
        try (Connection conn = DriverManager.getConnection(url);
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            return rs.getString(1);
            // SICHER: Alle Ressourcen werden automatisch geschlossen
        }
    }

    // SICHER: Stream wird automatisch geschlossen
    public String readFile(String path) throws IOException {
        try (FileInputStream fis = new FileInputStream(path)) {
            byte[] data = new byte[1024];
            fis.read(data);
            return new String(data);
        }
    }

    // SICHER: Lock wird in finally-Block freigegeben
    private final ReentrantLock lock = new ReentrantLock();

    public void processWithLock() {
        lock.lock();
        try {
            doProcessing();
        } finally {
            // SICHER: Wird immer freigegeben
            lock.unlock();
        }
    }

    // SICHER: Thread-Pool mit ordnungsgemäßem Herunterfahren
    private ExecutorService executor = Executors.newFixedThreadPool(10);

    public void shutdown() {
        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}
// SICHER: Node.js-Ressourcen-Lebenszyklus-Management
class SafeResourceManager {

    constructor() {
        this.listeners = [];
        this.intervals = [];
        this.children = [];
    }

    // SICHER: Listener verfolgen und bereinigen
    setupListeners() {
        const dataHandler = this.handleData.bind(this);
        const errorHandler = this.handleError.bind(this);

        this.emitter.on('data', dataHandler);
        this.emitter.on('error', errorHandler);

        // SICHER: Für Bereinigung verfolgen
        this.listeners.push(
            { event: 'data', handler: dataHandler },
            { event: 'error', handler: errorHandler }
        );
    }

    // SICHER: Timer für Bereinigung verfolgen
    startPolling() {
        const intervalId = setInterval(() => {
            this.poll();
        }, 1000);

        this.intervals.push(intervalId);
        return intervalId;
    }

    // SICHER: Ordnungsgemäße Stream-Behandlung
    async readFile(path) {
        const stream = fs.createReadStream(path);
        const chunks = [];

        try {
            for await (const chunk of stream) {
                chunks.push(chunk);
            }
            return Buffer.concat(chunks);
        } finally {
            // SICHER: Sicherstellen dass Stream zerstört wird
            stream.destroy();
        }
    }

    // SICHER: Datenbankverbindung mit ordnungsgemäßer Freigabe
    async query(sql) {
        const client = await pool.connect();
        try {
            return await client.query(sql);
        } finally {
            // SICHER: Wird immer freigegeben
            client.release();
        }
    }

    // SICHER: Alle Ressourcen bereinigen
    async cleanup() {
        // Alle Listener entfernen
        for (const { event, handler } of this.listeners) {
            this.emitter.off(event, handler);
        }
        this.listeners = [];

        // Alle Intervalle löschen
        for (const id of this.intervals) {
            clearInterval(id);
        }
        this.intervals = [];

        // Alle Kind-Prozesse beenden
        for (const child of this.children) {
            child.kill();
        }
        this.children = [];
    }
}

Ausgenutzt in der Praxis

Use-After-Free-Exploits

Speicherkorruption, die Remote-Code-Ausführung ermöglicht.

Ressourcenerschöpfung

Speicherlecks, die Denial of Service verursachen.

Informationspreisgabe

Nicht initialisierter Speicher, der sensible Daten preisgibt.


Tools zum Testen und Ausnutzen

  • Memory-Sanitizer (ASan, MSan).

  • Valgrind zur Speicherleck-Erkennung.

  • Statische Analysatoren für Ressourcen-Lebenszyklen.


CVE-Beispiele

  • CVE-2021-3156: Use-after-free in sudo.

  • CVE-2019-14287: Ressourcenmanagement-Fehler in sudo.


Referenzen

  1. MITRE. "CWE-664: Improper Control of a Resource Through its Lifetime." https://cwe.mitre.org/data/definitions/664.html

  2. RAII-Muster-Dokumentation.