Signal-Handler-Verwendung einer nicht-reentrantfähigen Funktion

Beschreibung

Die Signal-Handler-Verwendung einer nicht-reentrantfähigen Funktion ist eine Schwachstelle, bei der ein Signal-Handler eine Funktion aufruft, die nicht dafür ausgelegt ist, sicher unterbrochen und erneut aufgerufen zu werden. Nicht-reentrantfähige Funktionen können nicht sicher während der Ausführung unterbrochen und erneut aufgerufen werden, bevor ihre ursprüngliche Aufrufung abgeschlossen ist. Wenn solche Funktionen aus Signal-Handlern aufgerufen werden, können sie in undefinierte Zustände eintreten und Speicherkorruption verursachen. Funktionen wie syslog(), malloc(), free() und viele Standardbibliotheksfunktionen sind nicht-reentrantfähig, weil sie auf globale Datenstrukturen oder statische Puffer angewiesen sind, die bei gleichzeitigem Zugriff korrumpiert werden.

Risiko

Signal-Handler, die nicht-reentrantfähige Funktionen aufrufen, erzeugen schwerwiegende Sicherheitslücken. Speicherkorruption kann auftreten, wenn der Signal-Handler dieselbe Funktion während der Ausführung unterbricht und globale Datenstrukturen korrumpiert. Zum Beispiel, wenn malloc() von einem Signal-Handler unterbrochen wird, der ebenfalls malloc() aufruft, werden die Heap-Metadaten korrumpiert, was potenziell Write-What-Where-Bedingungen für beliebige Codeausführung ermöglicht. Diese Race Conditions sind während des Testens schwer zu reproduzieren, können aber von Angreifern, die das Signal-Timing kontrollieren, zuverlässig ausgelöst werden. Die Schwachstelle ist besonders gefährlich, weil sie Signal-Handling-Code in ein ausnutzbares Primitiv verwandeln kann.

Lösung

Gestalten Sie Signal-Handler so, dass sie minimale Operationen ausführen, typischerweise nur das Setzen eines Flags, das das Hauptprogramm prüft. Vermeiden Sie den Aufruf jeglicher nicht-reentrantfähiger Funktionen aus Signal-Handlern. Verwenden Sie nur async-signal-sichere Funktionen wie von POSIX definiert (wie write(), _exit(), Signal-Funktionen). Wenn komplexe Operationen bei Signalempfang erforderlich sind, lassen Sie den Handler ein Flag setzen und führen Sie die eigentliche Arbeit in der Hauptprogrammschleife durch. Erwägen Sie alternative Mechanismen wie signalfd() unter Linux oder sigwait(), um Signale synchron zu behandeln. Dokumentieren Sie, welche Funktionen in Ihrer Codebasis signal-sicher sind.

Häufige Auswirkungen

AuswirkungDetails
IntegritätUmfang: Integrität, Verfügbarkeit

Speicher modifizieren - Signal-Race-Conditions verursachen häufig Datenkorruption, wenn nicht-reentrantfähige Funktionen ihren internen Zustand durch gleichzeitigen Aufruf korrumpiert haben.
CodeausführungUmfang: Vertraulichkeit, Integrität, Verfügbarkeit

Unbefugten Code ausführen - Speicherkorruption durch Heap-Metadaten-Korruption kann beliebige Codeausführung durch Write-What-Where-Bedingungen ermöglichen.

Beispielcode

Anfälliger Code

// Anfällig: Signal-Handler ruft nicht-reentrantfähige Funktionen auf
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>
#include <string.h>

char *global_buffer = NULL;

// Anfällig: Handler ruft malloc, syslog auf - beide nicht reentrantfähig
void vulnerable_signal_handler(int sig) {
    // Anfällig: syslog() allokiert intern Scratch-Speicher
    // Wenn Signal während syslog()-Ausführung eintrifft, tritt Korruption auf
    syslog(LOG_ERR, "Signal %d empfangen", sig);

    // Anfällig: malloc() verwendet globale Heap-Metadaten
    // malloc() unterbrechen und erneut aufrufen korrumpiert den Heap
    char *temp = malloc(100);
    if (temp) {
        sprintf(temp, "Signal %d empfangen", sig);
        // Anfällig: free() ist ebenfalls nicht reentrantfähig
        free(global_buffer);
        global_buffer = temp;
    }

    // Anfällig: printf() ist nicht async-signal-sicher
    printf("Signal behandelt\n");
}

void setup_vulnerable_handler() {
    signal(SIGINT, vulnerable_signal_handler);
    signal(SIGTERM, vulnerable_signal_handler);
}

// Anfällig: Komplexer Signal-Handler mit Dateioperationen
void vulnerable_logging_handler(int sig) {
    FILE *log = fopen("/var/log/app.log", "a");
    if (log) {
        // Anfällig: fprintf ist nicht async-signal-sicher
        fprintf(log, "Signal %d um Zeit %ld\n", sig, time(NULL));
        fclose(log);
    }

    // Anfällig: Aufruf von Bereinigungsfunktionen die malloc/free verwenden
    cleanup_resources();
}

// Anfällig: Handler der gemeinsamen Zustand unsicher modifiziert
static int request_count = 0;
static char *last_request = NULL;

void vulnerable_stat_handler(int sig) {
    // Anfällig: Nicht-atomare Operationen auf gemeinsamem Zustand
    // Kann Daten korrumpieren wenn Signal während Aktualisierung eintrifft
    char buffer[256];
    sprintf(buffer, "Anfragen: %d, Letzte: %s", request_count, last_request);

    // Anfällig: strlen, memcpy nicht garantiert signal-sicher
    size_t len = strlen(buffer);
    write(STDOUT_FILENO, buffer, len);  // write IST signal-sicher, aber Setup nicht
}
# Anfällig: Python Signal-Handler mit nicht-reentrantfähigen Operationen
import signal
import logging
import sys

# Anfällig: Globale Logger-Konfiguration
logger = logging.getLogger(__name__)

def vulnerable_signal_handler(signum, frame):
    # Anfällig: Logging ist in Python nicht signal-sicher
    # Kann Deadlocks verursachen wenn Signal während Logging eintrifft
    logger.error(f"Signal {signum} empfangen")

    # Anfällig: print() ist nicht reentrantfähig
    print(f"Signal {signum} gefangen, räume auf...")

    # Anfällig: Komplexe Bereinigungsoperationen
    cleanup_connections()
    flush_caches()

    sys.exit(1)

def setup_handlers():
    signal.signal(signal.SIGINT, vulnerable_signal_handler)
    signal.signal(signal.SIGTERM, vulnerable_signal_handler)

Korrigierter Code

// Korrigiert: Signal-Handler setzt nur Flags, Arbeit wird anderswo erledigt
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>
#include <string.h>
#include <unistd.h>

// Korrigiert: volatile sig_atomic_t für Signal-Flags verwenden
static volatile sig_atomic_t got_sigint = 0;
static volatile sig_atomic_t got_sigterm = 0;

// Korrigiert: Minimaler Signal-Handler der nur ein Flag setzt
void safe_signal_handler(int sig) {
    // Korrigiert: Nur Flags setzen - das ist async-signal-sicher
    if (sig == SIGINT) {
        got_sigint = 1;
    } else if (sig == SIGTERM) {
        got_sigterm = 1;
    }
    // Korrigiert: write() ist async-signal-sicher wenn wir etwas ausgeben müssen
    const char msg[] = "Signal empfangen\n";
    write(STDERR_FILENO, msg, sizeof(msg) - 1);
}

void setup_safe_handler() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = safe_signal_handler;
    sa.sa_flags = SA_RESTART;  // Unterbrochene Syscalls neu starten
    sigemptyset(&sa.sa_mask);

    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
}

// Korrigiert: Hauptschleife prüft Flags und erledigt eigentliche Arbeit
void main_loop() {
    while (1) {
        // Korrigiert: Signal-Flags im Hauptkontext prüfen
        if (got_sigint) {
            got_sigint = 0;
            // Sicher nicht-reentrantfähige Funktionen hier aufzurufen
            syslog(LOG_INFO, "SIGINT behandeln");
            handle_interrupt();
        }

        if (got_sigterm) {
            syslog(LOG_INFO, "SIGTERM behandeln - fahre herunter");
            cleanup_and_exit();
            break;
        }

        // Normale Arbeit erledigen
        process_requests();
    }
}

// Korrigiert: Self-Pipe-Trick für Signal-Behandlung verwenden
static int signal_pipe[2];

void safe_handler_with_pipe(int sig) {
    // Korrigiert: write() in Pipe ist async-signal-sicher
    int saved_errno = errno;
    char c = (char)sig;
    write(signal_pipe[1], &c, 1);
    errno = saved_errno;
}

void setup_signal_pipe() {
    pipe(signal_pipe);

    // Schreibende nicht-blockierend machen
    int flags = fcntl(signal_pipe[1], F_GETFL);
    fcntl(signal_pipe[1], F_SETFL, flags | O_NONBLOCK);

    struct sigaction sa;
    sa.sa_handler = safe_handler_with_pipe;
    sa.sa_flags = SA_RESTART;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);
}

void event_loop_with_pipe() {
    fd_set readfds;

    while (1) {
        FD_ZERO(&readfds);
        FD_SET(signal_pipe[0], &readfds);
        // Andere File-Deskriptoren hinzufügen...

        if (select(signal_pipe[0] + 1, &readfds, NULL, NULL, NULL) > 0) {
            if (FD_ISSET(signal_pipe[0], &readfds)) {
                char sig;
                read(signal_pipe[0], &sig, 1);
                // Korrigiert: Sicher jede Funktion hier aufzurufen
                syslog(LOG_INFO, "Signal %d empfangen", sig);
                handle_signal((int)sig);
            }
        }
    }
}
# Korrigiert: Sichere Signal-Behandlung in Python
import signal
import sys
import logging
import threading

logger = logging.getLogger(__name__)

# Korrigiert: Flag-basierten Ansatz verwenden
shutdown_requested = threading.Event()

def safe_signal_handler(signum, frame):
    # Korrigiert: Nur Flag setzen, keine komplexen Operationen
    shutdown_requested.set()

def setup_handlers():
    signal.signal(signal.SIGINT, safe_signal_handler)
    signal.signal(signal.SIGTERM, safe_signal_handler)

def main_loop():
    while not shutdown_requested.is_set():
        # Normale Arbeit erledigen
        process_request()

        # Periodisch auf Shutdown prüfen
        if shutdown_requested.wait(timeout=0.1):
            break

    # Korrigiert: Sicher Bereinigung im Hauptkontext durchzuführen
    logger.info("Shutdown angefordert, räume auf...")
    cleanup_connections()
    flush_caches()
    sys.exit(0)

# Korrigiert: Alternative mit signal.set_wakeup_fd (Python 3.5+)
import os
import selectors

def setup_with_wakeup_fd():
    read_fd, write_fd = os.pipe()
    os.set_blocking(write_fd, False)

    # Signal schreibt in diesen fd
    signal.set_wakeup_fd(write_fd)
    signal.signal(signal.SIGINT, lambda s, f: None)

    return read_fd

def event_loop_with_wakeup(signal_fd):
    sel = selectors.DefaultSelector()
    sel.register(signal_fd, selectors.EVENT_READ)

    while True:
        events = sel.select(timeout=1.0)
        for key, mask in events:
            if key.fd == signal_fd:
                # Signal-Bytes leeren
                os.read(signal_fd, 1024)
                # Korrigiert: Sicher komplexe Arbeit hier zu erledigen
                logger.info("Signal empfangen, behandle...")
                handle_shutdown()
                return

CVE-Beispiele

  • CVE-2005-0893: Signal-Handler in wu-ftpd ruft Funktion auf die malloc() verwendet, ermöglicht Remote-Angreifern Denial of Service oder Codeausführung über Signale die während Speicherallokation gesendet werden.
  • CVE-2004-2259: SIGCHLD-Signal-Handler in vsftpd FTP-Server kann ausgelöst werden um Absturz zu verursachen wenn malloc() oder free() ausgeführt wird, führt zu Denial of Service unter hoher Last.

Referenzen

  1. MITRE Corporation. "CWE-479: Signal Handler Use of a Non-reentrant Function." https://cwe.mitre.org/data/definitions/479.html
  2. CERT C Secure Coding Standard. "SIG30-C. Call only asynchronous-safe functions within signal handlers."
  3. POSIX.1-2017. "Signal Concepts - Async-Signal-Safe Functions."