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
| Auswirkung | Details |
|---|---|
| Zugriffskontrolle | Umfang: Privilegieneskalation TOCTOU in setuid-Programmen kann Angreifern ermöglichen, auf privilegierte Dateien zuzugreifen oder Code mit erhöhten Privilegien auszuführen. |
| Integrität | Umfang: Datenmanipulation Angreifer können Dateien oder Ressourcen modifizieren, nachdem Sicherheitsprüfungen bestanden wurden. |
| Vertraulichkeit | Umfang: 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
-
CVE-2019-5736 — runc Container-Escape über TOCTOU.
-
CVE-2016-9806 — Linux-Kernel-Netlink-Race-Condition.
-
CVE-2018-6954 — systemd TOCTOU-Schwachstelle.
Referenzen
-
MITRE. "CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition." https://cwe.mitre.org/data/definitions/367.html
-
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