Thick Application Güvenliği: Çalışma Zamanı Bütünlüğü
Önceki yazıda SQL ve PFX parolası biçimindeki örnek verileri yalnızca kullanım anında çözmenin neden yeterli olmadığını görmüştük. Uygulama kendi tamponunu temizliyordu ama CryptUnprotectData çağrısının dönüşünde aldığımız kayıt kalıyordu. Bu kez aynı noktaya uygulama tarafından bakacağız: çözme fonksiyonuna müdahale edildiğini fark edip o çağrıyı başlatmayabilir miyiz?
Önceki incelemelerde kullandığımız SafiyeMonitor, çalışma zamanı analiz araçlarına bir örnek. Bu yazıdaki amacım, bu tür araçlarla yapılan incelemeleri belli bir seviyede zorlaştırmak. Bunun için geliştirdiğim Thick App Integrity Guard kütüphanesine bir çalışma zamanı kontrolü ekledim. Korumasız ve korumalı örneğe aynı analiz betiğini yükleyip verinin hangi durumda yakalanabildiğini karşılaştırdım.
Bu yazıdaki DLL ve laboratuvar kodları, yaklaşımı çalışan bir örnek üzerinden göstermek için hazırlandı. Daha kapsamlı ve dayanıklı uygulamalar yazılabilir; mevcut kod da izlenen fonksiyonlar, tespit yöntemleri, hata yönetimi ve performans açısından geliştirilebilir. Kendi ürününe eklemek isteyen bir geliştiricinin, bu örneği uygulamasının mimarisine uyarlayıp gerçek çalışma ortamında yeniden test etmesi gerekir.
Bu yazıdaki örnekler v0.4.0 sürümüne ait. Kaynak kod ve İngilizce entegrasyon rehberleri GitHub deposunda, Windows x64 DLL, yerel NuGet paketi ve örnek uygulama ise sürüm sayfasında bulunuyor.
Bu kontrolde çalışma zamanı analiz aracının sürece bağlanmasını engellemiyoruz. Bağlantı kuruluyor. Uygulama, izlediği fonksiyonların giriş kodu değişmişse hassas işlemi başlatmıyor. Kazanımı ve sınırını baştan ayırmak gerekiyor: belirli bir hook’u fark etmek, uygulamayı incelenemez hâle getirmekle aynı şey değil.
Çalışma zamanı analiz araçları kullanım anındaki veriyi nasıl görüyor?
Çalışma zamanı analiz araçları, çalışan bir sürece bağlanarak veya uygulamayı gözlem altında başlatarak fonksiyon çağrılarını izleyebilir. Bu laboratuvarda kullandığım analiz betiği, izlenecek native fonksiyonlara Interceptor.attach ile gözlem noktaları ekliyor. Farklı araçlar aynı gözlem için farklı yöntemler kullanabilir.
DPAPI örneğinde iki farklı an önemli:
CryptProtectDataçağrısına girerken uygulamanın şifrelenecek açık verisi var.CryptUnprotectDatabaşarılı döndüğünde uygulamanın kullanacağı çözülmüş veri var.
Analiz betiği, ikinci çağrının çıkışındaki DATA_BLOB yapısından baytları alıp crypto_event kaydı olarak gönderiyor. Bu aşama işletim sisteminin şifrelemeyi kırması değil. Uygulama kendi yetkisiyle veriyi çözüyor; gözlemci de ortaya çıktığı noktadan bir kopya alıyor.

onLeave içindeki kırmızı kutuda, başarılı dönüşten sonra açık verinin okunması ve kayıt olarak gönderilmesi görülüyor.Bu testte kullanılan altyapı Frida 16.6.4. Fonksiyon giriş ve çıkışlarını izleyen Interceptor arayüzünün kapsamı API belgesinde açıklanıyor. Sonuçlar burada denenen sürüm ve yönteme ait; başka çalışma zamanı analiz araçları veya değiştirilmiş ajanlar için aynı sonucu kendiliğinden varsaymıyorum.
Dosya bütünlüğü neden bu sorunu çözmüyor?
Diskteki DLL değişmemiş olabilir. İzlediğimiz müdahale, sürecin belleğine yüklenmiş fonksiyon üzerinde gerçekleşiyor. Bu yüzden dosyanın SHA-256 özetini kontrol edip doğru bulmak, o fonksiyonun bellekteki giriş kodunun aynı kaldığını göstermiyor.
Kütüphanedeki imzalı manifest kontrolünü korudum. Dağıtılan dosyaların beklenen sürümle eşleşmesi ayrı bir ihtiyaç. Fakat çalışma zamanı tespiti için ayrı bir RuntimeIntegritySession ekledim. IntegritySession dosyaları, yeni session ise belirlenen native fonksiyon girişlerini ve debugger durumunu kontrol ediyor.
| Kontrol | Baktığı yer | Bu laboratuvardaki görevi |
|---|---|---|
| İmzalı manifest ve dosya özeti | Diskteki dağıtım dosyaları | Dosyanın beklenen imzalı sürümle eşleşmesi |
| Native fonksiyon girişi | Sürecin belleğindeki seçili baytlar | Yaygın inline hook değişikliğini fark etmek |
| Debugger durumu | Geçerli sürecin debugger bilgisi | Debugger altında çalışmayı ayrıca değerlendirmek |
| İşlem öncesi uygulama kararı | Hassas çağrının önündeki kod | Başarısız kontrolde işlemi başlatmamak |
Bir DLL’yi projeye referans vermek bu kararları kendiliğinden uygulamıyor. Ana uygulamanın kontrol sonucunu kullanması ve başarısızsa ilgili işleme devam etmemesi gerekiyor.
Fonksiyonun değiştiğini nasıl anlıyoruz?
Yalnızca ilk baytın bir atlama komutu olup olmadığına bakmak istemedim. Meşru bir fonksiyon da atlamayla başlayabilir. Uygulama açılırken bellekte gördüğümüz baytları “temiz kopya” diye kaydetmek de yeterli değil; gözlemci daha uygulama başlamadan müdahale etmiş olabilir.
Bu nedenle karşılaştırma için fonksiyonun diskteki sistem DLL’sinde bulunan baytlarını kullanıyorum. Akış şöyle:
- Politikanın belirlediği sistem DLL’sini Windows sistem dizininden yüklüyoruz.
- İstenen export’un adresini çözüyoruz.
- Bu adresin gerçekten hangi yüklü modüle ait olduğunu buluyoruz.
- Modülün diskteki PE dosyasında aynı konuma karşılık gelen baytları okuyoruz.
- Fonksiyon girişindeki 32 baytı, bellekteki karşılığıyla karşılaştırıyoruz.
Export’un sahibi önemli. Bir DLL’nin dışa aktardığı isim başka bir DLL’ye yönlendirilebilir. Böyle bir durumda ilk DLL’nin dosya ofsetini kullanmak yanlış baytları karşılaştırır. Kod, çözülen adresin sahibi olan gerçek modülü buluyor ve PE içindeki konumu buna göre hesaplıyor.
Her fark da saldırı demek değil. Windows’un yükleme sırasında relocation uyguladığı bir alanı ham disk baytlarıyla karşılaştırırsak meşru fark üretebiliriz. Bu sürüm, karşılaştırılan 32 baytla relocation kaydı kesiştiğinde o alanı sessizce atlamıyor; desteklenmeyen karşılaştırma olarak başarısız dönüyor. Okunamayan, çözülemeyen veya desteklenmeyen bir hedef de başarı sayılmıyor.

RuntimeNativeEntryChanged dönüyor.Varsayılan politika şu dört fonksiyonu izliyor:
| Fonksiyon | Neden kapsamda? |
|---|---|
crypt32!CryptProtectData | Şifrelenecek açık verinin girişte gözlemlenmesi |
crypt32!CryptUnprotectData | Çözülen verinin dönüşte gözlemlenmesi |
bcrypt!BCryptFinishHash | Özet sonucunun üretildiği native sınır |
ncrypt!NCryptVerifySignature | İmza doğrulama sonucunun üretildiği native sınır |
Bu liste Windows’taki bütün kriptografi yollarını kapsamıyor. Kullanılan sağlayıcı, uygulamanın dili ve hangi API’leri çağırdığı ayrıca incelenmeli. Örneğin OpenSSL kullanan bir uygulamada aynı dört ismi izlemek tek başına o uygulamanın TLS çağrılarını kontrol etmiş olmuyor.
Debugger kontrolü bütün çalışma zamanı analizlerini gösterir mi?
IsDebuggerPresent, CheckRemoteDebuggerPresent ve .NET debugger bilgisi ayrı bir kontrol katmanı. Windows API’sinin bildirdiği debugger durumunu değerlendiriyoruz. CheckRemoteDebuggerPresent adındaki “remote”, başka bilgisayarı değil ayrı bir süreçteki debugger’ı anlatıyor; ayrıntı Microsoft belgesinde var.
Sonucun olumsuz olması, süreçte hiçbir gözlem aracı bulunmadığını göstermiyor. Bu karşılaştırmada da asıl tespit debugger bayrağından değil, izlenen fonksiyonların giriş baytlarındaki değişiklikten geldi.
Kod, çalışan süreçler arasında belirli bir analiz aracının adını arayıp karar vermiyor. Bir aracın adının değiştirilmesiyle ortadan kalkabilecek kontrolü ana koruma olarak kullanmadım. Burada baktığımız şey, hassas çağrının girişinin diskteki karşılığıyla eşleşip eşleşmediği.
Laboratuvarın düzeni
Örnek uygulama RuntimeProbe.exe. Aynı uygulamanın korumalı ve --unguarded ile açılan korumasız modu var. Üretilen veri LAB_RUNTIME_ işaretiyle başlayan, her çalıştırmada yeniden oluşturulan 60 baytlık sentetik bir tampon. Gerçek SQL hesabı, PFX dosyası veya dış sunucu yok.
Korumalı örnek önce gerekli DLL’leri yüklüyor ve başlangıç kontrolünü yapıyor. Kontrol başarılıysa sahte veriyi oluşturup DPAPI ile koruyor. Bu sıra önemli: veriyi önce şifreleyip kontrolü sonra yapsaydık, başlangıçta takılan hook CryptProtectData girişinde veriyi zaten görebilirdi.
Uygulama hazır olduğunda check, use ve exit komutlarını kabul ediyor. use komutu veriyi çözüyor, örnek iş için alıyor ve sahip olduğu tamponları temizliyor. Çıktıya açık veriyi yazmıyor; yalnızca bayt sayısını ve SHA-256 özetini ekliyor.
Testte analiz aracının hook betiğini değiştirmeden kullandım. Süreçleri ve komutlarını kontrol ettiğim bu karşılaştırmayı otomatik testle tekrarladım; ilk korumalı gözlemi REA üzerinden de doğruladım. Test, seçilen betiğin kurduğu hook’ları kapsıyor. Çalışma zamanı analiz araçlarının bütün özelliklerinin sınandığı anlamına gelmiyor.
Korumasız örnek: Veri yine yakalanıyor
İlk uygulamayı --unguarded ile açtım. Başlangıç yanıtı hazır olduktan sonra analiz aracını aynı sürece bağlayıp hook betiğini yükledim. Bu aşamada kriptografi DLL’leri zaten yüklüydü.
Ardından use komutunu gönderdim. Uygulama DPAPI verisini çözdü ve başarılı yanıt verdi. Analiz aracında da CryptUnprotectData için crypto_event oluştu.
Kaydın yalnızca aynı uzunlukta bir veriye ait olmadığını kontrol etmek için, yakalanan baytların SHA-256 özetini uygulamanın çözdüğü verinin özetiyle karşılaştırdım. İkisi eşleşti. Böylece önceki yazıdaki davranışı aynı laboratuvarda tekrar gördük: tampon daha sonra temizlense de kullanım anındaki kopya alınmıştı.
Bu sonuç yalnızca sentetik veriye ait. Test raporuna açık veriyi eklemedim. Olay sayısı, bayt sayısı ve eşleşmeyi gösteren özetler yeterliydi.
Korumalı örnek: Çözme çağrısından önce durmak
İkinci çalıştırmada aynı uygulamayı korumalı modda açtım. Başlangıç kontrolü RuntimeVerified verdi. Sonra yine çalışan sürece bağlandım ve aynı analiz betiğini yükledim.
use yanıtının karar alanları bu kez şöyleydi:
{
"operation": "use",
"guarded": true,
"allowed": false,
"code": "RuntimeNativeEntryChanged"
}
Yanıttaki bulgular, CryptProtectData ve CryptUnprotectData girişlerinin beklenen baytlarla eşleşmediğini gösteriyordu. Uygulama başarısız kontrolün ardından Dpapi.Unprotect çağrısına gitmedi. Bu işlem için analiz aracında da CryptUnprotectData verisi oluşmadı.

BeforeOperation kontrolü başarısızsa continue ile bu komutun işlenmesi bırakılıyor. Dpapi.Unprotect çağrısı bu kararın arkasında.Burada yalnızca “kayıt gelmedi” sonucuna dayanmıyoruz. Aynı betik korumasız örnekte veriyi yakalayabiliyor. Korumalı örnekte ise hem uygulamanın ret yanıtı var hem de kodda çözme çağrısı bu kararın arkasında. Test bu iki davranışı birlikte kontrol ediyor.
Analiz aracının süreçteki başka işlemleri görmesi devam edebiliyor. Örneğin REA üzerinden yaptığım gözlemde, doğrulayıcının sistem DLL’lerini açması dosya izleme kayıtlarında görünüyordu. Koruma, seçili hassas işlemi durduruyor; süreçteki bütün görünürlüğü kaldırmıyor.
Hook kaldırılınca ne oluyor?
Hook betiğini kaldırıp bağlantıyı kapattıktan sonra aynı uygulamada tekrar use komutu gönderdim. Sonuç RuntimeSessionBlocked oldu. İlk hatadan sonra o session engellenmişti; hook’un kaldırılması onu açmadı.
Yeni ve gözlem yapılmayan bir süreç başlattığımda ise başlangıç kontrolü geçti ve işlem çalıştı. Bu iki sonucu ayrı değerlendirmek gerekiyor: mevcut session hatayı koruyor, temiz yeni session baştan ölçüm yapıyor.

RuntimeSessionBlocked dönüşünü, alttaki kutu ise başarısız kontrolde blocked değerinin ayarlanmasını gösteriyor.Karşılaştırmayı .NET örneğinde ve gerçek NativeAOT DLL’yi çağıran ayrı bir Python sürecinde tekrarladım. Her iki tarafta 6 senaryo geçti. Bunlara analiz aracını yalnızca bağlayıp girişlere hook takmamak da dahil: bu durumda işlem çalıştı. BCryptFinishHash ve NCryptVerifySignature girişlerine eklenen gözlem hook’ları ise ayrı testlerde tespit edildi. Bu ölçümler çalışan laboratuvara sonradan bağlanma akışına ait; bütün erken enjeksiyon veya tespit atlatma yollarının sınandığı anlamına gelmiyor.
DLL’yi uygulamaya nasıl ekliyoruz?
.NET uygulamasında tek bir runtime session oluşturup ilgili özelliği açmadan ve her hassas işlemden önce kontrol edebiliriz:
using IntegrityGuard;
// Uygulama oturumu boyunca saklanır.
var runtime = new RuntimeIntegritySession();
var startup = runtime.Check(IntegrityCheckpoint.Startup);
if (!startup.Success)
return;
// Daha sonra, hassas çağrıdan hemen önce aynı session:
var beforeUse = runtime.Check(IntegrityCheckpoint.BeforeOperation);
if (!beforeUse.Success)
return;
// Ancak şimdi hassas çağrıyı başlatın.
Her işlemde yeni session oluşturmak önceki hatanın bilgisini bırakır. UI, timer ve arka plan işleri de aynı korunan iş akışı içinde ele alınmalı. Kütüphane kendiliğinden işlem sonlandırmıyor veya başka uygulamaları kapatmıyor. Hangi özelliğin engelleneceğine ana uygulama karar veriyor.
NativeAOT DLL’deki ig_runtime_check dışa aktarımı da varsayılan politikayı çağırıyor:
if (ig_runtime_check(IG_BEFORE_OPERATION) != IG_OK) {
return; /* Hassas işlemi başlatma. */
}
Native kontrol süreç ömrü boyunca aynı runtime session’ı tutuyor; dosya oturumu açıp kapatmak bu engeli temizlemiyor. Mevcut ig_check ise imzalı dosyaları kontrol etmeye devam ediyor. İki çağrının görevleri farklı. C başlığı ve sonuç kodları runtime rehberinde ve native API rehberinde açıklanıyor.
Çalıştırılabilir .NET laboratuvarını hazırlamak için:
dotnet build samples/RuntimeProbe -c Release -o artifacts/runtime-probe
.\artifacts\runtime-probe\RuntimeProbe.exe
# Korumasız karşılaştırma:
.\artifacts\runtime-probe\RuntimeProbe.exe --unguarded
Gerçek analiz betiğiyle süreç karşılaştırması tests/runtime_instrumentation_tests.py içinde. Betik yalnızca kendisinin başlattığı laboratuvar sürecine bağlanıyor. Gerçek bir müşteri uygulamasına veya başka bir çalışan sürece kendiliğinden bağlanmıyor.
Hangi incelemeyi zorlaştırıyoruz, hangisi kapsam dışında?
| Durum | Bu sürümün davranışı veya sınırı |
|---|---|
| İzlenen girişe yerleşen yaygın inline hook | Bayt farkını görebilir ve işlem öncesinde ret verebilir |
| Desteklenen debugger bilgisi olumlu | Runtime kontrolü başarısız olur |
| Hook kaldırıldıktan sonra aynı session | Engellenmiş kalır |
| Dosyanın diskte değiştirilmesi | Ayrı imzalı manifest kontrolü gerekir |
| Statik strings veya decompilation | Bu kontrol bunları engellemez |
| Analiz aracının yalnızca sürece bağlanması | Bağlantının reddedilmesi sağlanmaz |
| İzlenmeyen fonksiyon veya girişin dışındaki değişiklik | Bu ölçümün kapsamına girmeyebilir |
| Kontrolden sonra, çağrıdan önce müdahale | Yarış penceresi devam eder |
| Belleği başka yoldan okumak | Bütün bellek okumaları engellenmez |
| Kontrolün veya karar noktasının değiştirilmesi | Aynı süreçteki bu katman da hedef alınabilir |
32 baytlık giriş karşılaştırması bütün fonksiyonun veya uygulamanın tamamının bütünlük ölçümü değil. Debugger kontrolünü, modül adresini çözmeyi veya dosyayı okumayı sağlayan fonksiyonlar da süreç içinde çalışıyor. Bu mekanizmaları değiştiren, ölçümü yanıltan veya uygulamadaki ret dalını kaldıran bir müdahale ayrıca değerlendirilmeli.
Benzer şekilde hook’un yalnızca bir araç adıyla ilişkilendirilmesi doğru olmaz. Antivirüs, EDR ve başka meşru araçlar da native kodda değişiklik oluşturabilir. Windows güncellemesi sırasında diskteki dosya ile süreçteki görüntü farklı kalabilir. Kontrolün bildirdiği şey, politikanın izlediği girişin beklenen dosya baytlarıyla eşleşmemesidir. Hedef Windows sürümünde uyumluluk ve yanlış alarm testleri gerekir.
Tespit sonrası nasıl davranmalı?
Laboratuvarda hassas işlemi reddettim ve session’ı engelledim. Üretimde kullanıcıya neden işlemin açılamadığını anlatan bir mesaj, güvenilen yeniden başlatma veya destek yolu gerekir. Kaydedilmemiş işi kaybettiren ani kapanışlar yerine ilgili özelliği durdurmak daha uygun olabilir.
Kontrol sonucunu sunucuya gönderiyorsak o sonucu tek başına güven kanıtı saymamalıyız. RuntimeVerified yazabilen bir istemci bu metni sunucuya da gönderebilir. Kullanıcı, kaynak ve işlem yetkisi sunucuda ayrıca doğrulanmalı. Ortak SQL veya PFX sırrını istemciden kaldırmakla ilgili önceki yazıdaki mimari tercih de geçerliliğini koruyor.
Bu karşılaştırmada elde ettiğimiz sonuç daha sınırlı ama ölçülebilir: aynı analiz hook’u korumasız örneğin çözdüğü veriyi yakaladı; korumalı örnek, izlenen girişlerdeki değişikliği fark edip çözme çağrısını başlatmadı. Amaç da bu katmanda bu: çalışma zamanı analiz araçlarının kullandığı belirli yöntemleri fark etmek, hassas işlem için tepki vermek ve gözlemcinin işini zorlaştırmak. Bundan sonra bakılması gereken, hangi müdahalelerin bu ölçümün dışında kaldığı ve gerçek uygulamada aynı tepkinin hangi işlem noktalarına bağlanacağı.










