Security
Fuzzing Serüveni 101: "Rastgelelik" Nasıl Silaha Dönüşür?
December 19, 2025
Merhaba! Dürüst olayım, yıllar önce güvenliğe ilk merak saldığımda Fuzzing’i “bir programa rastgele çöp veri fırlatıp patlamasını beklemek” sanıyordum. Hatta yazdığım ilk basit python script’i ile saatlerce ekran başında bekleyip, “Neden bu aptal program çökmiyor?” diye kendi kendime söylendiğimi hatırlıyorum.
Meğer o zamanlar buzdağının sadece görünen kısmıyla oynuyormuşum. Bugün geldiğimiz noktada, eğer modern bir hedefi (örneğin Adobe Reader gibi devasa bir PDF okuyucuyu veya karmaşık bir oyun emülatörünü) sadece rastgele veriyle hacklemeye çalışırsanız, muhtemelen güneş sönene kadar beklersiniz. İşin rengi, Coverage-Guided (Kapsam Odaklı) kavramını anladığımda değişti.
Bu serinin ilk bölümünde, hem teknik detaylara gireceğiz hem de bu süreçte yaşadığım “aydınlanma” anlarından bahsedeceğim. Masamızda endüstri standardı AFL++, arkasında yatan matematik ve en önemlisi hedefi hazırlama (Harnessing) sanatı var.
1. Kör Talih Değil, Evrimsel Strateji
Fuzzing aslında benim gözümde bir optimizasyon problemi. Amacımız, o devasa samanlıkta iğneyi aramak değil; mıknatısa dönüşmek. Hedef, main() fonksiyonundan girip, o derinlerde saklanan, geliştiricinin bile unuttuğu if (ozel_durum) bloğuna ulaşmak.
İlk defa AFL’nin arayüzünü açıp “paths found” (bulunan yollar) sayısının arttığını gördüğümde hissettiğim tatmin duygusunu tarif edemem. Bu araçlar Genetik Algoritmalar kullanıyor ve süreç biyolojideki evrime çok benziyor:
Şekil 1: Modern bir fuzzer’ın (örneğin AFL++) çalışma döngüsü: Döngü, bir başlangıç girdisiyle (Corpus Input) başlar. Mutasyon motoru (Mutation Engine) bu girdiyi bit çevirme veya ekleme gibi yöntemlerle değiştirir. Değiştirilen girdi hedef programa gönderilir. Program çalışırken elde edilen “Coverage” (Kapsam) bilgisi, fuzzer’a hangi girdilerin yeni kod yollarını keşfettiğini söyler. Bu geri bildirim döngüsü ile fuzzer kendini eğitir ve en sonunda programı çökerten (Crash) girdiyi bulmaya çalışır.
- Corpus (Girdi Havuzu): Elimizde geçerli dosyalar vardır (örneğin bir PDF okuyucu için “hello.pdf”).
- Mutation (Mutasyon): Fuzzer bu dosyayı alır ve değiştirir. Bitleri çevirir (bitflip), baytları siler, aritmetik işlemler yapar (örneğin dosyadaki
0x10değerini0x11yapar) veya iki dosyayı birbirine ekler (splicing). - Execution & Feedback: Program çalıştırılır. Eğer yeni üretilen girdi, programın daha önce hiç çalışmamış bir kısmını çalıştırırsa, bu girdi “ilginç” (interesting) olarak işaretlenir ve havuza eklenir.
2. AFL++ Kaputun Altında Nasıl Çalışır?
AFL++’ın sihirli tarafı, programın “neresinin çalıştığını” bilmesidir. Buna Instrumentation (Enstrümantasyon) denir.
Coverage Map (Kapsam Haritası) ve Paylaşılan Bellek
Derleme sırasında (örneğin afl-clang-fast kullanırken), AFL her temel bloğun (basic block) veya dallanmanın (branch) içine küçük bir kod parçası enjekte eder. Program çalıştığında, bu kodlar Shared Memory (Paylaşılan Bellek - shm) adı verilen 64KB’lık bir haritayı günceller.
Şekil 2: AFL’nin çalışma mantığı. Sol taraftaki akış diyagramında (CFG) gerçekleşen her geçiş (örneğin A’dan B’ye), sağ taraftaki Paylaşılan Bellek haritasında belirli bir hücreye karşılık gelir. cur_loc ^ prev_loc formülü sayesinde her geçişin kimliği benzersiz (veya ona yakın) olur. Böylece fuzzer, hangi kod yollarından geçildiğini harita üzerindeki “işaretlenmiş” (hit) kutucuklardan anlar.
AFL’nin hangi yoldan gidildiğini anlama formülü şudur:
shared_mem[cur_location ^ prev_location]++;
Burada cur_location şu anki bloğun ID’si, prev_location ise bir önceki bloğun ID’isidir. XOR (^) işlemi sayesinde AFL sadece “A bloğuna gidildi” bilgisini değil, “A bloğundan B bloğuna geçildi” (Edge Coverage) bilgisini tutar. Bu sayede programın akış haritasını çıkarır.
Forkserver: Hız Canavarı
Her test girdisi için programı baştan başlatmak (execve sistem çağrısı) çok yavaştır. AFL, bunun yerine Forkserver mekanizmasını kullanır.
- Hedef program bir kez başlatılır ve
mainfonksiyonuna girmeden hemen önce durdurulur. - Fuzzer her yeni test için bu durdurulmuş süreçten bir kopya (fork) oluşturur.
fork()işlemi çok hızlıdır, bu sayede saniyede binlerce test yapılabilir.
3. Hedefi Hazırlamak: “Harnessing” Sanatı
Bir emülatörü veya sunucuyu olduğu gibi fuzzer’ın kucağına atamazsınız. Bunu acı yoldan öğrendim. Zamanında basit bir FTP sunucusunu fuzz’lamaya çalışırken, fuzzer’ın sürekli network timeout (zaman aşımı) beklediği için saniyede sadece 5 test yapabildiğini görmüştüm. O hızla bir bug bulmam aylar sürerdi.
İşte burada Harnessing devreye giriyor. Programı cerrah titizliğiyle modifiye etmemiz gerekiyor:
- Desocketing (Soketleri Söküp Atmak): O FTP sunucusu örneğine dönersek; ağ dinleyen
recv()fonksiyonunu iptal edip, veriyi direkt dosya sisteminden okuyacak şekilde kodu yamaladım (patchledim). Sonuç? Hız saniyede 5’ten 2000’e fırladı. O an “Harnessing her şeydir” sözünü dövme yaptırmayı bile düşündüm :) - GUI’yi Kapatmak: Bir keresinde görsel arayüzü olan bir uygulamayı fuzz’larken, programın her seferinde pencere açıp kapatmaya çalıştığını fark ettim. Grafik işlemleri CPU’yu sömürüyordu. Görüntü kodlarını
/dev/null‘a yönlendirip sadece işlemci mantığına (CPU logic) odaklandığımda, performans 100 kat arttı. - Persistent Mode (Kalıcı Mod): Programı her test için
forketmek (yeniden başlatmak) bile bir süre sonra yavaş kalıyor. Persistent Mode ile programı bir kez başlatıp, hedef fonksiyonu bir döngüye (while(__AFL_LOOP(10000))) aldığınızda, makine adeta makineli tüfeğe dönüşüyor.
4. Kaynak Kod Yoksa? (Binary-Only Fuzzing)
CTF yarışmalarında veya kapalı kaynak yazılımlarda kaynak kodunuz olmayabilir. Yine de çaresiz değiliz.
- QEMU Modu (
-Q): AFL++, programı QEMU emülatörü içinde çalıştırır. QEMU, programın her bir komutunu işlerken araya girer ve AFL’nin ihtiyaç duyduğu “Coverage” bilgisini toplar. Kaynak kodlu fuzzing’e göre daha yavaştır ama her binary üzerinde çalışır. - Static Rewriting:
E9AFLgibi araçlar, derlenmiş binary dosyasını alır ve içine AFL’nin takip kodlarını (trampolines) sonradan enjekte eder. Bu, QEMU modundan çok daha hızlıdır.
5. Görünmez Hataları Yakalamak: Sanitizer’lar
Programın “Segmentation Fault” verip çökmemesi, güvenli olduğu anlamına gelmez. Bu dersi, görünürde taş gibi çalışan ama arka planda sessizce bellek sızdıran bir C kütüphanesini test ederken almıştım. Program çökmüyordu ama aslında delik deşikti.
Klasik hatalar (örneğin bellek sızıntıları veya “off-by-one” taşmaları) hemen çökme yaratmaz ama sinsice orada bekler. Bu hayalet hataları yakalamak için derleme sırasında ASAN (Address Sanitizer) kullanıyoruz.
Şekil 3: Basit bir C programında klasik bir bellek taşması (Buffer Overflow) senaryosu. strcpy fonksiyonu, kopyaladığı verinin boyutunu kontrol etmez (Unsafe Function). Eğer input (kaynak) boyutu, buffer (hedef) boyutundan büyükse (örneğin 64 baytlık bir alana 100 bayt yazmaya çalışırsak), taşan veri bellekteki kritik bölgelerin (Stack) üzerine yazılır. Bu durum, programın çökmesine veya saldırganın kod çalıştırmasına (Exploit) yol açabilir.
ASAN, bellek bloklarının etrafına “zehirli bölgeler” (redzones) yerleştirir. Program çalışırken eğer bu yasaklı bölgeye tek bir byte bile dokunursa, ASAN anında “Hop dedik!” deyip programı durdurur ve size hatanın tam satır numarasını verir. İlk kullandığımda bana sihir gibi gelmişti.
Özetle: Fuzzing, sadece rastgele tuşlara basmak değil; programın iç yapısını haritalayan, genetik algoritmalarla en uygun girdiyi evrimleştiren ve işletim sistemi seviyesinde optimizasyonlar (forkserver, shared memory) gerektiren sofistike bir süreçtir.
Serinin 2. Bölümünde: Teori bitti. Bir sonraki yazıda basit bir C programını (veya pdfinfo gibi gerçek bir hedefi) alacağız, afl-clang-fast ile derleyip, ilk Harness‘ımızı yazacağız ve AFL++ arayüzündeki o hipnotize edici istatistik ekranını yorumlayacağız.
Hazır olun, işlemcileri ısıtacağız!
Yorumlar