Off-by-one-Fehler
Beschreibung
Off-by-one-Fehler ist ein Logikfehler, der auftritt, wenn ein Programm eine Grenze um genau eine Einheit falsch berechnet. Dies passiert typischerweise beim Iterieren durch Arrays (Verwendung von <= statt <), beim Allokieren von Puffern (Vergessen des NULL-Terminators) oder beim Berechnen von String-Längen. Off-by-one-Fehler verursachen, dass Programme ein Byte über die allokierten Puffergrenzen hinaus lesen oder schreiben. Während das Schreiben eines einzelnen Bytes über einen Puffer hinaus geringfügig erscheinen mag, kann es kritische Datenstrukturen beschädigen, Heap-Metadaten überschreiben oder durch sorgfältige Manipulation dieses einzelnen Bytes schwerwiegendere Ausnutzung ermöglichen.
Risiko
Off-by-one-Fehler sind trügerisch gefährlich. Obwohl sie nur ein einzelnes Byte betreffen, kann dieses Byte benachbarte Stack-Canaries, Heap-Chunk-Metadaten oder kritische Kontrolldaten beschädigen. Bei Heap-Allokationen kann ein einzelner Byte-Overflow das Größenfeld benachbarter Chunks beschädigen und Heap-Exploitation-Techniken ermöglichen. Auf dem Stack kann das Überschreiben eines Bytes eines gespeicherten Frame-Pointers die Ausführung umleiten. Moderne Exploitation-Techniken haben demonstriert, dass selbst einzelne Byte-Überschreibungen zu vollständiger Code-Ausführung führen können. Diese Schwachstellen sind häufig, weil sie leicht einzuführen und während der Code-Review schwer zu erkennen sind.
Lösung
Überprüfen Sie sorgfältig alle Schleifengrenzen und Puffergrößenberechnungen. Fügen Sie bei der Allokation von Strings immer 1 für den NULL-Terminator hinzu: malloc(strlen(s) + 1). Verwenden Sie < statt <= für Array-Iteration, es sei denn, Sie müssen spezifisch das letzte Element einschließen. Verwenden Sie strncpy und ähnliche Funktionen mit size - 1, um Platz für die NULL-Terminierung zu lassen. Aktivieren Sie Compiler-Warnungen und verwenden Sie statische Analyse, um Off-by-one-Bedingungen zu erkennen. Testen Sie Randfälle mit Eingaben maximaler Länge. Erwägen Sie die Verwendung von speichersicheren Sprachen, die automatisch Grenzenprüfungen durchführen.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Integrität | Bereich: Speicherbeschädigung Einzelne Byte-Überschreibung kann benachbarte Daten, Heap-Metadaten oder Stack-Canaries beschädigen. |
| Zugriffskontrolle | Bereich: Code-Ausführung Heap-Metadaten-Beschädigung oder Frame-Pointer-Überschreibung kann beliebige Code-Ausführung ermöglichen. |
| Verfügbarkeit | Bereich: Denial-of-Service Speicherbeschädigung verursacht typischerweise Abstürze und Systeminstabilität. |
Beispielcode + Lösungscode
Anfälliger Code
#include <string.h>
#include <stdlib.h>
// ANFÄLLIG: Off-by-one in Schleife (Zaunpfahl-Fehler)
void process_items(int *items, int count) {
// Sollte i < count sein, nicht i <= count
for (int i = 0; i <= count; i++) {
items[i] = 0; // Schreibt eins über das Ende wenn i == count
}
}
// ANFÄLLIG: Off-by-one bei String-Allokation
char *copy_string(const char *src) {
size_t len = strlen(src);
// Vergisst NULL-Terminator!
char *dst = malloc(len); // Sollte len + 1 sein
strcpy(dst, src); // Schreibt NULL ein Byte über Allokation hinaus
return dst;
}
// ANFÄLLIG: Off-by-one mit strncpy
void copy_filename(char *dest, const char *src, size_t dest_size) {
strncpy(dest, src, dest_size); // Möglicherweise keine NULL-Terminierung!
// Wenn src dest_size oder länger ist, ist dest nicht NULL-terminiert
// Sollte sein: strncpy(dest, src, dest_size - 1);
// dest[dest_size - 1] = '\0';
}
// ANFÄLLIG: MAX_NUM_WIDGETS Off-by-one
#define MAX_WIDGETS 10
Widget *widgets[MAX_WIDGETS];
void init_widgets(int count) {
if (count > MAX_WIDGETS) return; // Sollte >= sein
for (int i = 0; i < count; i++) {
widgets[i] = create_widget();
}
widgets[count] = NULL; // Off-by-one wenn count == MAX_WIDGETS
}
Korrigierter Code
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
// SICHER: Korrekte Schleifengrenze
void process_items_safe(int *items, size_t count) {
// Verwende < statt <=
for (size_t i = 0; i < count; i++) {
items[i] = 0;
}
}
// SICHER: Berücksichtigt NULL-Terminator
char *copy_string_safe(const char *src) {
size_t len = strlen(src);
// Addiere 1 für NULL-Terminator
char *dst = malloc(len + 1);
if (!dst) return NULL;
memcpy(dst, src, len);
dst[len] = '\0';
return dst;
}
// SICHER: Korrekte strncpy-Verwendung
void copy_filename_safe(char *dest, const char *src, size_t dest_size) {
if (dest_size == 0) return;
// Kopiere höchstens dest_size - 1 Zeichen
strncpy(dest, src, dest_size - 1);
// Immer NULL-terminieren
dest[dest_size - 1] = '\0';
}
// SICHER: Korrekte Grenzenprüfung
#define MAX_WIDGETS 10
Widget *widgets[MAX_WIDGETS + 1]; // Extra Slot für NULL
void init_widgets_safe(size_t count) {
// Korrekter Vergleich
if (count >= MAX_WIDGETS) {
count = MAX_WIDGETS;
}
for (size_t i = 0; i < count; i++) {
widgets[i] = create_widget();
}
widgets[count] = NULL; // Jetzt innerhalb der Grenzen
}
Ausgenutzt in der Praxis
Novell iManager Stack-Overflow (Novell, 2010)
CVE-2010-1929 und CVE-2010-1930 waren Off-by-one-Fehler in Novell iManager, die zu Stack-basierten Buffer-Overflows führten. Authentifizierte Benutzer könnten POST-Anfragen mit überlangen Klassennamen erstellen, was das Überschreiben von Rücksprungadressen und SEH für beliebige Code-Ausführung ermöglichte.
OpenBSD ftp (OpenBSD, 2001)
Ein berühmter Off-by-one-Fehler in OpenBSDs ftp-Client, der von bösartigen FTP-Servern ausgenutzt werden könnte und demonstrierte, dass selbst sicherheitsfokussierte Projekte diese subtilen Fehler enthalten können.
Mehrere Unternehmensprodukte (IBM, QNAP, 2025)
Aktuelle CWE-193-Schwachstellen betreffen IBM Storage Defender, IBM API Connect, QNAP QTS/QuTS hero und IBM QRadar SIEM mit öffentlich verfügbaren Exploits.
Tools zum Testen/Ausnutzen
-
AddressSanitizer — erkennt Off-by-one-Lese- und Schreiboperationen zur Laufzeit.
-
Valgrind — Speicherfehler-Detektor, der einzelne Byte-Overflows erkennt.
-
AFL++ — Fuzzer, der effektiv bei der Auslösung von Grenzbedingungen ist.
CVE-Beispiele
-
CVE-2010-1929 — Novell iManager Off-by-one ermöglicht Code-Ausführung.
-
CVE-2021-41617 — OpenSSH Privilege-Separation Off-by-one.
-
CVE-2022-37434 — zlib Heap-basierter Buffer-Over-Read durch Off-by-one.
Referenzen
-
MITRE. "CWE-193: Off-by-one Error." https://cwe.mitre.org/data/definitions/193.html
-
CERT. "STR31-C. Guarantee that storage for strings has sufficient space for character data and the null terminator." https://wiki.sei.cmu.edu/confluence/display/c/STR31-C