PatchGuard Analysis - Part 3
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:
- 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.
- 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.