PatchGuard Analysis - Part 1

PatchGuard Analysis - Part 1

Ağustos 6, 2025·0xbekoo
0xbekoo

Bu makale dark shadow B.‘ye adanmıştır. Tüm yardımın için teşekkürler, dark shadow B.

Introduction

Hepimizin adını en az bir kez duymuş olabileceği PatchGuard (Kernel Patch Protection), Microsoft tarafından geliştirilen ve 64-bit Windows işletim sistemlerinde kritik bir rol üstlenen güvenlik mekanizmasıdır. Görevi oldukça net: kernel-level yapılara yapılan yetkisiz müdahaleleri engelleyerek sistemin bütünlüğünü korumak. Bugün modern Windows security mimarisinin en temel bileşenlerinden biri olarak varlığını sürdürmektedir.

Benim için PatchGuard, Windows üzerinde reverse engineering ile tanıştığım ilk zamanlardan beri dikkatimi çeken konulardan biriydi. Ne kadar karmaşık görünse de bu yapının iç işleyişini anlamak, hem kişisel merakımı gidermek hem de Red Team çalışmalarında ve malware development perspektifinde kritik bir bakış açısı kazanmak adına değerli bir deneyim oldu.

Bu makalede, PatchGuard’ın nasıl initialize edildiğinden, yaptığı integrity checks süreçlerine kadar adım adım ilerleyecek; yani bu koruma katmanının perde arkasına kısa bir yolculuk yapacağız.

Son olarak şunu da belirtmek isterim ki bu çalışma önceki Windows sürümlerinde yapılan PatchGuard analizlerini Windows 11’e genişletmek ve zenginleştirmek amacıyla hazırlanmıştır. Amacım, sistemin kernel-level koruma mekanizmalarının güncel sürümlerde nasıl evrildiğini ve modern güvenlik mimarisine nasıl entegre edildiğini göstermek.

Başlamadan önce referanslara göz atmak isterseniz buraya tıklayabilirsiniz.

KiFilterFiberContext Function

PatchGuard’ın ayarlanması ve başlatılması işlemleri KiFilterFiberContext fonksiyonu tarafından gerçekleştirilmektedir. Bu fonksiyon bu görevleriyle beraber bir takım bayrak kontrolleri de gerçekleştirmektedir. Bu bayraklar Debugging ile alakalıdır. Eğer sistemde debugging aktif ise bu fonksiyon PatchGuard’ı başlatmayacaktır. Yani kısaca bu fonksiyon PatchGuard için gerçekten de önemli bir fonksiyon.

KiFilterFiberContext sistemin başlangıcında tam olarak iki defa çağırılmaktadır. Sistem başlangıcında bu fonksiyonun iki defa çağırılmasının ardında, PatchGuard’ın başlatılmasıyla alakalı iki farklı method içermekte. Bu yöntemlere göz atalım:

Method 1: Calling from KeInitAmd64SpecificState

Bu fonksiyon bazı kontroller sonrasında KiFilterFiberContext’i çağırmaktan sorumludur. Bu fonksiyonun başına göz atarsak bazı bayrakların kontrol edildiğini görebiliriz:

Özellikle InitSafeBootMode, KdPitchDebugger ve KdDebuggerNotPresent bayrakların kontrol edildiğine dikkat edin. Bu bayraklardan odağımız debugging ile alakalı olanlar olacaktır çünkü şimdi göreceğimiz gibi bu bayraklardan elde edilen değerler ile KiFilterFiberContext’in çağırılıp çağırılmayacağı belirlenecek. Eğer sistemde debugging aktif ise bu iki bayrak 0 değerini alacaktır ancak normal senaryolarda bu bayraklar 1 değerini alır. Bu bayrakların alınmasından sonraki işlemlerde aşağıdaki register’lar şu değeri elde edecektir:

  • rax => 0x80000000
  • rdx => 0x80000000
  • r8 => 0xfffffff

Tipik olarak hepimizin bildiği gibi debugging bayrakların kontrolü esasen bir koşul ile gerçekleştirilmiyor. Gerçekten garip ve güçlü bir yöntem kullanılmakta. Bayraklardan alınan değerler esasen bir bölme işleminde kullanılacaktır.

Bu bölme sonucunda ise kasıtlı olarak bir bölme hatası tetiklenir. Bu kasıtlı olarak bölme hatasının tetiklenmesi ise bayraklardan alınan değere göre belirlenir. Eğer bayraklardan elde edilen sonuç 1 ise - yani debugging olmadığını gösterir - kasıtlı olarak bir bölme hatası tetiklenecek ve KiFilterFiberContext çağırılacaktır. Ancak bayraklardan elde edilen sonuç 0 ise bu fonksiyon çağırılmayacaktır.

AMD64 dökümanına göre, eğer pozitif sonuç 0x7FFFFFFF’den büyük veya negatif sonuç 0x80000000’den küçük ise bölme hatası tetiklenecektir. Register’ların tuttuğu değeri tekrar kontrol edin. Yani bayraktan alınan değerlerin sonucu 1 ise bu bölme sonucunda kasıtlı olarak hata tetiklenmiş olacak. Bölme işlemi şu kısımda gerçekleşiyor:

Eğer KeInitAmd64SpecificState içerisine göz atarsanız bir exception handler olduğunu göreceksiniz:

Böylece KiFilterFiberContext çağırılmış olacak.

Method 2: Calling from ExpLicenseWatchInitWorker

Sistemin başlangıcında KiFilterFiberContext’i çağırmak için kullanılan bir başka yöntem ise ExpLicenseWatchInitWorker’dan çağırılmasıdır ancak burada işler biraz karmaşık hale geliyor. Bu fonksiyon KeInitAmd64SpecificState‘den önce çağırılmaktadır ve KiFilterFiberContext fonksiyonunu direkt olarak çağırmıyor; bunun yerine %4 oranla çağırılıp çağırılmayacağını belirliyor. Bu yöntem aslında birçok PatchGuard mekanizması tarafından kullanılan bir tekniktir.

Fonksiyonun başlangıcında PRCB (Process Register Control Block) yapısının adresini aldığını ve bu yapının HalReserved alanları üzerinden işlemler yaptığını görebiliriz:

Bahsedilen HalReserved alanından 0x70 ve 0x78 offset’lerden değer aldığını ve ardından hızlı bir şekilde bu alanları temizlediğini görebiliriz. Buradan alınan adreslere ileride değineceğim.

Ardından rdtsc instruction kullanarak random bir değer oluşturuluyor. Devam etmeden önce bu instruction’u AMD64 dökümanı ile tanıyalım:

“TSD biti, zaman damgası sayacının hangi ayrıcalık seviyesinde okunabileceğini yazılımın kontrol etmesini sağlar. TSD biti 0 olarak ayarlandığında, herhangi bir ayrıcalık seviyesinde çalışan yazılımlar RDTSC veya RDTSCP komutlarını kullanarak zaman damgası sayacını okuyabilir.”[1]

Analizimizin ilerisinde sık sık göreceğiz ki bu instruction ile random değer oluşturma işlemi gerçekten PatchGuard mekanizmaları tarafından sıklıkla kullanılıyor. Dolayasıyla şimdiden bunu kafamıza oturtmak iyi olacaktır.

Random değerin oluşturulması ardından bu değer 0x51eb851f sabit değeriyle çarpılmakta:

Değerin çarpılması ardından KiFilterFiberContext’i çağırıp çağırmayacağını %4 oranla karar veriyor:

HalReserved alanlarından alınan değerler burada kullanılıyor. Eğer bu fonksiyon %4 oranla çağırılırsa, rax register’ı KiFilterFiberContext’in adresini aktarırken rcx’e KiFilterFiberContext fonksiyonu için parametre olarak bir yapının adresi atanmakta.

KiFilterFiberContext’in aldığı bu yapıya literatürde KI FILTER FIBER PARAM adını veriyoruz. Bu yapının içeriği şu şekilde:

typedef struct _KI_FILTER_FIBER_PARAM
{
CHAR code_prefetch_rcx_retn[4]; // prefetchw byte ptr [rcx]; retn;
CHAR padding[4]; // Align
PVOID pPsCreateSystemThread;
PVOID UcpRetrieveCurrentConfigSettings+0x174;
PVOID pKiBalanceSetManagerPeriodicDpc;
}KI_FILTER_FIBER_PARAM, *PKI_FILTER_FIBER_PARAM;

İleride göreceğimiz gibi, yapıda bulunan pointer’lar PatchGuard’ın initialize edilmesi için farklı yöntemlerde kullanılacaktır.

HalReserved alanların ayarlanması KiLockServiceTable fonksiyonu tarafından gerçekleştirilmektedir. Ancak bu fonksiyona direkt olarak göz atarsanız içerisinde direkt olarak bu alanları ayarlayan bir koda denk gelemezsiniz. KeInitAmd64SpecificState’de gördüğümüz gibi burada da exception handler kullanılmakta ancak bir hata tetiklemek yerine direkt olarak fonksiyon çağırılmakta:

Bu handler'in kendisi aslında **KiFatalExceptionFilter** fonksiyonun bir stub'udur:

Yapının adresini alındıktan sonra 0x78 offset’e KiServiceTablesLocked isimli bir yapının adresini aktardığını görebiliriz ancak bu yanıltıcı bir isimdir. Bu adres aslında yukarıda bahsedildiği gibi KI_FILTER_FIBER_PARAM yapısının tam da kendisidir. Bunu dinamik analizimizde doğrulayabiliriz:

ExpLicenseWatchInitWorker’in ilk kısmına bir bp yerleştirdim ve yapının 0x78 offset’ine denk gelen adresi aldım. Şimdi bu adresi kontrol edelim:

kd> dps fffff803`0c3e71f8 L1
fffff803`0c3e71f8  fffff803`7d566010 nt!KiServiceTablesLocked

Göründüğü gibi KiServiceTablesLocked adresini tutmakta. Şimdi bu yapının içeriğine göz atalım:

Böylece bu yapının KI_FILTER_FIBER_PARAM olduğunu doğrulayabiliriz. Diğer kullanılan HalReserved alanına göz atarsak, KiFilterFiberContext’in adresini tuttuğunu da doğrulayabiliriz:

Diğer başlığımıza geçmeden önce önemli bir notu eklemem gerekiyor ki, KiFilterFiberContext’in bu yapıyla çağırılması sadece ExpLicenseWatchInıtWorker tarafından gerçekleştirilmektedir. KeInitAmd64SpecificState fonksiyonu ise direkt olarak NULL ile çağırmaktadır.

Decompiling KiFilterFiberContext

Daha önceden anlattığım gibi bu fonksiyonun başlıca görevi PatchGuard’ı başlatmasıdır. KiFilterFiberContext fonksiyonu PatchGuard’ın ayarlanması ve başlatılması için birkaç farklı yöntem kullanılıyor. Bunlardan biri ise literatürde KiInitializePatchGuardContext adı verilen fonksiyonu çağırması.

Fonksiyonun başına göz atarsak ilk olarak debugging’i devre dışı bıraktığını görebiliriz:

Ancak bu fonksiyonda bulunan bir garip ve PatchGuard’ın başlatılması için kullanılan bir farklı method olan detay ise KiFilterFiberContext ntoskrnl’de bulunmayan küçük bir callback fonksiyonu içermektedir:

ExNotifyCallback fonksiyonu birinci parametre olarak PatchGuardTVCallBack’in pointer’ını almaktadır. Bu pointer, mssecflt.sys içerisinde SecInitializeKernelIntegrityCheck tarafından ayarlanmaktadır. Dediğim gibi bu, PatchGuard’ın başlatılması için ayrı bir yöntem olarak kullanılmakta ve ileride ayrı başlık olarak göz atacağız dolayasıyla şimdi KiInitializePatchGuardContext’e göz atacağız.

KiInitializePatchGuardContext fonksiyonu KiFilterFiberContext tarafından üç defa farklı yöntemler ile çağırılmaktadır; ilk seferde ne olursa olsun direkt çağırılır, ikinci defada %50 oranla çağırılmaktadır, üçüncü defa ise ileride değineceğimiz farklı bir yöntemle çağırılmaktadır. Bu kısmın pseudocode’ına göz atabiliriz:

...
if (!g_GlobalCtxPointer && !KpgApiRegistered &&!pKiFilterFiberParam) {
	if (PsIntegrityCheckEnabled) {
		/* Initialize ObjectAttributes */
	    ObjectAttributes.Length = 48;
	    ObjectAttributes.ObjectName = (PUNICODE_STRING)L"TV";
        ObjectAttributes.RootDirectory = 0;
        ObjectAttributes.Attributes = 64;
	    &ObjectAttributes.SecurityDescriptor = 0;
	    
		Status = ExCreateCallback(&CallbackObject, &ObjectAttributes, 0, 0);
		if (Status >= 0) {
			ExNotifyCallback(CallbackObject, PatchGuardTVCallBack, &KpgApiConsumerRanges);
	        ObfDereferenceObject(CallbackObject);
		} 
	}
	...
	/* Initialize First Context */
	Result = KeExpandKernelStackAndCallout(KiInitPatchGuardContext, ParameterArray, 0xC000u)

	if (Result) {
		if (random5 < 6) {
			...
			/* Initialize second context */
			Result = KeExpandKernelStackAndCallout(KiInitPatchGuardContext, ParameterArray, 0xC000u);
		}
		if (Result) {
			if (!g_GlobalCtxPointer && !pKiFilterFiberParam && (KiSwInterruptPresent() >= 0) && KpgApiRegistered) {
				...
				Result = KeExpandKernelStackAndCallout(KiInitPatchGuardContext, ParameterArray, 0xC000u);
				
		}
	}
}

Bu fonksiyon, aracılığıyla çağrılmaktadır. KeExpandKernelStackAndCallout, parametre olarak verilen fonksiyonu (örneğin kodumuzdaki KiInitPatchGuardContext gibi) çağırmadan önce, yeterli miktarda kernel stack alanı bulunduğundan emin olmak için kullanılır. Eğer mevcut stack alanı yetersizse, geçici olarak daha büyük bir kernel stack segmenti ayırır ve hedef fonksiyonu (callout) bu genişletilmiş stack üzerinde çalıştırır.[2]

Pseudocode eklemediğim bir detay ise her KiInitPatchGuardContext çağırılmadan önce random değerler oluşturulması. ExpLicenseWatchInitWorker başlığı altında konuştuğumuz gibi yine burada da rdtsc instruction’ı aracılığıyla parametreler için random değerler oluşturulmakta.

KeExpandKernelStackAndCallout aracılığıyla çağırılan fonksiyonun içeriğine göz atabiliriz:

Daha önceden konuştuğumuz ve ileride detaylı bir şekilde göreceğimiz beş parametrenin hazırlandığını ve ardından fonksiyonun kendisi çağırıldığını görebiliriz.

Dinamik olarak hazırlanan bu parametrelere göz atabiliriz. KiInitializePatchGuardContext’in ilk defa çağırılması esnasında bu parametrelere göz atalım:

Parametreler hazırlandı ve KiInitPatchGuardContext çağırıldı. Alınan değerler şu şekilde:

  • RCX => 1 (Argument 1 olarak)
  • RDX => 5 (Argument 2 olarak)
  • R8 => 2 (Argument 3 olarak)
  • R9 => 0 (Argument 4 olarak)
  • RSP+0x20 =>1 (Argument 5 olarak)

Argument 5 stack’e aktarılmaktadır:

Hazırlanan random değerleri dinamik olarak gördük. Bunların ne amaçla kullanıldığını da ileride göreceğiz.

Initialization of PatchGuard Context

KiInitPatchGuardContext’ın üç defa farklı yöntemlerle çağırıldığını gördük. Ancak bunun detayına girmeden önce PatchGuard Context’in içeriğini anlamamız gerekecek.

PatchGuard Context

PatchGuard Context yapısı esasen üç bölüme ayrılmıştır: İlk bölüm 0x928 gibi büyük bir boyut olarak ayrılmış ve PatchGuard mekanizmalarının temel içeriğini saklar, ikinci bölüm orijinal verileri daha sonra kullanmak üzere saklayan veri alıcısını saklar ve üçüncü bölüm ise kontrol edilecek veriler hakkında bilgiler saklar.

First Section
  • CmpAppendDllSection

Yapının başlangıç kısmı, doğrudan yapıya kopyalanan CmpAppendDllSection fonksiyonun kodlarını içermektedir. Bu işlemleri loc_140BD65C4 adresinde görebiliriz:

CmpAppendDllSection’ın adresinin alınması ardından r14’e aktarılan yapıya kodlar tek tek aktarılmaktadır. CmpAppendDllSection fonksiyonu daha sonra PatchGuard tarafından bütünlük kontrolü tetiklendiğinde kullanılacaktır.

Bu fonksiyon esasen önemlidir çünkü ana görevi PatchGuard context yapısının geri kalanını random oluşturulmuş bir key ile decryptlemektedir (xor ile):

CmpAppendDllSection fonksiyonu iki parametre almaktadır: QWORD Parameter (Yapı adresi) ve INT64 Parameter (random oluşturulmuş key).


  • NTAPI Pointers

Yapının bir sonraki bölümü NTAPI fonksiyonları ile alakalıdır. Bu kısım, ntoskrnl’den birçok fonksiyonun adresini (100’den fazla) tutar. Daha sonra PatchGuard mekanizmaları bunları yer değiştirmeden bağımsız olarak kullanacaktır.

Bu pointer’lar 0x140BD66C9 adresinde aktarılmaktadır:


  • Global Variables

Yapının bu bölümü de PsLoadedModuleList, KiWaitNever veya KiWaitAlways gibi birçok global değişkeni tutmaktadır. Bu değerler boot sırasında rastgele başlatılır ve bu rastgele değerler daha sonra göreceğimiz gibi PatchGuard DPC Pointer’ları tarafından kodlama ve kod çözme için kullanılacaktır.

Bu kısmı 0x140BD6F20 adresinde görebiliriz:


  • Flags

İlk bölümün son kısmı iseana bayrakları tutar. (Kapsamlı olmayan liste) gibi boolean’ları temsil eden bir bitmap olarak kullanılır:

BIT 6 0x40 Only one processor 
BIT 8 0x100 Use of KiDpcDispatch 
BIT 9 0x200 Use of KiTimerDispatch 
BIT 15 0x8000 Use of KeSetEvent 
BIT 18 0x40000 Related to the ntoskrnl routines checksum 
BIT 20 0x100000 Should DR7 be cleared 
BIT 24 0x1000000 loc_1402F4907 
BIT 27 0x8000000 Should PTE be restored loc_1402F117F 
BIT 28 0x10000000 Scheduling method 7, use of KiInterruptThunk 
BIT 30 0x40000000 loc_1408A836F Again, scheduling method 7 
BIT 31 0x80000000 Result of KiSwInterruptPresent
...

Second Section
  • Critical Kernel Routines Save

Kritik kernel fonksiyon adresleri qword_140E00210 adlı bir yapıya aktarılır. İleride göreceğimiz gibi, bu fonksiyonlar KeBugCheck’i tetiklemeden hemen önce geri yükleniyor. Windows 11’de bu yapının adresleri aşağıdaki gibidir:

Ancak iş burada bitmiyor. İlerideki süreçlerde xHalHaltSystem’in adreside yapıya eklenir. Bu işlemi 0x140BD4449 veya 0x140BD56CF adreslerinde görebiliriz:

Bu bağlamda, yapının tüm üyeleri aşağıdaki gibidir:

Field Routine
Hal xHalHaltSystem
Ntoskrnl KeBugCheckEx
Ntoskrnl KeBugCheck2
Ntoskrnl KiBugCheckDebugBreak
Ntoskrnl KiDebugTrapOrFault
Ntoskrnl DbgBreakPointWithStatus
Ntoskrnl RtlCaptureContext
Ntoskrnl KeQueryCurrentStackInformation
Ntoskrnl KiSaveProcessorControlState
Ntoskrnl memmove
Ntoskrnl IoSaveBugCheckProgress
Ntoskrnl KeIsEmptyAffinityEx
Ntoskrnl VfNotifyVerifierOfEvent
Ntoskrnl _guard_check_icall_no_overrides
Ntoskrnl KeGuardDispatchICall
Third Section
  • Criticial Structure for Checks

Üçüncü kısımda, kontroller için kullanılan önemli bir yapı aşağıdaki gibidir:

struct pg_crit_struct_check_data { 
ULONG64 KeBugCheckType_0x0; 
ULONG64 pData_0x8; 
ULONG32 szData_0x10; 
ULONG32 hash_0x14; 
LONG64 specific[3]; 
};

İlk üye KeBugCheckType ayırt edici yapı tipidir. İkinci ve üçüncü değişkenler ise kontrol edilecek veriler için kullanılmaktadır.

Yapının önemli üyesi ise checksum sonucunu tutan değişkendir ve bu checksum PatchGuard’ın başlatılması sırasında hesaplanır. Cheksum gerçekten önemli çünkü PatchGuard ilgili yapının bütünlüğünü kontrol ettiğinde bu checksum’u referans olarak kullanılacaktır. Son üye ise özel verilerle ilgilidir.

Bu yapıyı loc_140BD3795‘de görebiliriz:

Bu bölüme göz atarsak eğer, yapının hazırlanmaya başlandığını görebiliriz. Yapının üyeleri KeBugCheckEx tarafından sıkça kullanılır. İşte bir örnek:

Initialization of PatchGuard

Artık PatchGuard’ın nasıl initialize edildiğine göz atabiliriz. Esasen PatchGuard’ın initialize edilmesi birkaç yöntem içeririyor ve yukarıda gördüğümüz gibi KiInitPatchGuardContext fonksiyonu kullanmak bu yöntemlerden biri.

Esasen KiInitPatchGuardContext fonksiyonu PatchGuard’ın başlatması için kendi içinde birkaç method kullanıyor. Methodun seçilmesi ise fonksiyona verilen parametrelerle alakalıdır.

KiFilterFiberContext’i decompiler ettiğimizde, KiInitPatchGuardContext fonksiyonu için random hazırlanan parametreleri dinamik olarak görmüştük. Bu fonksiyonun argümanları esasen şu amaçlarla kullanılmaktadır:

  • Argüman 1: DPC methodu için kullanılan index değeri

  • Argüman 2: Zamanlama (scheduling) yöntemi

  • Argüman 3: Kontrol edilecek maksimum boyutu belirlemek için kullanılan rastgele değer

  • Argüman 4: ExpLicenseWatchInitWorker’dan gelen yapının pointer’ı (sadece %4 oranla olduğunu unutmayın)

  • Argüman 5: NT rutinlerinin bütünlüğünün kontrol edilip edilmeyeceğine karar veren bir boolean değer

Dinamik analizle elde ettiğimiz değerleri tekrar hatırlayalım:

  • RCX => 1 (as Argument 1)
  • RDX => 5 (as Argument 2)
  • R8 => 2 (as Argument 3)
  • R9 => 0 (as Argument 4)
  • RSP+0x20 =>1 (as Argument 5)

Bu değerlerin random olarak hazırlandığını burada tekrar hatırlatmak istiyorum ve ileride bu argümanların ne amaçla kullanıldığını tekrar göreceğiz.

Method 0: Inserting a timer

Bu method içerisinde, PatchGuard tarafından PatchGuard Context Structure ve DPC başlatılacaktır. Devam etmeden belirtmem gerekiyor ki DPC hakkında bilginiz yoksa DPC ile alakalı dökümanıma hızlı bir şekilde göz atabilirsiniz.

İlk olarak bir timer ayarlanmaktadır. Daha sonra bu timer KeSetCoalescableTimer tarafından eşleşmeye alınmaktadır. Bu kısmı 0x140BF97F6 yakınlarında görebiliriz:

Daha sonrasında hızlı bir şekilde eşleşmeye alınan timer tarafından birinci parametre aracılığıyla (2 - 2'10 arasında) DPC başlatılacaktır. TolerableDelay parametresi random olarak 0 ile 0.001 saniyeye denk gelecek şekilde ayarlanmaktadır.

Method 1 and 2: Hiding DPC
  • AcpiReserved (0x140BF97DD)

Method 1 ve 2’in fikirleri aynıdır. Method 1’de DPC pointer’ı PRCB yapısının AcpiReserved bölümüne aktarılmaktadır:

  • HalReserved (0x140BF97C9)

Aynı şekilde Method 2’de ise DPC’in pointer’i daha önce yakından gördüğümüz PRCB yapısının HalReserved alanına saklamaktadır:

HalReserved alanı esasen bir array’dır ve PRCB’nin 0x48 ila 0x88 bölümünü kapsar:

kd> dt nt!_KPRCB fffff8026f748180+0x80
   +0x000 MxCsr            : 0
   +0x004 LegacyNumber     : 0 ''
   +0x005 ReservedMustBeZero : 0 ''
   ...
   +0x048 HalReserved      : [8] 1
   +0x088 MinorVersion     : 0x7000
   +0x08a MajorVersion     : 0xee23
   ...

DPC ise bu array’ın 0x80 offset’inde saklanmakta.

Daha sonra bu DPC HalpMcaQueueDpc tarafından eşleşmeye alınacaktır.

Method 3: System Thread

Method 3’ün gerçekleşebilmesi için KI_FILTER_FIBER_PARAM yapısına ihtiyaç duyar. Bu yapının içerisinde PsCreateSystemThread fonksiyonun adresi içermekte. Yapının üyelerini hatırlayın. Bu fonksiyon aracılığıyla yeni bir system thread oluşturulmakta. Bu kısmı esasen 0x140BFAC24 (Bu fonksiyonu Tetrane gibi Pg_InitMethodSystemThread olarak adlandıracağım) adresinde görebiliriz ve bu fonksiyona göz atarsak, PsCreateSystemThread rutinine 0x14068F650 adresini StartParameter için argüman olarak verilemekte ve bu fonksiyon adresi KI_FILTER_FIBER_PARAM’de bulunmaktadır:

Pg_InitMethodSystemThread fonksiyonu direkt olarak KiInitPatchGuardContext içerisinde çağırılmaktadır:


  • Pg_InitMethodSystemThread’dan Obfuscation Mekanizması

Bu fonksiyonun içinde ayrıca tespiti engellemek için kullanılan ilginç bir obsufcation mekanizması bulunmaktadır. Genel olarak, PatchGuard’ın thread’lerini tanımlamak için kullanılan bazı yöntemler ETHREAD yapısından StartAddress ve Win32StartAddress alanların kontrol etmesine dayanmaktadır. Buna karşı koymak için, Windows sistemlerde bu alanların üzerine kasıtlı olarak yaygın, şüpheli olmayan fonksiyon adresleri yazılır.

System Thread’ın oluşturulması ardından hızlı bir şekilde PatchGuard ETHREAD ile alakalı bir pointer alır ve bu yapının StartAddress ve Win32StartAddress alanlarını işlemeye başlar. Esasen zararsız görünen bir fonksiyonun adresini bu alanlardan biri içinde değiştirir:

off_140C63540 ise birkaç fonksiyon adresi tutan bir array’dır:

Array’ın son üyesi doğru olan adrestir. Böylece ETHREAD’ın bu alanların doğru olma olasılığının yedide bir olduğunu görebiliriz.

Method 4: Asynchronous Procedure Call

Method 4’te, bir PatchGuard context ve bir APC structure başlatılır ve mevcut bir system thread’e eklenir. NormalArgument için XHalTimerWatchdogStop’ın adresi verilirken, KernelRoutine parametre için ise KiDispatchCallout verilir

Bu parametrelerin 0x140BD9617‘de ayarlandığını görebiliriz:

Method 5: Hook a Regular DPC

Method 5, esasen method 3 gibi KI_FILTER_FIBER_PARAM yapısına ihtiyaç duyar. Eğer bu yapı yoksa method 0’a geri dönülecektir. Bu yapı içerisinden KiBalanceSetManagerPeriodicDpc kullanılacaktır. Bu değişken KDPC yapısına işaret etmektedir ve sistemin başlatılması esnasında KiInitSystem tarafından ayarlanmaktadır. Bunu 0x140C1A303 adresinde izini sürebiliriz:

loc_140C1A13A:
...
mov dword ptr cs:KiBalanceSetManagerPeriodicDpc, 113h
mov cs:KiBalanceSetManagerPeriodicDpc.DpcData, rdi
mov cs:KiBalanceSetManagerPeriodicDpc.ProcessorHistory, rdi

Bu yöntemi ilginç kılan bir detay, sistemin KiUpdateTime aracılığıyla düzenli olarak - kabaca her saniye - sıraya koyduğu meşru bir DPC’yi hedeflemesidir. 0x14026D299‘dan:

PatchGuard bu DPC’yi orijinal rutini, kendi rutini ile değiştirerek hook’lar. Sonuç olarak, sistemin orijinal rutinini çalıştırmak yerine, PatchGuard bunu her 120 yürütmede bir kendi özel DPC rutini ile değiştirir.

Bu, PatchGuard’ın sistemin normal davranışını bozmadan periyodik olarak bütünlük kontrollerini yapmasını sağlar - ve daha da önemlisi, tespit edilmesini önlemeye yardımcı olur. Olayın daha iyi anlaşılması için hazırlanmış bir şema:

Method 7: Nothingless

(Çalışmada Method 6 için bir açıklama yoktur.)

KiInitPatchGuardContext’a index olarak 7 verildiğinde esasen kendisi iki şey gerçekleştirecektir: Bunlardan ilki bir DPC initialize edecektir ve ardından bu DPC’i eşleşmeye alacaktır ancak bu DPC’i hiçbir şekilde kullanmadan geri silecektir. İkincisi ise bir PatchGuard Context yapısı oluşturucaktır edecektir. Bu oluşturulan yeni PatchGuard yapısı esasen global bir yapıdır.

Method 7’de bir DPC başlatılır ve rutin KiInterruptThunk veya KiMachineCheckControl fonksiyonlarından biri olarak tanımlanır. Bu fonksiyonlar için 16 stub bulunmaktadır. KiInterruptThunk fonksiyonu esasen KiInitPatchGuardContext tarafından kullanılmaktadır ancak bazı PatchGuard mekanizmaları KiMachineCheckControl fonksiyonu da kullanılmaktadır.

İlk olarak, rdtsc aracılığıyla 0 ila 0xf arasında bir random değer oluşturuluyor ve bu random değer stub’lar için bir index olarak kullanılıyor. Bu kısımdan bir örneği 0x140BCBADF‘de görebiliriz:

Bu kısımda KiMachineCheckControl’dan bir stub belirlenmektedir. Alınan random değer 3 bit sağ kaydırılıyor ve 0 ila 0xF değer arasından bir stub index belirleniyor.

KiInterruptThunk içerisinde iki farklı stub vardır. Bunlardan biri (0x1406AFE60):

$$1 proc near
	xor eax, eax
	mov dr7, rax
	jmp FsRtlTruncateSmallMcb
$$1 endp

Ve ikincisi (0x1406AFE70):

$$2 proc near
	xor     eax, eax
	nop
	nop
	nop
	jmp     FsRtlTruncateSmallMcb
$$2 endp

Bu iki stub’un boyutlarının aynı olduğunu da buraya söylemem gerekiyor (NOP instruction’ları sayesinde).

Method’a tekrar dönersek, esasen bu method hiçbir şey yapmıyor gibi gözüküyor ve problemimiz de tam olarak bu. Diğer method’lara kıyasla amacı olmayan bir yöntem gibi gözüksede bu method için gerçekten önemli kısımlar var. Şimdi bu method’a yakından bakacağız.

  • İlk Test Bayrağı: 0x8000000

KiInterruptThunk’ı kontrol edersek, 0x140BF8859 adresinde 0x8000000 yani bir test bayrağı olduğunu görebiliriz:

loc_140BF8859:
...
test    [rsp+2568h+var_150], 8000000h
jz      short loc_140BF88E1

Bu test bayrağı 0x140BD5661‘de oluşturuluyor:

Tetrane’in makalesinde aktardığına göre, bu kısımda Windows 11’den farklı olarak Windows 10 RS4’te 0x10000000 bayrağı kullanılmakta. 0x1408A82901 adresinden bunu görebiliriz:

loc_1408A8291:
test [rsp+2238h+var_140], 10000000h
jz short loc_1408A830E

Windows sürümlerine göre bayraklar değişiklik gösterebiliyor.

  • İkinci Test Bayrağı: 0x20000000

Ayrıca ikinci bir test bayrağı da var. 0x140BF893D‘de görebildiğimiz gibi, method dispatcher’in 0x20000000 bayrağı ile alınıp alınmayacağına karar verir:

Aynı şekilde bu ikinci bayrak ise 0x140BD7937‘de ayarlanıyor:

Farklı detay ise, r13d’nin 7 değeri ile kontrol edildiğine dikkat edin. Anlayabileceğimiz gibi, seçilen method’un 7 olup olmadığı kontrol ediliyor. Ardından birkaç modifikasyonlar ile ikinci bayrak oluşturuluyor.

Windows 10’da ise bu kısımda 0x40000000 bayrağı kullanılmakta (0x1408A836F):

  • Üçüncü Kontrol: Stack Değişkeninden

Son kontrol ise 0x140BF9E3F adresinde üçüncü kontrolü görebiliriz. Belirli parametrelerle KeSetEvent çağırılıp çağırılmayacağı karar veriliyor:

İlk olarak KeSetEvent’in çağırılıp çağırılmayacağı karar veriliyor:

mov     rax, [rsp+2568h+var_24C0]
test    rax, rax
jz      short loc_140BF9E70

Bu işlem ise rax’a verilen değer aracılığıyla kontrol ediliyor. var_24C0‘in ayarlanmasını 0x140BF677A adresinde gerçekleşiyor:

Set_Variable_To_Zero:
; Address: 0x140BF677A
xor     eax, eax
mov     [rsp+2568h+var_24C0], rax

Direkt olarak sıfır yani NULL ile ayarlanmakta. Ancak bu durum her zaman böyle değildir. Eğer method 3 seçilmişse ve system thread oluşturulduysa bu değişkene PsCreateSystemThread’e verilen StartContext pointer’ı verilecektir. Bu da esasen, Method 3 kısmında görüldüğümüz gibi Pg_InitMethodSystemThread fonksiyonun direkt çağırıldığı kısımda gerçekleşiyor:

Böylece bu değişken NULL olarak ayarlanmazsa, oluşturulan thread bu nesneyi bekleyecek ve KeSetEvent ise notify edecektir.

Daha önce tartıştığımız gibi, KiInitPatchGuardContext’e 7 indeksi verildiğinde küresel bir PatchGuard bağlam yapısını başlatır. ve bu küresel PatchGuard bağlamı aslında diğer yeni yöntemler tarafından kullanılır (Windows 8.1 ile karşılaştırıldığında). Bu yapıya 0x140FC4A48adresindeki MaxDataSize isimli yanıltıcı isme sahip global yapıdan erişilebilir:

ALMOSTRO:0000000140FC4A48 MaxDataSize     dq 0

Bu değişkenin bir yapı olarak kullanıldığını KiSwInterruptDispatch fonksiyondan görebiliriz (0x14050EB4D’den):

KiSwInterruptDispatch proc
...
mov     rdi, cs:MaxDataSize
mov     r13, rcx
test    dword ptr [rdi+9DCh], 100000h
jz      short loc_14050EB6A

Kontrol edilen değere dikkat edin. Farklı bir test flag’ı kontrol edilmekte.

Aynı zamanda KiInitPatchGuardContext’te (0x140BF86A2 adresinde), yapının adresi üzerine işlemler yapıldığını görebiliriz:

loc_140BF86A2:
...
mov     rax, cs:MaxDataSize
lea     r13d, [rbx-7]
mov     ecx, 0FFFFF000h
lea     r9, [r15+20h]
add     edi, r13d
mov     r8d, edi
shl     r8, 6
add     r9, r8
mov     [r8+r15+8], rax

Burada biraz duralım ve nefes alalım. Bir sonraki konuda bu fonksiyonun diğer parametrelerine yakından göz atarak devam edeceğiz.

Last updated on