在 Windows 上分配一块分页地址与物理地址相同且可读可写可执行的内存

有预感某个项目或许会失败,所以先把可能有价值的这部分记录一下。

* 代码是去年暑假写的。记忆力有限,我尽量保证这篇文章是准确的。

过程中涉及一些在 MSDN 中描述为禁止 / 不推荐的行为。在 Windows 10 1903 上测试通过。

整体分三步:

  1. 分配连续内存
  2. 映射到与物理地址相同的虚拟地址
  3. 添加可执行权限

分配连续内存

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;         //bit 12 ~ 51

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));
//if (!(*pml4Entry & 1)) return STATUS_UNSUCCESSFUL;

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));
//if (!(*pdpEntry & 1)) return STATUS_UNSUCCESSFUL;

UINT64 pdBase = 0;
ReadPhysicalAddress(pdpEntry, &pdBase, sizeof(UINT64));
pdBase &= 0x7fffff000;
UINT64* pdEntry = (UINT64*)(pdBase + pdIndex * sizeof(UINT64));
//if (!(*pdEntry & 1)) return STATUS_UNSUCCESSFUL;

UINT64 ptBase = 0;
ReadPhysicalAddress(pdEntry, &ptBase, sizeof(UINT64));
ptBase &= 0x7fffff000;
UINT64* ptEntry = (UINT64*)(ptBase + ptIndex * sizeof(UINT64));

UINT64 PTE = 0;
//for (size_t i = 0; i < 1; i++) 由于页表在物理内存中不一定连续,因此这里不能简单地使用循环来为多个页设置权限。我只需要一个页有可执行权限,因此只写了一条。
{
ReadPhysicalAddress((PVOID)((UINT64)ptEntry/* + i * sizeof(UINT64)*/), &PTE, sizeof(UINT64));
PTE &= ~(1ULL << 63);
WritePhysicalAddress((PVOID)((UINT64)ptEntry/* + i * sizeof(UINT64)*/), &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;
}

要让一个页有可执行权限,需要让它在页表中的项所在的“一串”项都有可执行权限。我动态调试观察了一下,所以只修改了需要修改的项。

这里我原本的思路是获取到页表的虚拟地址,但是实践起来发现并不容易。最终选择了用物理地址配合 MmCopyMemoryMmGetVirtualForPhysical 的方案。

需要关掉 SMAPSMEP。由于我们目前为内核态权限,这俩会阻止我们通过新分配的虚拟地址(为用户态内存)访问这块内存。

1
2
3
unsigned long long cr4 = __readcr4();
cr4 &= ~((1UL << 20) | (1UL << 21));
__writecr4(cr4);

最后,非常重要的,刷新页表缓存,使得刚才的修改立刻生效。

1
__invlpg(unrealNextProg);
表情 | 预览
Powered By Valine
v1.3.10