Time-of-check Time-of-use (TOCTOU) Race Condition

Beschreibung

Time-of-check Time-of-use (TOCTOU) Race Condition tritt auf, wenn ein Programm den Zustand einer Ressource prüft und diese Ressource dann basierend auf der Prüfung verwendet, aber der Zustand der Ressource sich zwischen der Prüfung und der Verwendung ändern kann, was die Ergebnisse der Prüfung ungültig macht. Dies ist eine Race Condition, die Angreifer ausnutzen können, indem sie die Ressource zwischen diesen beiden Operationen manipulieren. Häufige Beispiele umfassen das Prüfen von Dateiberechtigungen vor dem Zugriff auf eine Datei, das Verifizieren von Benutzeranmeldedaten vor der Zugriffsgewährung oder das Prüfen der Ressourcenverfügbarkeit vor der Allokation.

Risiko

TOCTOU-Schwachstellen sind besonders gefährlich in setuid-Programmen und privilegierten Diensten. Ein Angreifer kann symbolische Links erstellen, Dateien ersetzen oder Ressourcen während des Race-Fensters modifizieren, um unautorisierten Zugang zu erlangen, Privilegien zu eskalieren oder Daten zu manipulieren. Dateisystem-TOCTOU-Angriffe sind gut dokumentiert und können Sicherheitsprüfungen umgehen. Das Schwachstellenfenster mag klein sein, ist aber oft ausnutzbar, besonders auf Systemen unter Last oder mit angreiferkontrolliertem Timing.

Lösung

Verwenden Sie atomare Operationen, die Prüfung und Verwendung in einem einzigen Schritt kombinieren. Verwenden Sie Dateideskriptoren statt Dateinamen nach dem Öffnen. Implementieren Sie ordnungsgemäße Sperrmechanismen. Verwenden Sie fstat() auf einem offenen Dateideskriptor statt stat() auf einem Dateinamen. Wenden Sie das Prinzip der minimalen Privilegien an. Auf Unix-Systemen verwenden Sie das O_NOFOLLOW-Flag, um Symlink-Angriffe zu verhindern. Verwenden Sie openat() und verwandte Funktionen für sichere Dateioperationen. Erwägen Sie die Verwendung von Mandatory Access Controls (MAC), um die Auswirkungen des Race-Fensters zu begrenzen.

Häufige Auswirkungen

AuswirkungDetails
ZugriffskontrolleUmfang: Privilegieneskalation

TOCTOU in setuid-Programmen kann Angreifern ermöglichen, auf privilegierte Dateien zuzugreifen oder Code mit erhöhten Privilegien auszuführen.
IntegritätUmfang: Datenmanipulation

Angreifer können Dateien oder Ressourcen modifizieren, nachdem Sicherheitsprüfungen bestanden wurden.
VertraulichkeitUmfang: Informationsoffenlegung

TOCTOU kann das Lesen von Dateien ermöglichen, die unzugänglich sein sollten.

Beispielcode + Korrigierter Code

Anfälliger Code

// ANFÄLLIG: Klassisches TOCTOU mit access() dann open()
void read_file(const char *filename) {
    // Prüfen ob Benutzer Zugriff hat
    if (access(filename, R_OK) == 0) {
        // RACE-FENSTER: Angreifer ersetzt Datei durch Symlink
        FILE *fp = fopen(filename, "r");
        // Liest jetzt andere Datei (z.B. /etc/shadow)!
        char buffer[1024];
        fread(buffer, 1, sizeof(buffer), fp);
        fclose(fp);
    }
}

// ANFÄLLIG: stat() dann open()
void process_file(const char *filename) {
    struct stat st;
    if (stat(filename, &st) == 0) {
        // Prüfen ob es eine reguläre Datei ist
        if (S_ISREG(st.st_mode)) {
            // RACE-FENSTER: Datei durch Symlink oder Verzeichnis ersetzt
            int fd = open(filename, O_RDONLY);
            // Operiert auf anderer Datei!
        }
    }
}

// ANFÄLLIG: Existenz prüfen dann erstellen
void safe_create_file(const char *filename) {
    struct stat st;
    if (stat(filename, &st) != 0) {
        // Datei existiert nicht
        // RACE-FENSTER: Angreifer erstellt Datei/Symlink
        FILE *fp = fopen(filename, "w");
        // Schreibt an angreiferkontrollierten Ort!
        fprintf(fp, "sensible daten");
        fclose(fp);
    }
}
// ANFÄLLIG: TOCTOU in setuid-Programm
void setuid_write(const char *filename, const char *data) {
    // Läuft als Root, prüft ob echter Benutzer Zugriff hat
    if (access(filename, W_OK) == 0) {
        // RACE-FENSTER: Angreifer ersetzt durch Symlink zu /etc/passwd
        FILE *fp = fopen(filename, "w");
        // Schreibt als Root an Ziel des Angreifers!
        fputs(data, fp);
        fclose(fp);
    }
}

// ANFÄLLIG: Verzeichniserstellungs-Race
void create_directory(const char *dir) {
    struct stat st;
    if (stat(dir, &st) != 0) {
        // Verzeichnis existiert nicht
        // RACE-FENSTER: Angreifer erstellt Symlink
        mkdir(dir, 0755);
        // Verzeichnis am Symlink-Ziel erstellt!
    }
}

// ANFÄLLIG: Temp-Datei-Erstellung
void write_temp_file(const char *data) {
    char template[] = "/tmp/myapp.XXXXXX";
    // Eindeutigen Dateinamen holen
    char *filename = mktemp(template);  // Veraltet!

    // RACE-FENSTER: Angreifer erstellt Datei mit gleichem Namen
    FILE *fp = fopen(filename, "w");
    fputs(data, fp);
    fclose(fp);
}
# ANFÄLLIG: Check-Then-Access-Muster
import os

def read_user_file(filename):
    # Prüfen ob Datei existiert und zugänglich ist
    if os.path.exists(filename):
        if os.access(filename, os.R_OK):
            # RACE-FENSTER
            with open(filename, 'r') as f:
                return f.read()
    return None

# ANFÄLLIG: Typ prüfen dann verwenden
def process_path(path):
    if os.path.isfile(path):
        # RACE-FENSTER: Pfad zu Symlink geändert
        with open(path, 'r') as f:
            return f.read()
    elif os.path.isdir(path):
        # RACE-FENSTER: Pfad geändert
        return os.listdir(path)

Korrigierter Code

// SICHER: Öffnen dann prüfen (mit Dateideskriptor)
void read_file_safe(const char *filename) {
    // Zuerst öffnen - dies ist atomar
    int fd = open(filename, O_RDONLY | O_NOFOLLOW);
    if (fd < 0) {
        perror("Kann Datei nicht öffnen");
        return;
    }

    // Jetzt Eigenschaften mit Dateideskriptor prüfen
    struct stat st;
    if (fstat(fd, &st) == 0) {
        if (S_ISREG(st.st_mode)) {
            // Sicher - wir operieren auf der geöffneten Datei
            char buffer[1024];
            read(fd, buffer, sizeof(buffer));
        }
    }
    close(fd);
}

// SICHER: O_CREAT | O_EXCL für atomare Erstellung verwenden
void safe_create_file_fixed(const char *filename) {
    // O_EXCL scheitert wenn Datei existiert - atomares Prüfen+Erstellen
    int fd = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0600);
    if (fd < 0) {
        if (errno == EEXIST) {
            // Datei existiert - entsprechend behandeln
            return;
        }
        perror("Kann Datei nicht erstellen");
        return;
    }

    // Datei sicher erstellt
    write(fd, "sensible daten", 14);
    close(fd);
}

// SICHER: mkstemp für Temp-Dateien verwenden
void write_temp_file_safe(const char *data) {
    char template[] = "/tmp/myapp.XXXXXX";
    // mkstemp erstellt und öffnet die Datei atomar
    int fd = mkstemp(template);
    if (fd < 0) {
        perror("Kann Temp-Datei nicht erstellen");
        return;
    }

    // Datei ist bereits offen - kein Race
    write(fd, data, strlen(data));
    close(fd);

    // Optional Temp-Datei löschen wenn fertig
    unlink(template);
}
// SICHER: Privilegien vor Dateizugriff ablegen
void setuid_write_safe(const char *filename, const char *data) {
    uid_t real_uid = getuid();

    // Vorübergehend Privilegien ablegen
    if (seteuid(real_uid) != 0) {
        perror("Kann Privilegien nicht ablegen");
        return;
    }

    // Jetzt als echter Benutzer öffnen - keine Privilegieneskalation möglich
    int fd = open(filename, O_WRONLY | O_NOFOLLOW);
    if (fd < 0) {
        perror("Kann Datei nicht öffnen");
        seteuid(0);  // Wiederherstellen wenn nötig
        return;
    }

    // Als echter Benutzer schreiben
    write(fd, data, strlen(data));
    close(fd);

    // Privilegien wiederherstellen wenn für andere Operationen nötig
    seteuid(0);
}

// SICHER: openat() für verzeichnisrelative Operationen verwenden
void process_in_directory_safe(int dirfd, const char *filename) {
    // Relativ zu Verzeichnis-Dateideskriptor öffnen
    int fd = openat(dirfd, filename, O_RDONLY | O_NOFOLLOW);
    if (fd < 0) {
        return;
    }

    // Sichere Operationen auf geöffneter Datei
    struct stat st;
    fstat(fd, &st);
    // ...
    close(fd);
}

// SICHER: Atomare Verzeichniserstellung
void create_directory_safe(const char *dir) {
    // mkdir ist atomar - erstellt entweder oder scheitert
    if (mkdir(dir, 0755) != 0) {
        if (errno == EEXIST) {
            // Prüfen ob es tatsächlich ein Verzeichnis ist
            struct stat st;
            if (stat(dir, &st) == 0 && S_ISDIR(st.st_mode)) {
                // Verzeichnis existiert - OK
                return;
            }
            // Etwas anderes existiert mit diesem Namen
            fprintf(stderr, "Pfad existiert, ist aber kein Verzeichnis\n");
        }
        perror("Kann Verzeichnis nicht erstellen");
    }
}
# SICHER: Zuerst öffnen, dann prüfen
import os
import stat

def read_user_file_safe(filename):
    try:
        # Zuerst öffnen - atomar
        fd = os.open(filename, os.O_RDONLY | os.O_NOFOLLOW)
        try:
            # Mit Dateideskriptor prüfen
            st = os.fstat(fd)
            if stat.S_ISREG(st.st_mode):
                # Sicher zu lesen
                with os.fdopen(fd, 'r') as f:
                    return f.read()
        except:
            os.close(fd)
            raise
    except OSError as e:
        return None

# SICHER: Atomare Temp-Datei-Erstellung
import tempfile

def write_temp_safe(data):
    # mkstemp-Äquivalent - atomare Erstellung
    with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
        f.write(data)
        return f.name

# SICHER: Exklusive Erstellung verwenden
def create_file_safe(filename):
    try:
        # O_EXCL-Äquivalent - atomar
        fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
        with os.fdopen(fd, 'w') as f:
            f.write("sensible daten")
        return True
    except FileExistsError:
        return False

Ausgenutzt in der Praxis

Setuid /tmp Race Conditions (Historisch)

Zahlreiche Unix-setuid-Programme wurden über TOCTOU-Angriffe mit /tmp-Dateien ausgenutzt. Angreifer erstellen Symlinks während des Race-Fensters, um privilegierte Dateien zu überschreiben oder zu lesen.

Linux-Kernel-TOCTOU-Schwachstellen

Mehrere Linux-Kernel-Schwachstellen resultierten aus TOCTOU-Bedingungen in Systemaufrufen, einschließlich CVE-2016-9806, das Privilegieneskalation über Race Conditions in der Netlink-Behandlung ermöglichte.

Docker Container Escape (2019)

CVE-2019-5736 ermöglichte Container-Escape durch eine TOCTOU-Race-Condition in runc, was das Überschreiben der Host-runc-Binary ermöglichte.


Tools zum Testen/Ausnutzen

  • syzkaller — Kernel-Fuzzer, der Race Conditions finden kann.

  • RaceFuzzer — Forschungstool zur Erkennung von Races.

  • KLEE — Symbolische Ausführung, die TOCTOU erkennen kann.

  • ThreadSanitizer — Laufzeit-Race-Detektor.


CVE-Beispiele


Referenzen

  1. MITRE. "CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition." https://cwe.mitre.org/data/definitions/367.html

  2. CERT. "FIO01-C: Be careful using functions that use file names for identification." https://wiki.sei.cmu.edu/confluence/display/c/FIO01-C.+Be+careful+using+functions+that+use+file+names+for+identification