模块 6 - Windows 内存管理
恶意软件开发课程 - Windows 内存管理
模块 6 - Windows 内存管理#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
Windows 内存管理#
简介#
本模块介绍 Windows 内存的基础知识。理解 Windows 如何处理内存对于构建高级恶意软件至关重要。
💡 初学者提示:为什么内存管理很重要?
在恶意软件开发中,我们需要:
- 分配内存来存储 Payload(恶意代码)
- 修改内存保护属性(使代码可执行)
- 将代码注入到其他进程的内存中
- 理解内存如何被检测
掌握内存管理是几乎所有高级技术的基础!
虚拟内存和分页#
现代操作系统中的内存不会直接映射到物理内存(即 RAM)。相反,进程使用虚拟内存地址,这些地址映射到物理内存地址。这样做有几个原因,但最终目标是尽可能节省物理内存。虚拟内存可能映射到物理内存,但也可以存储在磁盘上。通过虚拟内存寻址,多个进程可以共享同一物理地址,同时拥有唯一的虚拟内存地址。虚拟内存依赖于内存分页的概念,它将内存划分为 4KB的块,称为”页 (pages)”。
参见Windows Internals 7th edition - part 1 ↗ 书中的下图。

📚 知识扩展:虚拟内存 vs 物理内存
物理内存 (Physical Memory):
- 实际的 RAM 硬件
- 整个系统共享
- 容量有限(例如 16GB)
虚拟内存 (Virtual Memory):
- 每个进程独有的”假”内存地址空间
- x64 系统:理论上最大 128TB
- 操作系统负责映射到实际物理内存
为什么使用虚拟内存?
- 隔离:每个进程有自己的地址空间,互不干扰
- 安全:进程 A 无法直接访问进程 B 的内存
- 灵活:可以分配超过物理 RAM 的内存(使用磁盘交换)
- 简化:程序员不用关心物理内存的实际位置
类比理解:
- 虚拟内存就像你家的门牌号(例如:123号)
- 物理内存是实际的房子位置
- 邮递员(操作系统)知道如何将门牌号转换成实际位置
- 每家(进程)都有自己的门牌号系统,互不冲突
分页 (Paging):
- 内存被分成 4KB 的小块(页)
- 像书的页码一样管理
- 可以将不常用的页换出到磁盘(页面文件)
页面状态#
进程虚拟地址空间内的页面可以处于 3 种状态之一:
-
空闲 (Free) - 页面既未提交也未保留。该页面对进程不可访问。它可以被保留、提交或同时保留和提交。尝试读取或写入空闲页面会导致访问违规异常。
-
保留 (Reserved) - 页面已为将来使用而保留。该地址范围不能被其他分配函数使用。该页面不可访问,没有与之关联的物理存储。它可以被提交。
-
提交 (Committed) - 已从 RAM 的总大小和磁盘上的页面文件中分配了内存费用。该页面可访问,访问由内存保护常量之一控制。系统仅在首次尝试读取或写入该页面时才初始化并加载每个已提交的页面到物理内存。当进程终止时,系统释放已提交页面的存储。
💡 初学者提示:理解三种页面状态
用买房来类比:
Free(空闲)= 市场上的房子
- 没人占用
- 你可以去买/租
- 但现在你不能进去
Reserved(保留)= 付了定金的房子
- 你已经声明要这个地址范围
- 别人不能用这个范围
- 但你还没付全款,不能住进去
- 没有实际占用物理内存(RAM)
Committed(提交)= 你真正买下并住进去的房子
- 已经付全款(占用了 RAM 或页面文件)
- 可以实际使用
- 有访问权限控制(读/写/执行)
在恶意软件开发中的应用:
c// 步骤 1: Reserve(保留地址空间) PVOID pAddress = VirtualAlloc( NULL, 1024, MEM_RESERVE, // 只保留,不提交 PAGE_READWRITE ); // 步骤 2: Commit(真正分配内存) VirtualAlloc( pAddress, 1024, MEM_COMMIT, // 提交内存 PAGE_READWRITE ); // 或者一步到位: PVOID pAddress = VirtualAlloc( NULL, 1024, MEM_RESERVE | MEM_COMMIT, // 同时保留和提交 PAGE_READWRITE );
页面保护选项#
一旦页面被提交,它们需要设置其保护选项。内存保护常量列表可以在这里 ↗找到,但下面列出了一些示例。
-
PAGE_NOACCESS- 禁用对已提交页面区域的所有访问。尝试读取、写入或执行已提交区域将导致访问违规。 -
PAGE_EXECUTE_READWRITE- 启用读取、写入和执行。强烈不建议使用此选项,通常被视为 IoC(威胁指标),因为内存同时可写和可执行是不常见的。 -
PAGE_READONLY- 对已提交的页面区域启用只读访问。尝试写入已提交区域会导致访问违规。
💡 初学者提示:常用的内存保护属性
内存保护就像文件权限:
保护属性 读 写 执行 常见用途 PAGE_NOACCESS❌ ❌ ❌ 未使用的内存 PAGE_READONLY✅ ❌ ❌ 常量数据 PAGE_READWRITE✅ ✅ ❌ 普通数据 PAGE_EXECUTE❌ ❌ ✅ 只执行(罕见) PAGE_EXECUTE_READ✅ ❌ ✅ 代码段(正常) PAGE_EXECUTE_READWRITE✅ ✅ ✅ 可疑! 为什么 PAGE_EXECUTE_READWRITE 可疑?
plaintext正常程序: - 代码段:可读可执行(不可写) - 数据段:可读可写(不可执行) 恶意软件: - 需要写入 Shellcode 到内存 - 然后执行它 - 所以需要 RWX 权限EDR/AV 监控:
- 大多数 EDR 会标记 RWX 内存分配
- 这是非常明显的恶意行为指标
更隐蔽的做法:
c// 1. 分配为 RW (可读可写) PVOID pAddress = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_READWRITE); // 2. 写入 Shellcode memcpy(pAddress, shellcode, size); // 3. 改为 RX (可读可执行) DWORD oldProtect; VirtualProtect(pAddress, size, PAGE_EXECUTE_READ, &oldProtect); // 4. 执行 ((void(*)())pAddress)();
内存保护机制#
现代操作系统通常具有内置的内存保护机制,以阻止漏洞利用和攻击。在构建或调试恶意软件时,这些也很重要,因为可能会遇到它们。
-
数据执行保护 (DEP - Data Execution Prevention) - DEP 是内置于操作系统中的系统级内存保护功能,从 Windows XP 和 Windows Server 2003 开始。如果页面保护选项设置为 PAGE_READONLY,则 DEP 将阻止在该内存区域执行代码。
-
地址空间布局随机化 (ASLR - Address Space Layout Randomization) - ASLR 是一种用于防止内存损坏漏洞利用的内存保护技术。ASLR 随机排列进程的关键数据区域的地址空间位置,包括可执行文件的基址以及堆栈、堆和库的位置。
📚 知识扩展:DEP 和 ASLR 详解
DEP (数据执行保护):
目的: 防止在数据段执行代码
工作原理:
- CPU 硬件支持(NX位 - No Execute)
- 将内存分为”可执行”和”不可执行”
- 尝试执行数据段中的代码 → 程序崩溃
为什么需要 DEP? 经典缓冲区溢出攻击:
plaintext1. 攻击者溢出堆栈缓冲区 2. 将恶意代码写入堆栈 3. 修改返回地址指向恶意代码 4. 函数返回时执行恶意代码 DEP 阻止步骤 4:堆栈标记为不可执行!绕过 DEP 的方法:
- ROP (Return-Oriented Programming)
- 使用 VirtualProtect 修改内存保护
- 使用已有的可执行内存区域
ASLR (地址空间布局随机化):
目的: 使攻击者难以预测内存地址
没有 ASLR 的情况:
plaintext每次运行,程序总是加载到相同的地址: - ntdll.dll: 0x77A00000 - kernel32.dll: 0x76D00000 攻击者可以硬编码这些地址!有 ASLR 的情况:
plaintext每次运行,地址都不同: 运行 1: - ntdll.dll: 0x7FFA1234000 - kernel32.dll: 0x7FF98765000 运行 2: - ntdll.dll: 0x7FFABCDE000 - kernel32.dll: 0x7FF9DEAF000ASLR 随机化什么?
- 可执行文件基址
- DLL 加载地址
- 堆栈位置
- 堆位置
绕过 ASLR 的方法:
- 信息泄露(读取实际地址)
- Brute force(不断尝试)
- 使用未启用 ASLR 的模块
x86 vs x64 内存空间#
处理 Windows 进程时,重要的是要注意进程是 x86 还是 x64。x86 进程的内存空间较小,为 4GB (0xFFFFFFFF),而 x64 的内存空间大得多,为 128TB (0xFFFFFFFFFFFFFFFF)。
💡 初学者提示:x86 vs x64 内存空间对比
特性 x86 (32位) x64 (64位) 最大虚拟内存 4 GB 128 TB 地址格式 32位 (8个十六进制字符) 64位 (16个十六进制字符) 示例地址 0x123456780x00007FF812345678用户空间 2 GB (默认) 约 128 TB 内核空间 2 GB 约 128 TB 指针大小 4 字节 8 字节 为什么这很重要?
1. 开发跨架构恶意软件:
c// 错误的做法(假设32位) DWORD address = 0x12345678; // 正确的做法(适用于32和64位) ULONG_PTR address = 0x12345678;2. Shellcode 兼容性:
- x86 Shellcode 无法在 x64 进程中运行
- 需要分别生成:
bash# x64 Shellcode msfvenom -p windows/x64/exec CMD=calc -f c # x86 Shellcode msfvenom -p windows/exec CMD=calc -f c3. WoW64 (Windows-on-Windows 64):
- 64位 Windows 可以运行 32位程序
- 通过 WoW64 子系统实现兼容
- 但 32位程序仍然只有 4GB 内存限制
分配内存示例#
此示例通过小代码片段更好地理解如何通过 C 函数和 Windows API 与 Windows 内存交互。与内存交互的第一步是分配内存。下面的代码片段演示了几种分配内存的方法,这实际上是在运行进程内保留内存。
// 分配 *100* 字节的内存缓冲区
// 方法 1 - 使用 malloc()
PVOID pAddress = malloc(100);
// 方法 2 - 使用 HeapAlloc()
PVOID pAddress = HeapAlloc(GetProcessHeap(), 0, 100);
// 方法 3 - 使用 LocalAlloc()
PVOID pAddress = LocalAlloc(LPTR, 100);c内存分配函数返回基地址,它只是指向已分配内存块开头的指针。使用上面的代码片段,pAddress 将是已分配内存块的基地址。使用此指针可以执行多种操作,例如读取、写入和执行。可以执行的操作类型将取决于分配给已分配内存区域的保护。
下图显示了pAddress 在调试器下的样子。

当内存被分配时,它可能是空的或包含随机数据。一些内存分配函数提供了在分配过程中将内存区域清零的选项。

💡 初学者提示:三种内存分配方法的区别
函数 来源 性能 用途 优点 缺点 malloc()C 运行时库 快 小内存分配 简单、跨平台 依赖 CRT HeapAlloc()Windows API 中 通用 灵活、Windows原生 稍复杂 LocalAlloc()Windows API 慢 兼容性 向后兼容 已过时 VirtualAlloc()Windows API 慢 大内存/特殊需求 完全控制 最复杂 推荐用法:
普通数据存储:
c// 使用 malloc (简单) char* buffer = (char*)malloc(100); // ... 使用 buffer free(buffer);恶意软件开发(需要控制保护属性):
c// 使用 VirtualAlloc (强大) PVOID pShellcode = VirtualAlloc( NULL, // 让系统选择地址 shellcodeSize, // 大小 MEM_COMMIT | MEM_RESERVE, // 提交并保留 PAGE_READWRITE // 先设为可读可写 );内存清零的重要性:
c// 不清零 - 可能包含敏感数据残留! PVOID pAddr1 = HeapAlloc(GetProcessHeap(), 0, 100); // pAddr1 可能包含之前释放的内存数据 // 清零 - 安全 PVOID pAddr2 = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 100); // pAddr2 保证全是 0
写入内存示例#
内存分配后的下一步通常是写入该缓冲区。可以使用多种选项写入内存,但在此示例中,使用了 memcpy。
PVOID pAddress = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 100);
CHAR* cString = "MalDev Academy Is The Best";
memcpy(pAddress, cString, strlen(cString));cHeapAlloc 使用 HEAP_ZERO_MEMORY 标志,这会使分配的内存初始化为零。然后使用 memcpy 将字符串复制到已分配的内存中。memcpy 中的最后一个参数是要复制的字节数。接下来,重新检查缓冲区以验证数据是否已成功写入。

💡 初学者提示:常用的内存操作函数
写入内存:
c// 方法 1: memcpy - 复制内存块 memcpy(dest, source, size); // 方法 2: strcpy - 复制字符串(不安全,避免使用) strcpy(dest, source); // 方法 3: memset - 填充相同的字节 memset(dest, 0x90, size); // 填充 NOP 指令 // 方法 4: WriteProcessMemory - 写入其他进程 WriteProcessMemory(hProcess, dest, source, size, NULL);实际应用示例:
c// 分配内存 PVOID pPayload = VirtualAlloc(NULL, payloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // 写入 Shellcode memcpy(pPayload, shellcode, payloadSize); // 修改保护属性为可执行 DWORD oldProtect; VirtualProtect(pPayload, payloadSize, PAGE_EXECUTE_READ, &oldProtect); // 执行 ((void(*)())pPayload)();常见错误:
c// ❌ 错误:写入超过分配的大小 PVOID p = malloc(10); memcpy(p, "This is a very long string", 26); // 缓冲区溢出! // ✅ 正确:确保大小足够 size_t len = strlen("This is a very long string") + 1; PVOID p = malloc(len); memcpy(p, "This is a very long string", len);
释放已分配的内存#
当应用程序使用完已分配的缓冲区后,强烈建议释放或取消分配缓冲区,以避免内存泄漏 ↗。
根据用于分配内存的函数,它将具有相应的内存释放函数。例如:
- 使用
malloc分配需要使用free函数。 - 使用
HeapAlloc分配需要使用HeapFree函数。 - 使用
LocalAlloc分配需要使用LocalFree函数。
下图显示了 HeapFree 的操作,释放地址 0000023ADE449900 处的已分配内存。请注意,地址 0000023ADE449900 仍然存在于进程中,但其原始内容被随机数据覆盖。这些新数据很可能是操作系统在进程内部执行的新分配导致的。


💡 初学者提示:内存泄漏和安全释放
什么是内存泄漏?
cvoid leakyFunction() { PVOID p = malloc(1000); // ... 使用 p // ❌ 忘记调用 free(p) return; // p 的地址丢失了,内存永远无法释放! } // 调用10000次后,泄漏了约 10MB 内存 for (int i = 0; i < 10000; i++) { leakyFunction(); }配对释放函数:
分配函数 释放函数 malloc()free()HeapAlloc()HeapFree()LocalAlloc()LocalFree()VirtualAlloc()VirtualFree()安全的内存管理模式:
cPVOID pData = NULL; // 分配 pData = malloc(size); if (pData == NULL) { printf("分配失败\n"); return -1; } // 使用 memcpy(pData, source, size); // 释放 if (pData != NULL) { free(pData); pData = NULL; // 防止 double-free }恶意软件中的考虑:
释放内存的利弊:
plaintext✅ 优点: - 减少内存占用 - 避免被识别为异常进程 ❌ 缺点: - 释放后内容可能被覆盖(证据清除) - 但也可能被取证工具恢复安全清除敏感数据:
c// 释放前清零 SecureZeroMemory(pSensitiveData, dataSize); // 或 RtlSecureZeroMemory(pSensitiveData, dataSize); // 然后释放 free(pSensitiveData);为什么使用 SecureZeroMemory?
- 普通的
memset可能被编译器优化掉SecureZeroMemory保证不会被优化- 确保敏感数据(如密钥、密码)被真正清除
🎯 本模块学习要点
- ✅ 理解虚拟内存和物理内存的区别
- ✅ 掌握三种页面状态:Free、Reserved、Committed
- ✅ 了解常见的内存保护属性
- ✅ 理解 DEP 和 ASLR 的工作原理
- ✅ 掌握多种内存分配和释放方法
- ✅ 了解 x86 和 x64 内存空间的差异
- ✅ 学会写入和操作内存
关键概念:
- 虚拟内存:每个进程独立的地址空间
- 分页:4KB 为单位的内存管理
- 内存保护:RWX 权限控制
- DEP:防止数据段执行代码
- ASLR:随机化内存布局
实践练习:
- 使用 VirtualAlloc 分配内存
- 写入一些数据
- 修改内存保护属性
- 使用 Process Hacker 查看内存区域
- 正确释放内存
下一步: 学习 Windows API 的详细使用