
这次我们来看一个非常“复古”的技术实践用经典的 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即可。现在你得到了一个基础的对话框程序。打开资源视图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 [eax000055F0], ecx。这里的eax是基址寄存器000055F0是偏移量Offset。记下这个偏移量例如0x55F0。关闭这个窗口回到主界面。在地址列表右键点击该地址现在选择Find out what accesses this address。同样操作一次CE 可能会显示mov ecx, [eax000055F0]确认偏移量。现在我们需要找到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.exeXXXXXX和偏移量0x55F0。这个“基地址偏移量”的组合就是我们代码中需要用来寻址的指针路径。假设我们最终找到的指针路径是Game.exe0x123456-偏移 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.exe0x123456” 中的偏移 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 编译按 CtrlF5 运行你的小程序。启动游戏运行《暗黑破坏神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 实现读写最后封装成一个有界面工具这个过程本身的价值远超修改游戏金钱这一结果。它为你打开了游戏修改、软件调试乃至安全分析领域的一扇窗。