teknikbakis

214.822 oturuma 753 analiz: istem “IP yazma” dedi, model yine yazdı

Aynı davranışı tekrarlayan botlar için tek bir yapay zekâ analizi yazıp onu binlerce oturuma kopyalamak, kapsamı yüzde 0,5'ten tamamına çıkardı. İlk sınamada model, istemdeki açık yasağa rağmen örnek oturumun IP'sini ve parolasını şablona yazdı.

DOSYA TB-2026-016
SİSTEM Python · LLM API · tuzak sensörü
KATMAN Veri hattı / model girdisi
SÜRE 1 gece · 44 dk geri doldurma
DURUM ÇÖZÜLDÜ
KANIT 3 ek

Semptom

Siber Tuzak, internete açık tuzak sensörlerine düşen saldırı oturumlarını topluyor. Bir yapay zekâ analizcisi her oturum için niyet, tehdit düzeyi, MITRE ATT&CK teknikleri, bir IOC özeti ve öneri yazıyor.

Sorun kapsamdı. Günde yaklaşık 4.400 aday oturum geliyordu ve analiz edilenlerin payı yüzde 0,5'te kalıyordu. Oysa oturumların çoğu birbirinin kopyasıydı: botlar aynı komut dizisini tekrar tekrar çalıştırıyor. Son 30 günün 214.822 oturumu 759 farklı davranışa indi.

27 Eylül 2026'da devreye giren üçüncü sürümün fikri buydu: her oturuma bir davranış parmak izi, her parmak izine tek bir LLM analizi. Aynı parmak izini taşıyan oturumlar bu analizi şablon olarak devralıyor. LLM çağrısı artık oturum sayısıyla değil, davranış sayısıyla büyüyecekti.

Toplu işe başlamadan önce birkaç şablonun metnini açıp okudum. İki sızıntı vardı. IOC özetinde örnek oturumun kaynak IP'si duruyordu; yorumda ise örnek oturumda denenen kullanıcı/parola çifti (root/123456 biçiminde). İstemde açık bir talimat vardı: IP, kullanıcı adı, parola gibi oturuma özgü ayrıntı yazma.

Tek kayda ait bir analizde bu yalnız gereksiz ayrıntı olurdu. Şablonda ise yanıltıcı: metin, o IP'yle hiç ilgisi olmayan oturumların detay sayfasında “bu oturumun IOC'si” olarak görünecekti. O sayfadan IOC alıp engelleme listesine yazan biri yanlış adresi engellerdi.

Yanlış yollar

İlk güvendiğim şey istemdeki talimattı. Yasak açıkça yazılmıştı, modelin uyacağını varsaydım. İlk sınama bu varsayımı iki örnekle çürüttü.

İkinci tuzak, girdiyi temizlediğimi sanmak olurdu. IP'yi yapısal alanlardan atmak yetmez; tuzak sensörü bağlantı olayını serbest metin olarak yazıyor ve IP o metnin içinde duruyor:

EK-1 Bağlantı olayının metni
[bağlantı] yeni bağlantı: 203.0.113.45:50122 → sensör:22 · oturum 7f3c…

Biçim temsilîdir; sensörün gerçek olay metni farklıdır ama aynı bilgiyi taşır. Adres belge aralığından, oturum kimliği kısaltıldı. Bu olay, oturum hakkında bağlantı künyesinden başka hiçbir şey söylemiyor.

Üçüncü tuzak otomatik denetime güvenmek olurdu. IOC özetine IP yazmak, alanın amacına tam uyuyor; şema denetimi, JSON ayrıştırma ve alan doluluk kontrolü bunu başarı sayar. Sızıntı bir biçim hatası değil, içerik hatası.

Akla gelen bir başka yol, çıktıyı sonradan süzmek. IP için düzenli ifade yazılabilir; ama serbest metne gömülmüş bir parolayı çıktıda güvenilir biçimde tanımanın yolu yok. Girdide ise hangi değerin kayda özgü olduğunu kesin olarak biliyorum.

Doğru test

İki sızıntıyı da yakalayan tek test, birkaç şablonun niyet ve IOC metnini bir insanın baştan sona okumasıydı. Okurken sorduğum soru şuydu: bu cümle, aynı parmak izini taşıyan başka bir oturumun sayfasında da doğru mu? IP de parola da bu sorudan geçemedi.

Gözle okumayı mekanik bir denetimle desteklemek mümkün: şablonu üretirken kullanılan örnek oturumun kayda özgü değerlerinden biri çıktı metninde geçiyorsa şablon reddedilir.

def sizinti(sablon_metni, ornek, izinli=frozenset()):
    adaylar = {ornek.kaynak_ip, ornek.oturum_id,
               *(parola for _, parola in ornek.denemeler)}
    return sorted(d for d in adaylar - izinli if d and d in sablon_metni)

Kısa parolalar sıradan sözcüklerle çakışıp yanlış alarm üretebilir; bu denetim gözle okumanın yerini tutmaz, onu ölçeklendirir. izinli kümesi, aşağıda anlatılan istisna için.

Kök neden

Model talimatı değil, girdide gördüğünü yazar. Bu vakada iki sinyal çatışıyordu. Çıktı şeması bir “IOC özeti” istiyor ve IOC'nin en tipik örneği bir IP adresi; girdide de tam olarak bir IP adresi var. Tek cümlelik yasak, hem alanın amacına hem girdideki somut kanıta karşı kaybetti. Parolada da aynısı oldu: komut dizisini anlatırken model, girişte denenen çifti anlatının doğal bir parçası saydı.

Asıl hata ise mimarideydi. Analiz tek bir oturum için üretiliyor, yüzlerce oturum için kullanılıyordu. Böyle bir düzende girdide kalan her kayda özgü değer, başka kayıtlara yapışmaya aday.

ÇIKARIM

Bir LLM çıktısı birden fazla kayda kopyalanacaksa, girdide kalan her kayda özgü değer bir sızıntı adayıdır. İstem talimatı olasılığı düşürür, sıfırlamaz. Girdide olmayan gerçek bir değeri model yazamaz; sınır istemde değil, girdide çizilir.

Çözüm

Girdi kaynağında maskelendi. Kayda özgü alanlar (kaynak IP, kaynak port, oturum kimliği) LLM girdisinden atıldı. Serbest metin alanlarında aynı değerler yer tutucuyla değiştirildi: <kaynak-ip>, <oturum>, kimlik bilgisi çiftleri için <kullanıcı>/<parola>. Yalnız bağlantı künyesi taşıyan olaylar (EK-1) hiç gönderilmiyor. Sadeleştirilmiş hali:

def llm_girdisi(oturum, olaylar):
    maske = [(oturum.kaynak_ip, "<kaynak-ip>"), (oturum.oturum_id, "<oturum>")]
    satirlar = []
    for olay in olaylar:
        if olay.yalniz_kunye:          # yalnız "yeni bağlantı" künyesi: hiç gönderme
            continue
        metin = olay.metin
        for deger, yer_tutucu in maske:
            if deger:                  # boş dizeyle replace her karakterin arasına yazar
                metin = metin.replace(deger, yer_tutucu)
        satirlar.append(metin)
    return "\n".join(satirlar)

Sınıfı belirleyen değer maskelenmedi. Bazı botlar Mirai türevlerinin bilinen varsayılan parola listesini dener. O parola oturuma özgü bir ayrıntı değil, davranışın kimliği; parmak izinde de bir işaret olarak yer alıyor. Onu <parola> yapmak, modelden sınıfı belirleyen kanıtı saklamak olurdu. Ayırt etme kuralı basit: değer, aynı parmak izini taşıyan bütün oturumlarda aynıysa kayda özgü değildir.

Metni etkileyen boyut parmak izine katıldı. Yorumlarda SSH istemci sürümü de geçiyordu. Parmak izi yalnız komut dizisinden oluşsaydı, aynı komutları başka bir istemciyle çalıştıran oturumlar, başka bir istemciyi anlatan yorumu devralacaktı. Sürüm parmak izine eklendi:

EK-2 Davranış parmak izinin bileşenleri
davranış parmak izi =
    sıralı komut dizisi
  + SSH istemci sürümü               ← yorumda geçtiği için eklendi
  + indirme / tünel portu kümesi
  + bilinen botnet parolası işareti  (var / yok)

Kaynak port kayda özgüdür ve atılır. İndirme ve tünel hedef portu ise davranışın parçasıdır, parmak izinde kalır.

Geri doldurma. Maskeli girdiyle son 30 gün baştan işlendi:

EK-3 Geri doldurma özeti
süre           01:24 – 02:08
LLM analizi    753      (0 hata)
aktarım        214.069  oturum
toplam         753 + 214.069 = 214.822 oturum

Her oturumun artık bir analizi var. LLM'e giden çağrı sayısı, oturum sayısının yaklaşık binde 3,5'i.

Yan kazanç: dış bir API'ye giden girdide artık saldırganın IP'si ve denediği parolalar yok. Model görmediği değeri yazamıyor; görmediği değeri saklayamıyor da.

Kalıcı ders

  • Model talimatı değil, girdiyi izler. “Yazma” bir olasılık ayarıdır, sınır değil. Sınır, girdiye neyin girdiğidir.
  • Şablon, kopyalamadır. Kopyalanan metin kaynak kayda değil, gösterildiği kayda aitmiş gibi okunur. Paylaşılacak bir çıktının girdisinde kayda özgü değer bırakma.
  • Maskelerken sınıfı silme. Bütün kopyalarda aynı olan değer kayda özgü değildir; onu saklamak analizi körleştirir.
  • Parmak izi, metni etkileyen her boyutu içermeli. Şablonda geçen somut bir ayrıntı parmak izinde yoksa, o ayrıntı yanlış kayda taşınacak demektir.
  • Birkaç çıktıyı gözle oku. Bu vakadaki iki sızıntıyı hiçbir biçim denetimi bulmazdı. Sızıntı, şablonlar 214 bin oturuma dağıtılmadan önce, okuyarak yakalandı.

Yapay zekâyla çalışan her ekip için ilke aynı: ilk cevabı kabul etme, çıktıyı oku. Bunun insan tarafını TB-2026-005'te anlattım.