Off-by-one error¶
Doğru değerden tam bir fazla (veya bir eksik) hesaplanan bir sınır, tek bir elemanın amaçlanan aralığın hemen dışında okunmasına ya da yazılmasına yol açar.
Mechanism¶
Program, doğru değerden tam bir farkla sapan yanlış bir maksimum ya da minimum
kullanır. İki klasik şekil, < N yerine <= N koşan bir loop (N index'ine, son
geçerli slot'tan bir sonrasına dokunur) ve sonlandıran NUL'ü unutan, N karakterlik
boyutlandırılmış bir buffer'dır; böylece N+1. elemanın write'ı allocation'dan bir
byte sonraya düşer. Tek aralık-dışı erişim küçüktür, ama komşu byte anlamlıysa — bir
uzunluk, bir flag, allocator metadata'sı, kaydedilmiş bir register byte'ı — o tek byte
program state'ini bozmaya yeter.
Note
CWE-193. MITRE'nin tanımı: "bir ürün, doğru değerden 1 fazla ya da 1 eksik, yanlış
bir maksimum ya da minimum değer hesaplar veya kullanır." Sonuçlar arasında crash'ler
ve "loop index değişkenlerini içeren overflow'larda" yüksek bir infinite loop
olasılığı, ayrıca wrap-around bir buffer overflow'a dönüştüğünde data corruption ve
code execution bulunur. Tek-byte durumu, glibc heap'inde
off-by-null-poison-null-byte'ın (o tek byte bir '\0') ve stack/global tek-byte
overflow'ların tohumudur.
Walkthrough¶
Ders kitabındaki loop-bound hatası — < yerine <=:
#include <stdio.h>
#include <string.h>
int main(void) {
char buf[8];
/* off-by-one: i goes 0..8, writing 9 bytes into an 8-byte buffer */
for (int i = 0; i <= sizeof(buf); i++)
buf[i] = 'A';
printf("done\n");
return 0;
}
Beklenen çıktı (stack protector ile)
$ gcc -fstack-protector-all off.c -o off && ./off
*** stack smashing detected ***: terminated
Aborted (core dumped)
buf[8]'e yapılan write off-by-one'dır; birçok layout'ta kaydedilmiş canary'nin
low byte'ını ezerek protector'ı tetikler.
CWE-193'ün kendi örneği eksik-terminator allocation'ıdır:
WidgetList = malloc(numWidgets * sizeof(Widget *));
for (i = 0; i < numWidgets; i++)
WidgetList[i] = InitializeWidget();
WidgetList[numWidgets] = NULL; // writes one element past the buffer
Warning
Size argument'ı NUL'ü tutarsız biçimde dışlayan ya da içeren fonksiyonlar kalıcı
bir kaynaktır: strncat n+1 byte'a kadar yazar (n karakter artı NUL), sprintf
bir NUL ekler ve read(fd, buf, sizeof buf) ardından gelen buf[n] = '\0',
n == sizeof buf olduğunda overflow eder. Ekstra byte'ı açıkça rezerve et.
Detection¶
- ASan /
-fsanitize=address1-byte'lık out-of-bounds erişimi tam olarak işaretler. -fstack-protector-allbirçok stack off-by-one'ı canary corruption üzerinden yakalar.- Static analysis (clang-tidy, Coverity)
<=-vs-<ve NUL-terminator boyutlandırma pattern'lerini işaretler.
Mitigation¶
- Bir terminator gerektiğinde buffer'ları
len + 1olarak boyutlandır; index sınırları için<kullan. - Length-aware API'leri (
snprintf,strlcpy) tercih et ve onların NUL semantiğini iki kez kontrol et. - Testleri ASan altında build et; sınır-uzunluk input'larını fuzz et.