PatchGuard Analysis - Part 3

PatchGuard Analysis - Part 3

Ağustos 6, 2025·0xbekoo
0xbekoo

Triggering Checks

Daha önce de gördüğümüz gibi, bazı çeşitli yöntemler context’leri ayarlamak için kullanılıyor. Bu bölümde, bu context’lerin nasıl tetiklendiğine göz atacağız.

DPC Execution

Bir kontrolün tetiklenmede kullanılan en çok yöntemlerden biri ise DPC kullanmaktır. Aşağıdaki listeden seçilen bir rutin aracılığıyla bu işlem gerçekleştirilir:

0 CmpEnableLazyFlushDpcRoutine
1 ExpCenturyDpcRoutine
2 ExpTimeZoneDpcRoutine
3 ExpTimeRefreshDpcRoutine
4 CmpLazyFlushDpcRoutine
5 ExpTimerDpcRoutine
6 IopTimerDispatch
7 IopIrpStackProfilerDpcRoutine
8 KiBalanceSetManagerDeferredRoutine
9 PopThermalZoneDpc
10 KiTimerDispatch OR KiDpcDispatch
11 KiTimerDispatch OR KiDpcDispatch
12 KiTimerDispatch OR KiDpcDispatch

0 ila 9 index arasındaki fonksiyonlarda kontrollerin başlatılması için bir exception handler kullanılıyor. KiTimerDispatch ve KiDpcDispatch fonksiyonlarında herhangi bir exception olmadan direkt olarak DPC çağırılıyor. Bir başka deyişle anlatmak gerekirse bu fonksiyonlar kendi işlerini yaparken aynı zamanda PatchGuard bunları hijack ederek kendini saklıyor.

Non-Canonical DeferredContext Pointer

Artık ilk hedef, bu rutinlerden biri çağrıldığında stacked DPC’nin bir PatchGuard DPC’si mi yoksa normal bir DPC mi olduğunu belirlemektir. Fonksiyonların hepsi bir DPC structure pointer’ı parametre olarak alır ve bu gelen DPC’in PatchGuard tarafından mı geldiğini belirler.

Ayrıca kurnazlık da var. X64 sistemlerde, bellek yönetimleri belirli kurallara uymak zorundadır, PatchGuard durumuna baktığımızda, kendisi kuralı çiğnemektedir. Normal durumda, adresler aşağıdaki gibi görülür:

  • Canonical (Normal): 0xFFFF803C98DABD1 (0xFFF ile)
  • Non-Canonical (Anormal): 0x803C98DABD1 (0xFFF olmadan)

Kuralı çiğneyerek, PatchGuard tam olarak Non-Canonical adresi kullanır. Böylece, PatchGuard tarafından gelen DPC için kontroller bu Canonical olmayan adres ile gerçekleşir.

Kontrol KDPC.DeferredContext ile ilişkili parametre ile tamamlanır; elde edilen adresin canonical adres mi yoksa değil mi bu kontrol ediliyor. Bu arada kontrol gerçekten çok basit. Mesela ExpCenturyDpcRoutine fonksiyonun 0x1403556C5 adresindeki kontrole bakabiliriz:

mov rax, rbx
sar rax, 2Fh
inc rax
cmp rax, 1
jbe ContextIsNotPatchGuard

mov eax,1
jmp Exit

ContextIsNotPatchGuard:
xor eax,eax

Exit:
ret

Eğer ilgili adres non-canonincal ise KiCustomAccessRoutineX çağırılıyor. Mesela aynı şekilde yine ExpCenturyDpcRoutine fonksiyonundan görebiliriz:

KiCustomAccessRoutine fonksiyonlarına da hemen değinelim. Esasen 0 ila 9 arasında KiCustomAccessRoutineX fonksiyonları bulunmaktadır. Bu fonksiyonların tek amacı yine aynı şekilde 0 ila 9 arasındaki KiCustomRecurseRoutineX tek tek çağırmaktır. Kafanız karışmasın bir sonraki başlıkta bu fonksiyonlara detaylıca göz atacağız.


Triggering the Exception Handler

Önceden dediğim gibi KiCustomAccessRoutineX fonksiyonları KiCustomRecurseRoutineX çağırır. Esasen RecurseRoutine’ler iki parametre alır: Bir Sayaç ve Canonical olmayan DeferredContex adres. Sayaç, DeferredContext’in son iki biti ve artı bir ile elde edilerek ayarlanıyor.

KiCustomRecurseRoutineX fonksiyonları bir döngü içinde çok basit bir işlem gerçekleştiriyor: Sayacı bir azaltıyor ve sonraki KiCustomRecurseRoutineX fonksiyonu çağırıyor. Bu, sayaç sıfıra ulaşana denk devam ediyor. İşte bir basit diyagram:

Dediğim gibi işlemler basit, parametre olarak verilen sayaç KiCustomRecurseRoutineX tarafından bir azaltılır ve diğeri çağırılır. İşte bir örnek kod:

Veya bir pseudocode:

int counter = (DeferredContext & 0x3) + 1;  

void KiCustomRecurseRoutine0(int count) { 
if (--count == 0) { 
	// BOOM! 
	*(int*)0xDEADBEEF = 0; 
} else { 
	KiCustomRecurseRoutine1(count); 
}

void KiCustomRecurseRoutine1(int count) { 
if (--count == 0) { 
	// BOOM! 
	*(int*)0xDEADBEEF = 0; 
} else { 
	KiCustomRecurseRoutine2(count); 
}
...

Buradaki ana fikir, PatchGuard’ın sayacı azaltmaya devam edeceği ve sonunda geçersiz bir işaretçinin dereferenced edileceğidir. Her orijinal fonksiyona bağlı olarak, try/except/finally işleyicilerinin bir kombinasyonu sonunda PatchGuard Context Structure’nin decrypt edilmesine yol açacaktır.


PatchGuard Context Decryption

İstisna işleyici PatchGuard Bağlam Yapısının ilk katmanının şifresini çözmekten sorumludur. Esasen iki kabaca iki şifre çözme katmanı vardır:

  • First Layer

Decrypt işleminin ilk katmanı tüm context structure’a odaklanır. Bunu yapmak için aşağıdaki listede özetlenen birden fazla farklı kod vardır:

Index Routine Layer Encryption
0 CmpEnableLazyFlushDpcRoutine Method 1
1 ExpCenturyDpcRoutine Method 1
2 ExpTimeZoneDpcRoutine Method 1
3 ExpTimeRefreshDpcRoutine Method 2
4 CmpEnableLazyFlushDpcRoutine Method 1
5 ExpTimerDpcRoutine Method 2
6 IopTimerDispatch Method 2
7 IopIrpStackProfilerDpcRoutine Method 1
8 KiBalanceSetManagerDeferredRoutine Method 1
9 PopThermalZoneDpc Method 2
10-12 KiTimerDispatch Method 1 + hardcoded key
10-12 KiDpcDispatch No 1st layer encryption

Bu encryption/decryption rutinleri KiWaitNever and KiWaitAlways değişkenlerinden random değerler kullanır. Bu global değişkenler random değerler tutar ve bu değerler boot esnasında üretiliyor. KiInitPatchGuardContext ise bu değerleri PatchGuard Context Structure encrypt etmek için kullanıyor. PopThermalZoneDpc fonksiyonun 0x1406C8E10 adresinden bir örneğine göz atabilirsiniz. Ayrıca, structure ile etkileşime geçmek isteyen bir saldırganın da bu global değişkenleri kullanabileceğini unutmayın.


  • Before Second Layer: First Layer (With a Half)

İkinci decrypt katmanından önce burada PatchGuard context structure’ın başındaki dört byte yeniden yazılır. İronik bir şekilde, bu byte’lar daha sonradan structure’ı decrypt etmek için bir şifre olarak kullanılacak. Örneğin, bunu ExpCenturyDpcRoutine’in 0x1406C587C adresinde görebiliriz:

mov byte ptr [r11], 2Eh
mov byte ptr [r11+1], 48h
mov byte ptr [r11+2], 31h
mov byte ptr [r11+3], 11h

Ayrıca 0x1406C9064 (PopThermalZoneDpc) adresinde görebiliriz ki, iki hardocoded değerle xor işlemi gerçekleştirilir:

*PatchGuardCtx = 0xAD1B6FF5 ^ 0xBC2A27DB; ; Result = 0x1131482E

ExpTimeZoneDpcRoutine’de, doğrudan bir DWORD32 dğişkeni yeniden yazar ve çağırır (0x1406C9508):

mov     qword ptr [rbp+38h], 31482E11h
mov     rdx, [rbp+38h]
shl     edx, 18h
mov     rcx, [rbp+38h]
shr     rcx, 8
or      rcx, rdx
mov     [rbp+38h], rcx ; 0x1131482E
mov     rax, [rbp+38h] 
mov     [r11], eax

; Call 0x1131482E
xor     r9d, r9d
xor     r8d, r8d
mov     rdx, [rbp+40h]
mov     rcx, r11
mov     rax, r11
call    _guard_dispatch_icall_no_overrides

Bu noktada, XOR kullanımı tipik Just-In-Time kodudur ve etrafındaki kod çok açık olmadığı için bu bir olasılıktır.


  • Second Layer

Bu katman ise CmpAppendDllSection ile alakalı. Yapının ilk bölümüne kopyalanan bu fonksiyonu tekrar hatırlayalım. Bu fonksiyon direkt olarak çağırılmakta.

Esasen bu fonksiyonun iki bölümü bulunmakta. Bunlardan biri o anda sahip olduğu instruction’u yeniden yazar ve sonraki instruction’u decrypt’ler:

CmpAppendDllSection proc near
; Rcx points to address which is current instruction
xor [rcx], rdx
xor [rcx+8], rdx
xor [rcx+10h], rdx
xor [rcx+18h], rdx
xor [rcx+20h], rdx
xor [rcx+28h], rdx
xor [rcx+30h], rdx
xor [rcx+38h], rdx
...

İkinci kısmı ise decryption döngüsü ile alakalıdır. Bu kısımda bir döngü ile tüm yapıyı decrypt eder:

loc_140BFC65F:
xor [rdx+rcx*8+0C0h], rax
ror rax, cl
btc rax, rax
loop loc_140BFC65F

Ve daha sonra the context structure kullanıma hazır olacaktır.


Passing Control to Verification Routine

Decryption işleminden sonra iki farkı fonksiyon sonradan çağırılacaktır. İlk fonksiyon direkt olarak yapıdan çağırılıyor ve bu fonksiyon sub_140BCADF0 rutinin bir kopyasıdır. Fonksiyon iki işlem gerçekleştiriyor:

  • PatchGuard Context Structure’ı ve 47 Routine’i Bütünlüğünü Doğrulamak

Fonksiyonun ana görevi yapının ve 47 rutinin bütünlüğünü kontrol etmektir. Örneğin ilk kodda kontrol edilecek ilk şey, KeBugCheckEx’i çağıran ExpWorkerThread’in epilogue’dır (0x1402C375B):

loc_1402C375B:
mov     r9, rsi
mov     [rsp+78h+BugCheckParameter4], 0FFFFFFFFFFFFFFFFh ; BugCheckParameter4
mov     r8, r15         ; BugCheckParameter2
mov     edx, 5          ; BugCheckParameter1
mov     ecx, 0E4h       ; BugCheckCode
call    KeBugCheckEx

İkinci kontrol ise ExpWorkerThread‘un exception handler’ıdır ve son kontrol ise KeIpiGenericCall ile alakalıdır.


  • WORK_QUEUE_ITEM Structure Başlatma

0x140BCBB8C adresinde görebileceğimiz gibi, bir WORK_QUEUE_ITEM yapısı başlatır:

StubIndex = Ctx + *(Ctx + 2064);
if ( (*(Ctx + 2520) & 0x8000000) != 0 ) {
	GetRandomValue = __rdtsc();
    RotatedValue = __ROR8__(GetRandomValue, 3); // Shift random value 3 bits right
    HashMix = ((RotatedValue ^ GetRandomValue) * 0x7010008004002001uLL) >> 64;
    StubIndex = KiMachineCheckControl + 16 * (((RotatedValue ^ GetRandomValue) ^ HashMix) & 0xF);
  }
...

*(Ctx + 1992) = StubIndex; /* 0x140BCBB8C */
*(Ctx + 2000) = v137;
*(Ctx + 1976) = 0;

WorkerRoutine, bir doğrulama rutinini WorkItem olarak çağıracak üç stub arasından seçilir. Bu üç stub şunlardır:


  1. Daha önce gördüğümüz gibi (method 7’yi kontrol edin), method 7 seçilirse KiMachineCheckControl‘den rastgele bir stub seçilir. Bu bağlamda Parametre alanı PatchGuard context’isine işaret eder.

  1. PatchGuard Context Structure’daki FsRtlUninitializeSmallMcb kopyası. Bu bağlamda Parametre aynı zamanda PatchGuard context’i sub_1401812E0 olacaktır.

İkinci çağrı ExQueueWorkItem ile ilgilidir. Hazırlanan WORK_QUEUE_ITEM yapısı parametre olara bu fonksiyona verilir. Bunu RtlpComputeEpilogueOffset’ten 0x140520E05 adresinde görebiliriz:

RtlpComputeEpilogueOffset fonksiyonu, DPC Execution yöntemi ile ilgili olan aşağıdaki fonksiyonlar tarafından çağrılır:

Ayrıca FsRtlTruncateSmallMcb tarafından da çağrıldığına dikkat edin. FsRtlTruncateSmallMcb içerisinde, KiCustomAccessRoutine0 rutininden sonra iki kez çağırılmakta (0x1406C9B20 ve 0x14068FC7E adreslerine göz atın).

System Thread Method

Daha önce tartıştığımız gibi, PatchGuard method 3’te bir System Thread oluşturur. Pg_InitMethod3SystemThread doğrudan KiInitPatchGuardContext içinde çağrılır.

Triggering the Exception Handler

Bu yöntemde PsCreateSystemThread exception handler aracılığıyla çağrılmaktadır. Exception Handler’i tetikleyen kodlar gerçekten tuhaf.

Yolculuğumuza 0x140BD7F02 adresinden başlayabiliriz. Burada KiInitPatchGuardContext tarafından cpuid komutu çalıştırılıyor:

loc_140BD7F02:
sti
xor     ecx, ecx
mov     eax, 80000008h
cpuid
mov     edi, eax
shr     edi, 8          ; Shift 8 bytes
mov     [r14+940h], dil ; Pass the result to the structure. 

İlk olarak, eax 0x80000008 değerini alır, ardından cpuid çalıştırılır. Yürütmeden sonra, sonuç 8 bayt sağa kaydırılır ve elde edilen sonuç yapının 0x940 ofsetine geçirilir.

0x80000008 değeri linear/physical address size data‘a karşılık gelir. Felix Clouiter’dan alıntı:[5]

“İki tür bilgi döndürülür: temel ve genişletilmiş fonksiyon bilgileri. CPUID.EAX için girilen bir değer, o işlemci için temel veya genişletilmiş fonksiyon için maksimum giriş değerinden yüksekse, en yüksek temel bilgi yaprağına ait veriler döndürülür. Örneğin, bazı Intel işlemcileri kullanıldığında, aşağıdakiler doğrudur: … CPUID.EAX = 80000008H ( linear/physical address boyutu verilerini döndürür. ) “

Daha sonra sonucu görmek istedim ve MASM Assembly ile bir driver oluşturdum:

extern DbgPrintEx:PROC

.data
	UnloadedMsg db "Driver Unloaded!",0

.code

UnloadDriver PROC
	sub rsp,28h
	lea r8,[UnloadedMsg]
	xor rdx,rdx
	xor rcx,rcx
	call DbgPrintEx
	add rsp,28h

	xor rax,rax
	ret
UnloadDriver ENDP

DriverEntry PROC
	; /* Prepare UnloadDriver */
	mov rax,rcx
	lea rcx,[UnloadDriver]
	mov qword ptr [rax+68h],rcx

	mov eax,80000008h
	cpuid

	mov edi,eax
	shr edi,8

	xor rax,rax
	ret
DriverEntry ENDP
END

İşte sonuç:

boyut sonucu 0x302d olduğunu görebiliriz.. Kaydırma işleminden sonra edi registeri 0x30 değerini alıyor:

kd> t
cpuid!DriverEntry+0x17:
fffff800`76591035 c1ef08          shr     edi,8
kd> r edi
edi=302d
kd> t
cpuid!DriverEntry+0x1a:
fffff800`76591038 33c0            xor     eax,eax
kd> r edi 
edi=30

Böylece, 0x940 ofsetinin 0x30 değerini içerdiğini görebiliriz.

Bu değer 0x140BFAEAD adresinde Pg_InitMethod3SystemThread fonksiyonunda exception’u tetiklemek için kullanılıyor:

loc_140BFAE88:
...
mov     al, [rsi+940h]  ; Get 0x30
dec     al
movzx   r10d, al        ; r10d -> 0x2F
mov     r11d, 3Fh ; '?'
sub     r11d, r10d      ; Result: 0x10
...
div     r11

Değer 0x3F değeri ile çıkarılır, böylece eğer maksimum sanal adres boyutu 0x3F ise rbx 0 olur ve exception handler tetiklenir:

Breakpoint 0 hit
nt!KiFilterFiberContext+0x29443:
fffff804`a6f5e973 49f7f3          div     rax,r11

kd> r r11
r11=0000000000000010
kd> r r11 = 0 

kd> g
Breakpoint 1 hit
nt!KiFilterFiberContext+0x2949b:
fffff804`a6f5e9cb 803dbf70370000  cmp     byte ptr [nt!KdDebuggerNotPresent (fffff804`a72d5a91)],0

Div instructionu loc_140BFAF3A fonksiyonu tetikler, böylece PsCreateSystemThread çağrılır.

Ayrıca 0x940 ofsetinde tutulan bu değer FsRtlMdlReadCompleteDevEx tarafından da kullanılıyor. Bu makalede bu fonksiyondan bahsetmedim ama bu fonksiyon PatchGuard rutinidir ve gerçekten uzun kodlar içerir ve anladığım kadarıyla ntoskrnl’ın en uzun rutini. Aynı şekilde bu değerde bir exception tetiklemek için kullanılıyor (0x140BC462C):

Matematik işlemleri için kullanılan bu register’lar 0x140BC45B8 adresinde hazırlanıyor:

loc_140BC45B8:
mov     r13d, 28h ; '('
lea     rcx, [r12+918h]
mov     r8d, r13d
lea     rdx, [rbp+8D0h+var_668]
lea     r9d, [r13-23h]
lea     r10d, [r13-27h] ; R10D: 0x1

Ama nereye yönlendirildiğinden emin değildim.

Creating New Thread

System Thread 0x140BFAF46 adresinde oluşturuluyor:

KI_FILTER_FIBER_PARAM yapısının PsCreateSystemThread’in pointer’ını içerdiğini ve PatchGuard tarafından kullanıldığını hatırlamakta fayda var. PsCreateSystemThread’e verilen StartContext parametresi, aşağıdaki gibi tanımlanabilen yeni bir yapı türüne ait bir pointer’dır:

struct pg_StartContext
{
	ULONG64 Event; Just a pointer to the event in the very
	; ... same structure
	ULONG64 Random_ShouldRunKeRundownApcQueues; set at 0x140BFAE17
	ULONG64 unknown_0x10;
	KEVENT_ Event;
};

The Even Object exception handler’dan önce ayarlanır ve sub_14068F650 fonksiyonunda KeWaitForSingleObject tarafından bu object 0x14068F6F3 adresinde sinyal göndermesini bekler:

Bu event KiInitPatchGuardContext’in sonunda bildirilir. Ayrıca, bu yöntem ilk kez kullanıldığında zaman aşımı olmadığını (0 olarak ayarlanmıştır) unutmayın. Pg_InitMethodSystemThread fonksiyonu yapıya bir pointer döndürür ve event KiInitPatchGuardContext’in sonunda 0x140BF9E3F adresinde bildirilir:

Ardından decryption işlemi başlatılacaktır.

The Decryption Process

Decryption işlemi esasen DPC’ler aracılığıyla kullanılan yöntemle aynıdır. İlk aşamada KiWaitNever ve KiWaitAlways kullanılır ve ikinci aşama tıpkı DPC’de olduğu gibi CmpAppendDllSection’ın kopyası tarafından gerçekleştirilir ve sonunda doğrulama rutini çağrılır.

Post verification for this case only

Doğrulama rutini sona erdiğinde, context KeDelayExecutionThread veya KeWaitForSingleObject aracılığıyla bekleme durumuna geri alınacaktır ancak bu sefer 2’ ile 2'10” arasında bir zaman aşımı ayarlanacaktır. FsRtlMdlReadCompleteDevEx’ten:

loc_140BC26F1:
	...
	mov r10, [rbp+8D0h+arg_8] ; Get Timeout

[...]

loc_140BC274A:
	mov r9, [rbp+8D0h+var_950] ; Get the address of KeDelayExecutionThread

loc_140BC274E:
	test r10, r10 ; If the timeout is 0, then execute KeDelayExecutionThread
	jz short loc_140BC2770

	; Call KeWaitForSingleObject
	mov rax, [rbp+8D0h+var_8F0] ; Get the address of the function
	lea r8, [rbp+8D0h+var_850]
	mov edx, r14d
	mov [rsp+9D0h+BugCheckParameter4], r10 ; Pass the timeout
	mov rcx, rsi
	call KeGuardDispatchICall

loc_140BC2770:
	; Call KeDelayExecutionThread
	xor edx, edx
	test r11, r11
	jnz short loc_140BC278A
	lea r8, [rbp+8D0h+var_850]
	xor ecx, ecx
	mov rax, r9 ; Pass the address KeDelayExecutionThread to rax
	call KeGuardDispatchICall

Timeout 0x140BC24E1 adresinde başlatılır:

loc_140BC24E1:
...
mov r10, [rsi+0AB8h]
mov ecx, 2
mov eax, [rsi+9DCh]
mov r14d, [rsi+804h]
mov r11, [rsi+0A40h]
mov r12d, [rsi+828h]
mov [rbp+8D0h+arg_8], r10

Ayrıca bu fonksiyon adresleri 0x140BC252F‘de ayarlanıyor ve aynı adreste fonksiyonlar için kullanılacak değerler random olarak ayarlanıyor:

loc_140BC252F:
mov rax, [rsi+2C8h] ; Get the Address of KeWaitForSingleObject
mov r9, [rsi+170h]  ; Get the address of KeDelayExecutionThread
mov [rbp+8D0h+var_8D8], rax
mov rax, [rsi+340h]
mov [rbp+8D0h+var_8F0], rax
mov [rbp+8D0h+var_950], r9
APC insertion

Daha önce gördüğümüz gibi, dördüncü yöntem System Thread kuyruğuna bir APC ekler. System Thread, StartAddress Parametresi olarak PopIrpWorkerControl pointer’ına sahip olması gerekiyor. Bunu 0x140BFB2AF‘dan görebiliriz:

Ayrıca KiInsertQueueApc’ye verilen KernelRoutine parametresinin KiDispatchCallout olduğunu hatırlayın.

DPC ve SystemThread yöntemlerine benzer şekilde, bu yöntem iki aşamalı bir şifre çözme işlemi kullanır ve context’in başlangıç adresini sabit bir XOR değeriyle yeniden yazar. APC’ler hızlı bir şekilde tamamlandığından ötürü için bu yaklaşım oldukça hızlıdır.

KiSwInterruptDispatch Method

Bu yöntem, Global PatchGuard Context Structure’ı kullanır. Bu, şifre çözme işlemi olmadığı ve doğrulama rutininin KiSwInterrupt’ın bir noktasında doğrudan çağrıldığı anlamına gelir.

Breadcrumbs

Breadcrumbs yöntemleri özeldir çünkü kontrollerini başlatmak için belirli bir kod olmadan direkt olarak otomatik olarak çalışırlar. Ancak, her zaman çalışmazlar. Örneğin, CcInitializeBcbProfiler’ı hatırlayın. Ya ilgili bir fonksiyonu kullanarak bir work item’ı kuyruğa alır ya da kendi başına çalışmaya devam eder. Diğer iki doğrulama işlevi, PspProcessDelete ve KiInitializeUserApc, ne zaman çalışacaklarını kontrol etmek için yalnızca basit bir zamanlayıcı kullanır.

Last updated on