有预感某个项目或许会失败,所以先把可能有价值的这部分记录一下。
* 代码是去年暑假写的。记忆力有限,我尽量保证这篇文章是准确的。
过程中涉及一些在 MSDN 中描述为禁止 / 不推荐的行为。在 Windows 10 1903 上测试通过。
整体分三步:
- 分配连续内存
- 映射到与物理地址相同的虚拟地址
- 添加可执行权限
分配连续内存
Windows 提供了MmAllocateContiguousMemorySpecifyCache
用于分配一块物理地址连续的内存。此函数的第二个和第三个参数限定了分配内存物理地址的范围。‘
最后一个参数控制内存的缓存类型,我使用了MmNonCached
。
配套的释放函数是MmFreeContiguousMemory
。
映射到与物理地址相同的虚拟地址
使用 IoAllocateMdl
创建这块内存的MDL
,并用MmProbeAndLockPages
(记得做异常处理)锁住它。
使用 MmBuildMdlForNonPagedPool
将刚刚创建好的描述虚拟页的 MDL
转换为描述对应的物理页。
使用 MmGetPhysicalAddress
获取刚刚分配好的内存的物理地址。
使用 MmMapLockedPagesSpecifyCache
通过 MDL
为分配好的内存对应的物理页创建自定义虚拟地址。第四个参数为自定义的虚拟地址,这里填写刚刚获取到的物理地址。
MSDN 将第四个参数描述为仅当第二个参数(即内存的访问模式)为 UserMode
时才生效,因此我们将内存的访问模式设为UserMode
。这会导致通过与物理地址相同的分页地址访问这块内存时缺少可执行权限。
使用 IoAllocateMdl
对与物理地址相同的虚拟地址创建 MDL
,并用MmProbeAndLockPages
锁住它。
添加可执行权限
我并未找到 Windows 提供的可用于给内存增加可执行权限的函数,因此这里通过直接修改页表来实现。
MSVC 编译器并未提供 x86_64 下的内联汇编功能,因此这里使用 intrin.h
头文件里的 编译器打洞 函数实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| UINT64 pml4Base = __readcr3() & 0xF'FFFF'FFFF'F000;
UINT64 pml4Index = ((ULONG64)unrealNextProg >> 39) & 0x1FF; UINT64 pdpIndex = ((ULONG64)unrealNextProg >> 30) & 0x1FF; UINT64 pdIndex = ((ULONG64)unrealNextProg >> 21) & 0x1FF; UINT64 ptIndex = ((ULONG64)unrealNextProg >> 12) & 0x1FF;
UINT64* pml4Entry = (UINT64*)(pml4Base + pml4Index * sizeof(UINT64));
UINT64 pdpBase = 0; ReadPhysicalAddress(pml4Entry, &pdpBase, sizeof(UINT64)); UINT64 PML4 = pdpBase; PML4 &= ~(1ULL << 63); WritePhysicalAddress(pml4Entry, &PML4, sizeof(UINT64)); pdpBase &= 0x7fffff000; UINT64* pdpEntry = (UINT64*)(pdpBase + pdpIndex * sizeof(UINT64));
UINT64 pdBase = 0; ReadPhysicalAddress(pdpEntry, &pdBase, sizeof(UINT64)); pdBase &= 0x7fffff000; UINT64* pdEntry = (UINT64*)(pdBase + pdIndex * sizeof(UINT64));
UINT64 ptBase = 0; ReadPhysicalAddress(pdEntry, &ptBase, sizeof(UINT64)); ptBase &= 0x7fffff000; UINT64* ptEntry = (UINT64*)(ptBase + ptIndex * sizeof(UINT64));
UINT64 PTE = 0;
{ ReadPhysicalAddress((PVOID)((UINT64)ptEntry), &PTE, sizeof(UINT64)); PTE &= ~(1ULL << 63); WritePhysicalAddress((PVOID)((UINT64)ptEntry), &PTE, sizeof(UINT64)); }
|
以下两个函数修改自网络,印象中好像是从看雪论坛上找的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| NTSTATUS ReadPhysicalAddress(IN PVOID64 address, OUT PVOID64 buffer, IN SIZE_T size) { MM_COPY_ADDRESS Read = { 0 }; Read.PhysicalAddress.QuadPart = (LONG64)address; SIZE_T BytesTransferred; return MmCopyMemory( buffer, Read, size, MM_COPY_MEMORY_PHYSICAL, &BytesTransferred); }
NTSTATUS WritePhysicalAddress(IN PVOID64 address, IN PVOID64 buffer, IN SIZE_T size) { PVOID map; PHYSICAL_ADDRESS Write = { 0 };
if (!address) { KdPrint(("Address value error: %p\r\n", address)); return STATUS_UNSUCCESSFUL; } Write.QuadPart = (LONG64)address; map = MmGetVirtualForPhysical(Write);
KdPrint(("WritePhysicalAddress.map: %p\r\n", map)); if (!map) { KdPrint(("Write Memory faild: %p, %p\r\n", address, map)); return STATUS_UNSUCCESSFUL; } RtlCopyMemory(map, buffer, size); return STATUS_SUCCESS; }
|
要让一个页有可执行权限,需要让它在页表中的项所在的“一串”项都有可执行权限。我动态调试观察了一下,所以只修改了需要修改的项。
这里我原本的思路是获取到页表的虚拟地址,但是实践起来发现并不容易。最终选择了用物理地址配合 MmCopyMemory
、MmGetVirtualForPhysical
的方案。
需要关掉 SMAP
和SMEP
。由于我们目前为内核态权限,这俩会阻止我们通过新分配的虚拟地址(为用户态内存)访问这块内存。
1 2 3
| unsigned long long cr4 = __readcr4(); cr4 &= ~((1UL << 20) | (1UL << 21)); __writecr4(cr4);
|
最后,非常重要的,刷新页表缓存,使得刚才的修改立刻生效。
1
| __invlpg(unrealNextProg);
|
v1.3.10