第 8 章:内存(第一部分)
# 第 8 章:内存(第一部分)
内存(Memory)对于任何计算机系统而言显然都至关重要。Windows 执行体中的内存管理器组件(Memory Manager component)负责管理一系列页表(page tables),使 CPU 能够正确地将虚拟地址(virtual addresses)转换为物理地址(physical addresses,即内存(RAM)地址)。从 CPU 和内存管理器的角度来看,内存始终以“页(pages)”为单位进行管理。
本章将涵盖以下内容:
- 引言
- 虚拟函数
- 内存查询
- 读写操作
- 其他虚拟 API
- 堆
- 堆信息
# 8.1 引言
表 8-1 列出了 Windows 支持的各架构对应的页大小(page sizes)。默认页大小为 4KB。
表 8-1:页大小(Page Sizes)
| 架构(Architecture) | 普通(小)页(Normal (Small)) | 大页(Large) | 巨页(Huge) | 备注(Remarks) |
|---|---|---|---|---|
| x86 | 4KB | 2MB | 不支持(N/A) | Windows 11 及以上版本不支持 |
| x64 | 4KB | 2MB | 1GB | 无 |
| ARM | 4KB | 4MB | 不支持(N/A) | Windows 11 及以上版本不支持 |
| ARM64 | 4KB | 2MB | 1GB | 无 |
简单来说,虚拟内存(Virtual Memory)的含义如下:
- 虚拟地址到物理地址存在某种映射关系,处理器会透明地遵循该映射。代码仅通过虚拟地址访问内存。
- 页(page)可能根本不在物理内存中,此时 CPU 会触发页错误异常(page fault exception),由内存管理器处理。如果该页位于某个文件(如页面文件(page file))中,内存管理器会分配一个空闲的物理页,读取数据到该物理页,修正页表,并通知处理器重试。这一过程对调用代码完全透明。
# 8.1.1 页状态(Page States)
进程或内核的虚拟地址空间(virtual address space)中的每个页都可能处于以下状态:
- 空闲(Free):该页无任何内容,无映射关系。访问此类页会引发访问冲突异常(Access Violation exception)。
- 已提交(Committed):与空闲状态相反。该页必定可访问,尽管它在不同时刻可能驻留在文件中。但由于页保护冲突(page protection conflicts),它仍可能无法访问。
- 已保留(Reserved):从访问角度来看与空闲状态相同——该页无任何内容。但该页已被预留用于未来使用,不会被考虑用于新的内存分配。
# 8.1.2 内存 API(Memory APIs)
用户模式(user-mode)中有多个层级的内存 API。最底层是所谓的“虚拟函数(Virtual functions)”,它们功能最强,因为最接近内存管理器。这些函数提供了内存管理器的全部功能,例如提交(committing)和保留(reserving)内存、修改保护属性(changing protection)、使用大页(using large pages)等。其缺点是始终以页为单位工作。例如,若使用这些函数之一分配 100 字节,实际会获得 4KB(一个页的大小);若分配 5KB,实际会返回 8KB。
API 层级如图 8-1 所示。

图 8-1:内存 API 层级(Memory API Layers)
虚拟 API(Virtual API)之上是堆 API(Heap API)。该 API 的目的是高效管理小型或非页大小的内存块。它们会在需要时调用虚拟 API。最后,最高层级的 API 是 C/C++ 运行时的一部分——例如 malloc、free、operator new 等函数。其实现依赖于编译器,但微软的编译器目前使用堆 API(与默认进程堆(default process heap)配合工作)。
# 8.2 虚拟函数(The Virtual Functions)
Windows API 变体包括 VirtualAlloc(Ex)、VirtualFree、VirtualProtect 等。本节将介绍底层的原生 API(native APIs),首先从基本分配函数 NtAllocateVirtualMemory 开始:
NTSTATUS NtAllocateVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_In_ ULONG_PTR ZeroBits,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG AllocationType,
_In_ ULONG Protect);
2
3
4
5
6
7
该函数在 Windows 驱动程序开发工具包(WDK)中有实际文档说明,其大多数参数与 Windows API 中的 VirtualAllocEx 对应的参数一致。以下是简要说明:
- ProcessHandle:要在其中进行内存分配的进程的句柄。典型情况是 NtCurrentProcess,但如果该句柄具有 PROCESS_VM_OPERATION 访问掩码(access mask),也可以在其他进程中进行分配。
- BaseAddress:提供要使用的地址。对于新分配,*BaseAddress 通常设为 NULL,表示告知内存管理器无首选地址。如果相关内存区域已被保留(reserved),则 *BaseAddress 可指定该区域内的某个地址(例如用于提交内存(committing memory))。函数成功返回时,*BaseAddress 指示实际使用的地址——该地址可能会从输入值向下舍入到最近的页边界;若输入时指定为 NULL,则返回内存管理器选择的实际地址。
- ZeroBits:通常为 0,但可用于影响最终选择的地址。详见第 6 章的说明。
- *RegionSize:受影响内存区域的大小(以字节为单位)。它可能会向上舍入(取决于 BaseAddress),以对齐到页边界。
- AllocationType:一组标志(部分标志互斥),指示要执行的操作。最常见的标志如下:
- MEM_COMMIT:提交内存区域。
- MEM_RESERVE:保留内存区域。
- MEM_COMMIT | MEM_RESERVED:同时保留和提交内存区域,用于新分配。
- Protect:已分配页的保护属性(protection),例如 PAGE_READWRITE、PAGE_READONLY、PAGE_EXECUTE_READWRITE 等。
有关这些参数的更多详细信息,请参阅 Windows SDK 中 VirtualAlloc 的文档。
与 NtAllocateVirtualMemory 对应的逆函数是 NtFreeVirtualMemory:
NTSTATUS NtFreeVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG FreeType);
2
3
4
5
前三个参数与 NtAllocateVirtualMemory 相同。但 *BaseAddress 不能为 NULL——它必须指定一个有效的地址,用于解除提交(de-commit)或释放(release,与保留(reserve)相反)内存。地址和内存区域大小将分别向下和向上舍入,使该范围对齐到页边界。最后,最常见的标志如下:
- MEM_DECOMMIT:解除提交页,使其变为保留状态。
- MEM_RELEASE:释放页,使其变为空闲状态。内存区域大小必须为 0,且 *BaseAddress 必须是初始调用 NtAllocateVirtualMemory 分配时返回的基地址。
有关更多信息,请参阅 SDK 中 VirtualFree 的文档。
Windows 10 1803 版本及更高版本提供了一个扩展分配函数:
NTSTATUS NtAllocateVirtualMemoryEx(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID* BaseAddress,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG AllocationType,
_In_ ULONG PageProtection,
_Inout_updates_opt_(ExtParamCount) PMEM_EXTENDED_PARAMETER ExtendedParameters,
_In_ ULONG ExtParameCount);
2
3
4
5
6
7
8
NtAllocateVirtualMemoryEx 是一个扩展函数,允许指定一组可变的扩展参数,这些参数在 WinNt.h 中声明:
typedef enum MEM_EXTENDED_PARAMETER_TYPE {
MemExtendedParameterInvalidType = 0,
MemExtendedParameterAddressRequirements,
MemExtendedParameterNumaNode,
MemExtendedParameterPartitionHandle,
MemExtendedParameterUserPhysicalHandle,
MemExtendedParameterAttributeFlags,
MemExtendedParameterImageMachine,
MemExtendedParameterMax
} MEM_EXTENDED_PARAMETER_TYPE, *PMEM_EXTENDED_PARAMETER_TYPE;
#define MEM_EXTENDED_PARAMETER_TYPE_BITS 8
typedef struct DECLSPEC_ALIGN(8) MEM_EXTENDED_PARAMETER {
struct {
DWORD64 Type : MEM_EXTENDED_PARAMETER_TYPE_BITS;
DWORD64 Reserved : 64 - MEM_EXTENDED_PARAMETER_TYPE_BITS;
};
union {
DWORD64 ULong64;
PVOID Pointer;
SIZE_T Size;
HANDLE Handle;
DWORD ULong;
};
} MEM_EXTENDED_PARAMETER, *PMEM_EXTENDED_PARAMETER;
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
Windows API 中的 VirtualAlloc2 会调用 NtAllocateVirtualMemoryEx。
MEM_EXTENDED_PARAMETER_TYPE 中的每个值代表一种自定义类型,其中部分类型在 CreateFileMapping2 的文档中有说明。CreateFileMapping2 仅支持 MemSectionExtendedParameterUserPhysicalFlags 和 MemSectionExtendedParameterNumaNode 值。以下是当前可用的参数类型:
# MemExtendedParameterAddressRequirements
该类型要求提供一个指向以下结构的指针:
typedef struct _MEM_ADDRESS_REQUIREMENTS {
PVOID LowestStartingAddress;
PVOID HighestEndingAddress;
SIZE_T Alignment;
} MEM_ADDRESS_REQUIREMENTS, *PMEM_ADDRESS_REQUIREMENTS;
2
3
4
5
这允许指定内存分配的最小和最大地址,以及最小对齐要求(alignment)。
# 其他参数类型
- MemExtendedParameterNumaNode:指定所需的非统一内存访问节点(NUMA node,在 ULong 成员中)。
- MemExtendedParameterPartitionHandle:指定所需的内存分区句柄(memory partition handle)(内存分区超出本章范围)。
- MemExtendedParameterUserPhysicalHandle:指定另一个节对象(section object)的句柄,用于复制与地址窗口扩展(Address Windowing Extensions,AWE)相关的信息,AWE 始终标识物理映射的内存(超出本章范围)。
- MemExtendedParameterAttributeFlags:指定一组附加标志,其中大多数在 WinNt.h 中定义,如表 8-2 所示:
表 8-2:扩展参数类型(Extended parameters types)
| 标志(MEM_EXTENDED_PARAMETER_) | 描述(Description) |
|---|---|
| GRAPHICS(1) | 分配/映射将用于与图形处理器(GPU)配合工作 |
| NONPAGED(2) | 使用始终非分页的 64KB 页 |
| ZERO_PAGES_OPTIONAL(4) | 提交内存时不需要零初始化页(Zero pages) |
| NONPAGED_LARGE(8) | 分配/映射将使用大页(2MB) |
| NONPAGED_HUGE(0x10) | 分配/映射将使用巨页(1GB) |
| SOFT_FAULT_PAGES(0x20) | 立即执行软页错误(soft page faults),以减少后续发生的可能性 |
| EC_CODE(0x40) | 用于仿真的内存(ARM64 架构) |
| IMAGE_NO_HPAT(0x80) | 用于映像(image)的热补丁(hot-patching) |
# 8.3 内存查询(Querying Memory)
NtQueryVirtualMemory API 提供指定进程中虚拟地址空间区域的相关信息:
typedef enum _MEMORY_INFORMATION_CLASS {
MemoryBasicInformation, // MEMORY_BASIC_INFORMATION
MemoryWorkingSetInformation, // MEMORY_WORKING_SET_INFORMATION
MemoryMappedFilenameInformation, // UNICODE_STRING
MemoryRegionInformation, // MEMORY_REGION_INFORMATION
MemoryWorkingSetExInformation, // MEMORY_WORKING_SET_EX_INFORMATION
MemorySharedCommitInformation, // MEMORY_SHARED_COMMIT_INFORMATION
MemoryImageInformation, // MEMORY_IMAGE_INFORMATION
MemoryRegionInformationEx, // MEMORY_REGION_INFORMATION
MemoryPrivilegedBasicInformation, // MEMORY_BASIC_INFORMATION
MemoryEnclaveImageInformation, // MEMORY_ENCLAVE_IMAGE_INFORMATION
MemoryBasicInformationCapped, // 仅 ARM64 架构支持
MemoryPhysicalContiguityInformation, // MEMORY_PHYSICAL_CONTIGUITY_INFORMATION
MemoryBadInformation, // MEMORY_BAD_IDENTITY_INFORMATION
MemoryBadInformationAllProcesses, // MEMORY_BAD_IDENTITY_INFORMATION
MaxMemoryInfoClass
} MEMORY_INFORMATION_CLASS;
NTSTATUS NtQueryVirtualMemory(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_In_ MEMORY_INFORMATION_CLASS MemoryInformationClass,
_Out_writes_bytes_(MemoryInformationLength) PVOID MemoryInformation,
_In_ SIZE_T MemoryInformationLength,
_Out_opt_ PSIZE_T ReturnLength);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
如你所见,该函数支持多种信息类,以下将对其进行描述。
# 8.3.1 MemoryBasicInformation
缓冲区类型为MEMORY_BASIC_INFORMATION,在Windows SDK中有相关文档说明。实际上,使用此信息类等同于Windows应用程序编程接口(API)的VirtualQueryEx。它返回地址空间中某一区域的信息,该区域在页面状态(page state)、映射类型(mapping type)和页面保护(page protection)方面具有相同属性。进程句柄所需的访问掩码为PROCESS_QUERY_LIMITED_INFORMATION或PROCESS_QUERY_INFORMATION。有关详细信息,请参阅VirtualQueryEx的文档。
在撰写本文时,微软的文档指出需要
PROCESS_QUERY_INFORMATION访问掩码,但这是不正确的。
这是Sysinternals工具
VMMap的基本功能。
以下示例展示了如何扫描指定进程的地址空间:
void QueryVM(HANDLE hProcess)
{
MEMORY_BASIC_INFORMATION mbi;
BYTE* address = nullptr;
while (NT_SUCCESS(NtQueryVirtualMemory(hProcess, address,
MemoryBasicInformation, &mbi, sizeof(mbi), nullptr)))
{
DisplayMemoryInfo(mbi);
address += mbi.RegionSize;
}
}
2
3
4
5
6
7
8
9
10
11
12
当地址超出用户模式地址空间时,NtQueryVirtualMemory最终会执行失败。DisplayMemoryInfo是一个本地函数,用于显示MEMORY_BASIC_INFORMATION结构的详细信息:
void DisplayMemoryInfo(MEMORY_BASIC_INFORMATION const& mi)
{
printf("%p %16zX %-10s %-8s %-15s %-15s\n ",
mi.BaseAddress, mi.RegionSize,
StateToString(mi.State), TypeToString(mi.Type),
ProtectionToString(mi.Protect).c_str(),
ProtectionToString(mi.AllocationProtect).c_str());
}
2
3
4
5
6
7
8
辅助函数将数值转换为字符串:
const char* StateToString(DWORD state)
{
switch (state)
{
case MEM_FREE: return "Free";
case MEM_COMMIT: return "Committed";
case MEM_RESERVE: return "Reserved";
}
return "";
}
const char* TypeToString(DWORD type)
{
switch (type)
{
case MEM_IMAGE: return "Image";
case MEM_MAPPED: return "Mapped";
case MEM_PRIVATE: return "Private";
}
return "";
}
std::string ProtectionToString(DWORD protect)
{
std::string text;
if (protect & PAGE_GUARD)
{
text += "Guard/";
protect &= ~PAGE_GUARD;
}
if (protect & PAGE_WRITECOMBINE)
{
text += "Write Combine/";
protect &= ~PAGE_WRITECOMBINE;
}
switch (protect)
{
case 0: break;
case PAGE_NOACCESS: text += "No Access"; break;
case PAGE_READONLY: text += "RO"; break;
case PAGE_READWRITE: text += "RW"; break;
case PAGE_EXECUTE_READWRITE: text += "RWX"; break;
case PAGE_WRITECOPY: text += "Write Copy"; break;
case PAGE_EXECUTE: text += "Execute"; break;
case PAGE_EXECUTE_READ: text += "RX"; break;
case PAGE_EXECUTE_WRITECOPY: text += "Execute/Write Copy"; break;
default: text += "<other>";
}
return text;
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
以下是我系统上Explorer.exe进程的(经过大幅精简的)输出:
| 地址(BaseAddress) | 区域大小(RegionSize) | 状态(State) | 类型(Type) | 保护属性(Protect) | 分配保护属性(AllocationProtect) |
|---|---|---|---|---|---|
| 0000000000000000 | 380000 | Free | No Access | ||
| 0000000000380000 | 10000 | Committed | Mapped | RW | RW |
| 0000000000390000 | 3000 | Committed | Mapped | RO | RO |
| 0000000000393000 | D000 | Free | No Access | ||
| 00000000003A0000 | 1F000 | Committed | Mapped | RO | RO |
| 00000000003BF000 | 1000 | Free | No Access | ||
| 00000000003C0000 | 4000 | Committed | Mapped | RO | RO |
| 00000000003C4000 | C000 | Free | No Access | ||
| 00000000003D0000 | 3000 | Committed | Mapped | RO | RO |
| 00000000003D3000 | D000 | Free | No Access | ||
| 00000000003E0000 | 2000 | Committed | Private | RW | RW |
| 00000000003E2000 | E000 | Free | No Access | ||
| 00000000003F0000 | 3000 | Committed | Mapped | RO | RO |
| 00000000003F3000 | D000 | Free | No Access | ||
| 0000000000400000 | 2000 | Committed | Private | RW | RW |
| 0000000000402000 | 1E000 | Reserved | Private | RW | |
| ... | ... | ... | ... | ... | ... |
| 0000000000600000 | 6F000 | Reserved | Private | RW | |
| 000000000066F000 | 3000 | Committed | Private | Guard/RW | RW |
| 0000000000672000 | E000 | Committed | Private | RW | RW |
| 0000000000680000 | 11000 | Committed | Mapped | RO | RO |
| 0000000000691000 | F000 | Free | No Access | ||
| 00000000006A0000 | 11000 | Committed | Mapped | RO | RO |
| ... | ... | ... | ... | ... | ... |
| 00007FF8A392F000 | 14000 | Committed | Image | RO | Execute/Write Copy |
| 00007FF8A3943000 | 1000 | Committed | Image | RW | Execute/Write Copy |
| 00007FF8A3944000 | 5000 | Committed | Image | RO | Execute/Write Copy |
| 00007FFFDB164000 | 5000 | Committed | Image | RO | Execute/Write Copy |
| 00007FFFDB169000 | 1000 | Committed | Image | RW | Execute/Write Copy |
| 00007FFFDB16A000 | 4000 | Committed | Image | RO | Execute/Write Copy |
| 00007FFFDB16E000 | 1B742000 | Free | No Access | ||
| 00007FFFF68B0000 | 1000 | Committed | Image | RO | Execute/Write Copy |
| 00007FFFF68B1000 | 4000 | Committed | Image | RX | Execute/Write Copy |
| 00007FFFF68B5000 | 2000 | Committed | Image | RO | Execute/Write Copy |
| 00007FFFF68B7000 | 1000 | Committed | Image | RW | Execute/Write Copy |
| 00007FFFF68B8000 | 3000 | Committed | Image | RO | Execute/Write Copy |
| 00007FFFF68BB000 | 1A5000 | Free | No Access | ||
| 00007FFFF6A60000 | 1000 | Committed | Image | RO | Execute/Write Copy |
| 00007FFFF6A61000 | 5000 | Committed | Image | RX | Execute/Write Copy |
| 00007FFFF6A66000 | 3000 | Committed | Image | RO | Execute/Write Copy |
| 00007FFFF6A69000 | 1000 | Committed | Image | RW | Execute/Write Copy |
| 00007FFFF6A6A000 | 4000 | Committed | Image | RO | Execute/Write Copy |
| 00007FFFF6A6E000 | 9582000 | Free | No Access |
完整代码位于
QueryVM项目中。
# 8.3.2 MemoryWorkingSetInformation
此信息类与进程的工作集(Working Set)相关——无需产生页面错误(page fault)即可访问的内存。进程句柄必须具有PROCESS_QUERY_INFORMATION访问掩码。
相关数据结构如下所示:
typedef struct _MEMORY_WORKING_SET_BLOCK
{
ULONG_PTR Protection : 5;
ULONG_PTR ShareCount : 3;
ULONG_PTR Shared : 1;
ULONG_PTR Node : 3;
#ifdef _WIN64
ULONG_PTR VirtualPage : 52;
#else
ULONG VirtualPage : 20;
#endif
} MEMORY_WORKING_SET_BLOCK, *PMEMORY_WORKING_SET_BLOCK;
typedef struct _MEMORY_WORKING_SET_INFORMATION
{
ULONG_PTR NumberOfEntries;
MEMORY_WORKING_SET_BLOCK WorkingSetInfo[1];
} MEMORY_WORKING_SET_INFORMATION, *PMEMORY_WORKING_SET_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
主结构仅作为容器,存储描述给定进程中工作集范围的条目。BaseAddress值无实际作用。这也意味着提供的数据大小不固定,可能会因STATUS_INFO_LENGTH_MISMATCH而执行失败,此时应扩大缓冲区并重新调用。
MEMORY_WORKING_SET_BLOCK的成员说明如下:
• Protection描述区域保护属性。其值与上一小节中看到的标准保护常量不同。
• ShareCount存储此区域的共享计数(如果是共享区域);最大值为7。
• Shared若设置,则表示此区域可与其他进程共享。
• Node是该物理内存页所在的非统一内存访问(NUMA,Non-Uniform Memory Access)节点。
• VirtualPage是虚拟页编号(左移12位可得到实际地址)。
此信息类提供的功能与Windows应用程序编程接口(API)QueryWorkingSet相同。有关数据成员的更多信息,请参阅SDK文档。
以下示例展示了如何根据进程句柄输出所有工作集页面:
bool QueryWS(HANDLE hProcess)
{
SIZE_T size = 1 << 17; // 任意值
std::unique_ptr<BYTE[]> buffer;
NTSTATUS status;
do
{
buffer = std::make_unique<BYTE[]>(size);
status = NtQueryVirtualMemory(hProcess, nullptr,
MemoryWorkingSetInformation, buffer.get(), size, nullptr);
if (NT_SUCCESS(status))
break;
size *= 2; // 大小不足(size too small)
} while (status == STATUS_INFO_LENGTH_MISMATCH);
if (!NT_SUCCESS(status))
return false;
auto ws = (MEMORY_WORKING_SET_INFORMATION*)buffer.get();
for (ULONG_PTR i = 0; i < ws->NumberOfEntries; i++)
{
//
// 获取单个条目(get single item)
//
auto& info = ws->WorkingSetInfo[i];
//
// 显示部分详细信息(show some details)
//
printf("%16zX Prot: %-17s Share: %d Shareable: %s Node: %d\n ",
info.VirtualPage << 12, ProtectionToString(info.Protection),
(int)info.ShareCount, (int)info.Shared ? "Y" : "N", (int)info.Node);
}
return true;
}
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
完整代码位于
QueryWS项目中。
# 8.3.3 MemoryMappedFilenameInformation
此信息类返回给定已提交内存区域(若有)中映射文件的UNICODE_STRING。以下示例返回给定地址中映射文件的std::wstring:
std::wstring Details(HANDLE hProcess, void* address)
{
BYTE buffer[1 << 10];
if (NT_SUCCESS(NtQueryVirtualMemory(hProcess, address,
MemoryMappedFilenameInformation, buffer, sizeof(buffer), nullptr)))
{
auto us = (PUNICODE_STRING)buffer;
return std::wstring(us->Buffer, us->Length / sizeof(WCHAR));
}
return L"";
}
2
3
4
5
6
7
8
9
10
11
进程句柄必须具有PROCESS_QUERY_LIMITED_INFORMATION或PROCESS_QUERY_INFORMATION访问掩码。所使用的地址不能包含私有内存(private memory),这意味着该地址所指向的页面类型必须是MEM_MAPPED或MEM_IMAGE。查询MEM_PRIVATE或未提交(non-committed)页面会执行失败,因此最好节省时间,避免查询此类区域。
完整示例位于
QueryVM样本的Details函数中。
# 8.3.4:MemoryRegionInformation和MemoryRegionInformationEx
这些信息类提供给定分配范围的额外详细信息。两者均返回以下定义的MEMORY_REGION_INFORMATION结构:
typedef struct _MEMORY_REGION_INFORMATION
{
PVOID AllocationBase;
ULONG AllocationProtect;
union
{
ULONG RegionType;
struct
{
ULONG Private : 1;
ULONG MappedDataFile : 1;
ULONG MappedImage : 1;
ULONG MappedPageFile : 1;
ULONG MappedPhysical : 1;
ULONG DirectMapped : 1;
ULONG SoftwareEnclave : 1;
ULONG PageSize64K : 1;
ULONG PlaceholderReservation : 1;
ULONG MappedWriteWatch : 1;
ULONG PageSizeLarge : 1;
ULONG PageSizeHuge : 1;
ULONG Reserved : 19;
};
};
SIZE_T RegionSize;
SIZE_T CommitSize;
ULONG_PTR PartitionId;
ULONG_PTR NodePreference;
} MEMORY_REGION_INFORMATION, *PMEMORY_REGION_INFORMATION;
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
调用MemoryRegionInformation时使用的结构可不含最后两个成员;最好使用扩展版本。
各成员说明如下:
• AllocationBase是初始分配的基地址。如果调用中的BaseAddress是分配块的起始地址,则AllocationBase可能与此不同。
• AllocationProtect是初始分配时赋予内存块的保护属性。请注意,当前保护属性可能不同——实际上,此分配区域的每个页面的保护属性都可能不同。
这也解释了为什么
MEMORY_BASIC_INFORMATION同时具有AllocationProtect成员(整个区域的初始保护属性)和Protect成员(该区域(可能是子集)的当前保护属性)。
与RegionType相关联的标志如下:
• Private若设置,则表示该内存区域是进程私有的(不共享)。
• MappedDataFile若设置,则表示该区域将文件映射为数据(而非可移植可执行文件(PE)镜像)。
• MappedImage若设置,则表示该区域映射到可移植可执行文件(PE)镜像。
• MappedPageFile若设置,则表示该区域映射到页面文件(page file)。
• MappedPhysical若设置,则表示该区域映射到非分页内存(例如大页面(large pages))。
• DirectMapped若设置,则表示该区域由内核驱动程序映射到设备内存(device memory)。
• SoftwareEnclave若设置,则表示该区域映射到内存隔离区(例如基于虚拟化的安全(VBS,Virtualization Based Security)保护的隔离区)。
• PageSize64K若设置,则表示该区域以64KB页面大小映射(使用区段对象(section objects)时可能出现)。
• PlaceholderReservation若设置,则表示该区域由内核保留(reserved by the kernel)。
• MappedWriteWatch若设置,则表示该内存已注册写入监视(请参阅Windows应用程序编程接口(API)InitializeProcessForWsWatch或NtSetInformationProcess(指定ProcessWorkingSetWatch))。
• PageSizeLarge若设置,则表示该区域使用大页面(large pages)映射。
• PageSizeHuge若设置,则表示该区域使用超大页面(huge pages)映射。
其余成员如下:
• RegionSize是所讨论区域的大小。
• CommitSize是该区域已提交部分的大小。
• PartitionId是该区域所属的内存分区(通常为0)。
• NodePreference是该区域的非统一内存访问(NUMA)节点优先级。若无优先级,则为(ULONG)-1。
所需的进程句柄必须具有PROCESS_QUERY_LIMITED_INFORMATION或PROCESS_QUERY_INFORMATION访问掩码位。
QueryVM样本已扩展为包含区域信息,如下所示(Details函数):
if (mbi .State == MEM_COMMIT)
{
MEMORY_REGION_INFORMATION ri;
if (NT_SUCCESS(NtQueryVirtualMemory(hProcess, mbi .BaseAddress,
MemoryRegionInformationEx, &ri, sizeof(ri), nullptr)))
{
details += MemoryRegionFlagsToString(ri .RegionType);
}
}
2
3
4
5
6
7
8
9
辅助函数MemoryRegionFlagsToString根据设置的标志构建字符串:
std::wstring MemoryRegionFlagsToString(ULONG flags)
{
std::wstring result;
static PCWSTR text[] =
{
L"Private" , L"Data File" , L"Image" , L"Page File" ,
L"Physical" , L"Direct" , L"Enclave" , L"64KB Page" ,
L"Placeholder Reserve" , L"Write Watch" ,
L"Large Page" , L"Huge Page" ,
};
for(int i = 0; i < _countof(text); i++)
if (flags & (1 << i))
{
result += text[i];
result += L", " ;
}
if ( ! result.empty())
return result.substr(0, result.length() - 2);
return L"" ;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 8.3.5 扩展内存工作集信息(MemoryWorkingSetExInformation)
该信息类在返回的详情方面是内存工作集信息(MemoryWorkingSetInformation)的扩展版本,但使用方式不同。它不会提供整个进程的工作集信息,而是由调用者选择感兴趣的虚拟地址(可仅为一个),并将其作为输出缓冲区的一部分传入。缓冲区会更新为所提供地址的属性。每个条目均为扩展内存工作集信息(MEMORY_WORKING_SET_EX_INFORMATION)类型,该类型包含一个扩展内存工作集块(MEMORY_WORKING_SET_EX_BLOCK):
typedef struct _MEMORY_WORKING_SET_EX_BLOCK {
union {
struct {
ULONG_PTR Valid : 1;
ULONG_PTR ShareCount : 3;
ULONG_PTR Win32Protection : 11;
ULONG_PTR Shared : 1;
ULONG_PTR Node : 6;
ULONG_PTR Locked : 1;
ULONG_PTR LargePage : 1;
ULONG_PTR Priority : 3;
ULONG_PTR Reserved : 3;
ULONG_PTR SharedOriginal : 1;
ULONG_PTR Bad : 1;
#ifdef _WIN64
ULONG_PTR Win32GraphicsProtection : 4;
ULONG_PTR ReservedUlong : 28;
#endif
};
struct {
ULONG_PTR Valid : 1;
ULONG_PTR Reserved0 : 14;
ULONG_PTR Shared : 1;
ULONG_PTR Reserved1 : 5;
ULONG_PTR PageTable : 1;
ULONG_PTR Location : 2;
ULONG_PTR Priority : 3;
ULONG_PTR ModifiedList : 1;
ULONG_PTR Reserved2 : 2;
ULONG_PTR SharedOriginal : 1;
ULONG_PTR Bad : 1;
#ifdef _WIN64
ULONG_PTR ReservedUlong : 32;
#endif
} Invalid;
};
} MEMORY_WORKING_SET_EX_BLOCK, *PMEMORY_WORKING_SET_EX_BLOCK;
typedef struct _MEMORY_WORKING_SET_EX_INFORMATION {
PVOID VirtualAddress;
union {
MEMORY_WORKING_SET_EX_BLOCK VirtualAttributes;
ULONG_PTR Long;
} u1;
} MEMORY_WORKING_SET_EX_INFORMATION, *PMEMORY_WORKING_SET_EX_INFORMATION;
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
38
39
40
41
42
43
44
45
虚拟地址(VirtualAddress)是输入地址,必须在调用前设置。相邻的虚拟属性(VirtualAttributes)是返回结果。虚拟属性(VirtualAttributes)包含一个联合(union)的两个部分:
- 如果地址有效(已提交且存在于内存(RAM)中),应参考第一个结构(有效(Valid)标志为1)。
- 如果地址无效,应参考第二个结构(无效(Invalid))。此时,无效(Invalid)结构中的有效(Valid)标志为0。
部分标志已在内存工作集块(MEMORY_WORKING_SET_BLOCK)中存在,以下是新增标志的描述:
- 有效(Valid):虚拟地址有效时置1,否则置0。
- Win32保护(Win32Protection):该地址的保护权限,使用标准保护常量(如
PAGE_READWRITE、PAGE_READONLY等)表示。 - 锁定(Locked):地址被锁定在物理内存中时置1。
- 优先级(Priority):页面优先级(0到7,7为最高),决定当内存(RAM)不足时该页面保留在内存(RAM)中的可能性。更多详情请参见《Windows内部原理》(Windows Internals)一书的第5章。
- 原始共享(SharedOriginal):页面由原型页表项(prototype PTE)支持时置1。
- 损坏(Bad):页面物理损坏或属于飞地(enclave)时置1。
- 页表(PageTable):该页面存储页表时置1(不适用于用户模式地址)。
- 位置(Location):设置为三个值之一:0(无效页面)、1(驻留)、2(页面文件)。
- 修改列表(ModifiedList):页面位于内存管理器(Memory Manager)维护的修改页面列表(Modified page list)中时置1。
- Win32图形保护(Win32GraphicsProtection):如果该内存供显卡使用,则设置Win32保护标志。
使用扩展内存工作集信息(MemoryWorkingSetExInformation)要求调用者的令牌具有单进程分析权限(SeProfileSingleProcessPrivilege),该权限通常授予管理员。
QueryVM项目包含相关代码,若调用者拥有所需权限,可显示大部分此类信息(参见详情函数(Details function))。
# 8.3.6 内存共享提交信息(MemorySharedCommitInformation)
该信息类返回指定进程中共享提交内存的大小(类型为SIZE_T),以页面(4KB块)为单位。以下示例显示共享提交内存的大小(以KB为单位):
SIZE_T committed;
if (NT_SUCCESS(NtQueryVirtualMemory(hProcess, nullptr,
MemorySharedCommitInformation, &committed, sizeof(committed), nullptr))) {
printf("Shared commit: %llu KB\n", committed << 2);
}
2
3
4
5
该信息类要求句柄具有有限进程查询权限(PROCSS_QUERY_LIMITED_INFORMATION)或进程查询权限(PROCESS_QUERY_INFORMATION)。基地址(BaseAddress)参数未被使用。
# 8.3.7 内存镜像信息(MemoryImageInformation)
该信息类提供映射到指定地址的镜像(若存在)的详情。返回的结构如下:
typedef struct _MEMORY_IMAGE_INFORMATION {
PVOID ImageBase;
SIZE_T SizeOfImage;
union {
ULONG ImageFlags;
struct {
ULONG ImagePartialMap : 1;
ULONG ImageNotExecutable : 1;
ULONG ImageSigningLevel : 4;
ULONG Reserved : 26;
};
};
} MEMORY_IMAGE_INFORMATION, *PMEMORY_IMAGE_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
镜像基址(ImageBase):镜像在进程中映射的虚拟地址。镜像大小(SizeOfImage):镜像的字节大小。部分映射镜像(ImagePartialMap):仅该页面内的部分镜像被映射时置1。不可执行镜像(ImageNotExecutable):映射不包含执行权限时置1。
最后,镜像签名级别(ImageSigningLevel)是<WinNt.h>中定义的以下值之一:
#define SE_SIGNING_LEVEL_UNCHECKED 0x00000000
#define SE_SIGNING_LEVEL_UNSIGNED 0x00000001
#define SE_SIGNING_LEVEL_ENTERPRISE 0x00000002
#define SE_SIGNING_LEVEL_CUSTOM_1 0x00000003
#define SE_SIGNING_LEVEL_DEVELOPER SE_SIGNING_LEVEL_CUSTOM_1
#define SE_SIGNING_LEVEL_AUTHENTICODE 0x00000004
#define SE_SIGNING_LEVEL_CUSTOM_2 0x00000005
#define SE_SIGNING_LEVEL_STORE 0x00000006
#define SE_SIGNING_LEVEL_CUSTOM_3 0x00000007
#define SE_SIGNING_LEVEL_ANTIMALWARE SE_SIGNING_LEVEL_CUSTOM_3
#define SE_SIGNING_LEVEL_MICROSOFT 0x00000008
#define SE_SIGNING_LEVEL_CUSTOM_4 0x00000009
#define SE_SIGNING_LEVEL_CUSTOM_5 0x0000000A
#define SE_SIGNING_LEVEL_DYNAMIC_CODEGEN 0x0000000B
#define SE_SIGNING_LEVEL_WINDOWS 0x0000000C
#define SE_SIGNING_LEVEL_CUSTOM_7 0x0000000D
#define SE_SIGNING_LEVEL_WINDOWS_TCB 0x0000000E
#define SE_SIGNING_LEVEL_CUSTOM_6 0x0000000F
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 8.3.8 特权内存基本信息(MemoryPrivilegedBasicInformation)
该信息类提供与内存基本信息(MemoryBasicInformation)相同的详情,但适用于受基于虚拟化的安全性(Virtualization Based Security,VBS)保护的“安全”(隔离用户模式 - Isolated User Mode,IUM)进程,例如LsaIso.exe(如果系统上运行了凭据保护(Credential Guard))。结果由安全内核提供,完全由其自行决定。
对于普通进程,其行为与内存基本信息(MemoryBasicInformation)相同。
# 8.3.9 内存飞地镜像信息(MemoryEnclaveImageInformation)
该信息类提供存储在飞地(enclaves)中的镜像的相关信息。用户模式似乎无法访问该信息类。
# 8.3.10 受限内存基本信息(MemoryBasicInformationCapped)
该信息类与内存基本信息(MemoryBasicInformation)相同,但仅支持ARM64架构。除了所提供的区域大小会决定最大地址范围(即使发生向下取整)外,它似乎与内存基本信息(MemoryBasicInformation)没有太大区别。
# 8.3.11 内存物理连续性信息(MemoryPhysicalContiguityInformation)
该信息类返回地址范围的物理内存信息,以物理内存连续性信息(MEMORY_PHYSICAL_CONTIGUITY_INFORMATION)结构形式呈现:
typedef enum _MEMORY_PHYSICAL_CONTIGUITY_UNIT_STATE {
MemoryNotContiguous,
MemoryAlignedAndContiguous,
MemoryNotResident,
MemoryNotEligibleToMakeContiguous,
} MEMORY_PHYSICAL_CONTIGUITY_UNIT_STATE;
typedef struct _MEMORY_PHYSICAL_CONTIGUITY_UNIT_INFORMATION {
union {
ULONG AllInformation;
struct {
ULONG State : 2; // MEMORY_PHYSICAL_CONTIGUITY_UNIT_STATE
ULONG Reserved : 30;
};
};
} MEMORY_PHYSICAL_CONTIGUITY_UNIT_INFORMATION;
typedef struct _MEMORY_PHYSICAL_CONTIGUITY_INFORMATION {
PVOID VirtualAddress;
ULONG_PTR Size;
ULONG_PTR ContiguityUnitSize;
ULONG Flags;
PMEMORY_PHYSICAL_CONTIGUITY_UNIT_INFORMATION ContiguityUnitInformation;
} MEMORY_PHYSICAL_CONTIGUITY_INFORMATION, *PMEMORY_PHYSICAL_CONTIGUITY_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
调用该信息类要求调用者的令牌具有单进程分析权限(SeProfileSingleProcessPrivilege)。
输入时,虚拟地址(VirtualAddress)必须设置为要查询的地址(必须与连续性单元大小(ContiguityUnitSize)对齐——通常为一个页面),大小(Size)必须设置为地址范围的大小(必须是连续性单元大小(ContiguityUnitSize)的倍数)。返回时,状态(State)成员会被设置为相应的值。标志(Flags)可以是0或1(若为1,则仅查询有效页面)。
遗憾的是,我未能成功调用该信息类——始终返回状态无效参数(STATUS_INVALID_PARAMETER)。
# 8.3.12 内存损坏信息(MemoryBadInformation)和所有进程内存损坏信息(MemoryBadInformationAllProcesses)
内存损坏信息(MemoryBadInformation)返回进程的损坏页面列表——要么是物理损坏的页面,要么是用于内存飞地(memory enclaves)的页面。基地址(BaseAddress)必须设置为NULL,返回的缓冲区包含以下结构的数组:
typedef struct _MEMORY_BAD_IDENTITY_INFORMATION {
union {
PVOID VirtualAddress; // 虚拟内存
ULONG_PTR PageFrameIndex; // 物理内存
};
union {
struct {
ULONG_PTR Poisoned : 1; // 不应访问
ULONG_PTR Physical : 1;
};
ULONG_PTR AllInformation;
};
} MEMORY_BAD_IDENTITY_INFORMATION, *PMEMORY_BAD_IDENTITY_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
14
物理(Physical)标志指示应使用页面帧索引(PageFrameIndex)(置1时)还是虚拟地址(VirtualAddress)(置0时)。
所有进程内存损坏信息(MemoryBadInformationAllProcesses)提供所有进程的类似信息,但用户模式无法访问。
# 8.4 读取和写入(Reading and Writing)
读取和写入当前进程的内存不需要任何特殊API——只需使用C风格的内存函数(如memcpy、memset)和常规代码即可完成读写操作。
读取和写入其他进程的地址空间需要以下API:
NTSTATUS NtReadVirtualMemory(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_Out_writes_bytes_(BufferSize) PVOID Buffer,
_In_ SIZE_T BufferSize,
_Out_opt_ PSIZE_T NumberOfBytesRead);
NTSTATUS NtWriteVirtualMemory(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_In_reads_bytes_(BufferSize) PVOID Buffer,
_In_ SIZE_T BufferSize,
_Out_opt_ PSIZE_T NumberOfBytesWritten);
2
3
4
5
6
7
8
9
10
11
12
13
这些函数是Windows API中的读取进程内存函数(ReadProcessMemory)和写入进程内存函数(WriteProcessMemory)的底层调用函数。参数含义大部分不言自明:基地址(BaseAddress)是目标进程中的虚拟地址。进程句柄(ProcessHandle)必须具有进程虚拟内存读取权限(PROCESS_VM_READ)(用于读取虚拟内存函数(NtReadVirtualMemory))或进程虚拟内存写入权限(PROCESS_VM_WRITE)(用于写入虚拟内存函数(NtWriteVirtualMemory))。
# 8.4.1 通过远程线程注入DLL
向其他进程内存写入数据的一个“经典”示例是,通过在目标进程中创建线程,并将线程指向该进程中的加载库函数(LoadLibrary function),从而向目标进程注入动态链接库(DLL)。以下是使用Windows API实现该功能的示例(为简洁起见,省略了错误处理):
int main(int argc, const char* argv[]) {
if (argc < 3) {
printf("Usage: Injector <pid> <dllpath>\n");
return 0;
}
auto pid = atoi(argv[1]);
HANDLE hProcess = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
PROCESS_CREATE_THREAD, FALSE, pid);
auto p = VirtualAllocEx(hProcess, nullptr, 1 << 12, MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
WriteProcessMemory(hProcess, p, argv[2], strlen(argv[2]) + 1, nullptr);
auto hThread = CreateRemoteThread(hProcess, nullptr, 0,
(LPTHREAD_START_ROUTINE)GetProcAddress(
GetModuleHandle(L"kernel32"), "LoadLibraryA"),
p, 0, nullptr);
// 等待远程线程退出
WaitForSingleObject(hThread, INFINITE);
// (可选)清理:释放目标进程中的内存
VirtualFreeEx(hProcess, p, 0, MEM_RELEASE);
CloseHandle(hThread);
CloseHandle(hProcess);
return 0;
}
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
下面我们看看如何实现该技术的“原生”版本。首先,与Windows API版本一样,接收进程ID和动态链接库(DLL)路径,但切换为Unicode编码:
int wmain(int argc, const wchar_t* argv[]) {
if (argc < 3) {
printf("Usage: InjectDllRemoteThread <pid> <dllpath>\n");
return 0;
}
2
3
4
5
这并非强制要求,但我们将使用宽字符加载库函数(LoadLibraryW)作为远程线程要执行的目标函数,这样可以简化代码。
下一步是打开目标进程的句柄,并获取所需的访问权限:
HANDLE hProcess;
CLIENT_ID cid{ ULongToHandle(wcstol(argv[1], nullptr, 0)) };
OBJECT_ATTRIBUTES procAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr, 0);
auto status = NtOpenProcess(&hProcess,
PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD,
&procAttr, &cid);
if(!NT_SUCCESS(status)) {
printf("Error opening process (status: 0x%X)\n", status);
return status;
}
2
3
4
5
6
7
8
9
10
接下来,我们需要在目标进程中分配内存,并将完整的 DLL 路径写入该内存。首先是内存分配操作:
PVOID buffer = nullptr;
SIZE_T size = 1 << 12; // 4 KB 应该足够
status = NtAllocateVirtualMemory(hProcess, &buffer, 0 , &size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if ( !NT_SUCCESS(status))
{
printf("Error allocating memory (status: 0x%X)\n " , status);
return status;
}
2
3
4
5
6
7
8
9
接下来,将待加载的 DLL 路径复制到已分配的远程缓冲区中:
status = NtWriteVirtualMemory(hProcess, buffer,
(PVOID)argv[2], sizeof(WCHAR) * (wcslen(argv[2]) + 1), nullptr);
if ( !NT_SUCCESS(status))
{
printf("Error writing to process (status: 0x%X)\n " , status);
return status;
}
2
3
4
5
6
7
8
现在,我们需要获取 kernel32 的模块句柄(module handle),以便定位 LoadLibraryW 函数:
UNICODE_STRING kernel32Name;
RtlInitUnicodeString(&kernel32Name, L"kernel32");
PVOID hK32Dll;
status = LdrGetDllHandle(nullptr , nullptr , &kernel32Name, &hK32Dll);
if ( !NT_SUCCESS(status))
{
printf("Error getting kernel32 module (status: 0x%X)\n " , status);
return status;
}
2
3
4
5
6
7
8
9
10
11
LdrGetDllHandle 是 Windows API 中 GetModuleHandle 函数的近似等效原生函数。现在我们可以定位 LoadLibraryW 了:
ANSI_STRING fname;
RtlInitAnsiString(&fname, "LoadLibraryW");
PVOID pLoadLibrary;
status = LdrGetProcedureAddress(hK32Dll, &fname, 0 , &pLoadLibrary);
if ( !NT_SUCCESS(status))
{
printf("Error locating LoadLibraryW (status: 0x%X)\n " , status);
return status;
}
2
3
4
5
6
7
8
9
10
11
你可能会疑惑,为什么我们不“完全使用原生 API”,而是用 LdrLoadDll(LoadLibrary 的原生等效函数)。原因是线程函数(thread function)只能接受一个参数,且该参数必须是 DLL 路径。但遗憾的是,LdrLoadDll 的 DLL 路径通过第三个参数传入,我们没有合适的方式为其提供该参数:
NTSTATUS LdrLoadDll(
_In_opt_ PWSTR DllPath,
_In_opt_ PULONG DllCharacteristics,
_In_ PUNICODE_STRING DllName, // DLL 路径
_Out_ PVOID *DllHandle);
2
3
4
5
因此,我们暂时选择 LoadLibraryW——它仅接受一个参数,即待加载的 DLL 路径。
最后一步是创建实际的线程,以执行 LoadLibraryW 并将目标进程中已分配的缓冲区作为参数传入:
HANDLE hThread;
status = RtlCreateUserThread(hProcess, nullptr , FALSE, 0 , 0 , 0 ,
(PUSER_THREAD_START_ROUTINE)pLoadLibrary, buffer,
&hThread, nullptr);
if ( !NT_SUCCESS(status))
{
printf("Error creating thread (status: 0x%X)\n " , status);
return status;
}
printf("Success!\n " );
2
3
4
5
6
7
8
9
10
11
12
如果此操作成功,线程将开始执行,并根据传入的路径加载 DLL。当然,如果路径无效(例如路径错误),加载可能会失败。从注入器(injector)的角度来看,后续只需执行清理操作:
NtWaitForSingleObject(hThread, FALSE, nullptr);
size = 0;
NtFreeVirtualMemory(hProcess, &buffer, &size, MEM_RELEASE);
NtClose(hThread);
NtClose(hProcess);
2
3
4
5
6
以下是完整的 main 函数(来自 InjectDllRemoteThread 项目),已移除错误处理:
int wmain(int argc, const wchar_t* argv[])
{
if (argc < 3)
{
printf("Usage: InjectDllRemoteThread <pid> <dllpath>\n " );
return 0;
}
HANDLE hProcess;
CLIENT_ID cid{ ULongToHandle(wcstol(argv[1], nullptr , 0)) };
OBJECT_ATTRIBUTES procAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr , 0);
auto status = NtOpenProcess(&hProcess,
PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD,
&procAttr, &cid);
PVOID buffer = nullptr;
SIZE_T size = 1 << 12; // 4 KB 应该足够
status = NtAllocateVirtualMemory(hProcess, &buffer, 0 , &size,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
status = NtWriteVirtualMemory(hProcess, buffer,
(PVOID)argv[2], sizeof(WCHAR) * (wcslen(argv[2]) + 1), nullptr);
UNICODE_STRING kernel32Name;
RtlInitUnicodeString(&kernel32Name, L"kernel32");
PVOID hK32Dll;
status = LdrGetDllHandle(nullptr , nullptr , &kernel32Name, &hK32Dll);
ANSI_STRING fname;
RtlInitAnsiString(&fname, "LoadLibraryW");
PVOID pLoadLibrary;
status = LdrGetProcedureAddress(hK32Dll, &fname, 0 , &pLoadLibrary);
HANDLE hThread;
status = RtlCreateUserThread(hProcess, nullptr , FALSE, 0 , 0 , 0 ,
(PUSER_THREAD_START_ROUTINE)pLoadLibrary, buffer,
&hThread, nullptr);
printf("Success!\n " );
NtWaitForSingleObject(hThread, FALSE, nullptr);
size = 0;
NtFreeVirtualMemory(hProcess, &buffer, &size, MEM_RELEASE);
NtClose(hThread);
NtClose(hProcess);
return 0;
}
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
38
39
40
41
42
43
44
45
46
47
48
# 8.5 其他虚拟内存 API(Other Virtual APIs)
本节将介绍“虚拟内存”(Virtual)系列中的其他一些 API。
新分配内存区域的初始页面保护属性(page protection)在调用 NtAllocateVirtualMemory 时设置。如果需要修改内存保护属性,可以使用 NtProtectVirtualMemory:
NTSTATUS NtProtectVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG NewProtect,
_Out_ PULONG OldProtect);
2
3
4
5
6
大多数参数到目前为止应该已经很熟悉了。ProcessHandle 可以是 NtCurrentProcess(),用于修改调用进程自身的内存保护属性(此场景下调用始终成功);如果操作目标是其他进程,则该句柄必须具备 PROCESS_VM_OPERATION 访问权限掩码。NewProtect 是请求的保护属性,使用我们之前遇到的与 Windows API 相同的常量(如 PAGE_READWRITE、PAGE_READONLY 等)。返回的 OldProtect 包含该内存区域第一页的原有保护属性。
Windows API 中的
VirtualProtect(Ex)系列 API 底层会调用此函数。
内存可以被“锁定”(locked)到物理内存中,防止其在“解锁”(unlocked)前被换出(paged out)。可使用以下 API 实现该功能:
NTSTATUS NtLockVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG MapType);
NTSTATUS NtUnlockVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG MapType);
2
3
4
5
6
7
8
9
10
11
MapType 只能取以下两个值之一:MAP_PROCESS(1)或 MAP_SYSTEM(2)。如果指定 MAP_SYSTEM,则调用方的令牌(token)中必须具备 SeLockMemoryPrivilege 权限。这两个标志在操作逻辑上似乎没有区别,但必须指定其中一个。
Windows API 中的
VirtualLock和VirtualUnlock使用MAP_PROCESS标志。
锁定内存会告知内存管理器(memory manager):只要进程中有线程在运行(而非等待状态),就将这些页面保留在进程的工作集(working set)中。
# 8.6 堆(Heaps)
“虚拟内存”(Virtual)系列 API 始终以页面(4 KB)为单位操作,这并不满足大多数应用程序的需求。堆(heaps)在虚拟内存 API 的基础上,提供了细粒度的内存管理功能。堆管理器(Heap Manager)是负责管理堆内内存的核心组件。
Windows API 提供了一系列与堆相关的函数,例如 HeapAlloc、HeapFree 等。这些函数都是原生堆 API 的轻量级包装层。
每个进程启动时都会拥有一个默认堆(Process Default Heap),该堆由 NtDll 在进程初始化过程中创建。可以通过 RtlProcessHeap 获取此堆:
#define RtlProcessHeap() (NtCurrentPeb()->ProcessHeap)
进程还可以创建更多堆,后续将简要说明创建额外堆的一些原因。
堆管理器同样在核内实现,我们接下来要介绍的部分函数在 WDK(Windows Driver Kit)中有文档说明。
# 8.6.1 基本堆管理(Basic Heap Management)
从堆中分配内存可通过 RtlAllocateHeap 实现:
PVOID RtlAllocateHeap(
_In_ PVOID HeapHandle,
_In_opt_ ULONG Flags,
_In_ SIZE_T Size);
2
3
4
HeapHandle 是堆的句柄(RtlProcessHeap() 是有效的句柄值)。Flags 可以是 0,也可以是以下值的组合:
HEAP_NO_SERIALIZE(1):分配操作无需与可能访问同一堆的其他线程同步。HEAP_ZERO_MEMORY(8):返回的缓冲区会被初始化为零(未指定此标志时,缓冲区内容保持不变)。HEAP_GENERATE_EXCEPTION(4):分配失败时,默认返回NULL;指定此标志后,会抛出STATUS_NO_MEMORY异常。HEAP_SETTABLE_USER_VALUE(0x100):允许为分配的内存块设置用户定义的值(详情见下一节)。
“未指定”某个标志并不意味着其对立面,而是表示使用堆的默认行为。例如,未指定 HEAP_NO_SERIALIZE 时,如果堆本身是通过该标志创建的,那么分配操作仍可能不同步。
进程默认堆是同步的。
Size 是分配的内存大小(以字节为单位)。返回值是分配的内存块指针;分配失败时返回 NULL(除非 Flags 中指定了 HEAP_GENERATE_EXCEPTION,此时失败会抛出异常)。
当内存不再需要时,可调用 RtlFreeHeap 释放该内存块:
BOOLEAN RtlFreeHeap(
_In_ PVOID HeapHandle,
_In_opt_ ULONG Flags,
_Frees_ptr_opt_ PVOID BaseAddress);
2
3
4
唯一有效的 Flags 是 HEAP_NO_SERIALIZE。BaseAddress 必须是之前通过 RtlAllocateHeap 返回的地址。
如果需要调整已分配内存块的大小,可以使用 RtlReAllocateHeap:
PVOID RtlReAllocateHeap(
_In_ PVOID HeapHandle,
_In_ ULONG Flags,
_Frees_ptr_opt_ PVOID BaseAddress,
_In_ SIZE_T Size);
2
3
4
5
新内存块的大小可以大于或小于 BaseAddress 所指向的现有内存块。如果请求的内存块更大且无法在现有块空间内扩展,数据会被复制到新的内存块中。即使是缩小内存块,也不保证返回原内存块(即返回值才是指向最终内存块的指针,无论是否移动过)。
Flags 支持的取值与 RtlAllocateHeap 相同。
# 8.6.2 创建堆(Creating Heaps)
进程中可以创建多个堆。以下是不使用默认进程堆、而是创建新堆的一些可能原因:
- 默认进程堆是同步的,如果堆不存在竞争(contention),同步机制会降低性能。创建独立堆可以控制同步行为;总体而言,创建堆能提供更多自定义空间。
- 如果应用程序频繁请求不同大小的内存块,堆可能会随着时间推移产生碎片(fragmented)。如果应用程序中常见固定大小的内存分配,一种解决方案是创建专门用于固定大小分配的堆。
- 如果堆访问存在错误(如堆损坏,heap corruptions),若这些错误发生在特定堆上,会更容易排查。
- 有时一次性销毁整个堆,比逐个释放所有分配的内存块更简便。
Windows 支持两种类型的堆:第一种是 NT 堆(NT Heap),是最初实现的堆管理方案;Windows 8 引入了一种新的堆类型——段堆(Segment Heap),在内存块管理和安全性方面提供了更优的实现。但由于兼容性考虑,普通进程默认仍使用 NT 堆。UWP 应用程序和某些系统进程使用段堆。
使用段堆的系统进程镜像名称包括:
Svchost.exe、Smss.exe、Csrss.exe、Lsass.exe、Services.exe、RuntimeBroker.exe、dwm.exe、WinInit.exe、WinLogon.exe、MsMpEng.exe、NisSrv.exe、SiHost.exe等。
可执行文件可以选择启用段堆:在注册表项 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\exename 中添加一个名为 FrontEndHeapDebugOptions 的值,设为 8 表示使用段堆,设为 4 表示禁用段堆。
RtlHeapCreate 是堆创建函数,在 WDK 中有文档说明(带有内核相关偏向):
PVOID RtlCreateHeap(
_In_ ULONG Flags,
_In_opt_ PVOID HeapBase,
_In_opt_ SIZE_T ReserveSize,
_In_opt_ SIZE_T CommitSize,
_In_opt_ PVOID Lock,
_In_opt_ PRTL_HEAP_PARAMETERS Parameters);
2
3
4
5
6
7
Flags 可以是 0,也可以是以下值的组合:
HEAP_NO_SERIALIZE(1):表示堆默认不同步。HEAP_GENERATE_EXCEPTION(4):分配函数失败时抛出STATUS_NO_MEMORY异常,而非返回NULL。HEAP_GROWABLE(2):表示堆可以增长到任意大小,受进程地址空间和可用内存限制。指定此标志时,HeapBase必须设为NULL。HEAP_REALLOC_IN_PLACE_ONLY(0x10):仅当无需分配新内存块时,重新分配(reallocations)操作才能成功。HEAP_TAIL_CHECKING_ENABLED(0x20):堆函数会执行额外检查,确保进程未使用超出分配额度的字节。具体实现是在分配请求的内存块之后填充几个额外字节(使用已知填充模式),并在释放内存块或查询其大小时验证该模式。这是一种调试辅助功能,用于在堆损坏发生前发现问题。HEAP_FREE_CHECKING_ENABLED(0x40):另一种堆调试功能,旨在确保释放操作(free operations)作用于正确的地址。HEAP_DISABLE_COALESCE_ON_FREE(0x80):不尝试自动合并(coalesce)空闲内存区域。可通过RtlCompactHeap强制合并空闲区域。
HEAP_TAIL_CHECKING_ENABLED、HEAP_FREE_CHECKING_ENABLED 和 HEAP_DISABLE_COALESCE_ON_FREE 也可以通过 NT 全局标志(NT Global Flags)指定;若通过全局标志指定,无论创建堆时是否显式设置这些标志,它们都会自动应用于新创建的堆。
HEAP_CREATE_ALIGN_16(0x10000):表示内存块应按 16 字节对齐。HEAP_CREATE_ENABLE_TRACING(0x20000):启用堆跟踪(heap tracing)。HEAP_CREATE_ENABLE_EXECUTE(0x40000):表示分配的内存具备PAGE_EXECUTE_READWRITE保护属性,即代码可以从堆中执行而不会引发异常。HEAP_CREATE_SEGMENT_HEAP(0x100):表示创建段堆(Segment Heap)而非 NT 堆。创建段堆时,必须同时指定HEAP_GROWABLE标志;此外,HeapBase、ReservedSize、CommitSize和Lock必须设为 0 或NULL。
各种“调试”标志(如
HEAP_TAIL_CHECKING_ENABLED、HEAP_FREE_CHECKING_ENABLED)会导致分配函数变慢,仅用于调试目的。
HeapBase 可以是 NULL(此时必须指定 HEAP_GROWABLE 标志),这种情况下堆管理器会分配新的内存区域来承载堆;如果 HeapBase 非 NULL,则使用该地址作为新堆的基础地址——该地址必须是进程之前已分配的(例如通过 NtAllocateVirtualMemory 分配)。
如果 ReservedSize 非 0,表示堆的初始保留内存大小(向上取整到页面单位);CommitSize 表示堆的初始提交内存大小。ReservedSize 和 CommittedSize 的关系如表 8-3 所示。
表 8-3:CommitSize 与 ReservedSize 的关系
| ReservedSize | CommitSize | 结果(Result) |
|---|---|---|
| 0 | 0 | 堆初始保留 64 个页面,提交 1 个页面 |
| 0 | 非 0 | ReservedSize 设为 CommitSize,并向上取整到最近的 64KB 大小 |
| 非 0 | 0 | 堆初始提交 1 个页面 |
| 非 0 | 非 0 | 若 CommitSize 大于 ReservedSize,则将 CommitSize 缩减为 ReservedSize |
接下来,Lock 参数是可选指针,指向已初始化的 CRITICAL_SECTION 结构,用于同步堆访问。如果指定了 HEAP_NO_SERIALIZE 标志,则 Lock 必须设为 NULL(因为该标志的目的是禁用堆同步)。
最后,Parameters 是一个可选指针,指向包含更多自定义选项的结构,其定义如下:
typedef NTSTATUS (NTAPI *PRTL_HEAP_COMMIT_ROUTINE)( _In_ PVOID Base,
_Inout_ PVOID *CommitAddress,
_Inout_ PSIZE_T CommitSize);
typedef struct _RTL_HEAP_PARAMETERS {
ULONG Length; // sizeof(RTL_HEAP_PARAMATERS)
SIZE_T SegmentReserve;
SIZE_T SegmentCommit;
SIZE_T DeCommitFreeBlockThreshold;
SIZE_T DeCommitTotalFreeThreshold;
SIZE_T MaximumAllocationSize;
SIZE_T VirtualMemoryThreshold;
SIZE_T InitialCommit;
SIZE_T InitialReserve;
PRTL_HEAP_COMMIT_ROUTINE CommitRoutine;
SIZE_T Reserved[2];
} RTL_HEAP_PARAMETERS, *PRTL_HEAP_PARAMETERS;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
表 8-4 总结了 RTL_HEAP_PARAMETERS 结构各成员的作用。
表 8-4:“RTL_HEAP_PARAMETERS”结构
| 成员 | 描述 | 指定为零时的值 |
|---|---|---|
| Length | 使用前必须设置为该结构的大小 | 必须指定 |
| SegmentReserve | 堆扩展时的保留段大小 | PEB->HeapSegmentReserve |
| SegmentCommit | 需要更多已提交内存时的提交段大小 | PEB->HeapSegmentCommit |
| DeCommitFreeBlockThreshold | 释放内存时触发释放提交(decommit)操作的块阈值 | PEB->HeapDeCommitFreeBlockThreshold |
| DeCommitTotalFreeThreshold | 触发释放所有堆内存提交的阈值 | PEB->DeCommitTotalFreeThreshold |
| MaximumAllocationSize | 单次分配允许的最大大小 | 比进程地址空间小一页 |
| VirtualMemoryThreshold | 用于决定内部创建数组的大小 | 0x7f000 |
| InitialCommit | 与 CommitSize 参数含义相同,非零时覆盖该参数 | 忽略 |
| InitialReserve | 与 ReservedSize 参数含义相同,非零时覆盖该参数 | 忽略 |
| CommitRoutine | 用于执行分配操作的自定义例程 | 忽略 |
| Reserved | 保留字段 | 必须清零 |
CommitRoutine 允许自定义块的分配过程。仅当堆不可扩展且 BaseAddress 不为 NULL 时,该例程才有效。在这种情况下,堆管理器(Heap Manager)会使用以下参数调用该例程:
- Base 是堆的基地址(即 RtlCreateHeap 函数中的 BaseAddress 参数)。
- CommitAddress 是存储分配地址的位置。
- CommitSize 是输入时的请求大小,该例程可将其修改为实际分配的字节数。
RtlCreateHeap 函数的返回值是一个堆句柄(heap handle)。它并非内核对象(kernel object)意义上的句柄,而是一个指向未公开结构的不透明指针(opaque pointer)。该指针可能指向两种结构之一:NT 堆(NT heap)对应的 HEAP 结构,或段堆(Segment heap)对应的 SEGMENT_HEAP 结构。尽管这些结构未公开,但可通过微软的公共符号(public Microsoft symbols)获取其定义。
以下是 WinDbg 调试会话的输出,该会话附加到一个用户模式进程(任意用户模式进程均可),展示了这两种结构(为简洁起见进行了截断):
0:000> dt ntdll!_HEAP
+0x000 Segment : _HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY
+0x010 SegmentSignature : Uint4B
+0x014 SegmentFlags : Uint4B
+0x018 SegmentListEntry : _LIST_ENTRY
+0x028 Heap : Ptr64 _HEAP
+0x030 BaseAddress : Ptr64 Void
+0x038 NumberOfPages : Uint4B
+0x040 FirstEntry : Ptr64 _HEAP_ENTRY
+0x048 LastValidEntry : Ptr64 _HEAP_ENTRY
+0x050 NumberOfUnCommittedPages : Uint4B
+0x054 NumberOfUnCommittedRanges : Uint4B
+0x058 SegmentAllocatorBackTraceIndex : Uint2B
+0x05a Reserved : Uint2B
+0x060 UCRSegmentList : _LIST_ENTRY
...
+0x238 Counters : _HEAP_COUNTERS
+0x2b0 TuningParameters : _HEAP_TUNING_PARAMETERS
0:000> dt ntdll!_SEGMENT_HEAP
+0x000 EnvHandle : RTL_HP_ENV_HANDLE
+0x010 Signature : Uint4B
+0x014 GlobalFlags : Uint4B
+0x018 Interceptor : Uint4B
+0x01c ProcessHeapListIndex : Uint2B
+0x01e AllocatedFromMetadata : Pos 0, 1 Bit
+0x020 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA
+0x020 ReservedMustBeZero1 : Uint8B
+0x028 UserContext : Ptr64 Void
+0x030 ReservedMustBeZero2 : Uint8B
+0x038 Spare : Ptr64 Void
+0x040 LargeMetadataLock : Uint8B
+0x048 LargeAllocMetadata : _RTL_RB_TREE
+0x058 LargeReservedPages : Uint8B
+0x060 LargeCommittedPages : Uint8B
+0x068 Tag : Uint8B
+0x070 StackTraceInitVar : _RTL_RUN_ONCE
+0x080 MemStats : _HEAP_RUNTIME_MEMORY_STATS
+0x0d8 GlobalLockCount : Uint2B
+0x0dc GlobalLockOwner : Uint4B
+0x0e0 ContextExtendLock : Uint8B
+0x0e8 AllocatedBase : Ptr64 UChar
+0x0f0 UncommittedBase : Ptr64 UChar
+0x0f8 ReservedLimit : Ptr64 UChar
+0x100 ReservedRegionEnd : Ptr64 UChar
+0x108 CallbacksEncoded : _RTL_HP_HEAP_VA_CALLBACKS_ENCODED
+0x140 SegContexts : [2] _HEAP_SEG_CONTEXT
+0x2c0 VsContext : _HEAP_VS_CONTEXT
+0x380 LfhContext : _HEAP_LFH_CONTEXT
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
38
39
40
41
42
43
44
45
46
47
48
49
50
这意味着,如果需要,你可以将堆句柄强制转换为上述两种结构之一,并查看其成员。对于一个并非由你创建的堆,如何判断其类型呢?你可以将其强制转换为 SEGMENT_HEAP 结构并查看 Signature 成员。如果该成员的值为 0xddeeddee,则表明这是一个段堆(Segment heap)。以下是一个示例,假设上述结构已转换为对应的 C 语言声明:
void PrintHeapType(PVOID heap)
{
auto segHeap = (SEGMENT_HEAP*)heap;
if(segHeap->Signature == 0xddeeddee)
printf("Segment Heap");
else
{
auto ntHeap = (HEAP*)heap;
printf("NT Heap, Base Address: 0x%p", ntHeap->BaseAddress);
}
}
2
3
4
5
6
7
8
9
10
11
使用 WinDbg 时,可以通过
!heap命令获取进程中所有堆或特定目标堆的详细信息。
当堆不再需要时,应将其销毁:
PVOID RtlDestroyHeap(_In_ _Post_invalid_ PVOID HeapHandle);
如果堆上仍有未释放的分配内存,在堆被完全销毁时,这些内存会被“释放”(released)。销毁成功时,返回值为 NULL;否则,返回值为 HeapHandle。
以下示例创建了一个新堆,在该堆上执行分配操作、释放分配的内存,最后销毁堆。显然,在实际场景中,创建的堆会用于多次分配操作。
auto heap = RtlCreateHeap(HEAP_GROWABLE | HEAP_NO_SERIALIZE, nullptr , 0 , 0 , nullptr , nullptr);
auto buffer = RtlAllocateHeap(heap, HEAP_ZERO_MEMORY, 30); // 30 bytes
// use buffer . . .
RtlFreeHeap(heap, 0, buffer);
RtlDestroyHeap(heap);
2
3
4
5
6
7
# 8.6.3 其他堆函数(Other Heap Functions)
本节将介绍其他堆相关函数。
# 8.6.3.1 堆分配大小(Heap Allocation Size)
可以查询堆分配的大小:
SIZE_T RtlSizeHeap(
_In_ PVOID HeapHandle,
_In_ ULONG Flags,
_In_ PVOID BaseAddress);
2
3
4
返回的大小(以字节为单位)是指定的原始分配大小,不包括任何填充字节(padding bytes)或元数据字节(metadata bytes)。
有时出于安全考虑,需要将已释放的块清零,因为通常情况下,这些数据会一直存在,直到被覆盖为止。RtlZeroHeap 函数的作用就是实现这一功能:
# 8.6.3.2 保护(Protection)
NTSTATUS RtlZeroHeap(
_In_ PVOID HeapHandle,
_In_ ULONG Flags);
2
3
堆的内存通常受 PAGE_READWRITE 或 PAGE_EXECUTE_READWRITE 保护(如果在创建堆时指定了 HEAP_CREATE_ENABLE_EXECUTE 标志)。可以通过 RtlProtectHeap 函数将堆内存的保护属性修改为只读(read-only):
VOID RtlProtectHeap(
_In_ PVOID HeapHandle,
_In_ BOOLEAN MakeReadOnly);
2
3
之后,若要恢复原有保护属性,可再次调用该函数,并将 MakeReadOnly 参数设置为 FALSE。
# 8.6.3.3 锁定(Locking)
可以将堆的访问权限锁定到当前线程,防止其他线程访问该堆。与多次调用(每次调用内部都会进行锁定/解锁操作)相比,这种方式可能具有更高的性能。此外,如果要遍历堆(heap walking),锁定堆也是必要的,因为其他线程的任何修改都会破坏遍历的状态。以下函数用于锁定和解锁堆:
BOOLEAN RtlLockHeap(_In_ PVOID HeapHandle);
BOOLEAN RtlUnlockHeap(_In_ PVOID HeapHandle);
2
这些函数支持递归调用,这意味着多次调用 Lock 函数后,需要调用相同次数的 Unlock 函数,才能真正解锁堆,允许正常访问。
# 8.6.3.4 用户数据(User Data)
可以为堆条目(heap entries)附加用户定义的值。要实现这一功能,分配内存时必须指定 HEAP_SETTABLE_USER_VALUE 标志:
BOOLEAN RtlSetUserValueHeap(
_In_ PVOID HeapHandle,
_In_ ULONG Flags,
_In_ PVOID BaseAddress,
_In_ PVOID UserValue);
2
3
4
5
理论上,还可以通过调用 RtlSetUserFlagsHeap 函数设置 2 位标志,但这种方式非常不稳定,因为它既不适用于段堆(Segment heap),也不适用于低碎片堆(Low Fragmentation Heap,LFH)层处于活跃状态的情况。
若要从堆分配中检索回附加的用户定义值,可调用 RtlGetUserInfoHeap 函数,该函数会返回用户定义的值和/或标志:
BOOLEAN RtlGetUserInfoHeap(
_In_ PVOID HeapHandle,
_In_ ULONG Flags,
_In_ PVOID BaseAddress,
_Out_opt_ PVOID *UserValue,
_Out_opt_ PULONG UserFlags);
2
3
4
5
6
以下示例展示了如何将 0x1234 设为用户定义值,并将其检索回来:
auto buffer = RtlAllocateHeap(heap, HEAP_SETTABLE_USER_VALUE, 40);
auto ok = RtlSetUserValueHeap(heap, 0, buffer, (PVOID)0x1234);
ok = RtlSetUserFlagsHeap(heap, 0 , buffer, 0, RTL_HEAP_SETTABLE_FLAG1);
// read back
PVOID value{ nullptr };
ok = RtlGetUserInfoHeap(heap, 0 , buffer, &value, nullptr);
assert(value == (PVOID)0x1234);
2
3
4
5
6
7
8
9
# 8.6.3.5 多次分配(More Allocations)
可以通过一次调用完成多个相同大小的内存分配:
ULONG RtlMultipleAllocateHeap(
_In_ PVOID HeapHandle,
_In_ ULONG Flags,
_In_ SIZE_T Size,
_In_ ULONG Count,
_Out_ PVOID* Array);
2
3
4
5
6
其中,Size 是每次分配的字节大小,Count 是分配次数,Array 是返回的指针数组。调用成功时,返回值为 Count;若返回值小于 Count,则表示成功分配的次数。
正如你所预期的,也可以一次性释放多个分配的内存:
ULONG RtlMultipleFreeHeap(
_In_ PVOID HeapHandle,
_In_ ULONG Flags,
_In_ ULONG Count,
_In_ PVOID *Array);
2
3
4
5
# 8.7 堆信息(Heap Information)
存在多个原生 API(native APIs)用于获取堆的相关信息。首先,我们来看如何获取调用进程(calling process)中的所有堆。有两种实现方式:一种是调用 RtlGetProcessHeaps 函数获取堆句柄数组,另一种是使用 RtlEnumProcessHeaps 函数通过回调枚举堆:
ULONG RtlGetProcessHeaps(
_In_ ULONG NumberOfHeaps,
_Out_ PVOID *ProcessHeaps);
typedef NTSTATUS (NTAPI *PRTL_ENUM_HEAPS_ROUTINE)(
_In_ PVOID HeapHandle,
_In_ PVOID Parameter);
NTSTATUS RtlEnumProcessHeaps(
_In_ PRTL_ENUM_HEAPS_ROUTINE EnumRoutine,
_In_ PVOID Parameter);
2
3
4
5
6
7
8
9
10
11
RtlGetProcessHeaps 函数接受两个参数:要检索的堆句柄数量,以及用于存储结果的数组。该函数返回进程中实际的堆数量。这意味着,如果返回值大于 NumberOfHeaps,则表示未检索到所有堆句柄。
RtlEnumProcessHeaps 函数会为进程中的每个堆调用一次回调函数(EnumRoutine),这种方式可能更便捷,因为无需提前分配内存,也无需“猜测”堆的数量。
# 8.7.1 堆遍历(Heap Walking)
给定一个堆句柄,可以对堆进行“遍历”(walk),即检索堆所包含的各个块,这在调试场景中非常有用。堆遍历需使用 RtlWalkHeap 函数,返回的条目以 RTL_HEAP_WALK_ENTRY 结构表示:
| #define | 宏定义 | 数值 |
|---|---|---|
| #define | RTL_HEAP_BUSY | (USHORT)0x0001 |
| #define | RTL_HEAP_SEGMENT | (USHORT)0x0002 |
| #define | RTL_HEAP_SETTABLE_VALUE | (USHORT)0x0010 |
| #define | RTL_HEAP_SETTABLE_FLAG1 | (USHORT)0x0020 |
| #define | RTL_HEAP_SETTABLE_FLAG2 | (USHORT)0x0040 |
| #define | RTL_HEAP_SETTABLE_FLAG3 | (USHORT)0x0080 |
| #define | RTL_HEAP_SETTABLE_FLAGS | (USHORT)0x00e0 |
| #define | RTL_HEAP_UNCOMMITTED_RANGE | (USHORT)0x1000 |
| #define | RTL_HEAP_PROTECTED_ENTRY | (USHORT)0x2000 |
| #define | RTL_HEAP_LARGE_ALLOC | (USHORT)0x4000 |
| #define | RTL_HEAP_LFH_ALLOC | (USHORT)0x8000 |
typedef struct _RTL_HEAP_WALK_ENTRY
{
PVOID DataAddress;
SIZE_T DataSize;
UCHAR OverheadBytes;
UCHAR SegmentIndex;
USHORT Flags; // see flags above
union
{
struct
{
SIZE_T Settable;
USHORT TagIndex;
USHORT AllocatorBackTraceIndex;
ULONG Reserved[2];
} Block;
struct
{
ULONG CommittedSize;
ULONG UnCommittedSize;
PVOID FirstEntry;
PVOID LastEntry;
} Segment;
};
} RTL_HEAP_WALK_ENTRY, *PRTL_HEAP_WALK_ENTRY;
NTSTATUS RtlWalkHeap(
_In_ PVOID HeapHandle,
_Inout_ PRTL_HEAP_WALK_ENTRY Entry);
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
堆遍历的起始条件是:RTL_HEAP_WALK_ENTRY 结构的 DataAddress 字段设置为 NULL。后续调用应使用上一次返回的地址来获取下一个条目。根据 Flags 成员中的 RTL_HEAP_SEGMENT 标志,返回的每个条目要么是一个块(block),要么是一个段(segment)。段(Segment)是一个管理实体,包含多个块,每个块代表一块内存(注意不要与段堆(Segment Heap)混淆)。
以下是对默认进程堆(default process heap)进行简单遍历的示例:
RTL_HEAP_WALK_ENTRY entry{};
while (NT_SUCCESS(RtlWalkHeap(RtlProcessHeap(), &entry)))
{
printf("Addr: 0x%p Size: 0x%08X Flags: 0x%04X\n " ,
entry.DataAddress, entry.DataSize, (int)entry.Flags);
}
2
3
4
5
6
DataSize 是块或段的大小。对于块(block),如果其状态为忙碌(已使用,RTL_HEAP_BUSY),则 DataSize 是提供给客户端的分配大小。OverheadBytes 是维护该分配所需的额外字节数。SegmentIndex 是该堆中的段索引,从 0 开始。
对于段(设置了 RTL_HEAP_SEGMENT 标志),联合体(union)中的 Segment 部分包含以下成员:
- CommittedSize:段中已提交的字节数(始终是页大小的整数倍)。
- UncommittedSize:当前未提交的字节数(始终是页大小的整数倍)。
- FirstEntry:段中的第一个条目。
- LastEntry:段中最后一个有效的条目。
对于块(未设置 RTL_HEAP_SEGMENT 标志),联合体(union)中的 Block 部分包含以下成员:
- Settable:附加到该块的用户定义值(如果设置了 RTL_HEAP_SETTABLE_VALUE 标志)。
- TagIndex:如果块带有标签(tagged),则为该块的标签索引。
- AllocatorBackTraceIndex:用于调试目的(本章暂不涉及)。
# 8.7.2 更多堆信息
可以通过 RtlQueryHeapInformation 函数检索堆信息,也可以通过 RtlSetHeapInformation 函数修改某些堆的详细配置:
NTSTATUS RtlQueryHeapInformation(
_In_ PVOID HeapHandle,
_In_ HEAP_INFORMATION_CLASS HeapInformationClass,
_Out_opt_ PVOID HeapInformation,
_In_opt_ SIZE_T HeapInformationLength,
_Out_opt_ PSIZE_T ReturnLength);
NTSTATUS RtlSetHeapInformation(
_In_ PVOID HeapHandle,
_In_ HEAP_INFORMATION_CLASS HeapInformationClass,
_In_opt_ PVOID HeapInformation,
_In_opt_ SIZE_T HeapInformationLength);
2
3
4
5
6
7
8
9
10
11
12
这些函数的使用模式与我们之前见过的其他查询/设置函数非常相似。以下是支持的信息类(部分定义在 <WinNt.h> 头文件中):
typedef enum _HEAP_INFORMATION_CLASS
{
HeapCompatibilityInformation = 0,
HeapEnableTerminationOnCorruption = 1,
HeapExtendedInformation = 2,
HeapOptimizeResources = 3,
HeapTaggingInformation = 4,
HeapStackDatabase = 5,
HeapMemoryLimit = 6,
HeapTag = 7,
HeapDetailedFailureInformation = (int)0x80000001,
HeapSetDebuggingInformation = (int)0x80000002
} HEAP_INFORMATION_CLASS;
2
3
4
5
6
7
8
9
10
11
12
13
# 8.7.3 堆兼容性信息(HeapCompatibilityInformation,0)
使用该信息类(information class)查询时,会返回一个无符号长整数(ULONG),用于描述所查询堆(heap)的某些特性。对于段堆(segment heap),该值始终为 2,意味着低碎片堆(Low Fragmentation Heap,LFH)层处于激活状态。对于标准堆(standard heap),该值可能为 0(未应用 LFH)或 2(已应用 LFH)。
在设置操作中,仅可对标准堆启用 LFH。这还要求堆具备可增长性(growable),且创建时未设置 HEAP_NO_SERIALIZE 标志。通常无需手动启用 LFH,因为堆管理器(Heap Manager)会在判断启用 LFH 有益时自动启用它。可使用来自 phnt 的以下枚举(enumeration):
typedef enum _HEAP_COMPATIBILITY_MODE {
HEAP_COMPATIBILITY_STANDARD = 0UL,
HEAP_COMPATIBILITY_LAL = 1UL, // 不适用(N/A)
HEAP_COMPATIBILITY_LFH = 2UL,
} HEAP_COMPATIBILITY_MODE;
2
3
4
5
# 8.7.4 堆损坏时终止进程(HeapEnableTerminationOnCorruption)
该信息类支持查询或设置一项选项:若检测到堆损坏(heap corruption),进程应立即终止(terminate),而非抛出可能被以某种方式处理但通常无法有效解决问题的异常(exception)。
预期的数据类型为无符号长整数(ULONG),非零值表示启用“堆损坏时终止进程”功能。目前,所有堆默认启用此功能。
# 8.7.5 堆扩展信息(HeapExtendedInformation)
该信息类通过 HEAP_EXTENDED_INFORMATION 结构体(structure)及其相关结构体,返回所查询堆或进程中所有堆的详细信息:
typedef struct _PROCESS_HEAP_INFORMATION {
ULONG_PTR ReserveSize;
ULONG_PTR CommitSize;
ULONG NumberOfHeaps;
ULONG_PTR FirstHeapInformationOffset;
} PROCESS_HEAP_INFORMATION, *PPROCESS_HEAP_INFORMATION;
typedef struct _HEAP_INFORMATION {
ULONG_PTR Address;
ULONG Mode;
ULONG_PTR ReserveSize;
ULONG_PTR CommitSize;
ULONG_PTR FirstRegionInformationOffset;
ULONG_PTR NextHeapInformationOffset;
} HEAP_INFORMATION, *PHEAP_INFORMATION;
typedef struct _HEAP_EXTENDED_INFORMATION {
HANDLE Process;
ULONG_PTR Heap;
ULONG Level;
PVOID CallbackRoutine;
PVOID CallbackContext;
union {
PROCESS_HEAP_INFORMATION ProcessHeapInformation;
HEAP_INFORMATION HeapInformation;
};
} HEAP_EXTENDED_INFORMATION, *PHEAP_EXTENDED_INFORMATION;
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
输入时,RtlQueryHeapInformation 函数的堆句柄(heap handle)参数不会被使用。相反,HEAP_EXTENDED_INFORMATION 的前五个成员决定了要请求的信息。该信息类的一个强大功能是能够从其他进程获取堆详情——通过 Process 成员传入目标进程的句柄(handle)即可。若调用进程(calling process)即为目标进程,Process 成员可设为 NtCurrentProcess()。若为跨进程查询,目标进程的句柄需具备较高权限,包括 PROCESS_QUERY_INFORMATION、PROCESS_CREATE_THREAD、PROCESS_VM_OPERATION、PROCESS_VM_READ、PROCESS_DUP_HANDLE 和 PROCESS_VM_WRITE。有时,仅这些权限仍不足,可能还需要 PROCESS_SET_QUOTA 和 PROCESS_SET_INFORMATION 权限。这是因为跨进程查询时,会在目标进程中创建一个线程(thread)来获取信息,并与目标进程共享一个区段对象(Section object)。
我发现,对于此类查询,请求 PROCESS_ALL_ACCESS 权限或使用 MAXIMUM_ALLOWED 权限会更便捷,可尝试这些权限是否能满足访问需求。
Level 成员指定了调用所需的详情级别。最通用的级别是 HEAP_INFORMATION_LEVEL_PROCESS(值为 1),该级别会填充 ProcessHeapInformation 成员。以下是示例代码(省略了错误处理):
#define HEAP_INFORMATION_LEVEL_PROCESS 1
void DisplayProcessHeapsTotals(HANDLE hProcess) {
HEAP_EXTENDED_INFORMATION info{};
info.Process = hProcess;
info.Level = HEAP_INFORMATION_LEVEL_PROCESS;
RtlQueryHeapInformation(nullptr, HeapExtendedInformation,
&info, sizeof(info), nullptr);
printf("总堆数(Total heaps): %u 已提交大小(Commit): 0x%zX 已保留大小(Reserved): 0x%zX\n",
info.ProcessHeapInformation.NumberOfHeaps,
info.ProcessHeapInformation.CommitSize,
info.ProcessHeapInformation.ReserveSize);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
phnt 头文件(header)中,HeapExtendedInformation 等常量以宏(macro)形式定义,而非枚举(enum)——因为 <WinNt.h> 中虽存在相关枚举,但未包含完整列表。我简化了代码,未添加向 HEAP_INFORMATION_CLASS 的强制类型转换(hard cast),尽管我更倾向于将该转换包含在宏中,或定义一个略有不同的枚举名称。这部分内容留给感兴趣的读者自行实现。
下一个级别是 HEAP_INFORMATION_LEVEL_HEAP(值为 2),可用于获取进程中每个堆的详情,同时也包含上述进程堆的总体信息。若需同时获取这两类信息,建议通过一次调用完成,而非两次单独调用。返回结果中,ProcessHeapInformation 成员的填充方式与前一个级别相同,但 NextHeapInformationOffset 会存储第一个堆详情的偏移量(在 HEAP_INFORMATION_LEVEL_PROCESS 级别下,该值为 0)。
通过该偏移量,后续数据均为 HEAP_INFORMATION 结构体,每个结构体描述一个堆,其中 NextHeapInformationOffset 指向下一个堆的信息。以下代码片段通过一次调用获取进程的总体详情及所有堆的详情:
// hProcess 是目标进程的句柄
// 为简化起见,假设堆数不超过 128 个
auto size = 128 * sizeof(HEAP_INFORMATION) + sizeof(HEAP_EXTENDED_INFORMATION);
auto buffer = std::make_unique<BYTE[]>(size);
auto info = (HEAP_EXTENDED_INFORMATION*)buffer.get();
info->Process = hProcess;
info->Level = HEAP_INFORMATION_LEVEL_HEAP;
RtlQueryHeapInformation(nullptr, HeapExtendedInformation, info, size, nullptr);
// 定位到第一个堆
auto hi = (HEAP_INFORMATION*)(buffer.get() +
info->ProcessHeapInformation.FirstHeapInformationOffset);
auto heaps = info->ProcessHeapInformation.NumberOfHeaps;
printf("总堆数(Total heaps): %u 已提交大小(Commit): 0x%zX 已保留大小(Reserved): 0x%zX\n",
heaps, info->ProcessHeapInformation.CommitSize,
info->ProcessHeapInformation.ReserveSize);
for (ULONG i = 0; i < heaps; i++) {
// 输出堆详情
printf("堆(Heap) %2d: 地址(Addr): 0x%p 已提交大小(Commit): 0x%08zX 已保留大小(Reserve): 0x%08zX %s\n",
i + 1, (PVOID)hi->Address, hi->CommitSize, hi->ReserveSize,
hi->Mode ? "(LFH)" : "");
// 移动到下一个堆
hi = (HEAP_INFORMATION*)(buffer.get() + hi->NextHeapInformationOffset);
}
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
以下是上述代码的示例输出(完整源代码包含在 Heaps 示例中):
| 总堆数(Total) | 16 | 已提交大小(Commit): 0x1DE20000 | 已保留大小(Reserved): 0x1F418000 |
|---|---|---|---|
| 堆(Heap)1 | 地址(Addr): 0x0000000000C50000 | 已提交大小(Commit): 0x1C287000 | 已保留大小(Reserve): 0x1CE01000 (LFH) |
| 堆(Heap)2 | 地址(Addr): 0x0000000000810000 | 已提交大小(Commit): 0x00001000 | 已保留大小(Reserve): 0x00010000 |
| 堆(Heap)3 | 地址(Addr): 0x0000000001210000 | 已提交大小(Commit): 0x01A01000 | 已保留大小(Reserve): 0x01F9D000 (LFH) |
| 堆(Heap)4 | 地址(Addr): 0x00000000011F0000 | 已提交大小(Commit): 0x00051000 | 已保留大小(Reserve): 0x001D1000 (LFH) |
| 堆(Heap)5 | 地址(Addr): 0x000000000C030000 | 已提交大小(Commit): 0x0004A000 | 已保留大小(Reserve): 0x001D1000 (LFH) |
| 堆(Heap)6 | 地址(Addr): 0x000000000CBA0000 | 已提交大小(Commit): 0x00004000 | 已保留大小(Reserve): 0x00004000 |
| 堆(Heap)7 | 地址(Addr): 0x0000000016AB0000 | 已提交大小(Commit): 0x00004000 | 已保留大小(Reserve): 0x00004000 |
| 堆(Heap)8 | 地址(Addr): 0x0000000016AD0000 | 已提交大小(Commit): 0x00004000 | 已保留大小(Reserve): 0x00004000 |
| 堆(Heap)9 | 地址(Addr): 0x000000001CBD0000 | 已提交大小(Commit): 0x000D0000 | 已保留大小(Reserve): 0x001D1000 (LFH) |
| 堆(Heap)10 | 地址(Addr): 0x000000001C7F0000 | 已提交大小(Commit): 0x00004000 | 已保留大小(Reserve): 0x00004000 |
| 堆(Heap)11 | 地址(Addr): 0x000000001C810000 | 已提交大小(Commit): 0x00004000 | 已保留大小(Reserve): 0x00004000 |
| 堆(Heap)12 | 地址(Addr): 0x000000001A880000 | 已提交大小(Commit): 0x00004000 | 已保留大小(Reserve): 0x00004000 |
| 堆(Heap)13 | 地址(Addr): 0x000000001A8A0000 | 已提交大小(Commit): 0x00004000 | 已保留大小(Reserve): 0x00004000 |
| 堆(Heap)14 | 地址(Addr): 0x00000000196A0000 | 已提交大小(Commit): 0x00004000 | 已保留大小(Reserve): 0x00004000 |
| 堆(Heap)15 | 地址(Addr): 0x00000000196C0000 | 已提交大小(Commit): 0x00004000 | 已保留大小(Reserve): 0x00004000 |
| 堆(Heap)16 | 地址(Addr): 0x0000000014E00000 | 已提交大小(Commit): 0x00008000 | 已保留大小(Reserve): 0x000D3000 (LFH) |
HEAP_EXTENDED_INFORMATION 的 Heap 成员可用于获取特定堆(而非进程中所有堆)的详情(若该成员值为 0,则查询所有堆)。该值为目标进程上下文中的堆句柄。
更细致的详情级别涉及堆内的内存区域(memory region),对应的结构体如下:
#define HEAP_INFORMATION_LEVEL_REGION 3
typedef struct _HEAP_REGION_INFORMATION {
ULONG_PTR Address;
SIZE_T ReserveSize;
SIZE_T CommitSize;
ULONG_PTR FirstRangeInformationOffset;
ULONG_PTR NextRegionInformationOffset;
} HEAP_REGION_INFORMATION, *PHEAP_REGION_INFORMATION;
2
3
4
5
6
7
8
9
HEAP_INFORMATION 结构体的 FirstRegionInformationOffset 成员指向该堆的第一个区域描述符(region descriptor),偏移量基于原始缓冲区(original buffer)计算。以下是输出堆的区域信息的示例(完整代码见 Heaps 示例):
void DisplayRegions(PBYTE buffer, HEAP_INFORMATION* hi) {
if (hi->FirstRegionInformationOffset == 0) // 无区域信息(no region info)
return;
auto region = (HEAP_REGION_INFORMATION*)(buffer +
hi->FirstRegionInformationOffset);
for (;;) {
printf(" 区域地址(Region Addr): 0x%p 已提交大小(Commit): 0x%08zX 已保留大小(Reserve): 0x%08zX\n",
(PVOID)region->Address, region->CommitSize, region->ReserveSize);
if (region->NextRegionInformationOffset == 0) // 无更多区域(no more regions)
break;
region = (HEAP_REGION_INFORMATION*)(buffer +
region->NextRegionInformationOffset);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
再下一级别的详情是内存范围(range),对应以下级别值和结构体:
#define HEAP_INFORMATION_LEVEL_RANGE 4
#define HEAP_RANGE_TYPE_COMMITTED 1
#define HEAP_RANGE_TYPE_RESERVED 2
typedef struct _HEAP_RANGE_INFORMATION {
ULONG_PTR Address;
SIZE_T Size;
ULONG Type;
ULONG Protection;
ULONG_PTR FirstBlockInformationOffset;
ULONG_PTR NextRangeInformationOffset;
} HEAP_RANGE_INFORMATION, *PHEAP_RANGE_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
以下是枚举内存范围的示例:
void DisplayRanges(PBYTE buffer, HEAP_REGION_INFORMATION* region) {
if (region->FirstRangeInformationOffset == 0)
return;
auto range = (HEAP_RANGE_INFORMATION*)(buffer + region->FirstRangeInformationOffset);
for (;;) {
printf(" 地址(Addr): 0x%p 大小(Size): 0x%08zX 类型(Type): %s 保护属性(Prot): %s\n",
(PVOID)range->Address, range->Size,
RangeTypeToString(range->Type),
ProtectionToString(range->Protection).c_str());
if (range->NextRangeInformationOffset == 0)
break;
range = (HEAP_RANGE_INFORMATION*)(buffer +
range->NextRangeInformationOffset);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RangeTypeToString 函数根据范围类型返回“已提交(Committed)”或“已保留(Reserved)”。ProtectionToString 是前文提及的函数,用于将保护属性常量(如 PAGE_READWRITE)转换为字符串。
最细致的详情级别(位于内存范围内)是内存块(block),对应以下级别值和结构体:
#define HEAP_INFORMATION_LEVEL_BLOCK 5
#define HEAP_BLOCK_BUSY 1
#define HEAP_BLOCK_EXTRA_INFORMATION 2
#define HEAP_BLOCK_LARGE_BLOCK 4
#define HEAP_BLOCK_LFH_BLOCK 8
typedef struct _HEAP_BLOCK_INFORMATION {
ULONG_PTR Address;
ULONG Flags; // 参见上述标志(see flags above)
SIZE_T DataSize;
SIZE_T OverheadSize;
ULONG_PTR NextBlockInformationOffset;
} HEAP_BLOCK_INFORMATION, *PHEAP_BLOCK_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
14
其数据结构模式与前文一致:一个内存范围通过 FirstBlockInformationOffset 指向其第一个内存块。以下是根据内存范围枚举内存块的示例:
void DisplayBlocks(PBYTE buffer, HEAP_RANGE_INFORMATION* range) {
if (range->FirstBlockInformationOffset == 0)
return;
auto block = (HEAP_BLOCK_INFORMATION*)(buffer +
range->FirstBlockInformationOffset);
for (;;) {
printf(" 块地址(Block Addr): 0x%p 数据大小(Size): 0x%04zX 开销大小(Overhead): %2zd %s\n",
(PVOID)block->Address, block->DataSize, block->OverheadSize,
BlockFlagsToString(block->Flags).c_str());
if (block->NextBlockInformationOffset == 0)
break;
block = (HEAP_BLOCK_INFORMATION*)(buffer +
block->NextBlockInformationOffset);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BlockFlagsToString 函数用于将标志转换为字符串:
std::string BlockFlagsToString(ULONG flags) {
static const struct {
ULONG flag;
PCSTR text;
} data[] = {
{ HEAP_BLOCK_BUSY, "忙碌(Busy)" },
{ HEAP_BLOCK_EXTRA_INFORMATION, "额外信息(Extra)" },
{ HEAP_BLOCK_LARGE_BLOCK, "大块(Large)" },
{ HEAP_BLOCK_LFH_BLOCK, "LFH 块(LFH)" },
};
std::string text;
for (auto& d : data) {
if ((d.flag & flags) == d.flag)
(text += d.text) += ", ";
}
return text.empty() ? "" : text.substr(0, text.length() - 2);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
内存块级别是最细致的详情级别,但如果设置了 HEAP_BLOCK_EXTRA_INFORMATION 标志,则表示该内存块存在额外信息,且这些信息紧跟在 HEAP_BLOCK_INFORMATION 结构体之后。其头部(header)定义如下:
typedef struct _HEAP_BLOCK_EXTRA_INFORMATION {
BOOLEAN Next;
ULONG Type;
SIZE_T Size;
// 后续为数据(data follows)
} HEAP_BLOCK_EXTRA_INFORMATION, *PHEAP_BLOCK_EXTRA_INFORMATION;
2
3
4
5
6
Next 成员指示当前额外信息块之后是否还有其他额外信息块。Type 为额外信息类型,目前仅支持值为 1。额外信息结构体本身定义如下:
typedef struct _HEAP_BLOCK_SETTABLE_INFORMATION {
SIZE_T Settable;
USHORT TagIndex;
USHORT AllocatorBackTraceIndex;
} HEAP_BLOCK_SETTABLE_INFORMATION, *PHEAP_BLOCK_SETTABLE_INFORMATION;
2
3
4
5
前文讨论 RtlWalkHeap 函数时曾提及类似信息。
# 8.7.5.1 回调函数(Callback)
HEAP_EXTENDED_INFORMATION 包含 CallbackRoutine 和 CallbackContext 成员。若 CallbackRoutine 不为空,则它指向一个回调函数(callback),该函数会在枚举每个项时被调用。这意味着,无论 Level 值如何,提供给 RtlQueryHeapInformation 的缓冲区大小只需为 sizeof(HEAP_EXTENDED_INFORMATION),无需更大。无需额外分配内存,因为回调函数会根据 Level 值,为每个请求的项(堆、范围、区域、块)被调用一次。
回调函数的原型(prototype)及关联结构体如下:
typedef struct _HEAP_INFORMATION_ITEM {
ULONG Level; // 报告的级别(reported level)
SIZE_T Size;
union {
PROCESS_HEAP_INFORMATION ProcessHeapInformation;
HEAP_INFORMATION HeapInformation;
HEAP_REGION_INFORMATION HeapRegionInformation;
HEAP_RANGE_INFORMATION HeapRangeInformation;
HEAP_BLOCK_INFORMATION HeapBlockInformation;
ULONG_PTR DynamicStart;
};
} HEAP_INFORMATION_ITEM, *PHEAP_INFORMATION_ITEM;
NTSTATUS HeapInformationCallback(
_In_ PHEAP_INFORMATION_ITEM item,
_In_ PVOID context);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
回调函数返回 STATUS_SUCCESS 会继续枚举,返回失败状态(failure status)则会中止后续调用,且该失败状态会从 RtlQueryHeapInformation 函数返回。
# 8.7.6 堆资源优化(HeapOptimizeResources)
该信息类仅对 RtlSetHeapInformation 函数有效,且需使用以下结构体:
typedef struct _HEAP_OPTIMIZE_RESOURCES_INFORMATION {
ULONG Version;
ULONG Flags;
} HEAP_OPTIMIZE_RESOURCES_INFORMATION, *PHEAP_OPTIMIZE_RESOURCES_INFORMATION;
2
3
4
目前,Version 必须设为 1,Flags 必须设为 0。
该调用会尝试通过释放(decommit)当前未使用的区域来优化堆。若 RtlSetHeapInformation 函数的堆句柄参数为 NULL,则会优化进程中的所有堆;若指定了特定堆句柄,则仅优化该堆。
# 8.7.7 堆内存限制(HeapMemoryLimit)
该信息类可通过 RtlSetHeapInformation 函数为指定堆设置一些限制。对应的信息结构体如下:
typedef struct _RTL_HEAP_MEMORY_LIMIT_DATA {
SIZE_T CommitLimitBytes;
ULONG_PTR CommitLimitFailureCode;
SIZE_T MaxAllocationSizeBytes;
ULONG_PTR AllocationLimitFailureCode;
} RTL_HEAP_MEMORY_LIMIT_DATA, *PRTL_HEAP_MEMORY_LIMIT_DATA;
typedef struct _RTL_HEAP_MEMORY_LIMIT_INFO {
ULONG Version; // 必须设为 1
RTL_HEAP_MEMORY_LIMIT_DATA Data;
} RTL_HEAP_MEMORY_LIMIT_INFO, *PRTL_HEAP_MEMORY_LIMIT_INFO;
2
3
4
5
6
7
8
9
10
11
上述成员的含义不言自明。大小参数必须以字节(byte)为单位,且为页面大小(page size)的整数倍。其余信息类要么无法使用,要么需要进一步研究。
# 8.8 总结
本章涵盖了与内存相关的大量内容,从虚拟内存 API(Virtual APIs)到堆。但这并非所有相关内容。在第 13 章中,我们将介绍更多与内存相关的 API,包括区段对象(Section object)、内存区(memory zone)和后备列表(lookaside list)。