Out-of-bounds write¶
Amaçlanan bir buffer'ın sonundan sonrasına (ya da başından öncesine) veri yazmak, allocator ya da compiler'ın oraya yerleştirdiği komşu memory'yi bozar.
Mechanism¶
Bir out-of-bounds (OOB) write, kod yazmayı amaçladığı buffer'ın sınırları dışındaki bir memory konumuna veri sakladığında olur. Buffer'ın tanımlı bir size'ı ve başlangıç adresi vardır; write ya son geçerli index'in ötesine ilerler ya da daha az sıklıkla ilk geçerli index'ten öncesine geçer. Her iki durumda da store, başka bir şeye ait memory'ye düşer — komşu bir allocation, allocator metadata'sı, kaydedilmiş bir register ya da bir return address.
Note
Kırılan invariant basittir: her store, hedef nesnesinin
[base, base + size) aralığına düşmelidir. C ve C++ bunu runtime'da zorlamaz,
dolayısıyla invariant yalnızca programcının aritmetiğinde yaşar. Write için kullanılan
index, uzunluk ya da size bounds check olmadan (ya da yanlış bounds check ile)
hesaplandığında ihlal edilir. CWE-787, olağan kök nedenleri sıralar: yetersiz bounds
check, yanlış size hesaplamaları, loop ya da index koşullarındaki off-by-one hataları,
check edilmemiş input uzunlukları ve eksik boyutlu bir allocation üreten integer
overflow'lar. Sonuç yalnızca bir crash değildir — komşu memory genellikle anlamlı
olduğundan (function pointer'lar, list pointer'ları, size'lar, return address'ler),
kontrollü bir OOB write control-flow hijack'e doğrudan bir basamak taşıdır.
OOB write, bu bilgi tabanındaki neredeyse her memory-corruption primitive'inin ebeveyn zayıflığıdır. Bir stack buffer overflow stack frame'ine bir OOB write'tır; bir heap buffer overflow bir sonraki chunk'ın metadata'sına bir OOB write'tır; bir off-by-one tek byte'lık bir OOB write'tır. Hepsini CWE-787 örnekleri olarak ele almak, exploitation'ın yalnızca target'ın yanında ne yaşadığına göre farklılaştığını netleştirir.
Walkthrough¶
Kanonik CWE-787 örneği, sınırlarının bir eleman ötesinden index'lenen bir array'dir:
int id_sequence[3]; /* valid indices: 0, 1, 2 */
id_sequence[0] = 123;
id_sequence[1] = 234;
id_sequence[2] = 345;
id_sequence[3] = 456; /* OUT OF BOUNDS: writes 4 bytes past the array */
id_sequence[3]'e yapılan write, array'in hemen ardından gelen memory'ye saklar.
Stack'te o memory başka bir local, kaydedilmiş bir frame pointer, canary ya da
kaydedilmiş return address olabilir — bu yüzden görünüşte zararsız görünen bir off-by-one,
kaydedilmiş return address'i overwrite etme yoluyla code execution'a dönüşebilir.
Daha gerçekçi bir biçim, uzunluğu input'tan gelen check edilmemiş bir copy'dir:
void copy(char *src, size_t n) {
char dst[64];
memcpy(dst, src, n); /* if n > 64, OOB write past dst */
}
AddressSanitizer ile bir OOB write'ı tespit etmek
$ gcc -fsanitize=address -g oob.c -o oob && ./oob
=================================================================
==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd...
WRITE of size 4 at 0x7ffd... thread T0
#0 0x... in main oob.c:8
Address 0x7ffd... is located in stack of thread T0 at offset 28
in frame #0 main
This frame has 1 object(s):
[16, 28) 'id_sequence' <== Memory access at offset 28 overflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow oob.c:8 in main
Warning
Crash etmeyen bir OOB write tehlikeli durumdur. Bir heap buffer'ın ötesine birkaç
byte yazmak, hiçbir anlık fault olmadan bir sonraki chunk'ın size'ını ya da
fd/bk pointer'larını bozabilir; process çalışmaya devam eder ve corruption
yalnızca sonraki bir malloc/free'de "tahsil edilir". Bir crash'in yokluğu
güvenlik kanıtı değildir.
Detection¶
AddressSanitizer (-fsanitize=address), Valgrind/Memcheck ve guard-page
allocator'ları, write sınırı geçtiği anda işaretler. -fsanitize=bounds (UBSan),
compiler'ın aralık dışı olduğunu kanıtlayabildiği array-index OOB write'larını yakalar.
Static analyzer'lar ve fuzzer'lar (libFuzzer, AFL++) OOB path'ini sürer ve onu yeniden
üretilebilir bir crash olarak ortaya çıkarır.
Mitigation¶
Write'tan önce her index'i ve uzunluğu hedefin gerçek size'ına karşı doğrula;
bounded API'leri (snprintf, strlcpy, explicit n check'leri) ve size-aware
container'ları (std::vector::at, Rust slice'ları) tercih et. Test build'lerinde
_FORTIFY_SOURCE=2/3, stack canary'leri ve ASan ile compile et. Bunların hiçbiri
bug'ı ortadan kaldırmaz — onu sessiz corruption'dan kontrollü bir abort'a dönüştürür.