这次我们来看一个非常“复古”的技术实践:用经典的 Visual C++ 6.0 来编写一个修改《暗黑破坏神2》游戏金钱的小程序。这听起来像是技术考古,但它能帮你快速理解游戏内存修改、进程读写和 Windows API 编程的核心原理。对于想学习逆向工程、游戏修改或单纯想重温 VC6 开发体验的开发者来说,这是一个绝佳的入门项目。
这个项目的核心不是开发一个复杂的商业工具,而是聚焦于“能不能用”、“怎么用”和“为什么能行”。我们将从零开始,在 VC6 环境下,一步步实现定位《暗黑破坏神2》进程、读取内存数据、分析金钱地址偏移、并实现修改功能。整个过程不依赖复杂的第三方库,纯粹使用 Windows API,让你清晰地看到数据在内存中是如何被找到和操作的。
本文会带你完成从环境搭建、代码编写、调试到最终测试的完整流程。你将了解到如何在现代操作系统上运行 VC6 项目、如何定位动态内存地址、以及编写一个稳定可用的内存修改工具的关键步骤。无论你是对游戏修改感兴趣的新手,还是想巩固 Windows 编程基础的老手,这篇文章都能提供直接的、可落地的代码和思路。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 开发环境 | Microsoft Visual C++ 6.0 (VC6) |
| 目标程序 | 《暗黑破坏神2》(Diablo II)经典版 |
| 核心功能 | 读取和修改游戏进程中的金钱数值 |
| 技术原理 | Windows API 进程内存读写、指针寻址 |
| 硬件门槛 | 极低,能运行 Windows 和 VC6 即可,无需独立显卡 |
| 启动方式 | 编译生成 EXE 后直接双击运行 |
| 内存占用 | 程序本身内存占用极小(通常 < 10MB) |
| 接口能力 | 无网络接口,纯本地单机工具 |
| 批量任务 | 不支持,为单次手动修改设计 |
| 适合场景 | 学习 Windows 进程内存操作、游戏数据修改原理、VC6/MFC 编程练习 |
2. 适用场景与使用边界
这个项目主要适用于以下几类开发者和爱好者:
- Windows C++ 编程学习者:通过一个有趣的目标(修改游戏),直观地学习
OpenProcess、ReadProcessMemory、WriteProcessMemory等关键 API 的使用。 - 游戏修改入门者:希望了解“游戏修改器”是如何工作的,理解静态地址、动态指针、偏移量等基本概念。
- 怀旧技术爱好者:体验在经典的 VC6 集成开发环境中完成一个完整小项目的过程,感受早期 Windows 开发的氛围。
- 《暗黑破坏神2》玩家:希望在不使用现成修改器的情况下,理解并实践如何修改自己的游戏数据(仅限单机模式)。
重要使用边界与合规提醒:
- 仅限单机与学习用途:本教程及所编写的工具严格限于修改本地单机游戏存档,旨在用于技术学习和原理验证。严禁用于任何形式的在线游戏、战网模式或干扰他人游戏体验,这可能违反游戏用户协议并导致账号封禁。
- 尊重知识产权:《暗黑破坏神2》是暴雪娱乐公司的知识产权。任何修改行为不应损害版权方的利益,且修改后的内容不应用于商业用途或非法传播。
- 风险自担:不当的内存读写可能导致游戏进程崩溃或存档损坏。操作前请备份好你的原始游戏存档文件(通常位于游戏安装目录的
Save文件夹内)。 - 技术原理通用,但地址会变:本文演示的金钱地址查找方法和代码逻辑是通用的,但具体的基地址、偏移量可能因游戏版本(如1.14d)、是否使用 PlugY 等大型仓库插件而不同。你需要掌握方法,并自行验证当前游戏环境下的正确地址。
3. 环境准备与前置条件
在开始编码之前,你需要准备好以下环境和工具:
- 操作系统:Windows 7 / 8 / 10 / 11。VC6 作为较老的开发环境,在较新的 Windows 系统上可能需要兼容性设置。
- 开发环境:Microsoft Visual C++ 6.0 (VC6)。你需要安装 VC6 的完整版本。由于它是旧软件,你可能需要在网上寻找安装包,并在安装和运行时为
MSDEV.EXE(VC6主程序) 和CL.EXE(编译器) 设置 Windows XP 兼容模式,并以管理员身份运行,以避免编译和调试问题。 - 目标游戏:《暗黑破坏神2》经典版(例如1.14d版本)。确保游戏可以正常运行。建议使用单机模式进行测试。
- 辅助工具(非必须但强烈推荐):
- Cheat Engine (CE):一款强大的内存扫描和调试工具。我们将使用它来定位游戏中的金钱地址,这是整个项目的关键第一步。你可以从其官网下载。
- 进程查看器(如 Process Explorer):帮助确认游戏进程的准确名称和PID。
环境检查清单:
- [ ] VC6 成功安装并可创建新项目。
- [ ] 《暗黑破坏神2》可正常启动并进入单机游戏。
- [ ] Cheat Engine 准备就绪。
- [ ] 知晓游戏存档备份位置。
4. 项目创建与基础框架
首先,我们在 VC6 中创建一个项目。
- 打开 VC6,点击
File->New。 - 在
Projects标签页中,选择MFC AppWizard (exe)。 - 在
Project name中输入项目名,例如D2GoldEditor,并选择好保存位置。 - 点击
OK,在接下来的向导中:- Step 1: 选择
Dialog based(基于对话框),创建一个简单的窗口程序。 - 后续步骤可以保持默认,直接点击
Finish,然后OK即可。
- Step 1: 选择
现在,你得到了一个基础的对话框程序。打开资源视图(ResourceView),编辑主对话框IDD_D2GOLDEDITOR_DIALOG。 删除默认的静态文本和“确定”按钮,添加以下控件:
- 一个
Edit Control(文本框),用于显示/输入金钱值,ID设为IDC_EDIT_GOLD。 - 一个
Button(按钮),用于读取金钱,ID设为IDC_BUTTON_READ。 - 一个
Button(按钮),用于写入金钱,ID设为IDC_BUTTON_WRITE。 - 一个
Static Text(静态文本),可以写上“金钱数值:”。
调整布局,使其看起来像一个简单的小工具界面。接下来,我们需要为这些控件添加变量和消息处理函数。
5. 关键原理:定位游戏金钱地址
这是整个项目的核心。我们使用 Cheat Engine (CE) 来完成。
步骤 1:启动游戏并附加进程
- 运行《暗黑破坏神2》,进入单机游戏,让角色拥有一些金钱(例如 1000)。
- 打开 Cheat Engine,点击左上角的电脑图标(“Select a process to open”)。
- 在进程列表中,找到
Game.exe(这是《暗黑破坏神2》的主进程),选中它并点击Open。
步骤 2:首次扫描
- 在
Value输入框旁边,选择Value Type为4 Bytes(因为金钱值通常以4字节整数存储)。 - 在
Value输入框中输入你当前游戏中的金钱数(例如 1000)。 - 点击
First Scan。CE 会扫描出大量地址。
步骤 3:改变数值并再次扫描
- 回到游戏,通过买卖物品等方式改变你的金钱数(例如,花掉 100,变成 900)。
- 在 CE 的
Value输入框中,输入新的金钱数(900)。 - 点击
Next Scan。列表中的地址数量会大幅减少。 - 重复步骤1-3(改变金钱值,输入新值,点
Next Scan),直到列表中只剩下少数几个(理想情况下是1-2个)地址。
步骤 4:验证地址
- 在地址列表中,双击最有可能的地址,将其添加到下方的地址列表中。
- 在下方地址列表中,直接修改该地址的数值,然后切回游戏查看金钱是否变化。如果变化,说明找到了正确的地址。
- 重要:找到的地址是动态地址,每次启动游戏都会变化。我们需要找到指向它的静态指针。
步骤 5:查找指针(关键步骤)
- 在 CE 下方的地址列表中,右键点击你找到的金钱地址,选择
Find out what writes to this address。 - 切回游戏,进行一次会改变金钱的操作(比如再买点东西)。
- CE 会捕获到一条汇编指令,例如
mov [eax+000055F0], ecx。这里的eax是基址寄存器,000055F0是偏移量(Offset)。 - 记下这个偏移量(例如
0x55F0)。 - 关闭这个窗口,回到主界面。在地址列表右键点击该地址,现在选择
Find out what accesses this address。 - 同样操作一次,CE 可能会显示
mov ecx, [eax+000055F0],确认偏移量。 - 现在,我们需要找到
eax的值(即基地址)。在 CE 主界面,点击Memory View按钮。 - 在 Memory View 窗口中,点击
Tools->Dissect data/structures->Dissect PE headers。这有助于分析。 - 更直接的方法是:在 Memory View 中,点击
Tools->Auto Assemble->Template->Find out what addresses this pointer points to。 - 在弹出的脚本窗口中,它会提示你输入偏移量。输入你记下的偏移量(
55F0),点击OK。 - CE 会尝试找出哪些指针指向
[基址+55F0]这个位置。它可能会给出一个或多个“基地址”(Base Address),这些地址通常位于游戏的模块(如Game.exe)内部,并且每次启动游戏是固定的(静态地址)。 - 记下这个基地址(例如
Game.exe+XXXXXX)和偏移量(0x55F0)。这个“基地址+偏移量”的组合就是我们代码中需要用来寻址的指针路径。
假设我们最终找到的指针路径是:Game.exe+0x123456->偏移 0x55F0。这意味着:
- 先获取
Game.exe模块的基址。 - 在该基址上加上
0x123456,得到一个地址 A。 - 读取地址 A 中存储的值,这个值是一个指针 B。
- 在指针 B 的值上加上
0x55F0,得到最终的金钱地址 C。 - 对地址 C 进行读写操作。
6. 代码实现:内存读写功能
现在,我们将寻址逻辑和 Windows API 调用写入 VC6 项目。首先,在对话框类的头文件(D2GoldEditorDlg.h)中添加必要的函数声明和变量。
// D2GoldEditorDlg.h : header file // 在类声明中添加 class CD2GoldEditorDlg : public CDialog { // ... 其他代码 ... private: // 自定义函数 DWORD GetD2ProcessId(); // 获取暗黑2进程ID HANDLE OpenD2Process(); // 打开进程句柄 DWORD_PTR FindGoldAddress(HANDLE hProcess); // 根据指针路径查找金钱地址 BOOL ReadGoldValue(HANDLE hProcess, DWORD_PTR goldAddr, DWORD& gold); // 读取金钱 BOOL WriteGoldValue(HANDLE hProcess, DWORD_PTR goldAddr, DWORD gold); // 写入金钱 // 根据CE查找结果定义的指针路径(示例值,需替换!) const DWORD_PTR BASE_OFFSET = 0x00123456; // “Game.exe+0x123456” 中的偏移 const DWORD GOLD_OFFSET = 0x55F0; // 最终偏移 };接下来,在实现文件(D2GoldEditorDlg.cpp)中实现这些函数。
// D2GoldEditorDlg.cpp : implementation file #include "stdafx.h" #include "D2GoldEditor.h" #include "D2GoldEditorDlg.h" #include <tlhelp32.h> // 用于进程快照 #include <psapi.h> // 用于模块枚举 (如果需要) #pragma comment(lib, "psapi.lib") // 链接 Psapi.lib // ... 其他代码 ... // 函数1:根据进程名获取进程ID DWORD CD2GoldEditorDlg::GetD2ProcessId() { DWORD pid = 0; HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnapshot != INVALID_HANDLE_VALUE) { PROCESSENTRY32 pe32; pe32.dwSize = sizeof(PROCESSENTRY32); if (Process32First(hSnapshot, &pe32)) { do { // 进程名可能是 "Game.exe" 或 "Diablo II.exe",根据你的游戏版本确定 if (_tcsicmp(pe32.szExeFile, _T("Game.exe")) == 0) { pid = pe32.th32ProcessID; break; } } while (Process32Next(hSnapshot, &pe32)); } CloseHandle(hSnapshot); } return pid; } // 函数2:打开进程,获取具有读写权限的句柄 HANDLE CD2GoldEditorDlg::OpenD2Process() { DWORD pid = GetD2ProcessId(); if (pid == 0) { AfxMessageBox(_T("未找到《暗黑破坏神2》进程!请确保游戏已运行。")); return NULL; } // PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION 是读写内存所需权限 HANDLE hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid); if (hProcess == NULL) { AfxMessageBox(_T("无法打开进程,请尝试以管理员身份运行本程序。")); } return hProcess; } // 函数3:根据指针路径计算最终的金钱地址 DWORD_PTR CD2GoldEditorDlg::FindGoldAddress(HANDLE hProcess) { if (hProcess == NULL) return 0; // 第一步:获取Game.exe模块的基址 // 方法A:简单假设为0x400000(常见),但不准确 // DWORD_PTR gameBase = 0x400000; // 方法B:枚举进程模块获取精确基址(更可靠) HMODULE hMods[1024]; DWORD cbNeeded; if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) { // 通常第一个模块就是主模块 Game.exe MODULEINFO modInfo; if (GetModuleInformation(hProcess, hMods[0], &modInfo, sizeof(modInfo))) { DWORD_PTR gameBase = (DWORD_PTR)modInfo.lpBaseOfDll; // 第二步:计算一级指针地址 DWORD_PTR pointerAddr = gameBase + BASE_OFFSET; // 第三步:读取一级指针的值 DWORD_PTR basePointerValue = 0; SIZE_T bytesRead = 0; if (ReadProcessMemory(hProcess, (LPCVOID)pointerAddr, &basePointerValue, sizeof(basePointerValue), &bytesRead)) { if (bytesRead == sizeof(basePointerValue) && basePointerValue != 0) { // 第四步:计算最终的金钱地址 DWORD_PTR goldAddr = basePointerValue + GOLD_OFFSET; return goldAddr; } } } } // 如果上述方法失败,回退到方法A(需确保BASE_OFFSET是相对于0x400000的) // DWORD_PTR pointerAddr = 0x400000 + BASE_OFFSET; // ... 后续读取逻辑相同 return 0; } // 函数4:从指定地址读取金钱值 BOOL CD2GoldEditorDlg::ReadGoldValue(HANDLE hProcess, DWORD_PTR goldAddr, DWORD& gold) { if (hProcess == NULL || goldAddr == 0) return FALSE; SIZE_T bytesRead = 0; return ReadProcessMemory(hProcess, (LPCVOID)goldAddr, &gold, sizeof(gold), &bytesRead) && (bytesRead == sizeof(gold)); } // 函数5:向指定地址写入金钱值 BOOL CD2GoldEditorDlg::WriteGoldValue(HANDLE hProcess, DWORD_PTR goldAddr, DWORD gold) { if (hProcess == NULL || goldAddr == 0) return FALSE; SIZE_T bytesWritten = 0; // 注意:写入前最好先确认地址可写,这里简单演示 return WriteProcessMemory(hProcess, (LPVOID)goldAddr, &gold, sizeof(gold), &bytesWritten) && (bytesWritten == sizeof(gold)); }关键点说明:
BASE_OFFSET和GOLD_OFFSET必须替换为你在 Cheat Engine 中找到的实际值。- 获取模块基址的
EnumProcessModules方法比硬编码0x400000更可靠,因为游戏可能被重定位。 - 所有内存操作都必须检查返回值,确保成功。
7. 界面交互与功能测试
现在,为对话框上的按钮添加事件处理函数。
- 在 VC6 的资源编辑器中,双击“读取”按钮,为其添加
BN_CLICKED消息处理函数OnButtonRead。 - 同理,为“写入”按钮添加
OnButtonWrite函数。
实现这两个函数:
// “读取”按钮点击事件 void CD2GoldEditorDlg::OnButtonRead() { // 1. 打开进程 HANDLE hProcess = OpenD2Process(); if (hProcess == NULL) return; // 2. 查找金钱地址 DWORD_PTR goldAddr = FindGoldAddress(hProcess); if (goldAddr == 0) { AfxMessageBox(_T("无法定位金钱地址,请检查指针偏移设置或游戏版本。")); CloseHandle(hProcess); return; } // 3. 读取金钱值 DWORD goldValue = 0; if (ReadGoldValue(hProcess, goldAddr, goldValue)) { // 将数值显示在编辑框中 CString strGold; strGold.Format(_T("%u"), goldValue); SetDlgItemText(IDC_EDIT_GOLD, strGold); } else { AfxMessageBox(_T("读取金钱失败!")); } // 4. 关闭进程句柄 CloseHandle(hProcess); } // “写入”按钮点击事件 void CD2GoldEditorDlg::OnButtonWrite() { // 1. 从编辑框获取用户输入的数值 CString strGold; GetDlgItemText(IDC_EDIT_GOLD, strGold); DWORD goldValueToWrite = _ttoi(strGold); // 简单验证 if (goldValueToWrite <= 0) { AfxMessageBox(_T("请输入有效的金钱数值(大于0)。")); return; } // 2. 打开进程 HANDLE hProcess = OpenD2Process(); if (hProcess == NULL) return; // 3. 查找金钱地址 DWORD_PTR goldAddr = FindGoldAddress(hProcess); if (goldAddr == 0) { AfxMessageBox(_T("无法定位金钱地址,请检查指针偏移设置或游戏版本。")); CloseHandle(hProcess); return; } // 4. 写入金钱值 if (WriteGoldValue(hProcess, goldAddr, goldValueToWrite)) { AfxMessageBox(_T("金钱修改成功!请切回游戏查看。")); // 写入后可以立即读取一次显示新值 OnButtonRead(); // 调用读取函数刷新显示 } else { AfxMessageBox(_T("写入金钱失败!")); } // 5. 关闭进程句柄 CloseHandle(hProcess); }功能测试流程:
- 编译运行:在 VC6 中按 F7 编译,按 Ctrl+F5 运行你的小程序。
- 启动游戏:运行《暗黑破坏神2》,进入单机游戏。
- 首次读取:点击小程序上的“读取”按钮。如果一切正常,当前角色的金钱数值应该会显示在文本框中。
- 修改测试:在文本框中输入一个新的数值(例如 50000),点击“写入”按钮。程序会提示“金钱修改成功!”。
- 游戏内验证:立即切回游戏,查看角色金钱是否已变为 50000。如果成功,说明你的工具工作正常!
- 动态测试:在游戏中通过打怪或买卖再次改变金钱,然后点击“读取”按钮,看文本框是否同步更新。
8. 常见问题与排查方法
在开发和测试过程中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 点击“读取”无反应或报错“未找到进程” | 1. 游戏未运行。 2. 游戏进程名不匹配。 3. 工具未以管理员权限运行。 | 1. 确认游戏Game.exe进程存在。2. 使用任务管理器或 Process Explorer 确认进程名。 3. 检查工具是否以管理员身份运行。 | 1. 启动游戏。 2. 修改 GetD2ProcessId函数中的进程名字符串。3. 右键点击 EXE,选择“以管理员身份运行”。 |
| “读取”成功但数值为0或明显错误 | 1. 指针路径(基址+偏移)错误。 2. 游戏版本/插件(如PlugY)导致地址变化。 3. 模块基址获取错误。 | 1. 用 Cheat Engine 重新扫描并验证指针路径。 2. 确认使用的游戏版本和插件环境与扫描时一致。 3. 在 FindGoldAddress函数中输出调试信息,检查每一步计算出的地址。 | 1. 重新进行第5章的地址查找步骤,更新BASE_OFFSET和GOLD_OFFSET。2. 针对不同游戏环境可能需要多套偏移量配置。 |
| “写入”失败 | 1. 地址错误(同读取错误)。 2. 内存区域不可写(受保护)。 3. 数值超出合理范围(虽能写入但游戏可能忽略)。 | 1. 先确保“读取”功能正常。 2. 在 Cheat Engine 中尝试直接修改该地址,确认是否可写。 3. 写入一个合理的数值(如 10000-1000000)。 | 1. 修正地址。 2. 某些游戏数据可能通过特定函数更新,直接写内存可能无效,需要更深入分析。通常金钱可以直接修改。 3. 写入后立即读取验证。 |
| 程序编译或链接错误 | 1. VC6 环境配置问题。 2. 缺少 #pragma comment(lib, “psapi.lib”)。3. 使用了不安全的函数(如 sprintf)。 | 1. 检查项目设置,确保包含正确的头文件和库目录。 2. 确认 psapi.lib在库依赖中。3. 在项目属性中关闭SDL检查或使用安全版本( sprintf_s)。 | 1. 对于psapi.lib,确保已添加#pragma comment或手动在项目设置中添加。2. 在项目设置 C/C++->Preprocessor中添加_CRT_SECURE_NO_WARNINGS来禁用安全警告。 |
| 游戏在修改后崩溃 | 1. 写入了非法内存地址。 2. 写入的数据类型或大小错误。 3. 游戏有反作弊或内存校验(单机版暗黑2基本没有)。 | 1. 使用 Cheat Engine 的内存查看器,确认你写入的地址和数值是正确的4字节整数。 2. 检查 WriteProcessMemory调用参数。 | 1. 绝对确保地址正确。从读取功能开始调试。 2. 写入前备份存档。 |
9. 扩展思路与最佳实践
这个基础工具完成后,你可以考虑以下扩展方向,使其更健壮、更易用:
- 支持多级指针:有时金钱地址可能需要两级甚至三级指针才能定位(例如
[[Base+偏移1]+偏移2]+偏移3)。你需要修改FindGoldAddress函数,循环读取每一级指针。 - 自动获取模块基址:我们已经实现了使用
EnumProcessModules,这比硬编码好。可以将其封装成一个通用函数。 - 添加地址配置功能:将
BASE_OFFSET和GOLD_OFFSET等偏移量放到配置文件(如 INI 文件)或注册表中,这样当游戏更新或使用不同版本时,无需重新编译程序,只需修改配置。 - 错误处理与日志:增加更详细的错误提示和日志记录功能,方便排查问题。例如,记录打开进程失败的具体错误码(
GetLastError)。 - 扫描特征码:更高级的方法是扫描内存特征码来定位地址,而不是依赖固定的偏移量。这能更好地适应游戏更新,但实现更复杂。
- UI 美化与更多功能:使用 MFC 更丰富的控件,添加状态栏、进度条(用于扫描)、以及修改其他属性(如生命、魔法、技能点)的选项卡。
最佳实践建议:
- 备份存档:在进行任何内存修改测试前,务必备份你的游戏存档。
- 小范围测试:先尝试修改一个较小的数值(如将 1000 改为 2000),验证功能正常后再进行大幅修改。
- 理解原理:不要仅仅满足于让工具跑起来。通过调试,观察每一步内存读取的结果,真正理解指针寻址的过程。
- 代码注释:为关键的地址偏移和 API 调用添加详细注释,方便日后维护或与他人交流。
- 合规使用:再次强调,此工具和知识请仅用于单机游戏和个人学习。
通过这个“怀旧”的 VC6 项目,你不仅重温了经典的开发环境,更重要的是掌握了 Windows 平台下进行进程内存操作的一套基本方法论。从使用 Cheat Engine 进行逆向分析,到调用 Windows API 实现读写,最后封装成一个有界面工具,这个过程本身的价值远超修改游戏金钱这一结果。它为你打开了游戏修改、软件调试乃至安全分析领域的一扇窗。