Bau- und Bauingenieurwesen
Wie man sicheren C-Code schreibt, um Pufferüberläufe zu verhindern
Table of Contents
Pufferüberläufe bleiben eine der hartnäckigsten und gefährlichsten Sicherheitslücken in der C-Programmierung. Obwohl sie seit Jahrzehnten gut dokumentiert sind, verursachen sie weiterhin ernste Probleme wie Datenkorruption, Systemabstürze und Remote-Codeausführung. Sicherer C-Code zu schreiben erfordert ein tiefes Verständnis davon, wie Pufferüberläufe auftreten, und einen disziplinierten Ansatz, um sie zu verhindern. Dieser Artikel bietet einen umfassenden Leitfaden zum Schreiben von robustem, überlaufresistentem C-Code, der grundlegende Konzepte, sichere Funktionen, Validierungstechniken, Compilerschutz und moderne Abwehrmaßnahmen abdeckt.
Buffer Overflows verstehen
Da sich Puffer im Stapel- oder Heap-Speicher befinden, überschreiben sie benachbarte Speicherplätze, was zu einer Veränderung des Programmzustands, zu unvorhersehbarem Verhalten führen oder von einem Angreifer ausgenutzt werden kann, um beliebigen Code einzufügen und auszuführen.
Die Konsequenzen hängen davon ab, was überschrieben wird. Das Überschreiben einer Rücksendeadresse auf dem Stack kann die Ausführung auf angreifergesteuerten Code umleiten. Das Überschreiben von Zeigern kann zu willkürlichen Speicherschreiben führen. Selbst einfache Abstürze können für Denial-of-Service-Angriffe genutzt werden. Das Verständnis der Mechanik ist der erste Schritt zur Prävention.
Stackbasierte Überläufe
Lokale Variablen, einschließlich Puffer, die innerhalb von Funktionen deklariert sind, werden auf dem Stapel gespeichert. Der Stapel enthält auch die Rückgabeadresse, gespeicherte Frame-Pointer und andere Steuerdaten. Wenn ein linearer Puffer wie überfahren wird, fließen Daten in die Rückgabeadresse und darüber hinaus. Klassische Exploits wie der Morris-Wurm (1988) verwendeten Stapelüberläufe, um unberechtigten Zugriff zu erhalten.
Heap-Based Overflows
Überläufe können Metadaten, die vom Allokator verwendet werden, verfälschen, was zu Abstürzen oder Ausnutzung durch Heap-Spraying oder nutzungsfreie Angriffe führt.
Gemeinsame anfällige Funktionen und ihre sicheren Alternativen
Die C-Standardbibliothek bietet mehrere Funktionen, die keine Bounds-Checking durchführen. Ihre Verwendung ist die häufigste Ursache für Pufferüberläufe. Ihre Ersetzung durch sicherere Pendants ist eine grundlegende Best Practice.
String Copy und Concatenation
- – Unsicher: Kopien bis zum Nullterminator, keine Längenbegrenzung.
Safe alternative: – Kopien mit höchstens n Zeichen; beachten Sie, dass es nicht null-terminiert, wenn die Quelle länger als n ist, also immer manuell null-terminieren. - Besser noch: FLT:5 - verfügbar auf BSD und vielen Linux-Systemen; immer null-terminiert und gibt die Länge des Quellzeichens für die Abrunkationserkennung zurück.
- — Unsicher: verkettet ohne Grenzen.
Sichere Alternative: — fügt höchstens n Zeichen hinzu und beendet immer null.
Formatierte Ausgabe und Input
- — Unsicher: schreibt formatierte Ausgabe in einen Puffer ohne Größenüberprüfung.
Safe alternative: — limitiert die Ausgabe auf Größen-1-Zeichen plus Nullterminator. - ] — Ähnliches Risiko; stattdessen verwenden.
- — Extrem gefährlich; entfernt vom C11-Standard.
- — Keine Grenzen-Überprüfung.
Memory Copy und Move
- — Safe only if n is confirmed to not exceed destination buffer size.
Safer alternative: (handhabt sich überlappend) und immer n ≤ dest size. - Einige Plattformen bieten aus Anhang K (optional in C11), aber die Annahme ist begrenzt.
Validierung und Größenmanagement
Selbst bei sicheren Funktionen müssen Sie Eingabelängen validieren, für angemessene Puffergrößen sorgen und mögliche Abrisse anmutig handhaben.
Prüfen Sie die Länge der Eingabe
Vor dem Kopieren oder Verarbeiten externer Eingaben (Benutzereingaben, Netzwerkdaten, Dateiinhalte) ist die maximal zulässige Länge zu bestimmen und Daten, die diese überschreiten, abzulehnen oder zu kürzen, beispielsweise:
#define MAX_INPUT 255
char buffer[MAX_INPUT + 1]; // +1 for null
if (strlen(user_input) > MAX_INPUT) {
// Handle error: reject or truncate
fputs("Input too long", stderr);
return -1;
}
strncpy(buffer, user_input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
Verwenden Sie Fixed-Size-Puffer mit bekannten Grenzen
Wenn möglich, definieren Sie Puffer mit einer konstanten Größe und erzwingen Sie sie im gesamten Code. Vermeiden Sie Arrays mit variabler Länge (VLAs), die bei großen Größen zu Stapelüberläufen führen können.
Behandeln Truncation ausdrücklich
Funktionen wie und können Daten verkürzen. Beachten Sie den Rückgabewert, um eine Verkürzung zu erkennen und zu entscheiden, ob die gekürzten Daten akzeptabel sind oder ob ein Fehler ausgelöst werden sollte.
Compiler-Sicherheitsflags und Laufzeitschutz
Moderne Compiler bieten Flags, die Pufferüberlauferkennung und -minderung ohne Codeänderungen hinzufügen. Aktivieren Sie sie in Ihrem Build-System.
- / — Setzt Stapel-Kanarien (Zufallswerte) vor den Rücksendeadressen ein. Wenn ein Pufferüberlauf den Kanarienvogel vor der Änderung der Rücksendeadresse überschreibt, bricht das Programm ab, bevor der Exploit abgeschlossen ist.
- ersetzt Aufrufe zu unsicheren Funktionen wie und durch geprüfte Versionen, die abbrechen, wenn der Zielpuffer zu klein ist. Erfordert oder höhere Optimierung.
- – Warnt vor Formatstring-Schwachstellen, die zu Pufferüberläufen oder Informationslecks führen können.
- — AddressSanitizer (ASan) instruments code to detect buffer overflows, use-after-free, and other memory errors at runtime. Verlangsamt die Ausführung, ist aber für Tests von unschätzbarem Wert.
- — Vermeidet es, Überlaufprüfungen zu optimieren (mit Vorsicht verwenden).
Schutz des Betriebssystems
Kanarienvögel sind nur eine Schicht.
- Data Execution Prevention (DEP) / NX bit - Markiert Stack und Heap als nicht ausführbar, wodurch die Ausführung von Shellcode verhindert wird.
- Address Space Layout Randomization (ASLR) – Randomisiert Speicheradressen (Stack, Heap, Shared Libraries), um die Vorhersage von Zielen zu erschweren.
- Relocation Read-Only (RELRO) - Schützt GOT (Global Offset Table) vor Überschreiben.
Durch die Aktivierung dieser Schutzmaßnahmen (normalerweise standardmäßig) wird die Messlatte für die Ausnutzung erhöht, die sichere Codierung wird jedoch nicht ersetzt.
Code Audits und Statische Analyse
Menschliche Überprüfungen in Kombination mit automatisierter statischer Analyse können Pufferüberlaufprobleme frühzeitig auffangen. Integrieren Sie diese in Ihren Entwicklungsworkflow.
- Manuelle Codeüberprüfung — Suchen Sie nach Verwendungen unsicherer Funktionen, fehlender Größenüberprüfungen und Schleifen, die über Puffergrenzen hinaus schreiben.
- Statische Analysetools — Tools wie , , und erkennen potenzielle Überläufe, die Verwendung gefährlicher Funktionen und Fehler von einem zum anderen. Sie können in CI-Pipelines ausgeführt werden.
- Fuzzing – Verwenden Sie libFuzzer, AFL oder andere Fuzzer, um die Eingabeverarbeitung automatisch mit unerwarteten Daten zu testen, die Überläufe auslösen können.
Praktische Beispiele für Secure Code
Sichere String-Kopie mit Bounds Checking
#include <stdio.h>
#include <string.h>
int safe_string_copy(char *dest, size_t dest_size, const char *src) {
if (!dest || !src || dest_size == 0) {
return -1; // Invalid parameters
}
size_t src_len = strlen(src);
if (src_len >= dest_size) {
// Source too large; truncation or error
// Option: copy what fits and null-terminate
strncpy(dest, src, dest_size - 1);
dest[dest_size - 1] = '\0';
return 1; // Truncation occurred
}
strncpy(dest, src, dest_size);
// strncpy fills remaining with null, so dest_size fits; no need to null-terminate if src shorter
return 0; // Success, no truncation
}
Sicheres Integer Handling für Puffergrößen
Buffer Overflow kann auch aus ganzzahligen Überläufen bei der Berechnung von Größen resultieren.
#include <stdlib.h>
#include <limits.h>
#include <errno.h>
void *safe_malloc_array(size_t nmemb, size_t size) {
if (nmemb == 0 || size == 0) {
return NULL; // Or handle zero-size allocation
}
if (nmemb > SIZE_MAX / size) {
// Integer overflow would occur
errno = ENOMEM;
return NULL;
}
return malloc(nmemb * size);
}
Verwenden von snprintf für formatierte Strings
char log_message[256];
int ret = snprintf(log_message, sizeof(log_message),
"User %s logged in from %s", username, ip_address);
if (ret < 0) {
// Output error
} else if ((size_t)ret >= sizeof(log_message)) {
// Truncation occurred; handle if needed
}
Zusätzliche Best Practices
- Initialisieren Puffer - Immer Null-Initialisieren Puffer zu vermeiden, uninitialisierte Speicher zu verlieren.
- Vermeiden Sie Rekursionen mit unbegrenzter Tiefe — Stapelüberläufe können aus tiefer Rekursion auftreten; verwenden Sie Iteration oder Limit-Tiefe.
- Verwenden Sie -Qualifier - Hilft dem Compiler zu optimieren und kann Aliasing-Probleme auffangen, ohne Überläufe direkt zu verhindern.
- Prefer -correctness] — Verhindert versehentliche Modifikation von Eingabezeichenfolgen und erzwingt Absicht.
- Implementieren Sie Fehlerbehandlung — Ignorieren Sie nicht die Rückgabewerte von Funktionen wie , , , etc.
Ressourcen für weiteres Lernen
- SEI CERT C Coding Standard — Umfassende Regeln für sichere C-Codierung.
- CWE-120: Buffer Copy without Checking Size of Input — MITRE’s Classification of buffer overflow weaknesses.
- OWASP Buffer Overflow — Praktische Anleitung aus dem Open Web Application Security Project.
- GNU C Library Manual: String and Array Utilities — Dokumentation für sichere String-Funktionen.
- AddressSanitizer — Ein schneller Speicherfehlerdetektor.
Schlussfolgerung
Pufferüberläufe in C zu verhindern ist nicht optional; es ist eine grundlegende Verantwortung eines jeden Entwicklers, der mit der Sprache arbeitet. Durch das Verständnis der Mechanismen von Überläufen, das Ersetzen gefährlicher Funktionen durch sicherere Alternativen, die strenge Validierung von Eingaben und Größen, die Aktivierung von Compiler-Schutz und die Verwendung statischer Analysen und Tests kann das Risiko dieser Sicherheitslücken drastisch reduziert werden. Keine einzige Technik ist ausreichend; Verteidigung in der Tiefe - die Kombination von Codierdisziplin, Compiler-Flags, Betriebssystem-Abwehr und gründlichen Tests - bietet den stärksten Schutz. Mit diesen Praktiken können Sie C-Code schreiben, der sowohl leistungsstark als auch sicher ist, in kritischen Systemen sicher laufen kann. Denken Sie daran: Sichere Codierung ist eine ständige Praxis, keine einmalige Lösung. Bleiben Sie über neue Sicherheitslücken informiert und überprüfen Sie ständig Ihren Code und verbessern Sie Ihren Code.