什么是PTE Hook
常规的inlineHook思路是直接修改目标函数的代码,使其先执行我们自己的函数,再跳转回来执行原函数。这种Hook是全局的,即Windows中每一个进程一旦调用被Hook的函数,就会受到我们的影响,也很容易被PatchGuard检查到。
因此提出一种新的Hook思路,隔离具体进程的四级页表,使我们的Hook不影响全局。当然具体的Hook方法依然是inlineHook
理论知识
CPU访问物理地址
首先解释一下x64架构下,CPU访问一个虚拟地址(也称线性地址)时的流程。Windows使用了四级页表结构,分别是PML4 (Page Map Level 4)、PDPT (Page Directory Pointer Table)、PD (Page Directory)、PT (Page Table)。我们知道一个虚拟地址共64位,其中高16位是不使用的,因此总共只有48位,这48位按照9-9-9-9-12的结构拆分,前四个9表示四级页表的索引,最后一个12表示一个物理页(通常是4K)的页内偏移,这样CPU就找到了一个虚拟地址对应的物理地址。下面演示一个虚拟地址是如何拆分的。
1比如这样一个虚拟地址 : 0xfffff8037888e000
2高16位不使用,可以看到全部被置为了1,因此有效地址为 : 0xf8037888e000
3转为二进制: 1111 1000 0000 0011 0111 1000 1000 1000 1110 0000 0000 0000
4第一个9位: 1 1111 0000 -> 0x1f0 PML4 Index
5第二个9位: 0 0000 1101 -> 0xd PDPT Index
6第三个9位: 1 1100 0100 -> 0x1c4 PT Index
7第四个9位: 0 1000 1110 -> 0x8e PD Index
8最后12位页内偏移: 00 0000 0000 -> 0 Offset
CR3寄存器是x64架构下一个重要的系统寄存器,其中存储的值是当前进程PML4表的物理基地址,之前已经计算出Index,因此通过CR3拿到基地址后,可以逐层的索引到物理地址。
页表自映射
根据上面讲的,我们明白CPU是这样找到物理地址的:PML4->PDPT->PD->PT->Physical Page,共有4次访问内存。由于CPU寻址是一个高频操作,为了简化这一流程,Windows引入了一个很精妙的机制:页表自映射。
页表自映射,即在PML4这张表的其中一项中,存储PML4表基地址。比如假设在索引0x100处存储:

因此,在访问PML4索引0x100处,其实就是在访问PML4自身,所以被称为页表自映射。
你可能会想,这么做白白浪费了8字节空间,为什么要设置一个这样的页表项,他的精妙之处是这样的:
我们先从PML4入手,我们知道一个48位有效地址的高9位是PML4的索引,这个索引对应的页表项存储的本应该是下一级页表,即PDPT的物理基地址,但是由于引入了页表自映射,因此就必然有一个索引对应的页表项存储的是PML4自身的基地址,不妨将这一索引设为S(在上图中这个索引是0x100)。
我们先采用这样的格式表示一个48位的虚拟地址(A)(B)(C)(D)(Offset),其中A是PML4的索引
当 A == B == C == D == S && Offset == 0 时,CPU访问这个虚拟地址时会出现这样的寻址情况:
PML4 Index == S,指向PML4表物理基地址,PDPT Index == S ,又指向PML4表物理基地址,以此类推,PD和PT也都指向PML4表物理基地址。因此(S)(S)(S)(S)(000000000000)对应的物理地址就是PML4表的物理基地址,不妨把这个虚拟地址称作PML4的表虚拟基地址。用数学公式将之表达:
1PML4_VirtualBase = (S << 39) | (S << 30) | (S << 21) | (S << 12)
2//S << 39 就是将一串48位的二进制数的高9位设为S,或运算就是将每次设置的值连起来放在一个数上
3//假设 S << 39 == 0xF6E0000000000000 S << 30 == 0x0000074000000000
4//那么 (S << 39) | (S << 30) == 0xF6E0074000000000
根据上面的理论,显然当A == B == C == S && D == PDPT_Index && Offset == 0时,也就是说其中一位指向了PDPT的基地址,可以得到PDPT的表虚拟基地址
1PDPT_VirtualBase = (S << 39) | (S << 30) | (S << 21) | (PDPT_Index << 12)
以此类推,我们就可以得到计算最低一级的页表PT的表虚拟基地址的计算公式,这里顺便展示PD的计算公式:
1PT_VirtualBase = (S << 39) | (PDPT_Index << 30) | (PD_Index << 21) | (PT_Index << 12)
2PD_VirtualBase = (S << 39) | (S << 30) | (PDPT_Index) | (PD_Index << 12)
以上是利用页表自映射机制定位到PT表基地址,实际上也可以利用这个原理定位到任意一个VA(虚拟地址)对应的PTE(Page Table Entry,即PT条目,也即PT表中的一项)。
显然以下公式成立:
1PTE_VirtualAddress = (S << 39) | (PML4_Index << 30) | (PDPT_Index << 21) | (PD_Index << 12) | (PT_Index << 3)
2/*
3理解最后的(PT_Index << 3)的这一项,因为每个PTE占8字节,左移3位即乘8,也理解为i*8作为偏移来找到具体的
4的PTE,实际上就是通过自映射机制将PT表当作了原来的物理页面进行查找
5*/
通过我们构造出的这个虚拟地址,可以直接读写PTE而不需要知道其物理地址。
最后再将一个很简洁的通过PML4的表虚拟基地址得到其他三级页表虚拟基地址的公式,我们知道:
1PML4_VirtualBase = (S << 39) | (S << 30) | (S << 21) | (S << 12)
如果将后两个S置为0,即不考虑偏移且往后找一级页表,就可以得到PDPT的表虚拟基地址
1PDPT_VirtualBase = (S << 39) | (S << 30) | (0 << 21) | (0 << 12)
数学上,上面这个式子实际上等价于PML4的虚拟基地址的低21位置0,即
1PDPT_VirtualBase = (PML4_VirtualBase >> 21) << 21
同理:
1PD_VirtualBase = (PML4_VirtualBase >> 30) << 30
2PT_VirtualBase = (PML4_VirtualBase >> 39) << 39
另外,由于上面计算VA对应的PTE公式过于复杂,是主动构造了9-9-9-9-12的虚拟地址结构,还有一个通过基地址+偏移的方法来计算:
1PTE_VirtualAddress = PTE_Base + (VA >> 12) << 3
2/*
3VA >> 12 其实就是 VA / 4096,我们知道一个页就是4096K,所以VA >> 12 就是虚拟页号,每一页对应一个PTE,
4一个PTE是8字节,那么上式就变成了:
5*/
6PTE_VirtualAddress = PTE_Base + Offset * 8
由于Windows开启了基址随机化,页表的虚拟基地址每次开机都不一样,因此需要一个巧妙的方法定位页表基地址,这里给出鹅厂的方法:利用页表自映射定位。由于存在页表自映射这一机制,因此,在PML4表的512个地址中,必然有一个存放着PML4的表物理地址,即CR3的值。所以可以通过映射CR3物理地址的虚拟地址,遍历这个地址页面的512个地址,哪个地址等于CR3的值,哪个地址就是PML4的表虚拟基地址。考虑以下算法:
1ULONG64 GetPml4Base()
2{
3 PHYSICAL_ADDRESS pCr3 = { 0 };
4 pCr3.QuadPart = __readcr3();
5 PULONG64 pCmpArr = (PULONG64)MmGetVirtualForPhysical(pCr3);
6
7 int count = 0;
8 /*
9 *pCmpArr(当前条目,即PML4E的值)表示指向下一级页表的物理地址
10 &0xFFFFFFFFF000即获取Page_Frame_Number 页帧号
11 */
12 while ((*pCmpArr & 0xFFFFFFFFF000) != pCr3.QuadPart)
13 {
14 if (++count >= 512)
15 {
16 return -1;
17 }
18 pCmpArr++;
19 }
20
21 return (ULONG64)pCmpArr & 0xFFFFFFFFFFFFF000;//忽略后12位标志位
22}
理论知识就到这里了。
PTE Hook原理
首先思考一个问题,为什么常规Hook只是修改了一个进程的内核函数,却导致全局的内核函数被修改而被PG检查到。这是因为,用户态下的进程对应的PML4表的高256项都是相同的,指向了共享的内核PDPT,一旦我们修改任意一项PTE,就会产生连锁的影响PT->PD->PDPT->PML4,又由于PML4是共享的,所以全局的函数都被修改了。
因此我们可以先替换掉一项PML4E,这一项PML4E被我们替换后,指向一个伪造的PDPT表,再指向一个伪造的PD表,一个伪造的PT表,最后指向我们Hook的函数。这样被修改的函数只局限于这一个进程,在一定程度上可以规避PG。
此外,还需要考虑大小页的问题。小页指的就是一般的4K大小的页,大页指的是2M的页,是操作系统为了提高访问内存性能而开发的。根据上面的伪造替换规则,一般来说会想到:大页换大页,小页换小页。但是由于页表的物理内存需要是连续的,而Windows的物理内存机制是碎片化的,开机越久越难申请到2M的连续内存,所以考虑使用页表分割的方法,即将一个2M的大页分成512个小页。在如下的代码中,我们只申请了一页连续的内存,用来存放指向512个小页的PTE,每个都用来指向原大页的不同4K部分。
实战
页表分割:
1bool splitLargePages(pde_64* in_pde, pde_64* out_pde)
2{
3 PHYSICAL_ADDRESS MaxAddrPa{ 0 }, LowAddrPa{ 0 };
4 MaxAddrPa.QuadPart = MAXULONG64;
5 LowAddrPa.QuadPart = 0;
6 pt_entry_64* Pt;
7 auto start_pfn = in_pde->page_frame_number;
8 Pt = (pt_entry_64*)MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE, LowAddrPa, MaxAddrPa, LowAddrPa, MmCached);//默认对齐
9 if (!Pt) {
10 DbgPrintEx(77, 0, "failed to alloc contiguous for new pt.\r\n");
11 return false;
12 }
13 for (int i = 0; i < 512; i++) {
14 //分割成小页,构建Pt
15 Pt[i].flags = in_pde->flags;
16 Pt[i].large_page = 0;
17 Pt[i].global = 0;
18 Pt[i].page_frame_number = start_pfn + i;
19 }
20 out_pde->flags = in_pde->flags;
21 out_pde->large_page = 0;
22 out_pde->page_frame_number = va_to_pa(Pt) / PAGE_SIZE;
23 return true;
24}
下面是页表伪造部分的代码:
1typedef struct _PTE_TABLE {
2 void* LineAddress;
3 pte_64* PteAddress;
4 pde_64* PdeAddress;
5 pdpte_64* PdpteAddress;
6 pml4e_64* Pml4eAddress;
7}PTE_TABLE, * PPTE_TABLE;
1bool isolationPageTable(cr3 cr3_reg, void* replaceAlignAddr, pde_64* splitPDE)
2{
3 //均指向4kb内存
4 uint64_t* VaPt, * Va4kb, * VaPdt, * VaPdpt, * VaPml4t;
5
6 PTE_TABLE Table{ 0 };
7 PHYSICAL_ADDRESS MaxAddrPa{ 0 }, LowAddrPa{ 0 };
8 MaxAddrPa.QuadPart = MAXULONG64;
9 LowAddrPa.QuadPart = 0;
10 //这里申请伪造页表的内存
11 VaPt = (uint64_t*)MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE, LowAddrPa, MaxAddrPa, LowAddrPa, MmCached);
12 Va4kb = (uint64_t*)MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE, LowAddrPa, MaxAddrPa, LowAddrPa, MmCached);
13 VaPdt = (uint64_t*)MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE, LowAddrPa, MaxAddrPa, LowAddrPa, MmCached);
14 VaPdpt = (uint64_t*)MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE, LowAddrPa, MaxAddrPa, LowAddrPa, MmCached);
15 VaPml4t = (uint64_t*)pa_to_va(cr3_reg.address_of_page_directory * PAGE_SIZE);
16
17 if (!VaPt || !Va4kb || !VaPdt || !VaPdpt) {
18 DbgPrintEx(77, 0, "failed to alloc page table entry.\r\n");
19 return false;
20 }
21 Table.LineAddress = replaceAlignAddr;
22 getPagesTable(Table);//这个函数是利用理论知识讲的公式获取任意VA对应的PTE、PDPTE、PDE、PXE
23
24 //获取索引
25 UINT64 pml4eindex = ((uint64_t)replaceAlignAddr & 0x0000FF8000000000) >> 39;
26 UINT64 pdpteindex = ((uint64_t)replaceAlignAddr & 0x0000007FC0000000) >> 30;
27 UINT64 pdeindex = ((uint64_t)replaceAlignAddr & 0x000000003FE00000) >> 21;
28 UINT64 pteindex = ((uint64_t)replaceAlignAddr & 0x00000000001FF000) >> 12;
29
30 //判断是否为大页,因为大页的PT是没有值的,所以要将VaPt指向分割成小页后的PT表,就是上面代码展示的
31 if (Table.PdeAddress->large_page) {
32 MmFreeContiguousMemorySpecifyCache(VaPt, PAGE_SIZE, MmCached);
33 VaPt = (uint64_t*)pa_to_va(splitPDE->page_frame_number * PAGE_SIZE);
34 }
35 else {
36 //小页,Pt数组是有值的,先复制
37 memcpy(VaPt, Table.PteAddress - pteindex, PAGE_SIZE);
38 }
39 //这里我的Table结构中的页表均为指针,因此这里做的减法是指针减法,不需要乘以8
40 //另外这里做减法的意思是获取页表的起始地址,即基地址
41 memcpy(Va4kb, replaceAlignAddr, PAGE_SIZE);
42 memcpy(VaPdt, Table.PdeAddress - pdeindex, PAGE_SIZE);//指针减法
43 memcpy(VaPdpt, Table.PdpteAddress - pdpteindex, PAGE_SIZE);
44
45 //替换页表的页框号,从Pte开始一直到Pml4e
46 _disable();//关中断防止替换被打断
47 auto pReplacePte = (pte_64*)&VaPt[pteindex];
48 pReplacePte->page_frame_number = va_to_pa(Va4kb) / PAGE_SIZE;
49 auto pReplacePde = (pde_64*)&VaPdt[pdeindex];
50 pReplacePde->page_frame_number = va_to_pa(VaPt) / PAGE_SIZE;
51 pReplacePde->large_page = 0;
52 pReplacePde->ignored_1 = 0;
53 pReplacePde->page_level_cache_disable = 1;
54 auto pReplacePdpte = (pdpte_64*)&VaPdpt[pdpteindex];
55 pReplacePdpte->page_frame_number = va_to_pa(VaPdt) / PAGE_SIZE;
56 auto pReplacePml4e = (pml4e_64*)&VaPml4t[pml4eindex];
57 pReplacePml4e->page_frame_number = va_to_pa(VaPdpt) / PAGE_SIZE;
58
59 //刷新TLB
60 __invlpg(pReplacePml4e);
61
62 _enable();
63 return true;
64
65}
最后写一段代码调用上面两个函数:
1bool isolationPages(HANDLE pid, void* iso_address)
2{
3 if (!MmIsAddressValid(iso_address)) {
4 DbgPrintEx(77, 0, "Invalid address: %p\r\n", iso_address);
5 return false;
6 }
7
8 PEPROCESS Process;
9 KAPC_STATE Apc{ 0 };
10 NTSTATUS status;
11 void* AliginIsoAddr;
12 PTE_TABLE Table{ 0 };
13 status = PsLookupProcessByProcessId(pid, &Process);
14 //附加要隔离的进程,每个进程的空间是独立的,这一步一定要做
15 KeStackAttachProcess(Process, &Apc);
16 AliginIsoAddr = PAGE_ALIGN(iso_address);
17 Table.LineAddress = AliginIsoAddr;
18
19 getPagesTable(Table);
20
21 bool bSuc = false;
22 while (1) {
23 //大页分割
24 pde_64 splitPDE{ 0 };
25 if (Table.PdeAddress->large_page) {
26 bSuc = splitLargePages(Table.PdeAddress, &splitPDE);
27 if (!bSuc)break;
28 if (Table.PdeAddress->flags & 0x100) {
29 Table.PdeAddress->flags &= ~0x100;
30 /*
31 这里以及下面的的Table.PteAddress->global = 0;是在关闭G位
32 */
33 }
34 }
35 else {
36 if (Table.PteAddress->global) {
37 Table.PteAddress->global = 0;
38 }
39 }
40
41 cr3 Cr3;
42 Cr3.flags = __readcr3();
43 bSuc = isolationPageTable(Cr3, AliginIsoAddr, &splitPDE);
44
45 if (bSuc) {
46 DbgPrintEx(77, 0, "isolation succeed.\r\n");
47 break;
48 }
49 else {
50 DbgPrintEx(77, 0, "failed to isolation pages.\r\n");
51 break;
52 }
53 }
54 KeUnstackDetachProcess(&Apc);
55 ObDereferenceObject(Process);
56 return bSuc;
57}
这里有一步之前没讲的操作,设置了G位。在CPU的内部有一个表叫做TLB,学过计组的都知道,这玩意叫做快表,其实是一个Cache,内部记录了很多东西,比如直接记录一个虚拟地址对应的物理地址,而不需要通过四级页表机制来查找。每次切换进程时,CR3都会改变,而TLB是跟着CR3变的。但是由于操作系统的高位映射是基本不变的,如果每次切换CR3都重新维护一个TLB,就会造成很大开销。
因此诞生了Global,即全局位。这个位一旦被置1,那么切换进程CR3的时候,就不会刷新PDE或PTE的G位为1的页。这将导致:进程A切换到进程B,而进程A的某个虚拟地址还是保存在进程B的TLB中,下次进程B查找这个虚拟地址的值后,就会在TLB中找到进程A虚拟地址中对应的物理地址。
这对我们的隔离操作是有害的, 因为若进程A恰好是我们隔离的进程,我们Hook了某个函数后,其他进程又从TLB中找到了被Hook的这个函数,那么隔离就失效了。因此,我们关闭伪造页对应的PTE或PDE的G位,强制切换进程时刷新TLB,其他进程就找不到我们的Hook了。
讲完上面的代码,PTE Hook的核心进程隔离部分已经实现了,接下来只需要使用inlineHook框架就好了。下面是一个例子:
1bool PTEHookManager::PTEHook(HANDLE pid, void** oFuncAddr, void* targetFuncAddr)
2{
3 static bool bFirst = true;
4 if (bFirst) {
5 m_PTEBase = nullptr;
6 m_trampLine = (char*)ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE * 5, 'Line');
7 if (!m_trampLine) {
8 DbgPrintEx(77, 0, "failed to create trampline.\r\n");
9 return false;
10 }
11 memset(&m_info, 0, sizeof(m_info));
12 memset(&m_globalBit, 0, sizeof(m_globalBit));
13 m_trampLineUsed = 0;
14 bFirst = false;
15 }
16 PEPROCESS Process{ 0 };
17 KAPC_STATE Apc{ 0 };
18 NTSTATUS status;
19 const uint32_t breakBytesLeast = 14;//ff 25
20 const uint32_t trampLineBreakBytes = 20;
21 uint32_t uBreakBytes = 0;
22 char* TrampLine = m_trampLine + m_trampLineUsed;
23 hde64s hde_info{ 0 };
24 char* JmpAddrStart = (char*)*oFuncAddr;
25 if (m_curHookCount == MAX_HOOK_COUNT) {
26 DbgPrintEx(77, 0, "Hook too many.\r\n");
27 return false;
28 }
29 status = PsLookupProcessByProcessId(pid, &Process);
30 if (!NT_SUCCESS(status)) {
31 DbgPrintEx(77, 0, "failed to get pid.\r\n");
32 return false;
33 }
34 auto ret = isolationPages(pid, *oFuncAddr);
35 if (!ret)return false;
36 DbgPrintEx(77, 0, "ready to diasm.\r\n");
37 while (uBreakBytes < breakBytesLeast) {
38 if (!hde64_disasm(JmpAddrStart + uBreakBytes, &hde_info)) {
39 DbgPrintEx(77, 0, "failed to diasm addr.\r\n");
40 ObDereferenceObject(Process);
41 return false;
42 }
43 uBreakBytes += hde_info.len;
44 }
45 DbgPrintEx(77, 0, "finish disasm.\r\n");
46 unsigned char trampLineCode[trampLineBreakBytes] = {
47 0x6A, 0x00, // push 0
48 0x3E, 0xC7, 0x04, 0x24, 0x00, 0x00, 0x00, 0x00, // mov dword ptr ss : [rsp] , 0x00
49 0x3E, 0xC7, 0x44, 0x24, 0x04, 0x00, 0x00, 0x00, 0x00, // mov dword ptr ss : [rsp + 4] , 0x00
50 0xC3 // ret
51 };
52 char absolutejmpCode[14] = { 0xFF,0x25,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 };
53 *((PUINT32)&trampLineCode[6]) = (UINT32)(((uint64_t)JmpAddrStart + uBreakBytes) & 0XFFFFFFFF);
54 *((PUINT32)&trampLineCode[15]) = (UINT32)((((uint64_t)JmpAddrStart + uBreakBytes) >> 32) & 0XFFFFFFFF);
55
56 memcpy(TrampLine, JmpAddrStart, uBreakBytes);
57 memcpy(TrampLine + uBreakBytes, trampLineCode, trampLineBreakBytes);
58 //添加Hook信息
59 for (int i = 0; i < MAX_HOOK_COUNT; i++) {
60 if (m_info[i].pid == 0) {
61 m_info[i].oriAddr = JmpAddrStart;
62 memcpy(m_info[i].oriBytes, JmpAddrStart, 14);
63 m_info[i].pid = pid;
64 m_curHookCount++;
65 break;
66 }
67 }
68 DbgPrintEx(77, 0, "ready to create trampline.\r\n");
69 *((ULONG64*)(&absolutejmpCode[6])) = (ULONG64)targetFuncAddr;
70 KeStackAttachProcess(Process, &Apc);
71 //auto oIrpl = WPOFF();
72 //memcpy(JmpAddrStart, absolutejmpCode, 14);
73 //DbgPrintEx(77, 0, "[JmpAddrStart]%p\r\n",JmpAddrStart);
74 DbgPrintEx(77, 0, "[absolutejmpCode]");
75 for (int i = 0; i < 14; i++) {
76 DbgPrintEx(77, 0, "%02X", (unsigned char)absolutejmpCode[i]);
77 }
78 DbgPrintEx(77, 0, "\r\n");
79 BOOLEAN success = MDLWriteMemory(JmpAddrStart, absolutejmpCode, 14);
80 if (!success) {
81 DbgPrintEx(77, 0, "failed to MDL write jmpcode.\r\n");
82 return false;
83 }
84 KeUnstackDetachProcess(&Apc);
85 *oFuncAddr = TrampLine;
86 m_trampLineUsed += uBreakBytes + trampLineBreakBytes;
87 ObDereferenceObject(Process);
88 return true;
89}
下面我尝试Hook explore.exe进程的NtCreateFile函数看看效果,系统版本:Win10 1903,另外我还测试了Win10 21H2、Win11 21H2,挂了6小时左右未出现PG
1NTSTATUS HookNtCreateFile(
2 OUT PHANDLE FileHandle,
3 IN ACCESS_MASK DesiredAccess,
4 IN POBJECT_ATTRIBUTES ObjectAttributes,
5 OUT PIO_STATUS_BLOCK IoStatusBlock,
6 IN PLARGE_INTEGER AllocationSize OPTIONAL,
7 IN ULONG FileAttributes,
8 IN ULONG ShareAccess,
9 IN ULONG CreateDisposition,
10 IN ULONG CreateOptions,
11 IN PVOID EaBuffer OPTIONAL,
12 IN ULONG EaLength)
13{
14 DbgPrintEx(77, 0, "[+]Create Files.\r\n");
15 if (ObjectAttributes && ObjectAttributes->ObjectName && ObjectAttributes->ObjectName->Buffer) {
16 wchar_t* name = (wchar_t*)ExAllocatePoolWithTag(NonPagedPool, ObjectAttributes->ObjectName->Length + sizeof(wchar_t), 'name');
17 RtlZeroMemory(name, ObjectAttributes->ObjectName->Length + sizeof(wchar_t));
18 RtlCopyMemory(name, ObjectAttributes->ObjectName->Buffer, ObjectAttributes->ObjectName->Length);
19 if (wcsstr(name, L"MyHook.txt")) {
20 ExFreePool(name);
21 return STATUS_ACCESS_DENIED;
22 }
23 ExFreePool(name);
24 }
25
26 return g_OriginNtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, AllocationSize, FileAttributes, ShareAccess, CreateDisposition, CreateOptions, EaBuffer, EaLength);
27}
效果图:

结语
PTE Hook其实已经是一种比较老的方法了,不过研究完之后,我觉得对于x64架构的认识以及Windows内核的入门还挺有帮助的。