Windows 进程内存探秘:从读取到改写
有没有想过,你的程序可以像"黑客"一样,窥探甚至修改其他进程的数据?今天我们来玩两个有趣的 Windows API ——
ReadProcessMemory和WriteProcessMemory,看看如何在 Windows 平台下实现进程间的内存操作。
原理简介
ReadProcessMemory 和 WriteProcessMemory 是 Windows 提供的用于跨进程内存操作的 API:
- ReadProcessMemory:读取指定进程的内存数据
- WriteProcessMemory:向指定进程的内存写入数据
⚠️ 安全提示:这些 API 通常需要管理员权限,且可能被安全软件标记为可疑行为。请仅在合法的学习和调试场景中使用!
第一步:准备目标进程
首先,我们需要一个"小白鼠"——一个会不断输出变量地址和值的程序。
目标进程代码
#include <iostream>
#include <thread>
#include <format>
#include <Windows.h>
using namespace std;
// 这是我们想要被外部程序读取和修改的变量
int num = 1;
int main()
{
// 获取当前进程的 PID,方便其他程序定位
DWORD pid = GetCurrentProcessId();
cout << format("当前进程 PID: {}", pid) << endl;
cout << "等待外部程序连接..." << endl;
// 无限循环,每 5 秒打印一次 num 的地址和值
while (1)
{
// 打印 num 的内存地址和当前值
// 使用 static_cast<void*> 将 int* 转换为 void*,以便正确显示地址
std::cout << format("[心跳] num 的地址: {}, 当前值: {}",
static_cast<void*>(&num), num)
<< std::endl;
// 休眠 5 秒
std::this_thread::sleep_for(std::chrono::seconds(5));
}
return 0;
}
编译并运行
编译后运行这个程序,你会看到类似这样的输出:
当前进程 PID: 203532
等待外部程序连接...
[心跳] num 的地址: 0x7ff7028be000, 当前值: 1
记下这两个关键信息:
- PID(进程 ID):如
203532 - num 的地址:如
0x7ff7028be000
这些信息将在下一步用到。
第二步:读取目标进程内存
现在我们来编写"窥探者"程序,读取目标进程中 num 的值。
读取内存代码
#include <iostream>
#include <Windows.h>
#include <format>
using namespace std;
int main()
{
// ==================== 配置参数 ====================
// 目标进程的 PID,需要替换为你实际运行的目标进程 PID
DWORD processId = 203532;
// num 变量在目标进程中的内存地址
// 注意:每次运行目标程序,这个地址可能会变化!
LPVOID numAddress = (LPVOID)0x7ff7028be000;
// ==================================================
// 1. 打开目标进程,获取进程句柄
// PROCESS_ALL_ACCESS 表示请求所有访问权限
// FALSE 表示不继承句柄
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId);
// 检查是否成功打开进程
if (!process) {
cerr << "错误:无法打开目标进程!请检查 PID 是否正确。" << endl;
return -1;
}
cout << "成功连接到目标进程!" << endl;
// 2. 准备读取数据的缓冲区
SIZE_T readCnt = 0; // 实际读取的字节数
int num = 0; // 用于存储读取到的数据
// 3. 读取目标进程的内存
// 参数说明:
// - process: 目标进程句柄
// - numAddress: 要读取的内存地址
// - &num: 接收数据的缓冲区
// - 4: 要读取的字节数(int 类型占 4 字节)
// - &readCnt: 实际读取的字节数(可选,可为 NULL)
BOOL readResult = ReadProcessMemory(
process, // 目标进程句柄
numAddress, // 目标内存地址
&num, // 数据缓冲区
sizeof(int), // 读取大小
&readCnt // 实际读取字节数
);
// 检查读取是否成功
if (!readResult) {
cerr << "错误:读取内存失败!错误代码: " << GetLastError() << endl;
CloseHandle(process);
return -1;
}
// 4. 输出读取结果
cout << format("✓ 成功读取!目标进程中的 num = {}", num) << endl;
cout << format(" 读取字节数: {} bytes", readCnt) << endl;
// 5. 关闭进程句柄,释放资源
CloseHandle(process);
return 0;
}
运行效果
运行程序后,你会看到:
成功连接到目标进程!
✓ 成功读取!目标进程中的 num = 1
读取字节数: 4 bytes
太棒了!我们已经成功"偷看"到了其他进程的数据!
第三步:改写目标进程内存
光看不练假把式,接下来我们尝试修改目标进程的 num 值!
改写内存代码
在上面的代码基础上,添加写入操作:
// ...(前面的代码与读取示例相同)...
// 读取成功后,我们尝试写入新值
int newValue = 100; // 要写入的新值
// 使用 WriteProcessMemory 写入数据
// 参数与 ReadProcessMemory 类似
BOOL writeResult = WriteProcessMemory(
process, // 目标进程句柄
numAddress, // 目标内存地址
&newValue, // 要写入的数据
sizeof(int), // 写入大小
&readCnt // 实际写入字节数
);
// 检查写入是否成功
if (!writeResult) {
cerr << "错误:写入内存失败!错误代码: " << GetLastError() << endl;
CloseHandle(process);
return -1;
}
cout << format("✓ 成功写入!已将 num 修改为: {}", newValue) << endl;
cout << format(" 写入字节数: {} bytes", readCnt) << endl;
// ...(关闭句柄等后续代码)...
完整写入示例
#include <iostream>
#include <Windows.h>
#include <format>
using namespace std;
int main()
{
// ==================== 配置参数 ====================
DWORD processId = 203532; // 目标进程 PID
LPVOID numAddress = (LPVOID)0x7ff7028be000; // num 变量的地址
// ==================================================
// 1. 打开目标进程
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId);
if (!process) {
cerr << "错误:无法打开目标进程!" << endl;
return -1;
}
cout << "=== 进程内存操作演示 ===" << endl;
cout << "目标 PID: " << processId << endl;
cout << "目标地址: " << numAddress << endl << endl;
// 2. 读取当前值
int currentValue = 0;
SIZE_T bytesProcessed = 0;
BOOL readResult = ReadProcessMemory(
process, numAddress, ¤tValue, sizeof(int), &bytesProcessed
);
if (!readResult) {
cerr << "读取失败!错误: " << GetLastError() << endl;
CloseHandle(process);
return -1;
}
cout << "【读取】当前值: " << currentValue << endl;
// 3. 写入新值
int newValue = 100;
BOOL writeResult = WriteProcessMemory(
process, numAddress, &newValue, sizeof(int), &bytesProcessed
);
if (!writeResult) {
cerr << "写入失败!错误: " << GetLastError() << endl;
CloseHandle(process);
return -1;
}
cout << "【写入】已修改为: " << newValue << endl;
// 4. 验证写入结果(再次读取)
int verifyValue = 0;
ReadProcessMemory(process, numAddress, &verifyValue, sizeof(int), &bytesProcessed);
cout << "【验证】读取确认: " << verifyValue << endl;
// 5. 清理资源
CloseHandle(process);
cout << "\n操作完成!" << endl;
return 0;
}
运行效果
当你运行写入程序后,目标进程的输出会从:
[心跳] num 的地址: 0x7ff7028be000, 当前值: 1
变成:
[心跳] num 的地址: 0x7ff7028be000, 当前值: 100
神奇吧!我们已经成功"黑"进了另一个进程,并修改了它的数据!
API 参考速查表
| API | 功能 | 关键参数 |
|---|---|---|
OpenProcess | 打开目标进程,获取句柄 | dwDesiredAccess(权限)、dwProcessId(PID) |
ReadProcessMemory | 读取进程内存 | hProcess(句柄)、lpBaseAddress(地址)、lpBuffer(缓冲区)、nSize(大小) |
WriteProcessMemory | 写入进程内存 | 同上 |
CloseHandle | 关闭进程句柄,释放资源 | hObject(句柄) |
常用进程访问权限
PROCESS_VM_READ // 读取内存权限
PROCESS_VM_WRITE // 写入内存权限
PROCESS_VM_OPERATION // 内存操作权限
PROCESS_ALL_ACCESS // 所有权限(最宽松)
常见问题与注意事项
❌ 问题 1:OpenProcess 返回 NULL
可能原因:
- PID 错误,目标进程未运行
- 权限不足(尝试以管理员身份运行)
- 目标进程受到保护(如系统进程、安全软件)
解决方案:
- 仔细检查 PID 和地址
- 右键 → 以管理员身份运行
- 确保目标进程是普通用户进程
❌ 问题 2:读写操作失败
可能原因:
- 地址无效或不可访问
- 目标内存页没有读写权限
- 地址是随机的,每次运行都会变化
解决方案:
- 确认地址正确(检查目标程序的输出)
- 注意:开启 ASLR(地址空间布局随机化)的程序,地址每次运行都会变
⚠️ 安全警告
- 杀毒软件:这些 API 常被恶意软件使用,可能会被杀毒软件拦截
- 系统稳定性:随意修改其他进程内存可能导致崩溃
- 法律风险:未经授权访问他人计算机进程可能违法
进阶话题
1. 查找动态地址
实际应用中,地址不是固定的。你需要:
- 通过模块基址 + 偏移量计算
- 使用
CreateToolhelp32Snapshot枚举进程模块 - 遍历内存查找特征码
2. 注入代码
除了读写数据,还可以:
- 使用
VirtualAllocEx在目标进程分配内存 - 使用
WriteProcessMemory写入代码 - 使用
CreateRemoteThread执行远程线程
⚠️ 严肃警告:代码注入技术常被用于恶意目的,请务必遵守法律法规!
3. 防护方案
如果你是软件开发者,想防止被他人读写内存:
- 使用代码混淆和加壳
- 检测调试器和内存修改
- 关键数据加密存储
- 使用驱动级保护
总结
通过本文,我们学习了:
✅ 使用 OpenProcess 打开目标进程
✅ 使用 ReadProcessMemory 读取进程内存
✅ 使用 WriteProcessMemory 修改进程内存
✅ 了解了进程内存操作的基本原理和注意事项
这仅仅是 Windows 进程间操作的冰山一角。掌握这些 API 后,你可以开发游戏修改器、调试工具、进程监控软件等。但请记住:能力越大,责任越大,请在合法合规的前提下使用这些技术!
参考资料
本文仅供学习交流使用,请勿用于非法用途!