Bir Fuzzing Macerası: Doyensec'in ksmbd Araştırmasından Notlar
Geçenlerde Doyensec’teki arkadaşların Linux kernel’daki SMB sunucusu (ksmbd) üzerine yaptıkları araştırmanın ikinci kısmına denk geldim ve gerçekten çok ilginç detaylar yakaladım.
Özellikle fuzzing konusuna yaklaşımları, standart “tara ve geç” mantığından çok öte. Adamlar oturup sistemi bir mühendis gibi analiz etmiş ve araçlarını ona göre modifiye etmişler. Okurken “bak bu çok mantıklıymış” dediğim noktaları, özellikle de kullandıkları o zekice kod yapılarını ve diyagramları, kendi notlarımla beraber sizinle de paylaşmak istedim.
Kernel güvenliğiyle uğraşanlar bilir; bazen en iyi araç bile, doğru strateji olmadan kör bir bıçaktan farksızdır. Bu araştırmada ekip, Syzkaller fuzzer’ını standart haliyle kullanmak yerine, onu hedef sisteme özel bir silaha dönüştürmüş. Ve sonuç: Tam 23 tane güvenlik açığı (CVE).
Peki bunu nasıl başardılar? İşte araştırmadan derlediğim teknik detaylar.
1. Kapalı Kapıları Açmak
İlk fark ettikleri şey şu olmuş: Varsayılan (default) konfigürasyonlar genelde en güvenli, ama aynı zamanda en “kısır” olanlardır. Çünkü çoğu özellik kapalı gelir. Kod çalışmazsa, bug da oluşmaz.
Bu yüzden test ortamında normalde kapalı olan oplocks, server multi channel ve özellikle durable handles gibi özellikleri elle açmışlar. Yani saldırı yüzeyini bile isteye genişletmişler. “Kodun çalışmadığı yerde bug bulamazsın” mantığı, burada devreye giriyor.
2. State Yönetimi: Fuzzer’a Dil Öğretmek
SMB protokolü, HTTP gibi “at ve unut” tarzı değil. Bir senaryo var, bir sıra var. Dosya üzerinde işlem yapacaksan önce Oturum ID’si alacaksın, sonra Ağaç (Tree) ID’si, en son da Dosya ID’si…
Standart bir fuzzer rastgele veri basar, sırayı bilmez. Ekip burada Syzkaller’a küçük bir modifikasyon yapmış: Gönderdikleri pakete sunucudan gelen cevabı okuyan ve dönen ID’leri bir sonraki paketin içine yerleştiren bir “pseudo-syscall” (sahte sistem çağrısı) mekanizması kurmuşlar.
İşte o kritik ayrıştırma (parsing) kodunu şöyle implemente etmişler:
// Doyensec: Yanıt ayrıştırma mantığı (Pseudo-syscall harness)
void process_buffer(int msg_no, const char *buffer, size_t received) {
// ... snip ...
// SMB2 komutunu ayıkla
uint16_t cmd_rsp = u16((const uint8_t *)(buffer + CMD_OFFSET));
debug("Response command: 0x%04x \n", cmd_rsp);
switch (cmd_rsp) {
case SMB2_TREE_CONNECT:
if (received >= TREE_ID_OFFSET + sizeof(uint32_t)) {
// TreeID'yi yakalayıp saklıyoruz
tree_id = u32((const uint8_t *)(buffer + TREE_ID_OFFSET));
debug("Obtained tree_id: 0x%x \n", tree_id);
}
break;
case SMB2_SESS_SETUP:
// Oturum kurulum yanıtından SessionID'yi çekiyoruz
if (msg_no == 0x01 && received >= SESSION_ID_OFFSET + sizeof(uint64_t)) {
session_id = u64((const uint8_t *)(buffer + SESSION_ID_OFFSET));
debug("Obtained session_id: 0x%llx \n", session_id);
}
break;
case SMB2_CREATE:
// Dosya oluşturulduysa, kalıcı ve geçici dosya ID'lerini al
if (received >= CREATE_VFID_OFFSET + sizeof(uint64_t)) {
persistent_file_id = u64((const uint8_t *)(buffer + CREATE_PFID_OFFSET));
volatile_file_id = u64((const uint8_t *)(buffer + CREATE_VFID_OFFSET));
debug("Obtained p_fid: 0x%llx, v_fid: 0x%llx \n", persistent_file_id, volatile_file_id);
}
break;
default:
debug("Unknown command (0x%04x) \n", cmd_rsp);
break;
}
}
Bu mantığı şöyle görselleştirebiliriz; kilitli kapıları rastgele zorlamak yerine, her odadan çıkan anahtarı alıp bir sonraki kapıyı açmak gibi:
Sırasıyla ID’leri toplayarak ilerleyen mantığın şematik hali.
Bu sayede fuzzer, “login olamadım” hatasıyla boğuşmak yerine sistemin derinliklerine inebilmiş.
3. Stratejiler: Syzkaller’ı Eğitmek
Sadece ID takibi yetmiyor, ne göndereceğini de bilmek lazım. Burada üç farklı yol izlemişler.
A. Gramer Oluşturmak
Microsoft’un resmi spesifikasyonlarını (MS-SMB2) alıp Syzkaller’ın anlayacağı go dilinde gramerlere çevirmişler. Örneğin bir IOCTL isteği için yazdıkları yapı şuna benziyor:
smb2_ioctl_req {
Header_Prefix SMB2Header_Prefix
Command const[0xb, int16]
Header_Suffix SMB2Header_Suffix
StructureSize const[57, int16]
Reserved const[0, int16]
CtlCode union_control_codes
PersistentFileId const[0x4, int64]
VolatileFileId const[0x0, int64]
InputOffset offsetof[Input, int32]
InputCount bytesize[Input, int32]
MaxInputResponse const[65536, int32]
// ... diğer alanlar ...
Input array[int8]
Output array[int8]
} [packed]
B. Focus Areas (Odaklanmış Alanlar)
Fuzzer’ın her yere saldırması yerine, şüphelendikleri fonksiyonlara yoğunlaşmasını sağlamışlar. Syzkaller’ın focus_areas özelliğini kullanarak smb_check_perm_dacl fonksiyonuna 20 kat fazla ağırlık vermişler:
"focus_areas": [
{
"filter": { "functions": ["smb_check_perm_dacl"] },
"weight": 20.0
},
{
"filter": { "files": ["^fs/smb/server/"] },
"weight": 2.0
},
{ "weight": 1.0 }
]
Özelliği kullanmak, fuzzer’ın enerjisini bir huniden geçirir gibi belirli bir noktaya (smb_check_perm_dacl) odaklamasını sağlıyor.
Ve sonuç? Bu odaklanma sayesinde, bir Integer Overflow yakalamışlar. Normalde bulması çok zor olan bu hatayı tetiklemek için gereken özel ACL yapısını daPython scriptiyle simüle etmişler:
import struct
# Zafiyetli ACL yapısını oluşturan script
def build_sd():
sd = bytearray(0x14)
# ... header ayarları ...
struct.pack_into("<I", sd, 0x0C, 0x10000)
struct.pack_into("<I", sd, 0x10, 0xFFFFFFFF) # ! Burası overflow'u tetikleyen dacloffset
while len(sd) < 0x78:
sd += b"A" # Padding
sd += b"\x01\x01\x00\x00\x00\x00\x00\x00"
sd += b"\xCC" * 64
return bytes(sd)
Bellek sınırlarının nasıl aşıldığını ve taşmanın mekanizmasını gösteren teknik şema.
C. Gerçek Dünya Verisi: ANYBLOB
Burası bence araştırmanın en yaratıcı kısmı. Gramer tabanlı fuzzing kurallara bağımlıdır. Ama bazen kuralların dışına çıkan “kirli” veriler asıl hataları tetikler.
Ekip internetten buldukları gerçek ağ trafiği (PCAP) dosyalarını almış ve bir python scripti ile bunları Syzkaller’ın anlayacağı formata çevirmiş. Buna “ANYBLOB” demişler.
# PCAP'ten Syzkaller Corpus'una dönüşüm mantığı
if __name__ == "__main__":
packets = load_packets(json_file)
for i, packet in enumerate(packets):
pdu_size = len(packet)
# Paketi ham veri (ANYBLOB) olarak fuzzer'a veriyoruz
f.write(f"syz_ksmbd_send_req(&(0x7f...)=ANY=[@ANYBLOB=\"{packet}\"], {hex(pdu_size)}, ...)")
Wireshark kayıtlarını fuzzer girdisine dönüştüren akış.
Bu yöntemle, gramer kurallarıyla asla üretemeyecekleri bir senaryoyu yakalayıp CVE-2025-22041 (Use-After-Free) zafiyetini bulmuşlar. Bazen en basit yöntem (gerçek veriyi kullanmak), en karmaşık algoritmadan daha etkili olabiliyor.
4. Gözden Kaçanları Yakalamak: KUBSAN
Bellek güvenliği denince akla hep KASAN (Kernel Address Sanitizer) gelir. Ama bu araştırmada ilginç bir vaka yaşanmış.
smb_sid yapısında, dizinin -1. elemanına erişilen bir hata varmış. Sorun şu ki, bu -1. eleman hafızada hala o struct yapısının sınırları içinde kalıyormuş.
// Hatalı erişim noktası: num_subauth 0 olduğunda -1. indekse erişiyor
id = le32_to_cpu(psid->sub_auth[psid->num_subauth - 1]);
struct smb_sid {
__u8 revision;
__u8 num_subauth;
__u8 authority[NUM_AUTHS];
__le32 sub_auth[SID_MAX_SUB_AUTHORITIES]; // sub_auth[-1] aslında authority dizisine denk geliyor
} __attribute__((packed));
Bu yüzden KASAN “Hafıza sınırını aşmadın, sıkıntı yok” demiş. Ama ekip KUBSAN (Undefined Behavior Sanitizer) da kullanmış. KUBSAN ise olaya bellek adresi olarak değil, mantık olarak bakmış ve “Hop, dizinin boyutu belli, negatif indeks kullanamazsın!” diyerek hatayı yakalamış.
KASAN’ın temiz dediği yere KUBSAN’ın dur dediği o an.
Sonuç
Bu çalışma bana şunu hatırlattı: Fuzzing artık sadece işlemci gücüyle alakalı değil, zeka ve stratejiyle alakalı bir iş. Hedef sistemi ne kadar iyi anlarsanız, aracınızı ona göre modifiye ederseniz, o kadar derin hataları buluyorsunuz.
Doyensec ekibini tebrik etmek lazım, 23 CVE az buz bir iş değil. Meraklısı için araştırmanın orijinal linklerini aşağıya bırakıyorum.
Güvenli kodlar!
Yorumlar