ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C++游戏内存逆向实战:使用Cheat Engine定位与修改整数变量

2026/8/9 5:37:20 拓冰建站 浏览量
C++游戏内存逆向实战:使用Cheat Engine定位与修改整数变量 1. 这篇文章真正要解决的问题你是否曾好奇那些游戏外挂是如何精准地修改你的血量、金币或经验值的网上流传的“外挂教程”往往语焉不详要么是直接丢给你一个成品工具要么是讲一堆高深的汇编和反调试技巧让初学者望而却步。对于刚接触逆向工程或C游戏开发的爱好者来说最核心、最基础的一步——定位并修改游戏内存中的整数变量——反而被忽略了。这篇文章要解决的正是这个最基础也最关键的问题。我们不讨论复杂的注入、Hook或反反调试而是聚焦于一个核心场景如何在一个运行中的C游戏进程里找到并修改一个整数变量比如生命值、弹药数。这不仅是理解游戏内存结构的第一步更是学习软件逆向、动态调试和内存操作原理的绝佳切入点。很多人以为做外挂就是写个DLL注入进去但实际上如果你连目标数据在内存的哪个位置都找不到一切都是空谈。本文将从零开始带你使用最经典的工具Cheat Engine和最基本的原理完成一次完整的“逆向”实践。通过这个过程你将深刻理解程序运行时变量在内存中是如何存在的。如何通过动态调试工具扫描和定位特定数值。C中整数变量int, DWORD等在内存中的存储格式。如何安全地读写另一个进程的内存空间。请注意本文的所有技术和知识仅用于合法的安全研究、软件调试、游戏模组Mod开发及学习目的。未经授权修改他人软件是违法行为请务必在你自己拥有完全控制权的程序或明确允许修改的软件上进行实践。2. 基础概念与核心原理在动手之前我们必须先理清几个关键概念。这能帮你理解每一步操作背后的“为什么”而不是机械地模仿步骤。2.1 进程与内存空间每个运行中的程序进程都拥有自己独立的虚拟内存空间。操作系统保证了进程间的内存隔离使得一个进程无法直接访问另一个进程的内存。我们要做的“逆向”本质上就是突破这种隔离以调试器或特定API的身份去读取和修改目标进程的内存数据。2.2 变量在内存中的表示在C/C中一个整数变量如int health 100;会被编译成在内存中连续存储的若干个字节。对于32位程序一个int通常是4个字节32位。数值100在内存中以**小端序Little-Endian**存储即低位字节在前。所以100十六进制0x64在内存中看起来是64 00 00 00。2.3 指针与地址变量的值存储在内存的某个具体位置这个位置由一个内存地址来标识。指针变量本身存储的就是这个地址。当我们说“找到血量的地址”就是指找到存储血量数值的那个内存单元的起始地址。2.4 动态内存与静态地址这是逆向中最容易混淆的点。动态地址指针链游戏中的大部分数据如玩家对象、怪物对象是在堆上动态分配的其地址每次启动游戏甚至每次加载场景都会变化。要稳定地找到它们需要找到指向它们的静态指针。这个静态指针存储在程序的模块如Game.exe中一个固定的偏移位置。静态地址少数全局变量可能拥有不变的静态地址但现代游戏中极少见。我们的目标通常是找到一个稳定的静态地址它指向一个动态分配的对象而这个对象内部有一个偏移量指向我们想要的整数变量如血量。这个过程被称为“寻址”或“找指针”。2.5 工具选择为什么是Cheat Engine对于内存扫描和修改Cheat Engine (CE)是无可争议的入门神器。它免费、强大、社区资源丰富完美契合我们“查找并修改整数变量”的需求。它本质上是一个高级的内存扫描器和调试器前端能帮我们完成“未知初始值扫描”、“数值变化扫描”、“查找访问/改写该地址的代码”等核心工作。3. 环境准备与前置条件工欲善其事必先利其器。请确保准备好以下环境操作系统Windows 10 或 Windows 11。本文演示基于Windows环境。目标程序一个用于练习的、你自己编写的或明确允许调试的C程序。强烈建议你自己写一个简单的测试程序这样你对其内存布局了如指掌。调试工具Cheat Engine (CE)从官网https://www.cheatengine.org/下载并安装。安装过程中注意取消勾选不必要的捆绑软件。Visual Studio用于编写测试程序和理解C代码与内存的对应关系。社区版即可。知识准备基础的C/C语法知识了解什么是指针、变量类型int, float。3.1 创建我们的“靶子”程序让我们用Visual Studio创建一个最简单的控制台程序作为我们逆向练习的目标。打开VS创建新项目 - “控制台应用” - 项目名称为MemoryTarget。将main.cpp修改为以下代码// MemoryTarget.cpp : 此文件包含 main 函数。程序执行将在此处开始并结束。 #include iostream #include windows.h // 用于Sleep函数 int main() { int playerHealth 100; int playerAmmo 30; int playerGold 9999; // 一个很大的数方便区分 std::cout 简易游戏内存靶场 std::endl; std::cout 程序PID: GetCurrentProcessId() std::endl; std::cout 玩家血量地址: 0x std::hex playerHealth std::dec std::endl; std::cout 玩家弹药地址: 0x std::hex playerAmmo std::dec std::endl; std::cout 玩家金币地址: 0x std::hex playerGold std::dec std::endl; std::cout std::endl; while (true) { std::cout \n状态 - 血量: playerHealth | 弹药: playerAmmo | 金币: playerGold std::endl; std::cout 输入命令 (h[-]值, a[-]值, g[-]值, q退出): ; char cmd; int value; std::cin cmd value; switch (cmd) { case h: playerHealth value; break; case a: playerAmmo value; break; case g: playerGold value; break; case q: return 0; default: std::cout 未知命令。 std::endl; } // 防止控制台刷新太快 Sleep(50); } return 0; }按CtrlF5开始执行不调试运行程序。你会看到一个控制台窗口直接打印出了三个变量的当前内存地址和PID。记住这个PIDCheat Engine会用到。现在我们的“游戏”已经运行起来了它有血量、弹药和金币三个整数变量并且我们可以通过命令行修改它们。4. 核心流程拆解使用Cheat Engine定位并修改整数变量现在我们进入正题。假设我们不知道上面那段源代码只知道这是一个游戏程序我们想修改它的“血量”playerHealth。我们将遵循一个标准的逆向流程。4.1 第一步附加进程与首次扫描打开Cheat Engine。点击CE左上角的电脑图标“选择进程”在弹出的进程列表中找到我们的MemoryTarget.exe也可以通过PID查找选中它并点击“打开”。现在CE已经附加到目标进程。在“数值”输入框旁边确保扫描类型是“精确数值”。我们知道血量初始值是100。在“数值”框输入100点击“首次扫描”。扫描结束后左侧的地址列表会显示成千上万个值为100的地址。这很正常因为内存中值为100的整数太多了。我们无法确定哪个是我们的血量。4.2 第二步通过数值变化缩小范围这是逆向中最关键的一步——利用数据的动态变化来过滤地址。回到我们的“游戏”控制台输入命令h -10并按回车。这将血量减少10变为90。回到Cheat Engine在“数值”框输入变化后的值90然后点击“再次扫描”。CE会从上一次扫描的结果所有值为100的地址中筛选出当前值等于90的地址。结果列表会大幅减少可能只剩下几十个甚至几个。如果地址还很多就继续改变游戏内的数值。在游戏里输入h 5血量变为95。回到CE输入95点击“再次扫描”。如此反复直到地址列表减少到一个或几个。4.3 第三步验证与锁定地址当列表只剩下少数几个地址时比如3-5个我们可以逐一验证。在CE的地址列表中双击某个地址它会出现在下方的地址列表中。在下方的地址列表中双击该地址的“值”栏手动修改它比如改成200。立即切换回游戏控制台查看血量显示是否同步变成了200。如果变了恭喜你找到了正确的地址找到正确地址后可以点击地址左边的复选框来“锁定”这个值。锁定后游戏尝试修改这个内存位置时CE会立刻将其改回你设定的值比如一直锁定为200。练习时请谨慎使用锁定功能可能导致程序逻辑异常。4.4 第四步理解扫描类型我们刚才使用的是“精确数值”扫描。CE还有其他强大的扫描类型未知初始值当你完全不知道数值是多少时使用比如进度条。变动的数值/未变动的数值用于过滤。大于/小于用于扫描范围。浮点数如果游戏用float或double存储血量常见于Unity游戏就需要选择这个。对于我们的整数血量精确数值是最直接有效的。5. 深入处理动态地址与寻找指针在上面的练习中我们找到的地址很可能是动态地址。如果你关闭MemoryTarget程序再重新打开然后用CE附加按照同样的步骤扫描会发现血量的地址和上次完全不同。这就是因为变量在栈上分配每次运行地址都随机化ASLR。为了制作一个“稳定”的外挂我们需要找到指向这个动态地址的静态指针。这涉及到“找出是什么改写了这个地址”的功能。5.1 找出访问/修改该地址的代码在CE中在下方地址列表里右键点击你找到的血量地址。选择“找出是什么改写了这个地址”。会弹出一个新窗口。回到游戏执行一个会改变血量的操作比如输入h -1。CE的窗口会立即捕获到一条汇编指令例如地址00FFE834 汇编mov [esi00000123], eax 模块MemoryTarget.exe这条指令的意思是将寄存器eax的值写入到以esi寄存器值为基址、加上偏移0x123计算得到的内存地址中。这里的[esi0x123]很可能就是我们血量的地址。5.2 分析指针链在上面的窗口中点击“显示反汇编程序”。CE会打开内置的反汇编器定位到那条指令。我们需要知道esi的值从哪里来。通常需要向上查看代码看esi是否是从某个固定的内存地址Game.exe偏移加载来的。在反汇编窗口右键点击涉及esi的指令比如mov esi, [MemoryTarget.exe123456]选择“找出指令访问的地址”。这会帮你找到esi的来源。如此反复你可能找到一条链最终血量地址 [[[“MemoryTarget.exe”基址偏移] 偏移A] 偏移B] 偏移C“MemoryTarget.exe”基址偏移就是一个静态地址它指向一个模块内的固定位置不随程序重启而改变除非游戏更新。后面的偏移则描述了在数据结构中的层层寻址。5.3 手动添加指针找到指针链后你可以在CE的地址列表手动添加一个“指针”地址。在CE下方点击“手动添加地址”。勾选“指针”。在“地址”栏你需要填写指针链。例如如果基址是MemoryTarget.exe0x123456第一层偏移是0x10第二层偏移是0x4最终血量偏移是0x8。那么你需要这样计算最终的地址是[[MemoryTarget.exe0x123456]0x10]0x4]0x8。在CE中添加时可能需要逐层添加或者使用“指针扫描”功能来辅助。这个过程是逆向工程中较难的部分需要一定的汇编知识和耐心。对于我们的简单示例变量在栈上可能没有复杂的多级指针。但在真正的游戏中这是必经之路。6. 编写一个简单的“外部”修改器C示例使用CE图形界面修改只是学习和验证的手段。最终我们可能需要一个独立的程序即所谓的“外部挂”来执行修改。这里用C演示如何使用Windows API读写另一个进程的内存。警告以下代码仅用于学习目的请勿用于非法用途。// SimpleMemoryWriter.cpp #include windows.h #include iostream #include tlhelp32.h // 用于进程快照 // 根据进程名查找PID DWORD FindProcessId(const wchar_t* processName) { DWORD pid 0; HANDLE snapshot CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot ! INVALID_HANDLE_VALUE) { PROCESSENTRY32W entry; entry.dwSize sizeof(entry); if (Process32FirstW(snapshot, entry)) { do { if (_wcsicmp(entry.szExeFile, processName) 0) { pid entry.th32ProcessID; break; } } while (Process32NextW(snapshot, entry)); } CloseHandle(snapshot); } return pid; } int main() { // 1. 找到目标进程 const wchar_t* targetProcess LMemoryTarget.exe; DWORD pid FindProcessId(targetProcess); if (pid 0) { std::wcout L未找到进程: targetProcess std::endl; return 1; } std::cout 找到进程PID: pid std::endl; // 2. 打开进程获取读写内存的权限 HANDLE hProcess OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid); if (hProcess NULL) { std::cerr 打开进程失败。错误代码: GetLastError() std::endl; return 1; } // 3. 假设我们已经通过CE找到了血量的动态地址例如 0x00FFE834 // **注意这是一个动态地址每次运行都会变这里仅为演示。** // 实际应用中你需要找到静态基址和偏移并计算最终地址。 uintptr_t healthAddress 0x00FFE834; // 请替换为CE中找到的实际地址 // 4. 读取当前血量 int currentHealth 0; SIZE_T bytesRead 0; if (ReadProcessMemory(hProcess, (LPCVOID)healthAddress, currentHealth, sizeof(currentHealth), bytesRead)) { std::cout 当前血量 (读取): currentHealth std::endl; } else { std::cerr 读取内存失败。错误代码: GetLastError() std::endl; CloseHandle(hProcess); return 1; } // 5. 修改血量 int newHealth 999; SIZE_T bytesWritten 0; if (WriteProcessMemory(hProcess, (LPVOID)healthAddress, newHealth, sizeof(newHealth), bytesWritten)) { std::cout 血量已修改为: newHealth std::endl; } else { std::cerr 写入内存失败。错误代码: GetLastError() std::endl; } // 6. 再次读取验证 int verifyHealth 0; ReadProcessMemory(hProcess, (LPCVOID)healthAddress, verifyHealth, sizeof(verifyHealth), bytesRead); std::cout 验证血量 (读取): verifyHealth std::endl; // 7. 清理 CloseHandle(hProcess); std::cout 按回车键退出... std::endl; std::cin.get(); return 0; }代码关键点解释FindProcessId: 通过进程快照API遍历系统进程根据名称找到目标进程的PID。OpenProcess: 这是关键函数。它用指定的权限PROCESS_VM_READ等打开目标进程返回一个进程句柄。没有这个句柄你无法操作其他进程的内存。ReadProcessMemory/WriteProcessMemory: 这两个API是核心用于跨进程读写内存。参数分别是进程句柄、目标地址、本地缓冲区、大小和实际读写字节数指针。地址问题示例中使用了硬编码的动态地址0x00FFE834这在实际中完全不可用。你必须将healthAddress的计算替换为通过静态基址多级偏移计算最终地址的逻辑。这通常需要先通过逆向分析找到指针链。7. 常见问题与排查思路在实践过程中你几乎一定会遇到下面这些问题。问题现象可能原因排查方式解决方案Cheat Engine 扫描不到任何地址或地址过多1. 数值类型选错如该用4字节用了1字节2. 扫描范围不对3. 游戏使用了加密或校验和1. 确认变量类型int通常4字节short 2字节2. 尝试“所有类型”或“浮点数”3. 尝试“未知初始值”扫描1. 使用正确的扫描类型和值类型2. 结合“变动的值”和“未变动的值”过滤3. 学习更高级的逆向技巧如查找加密函数找到的地址修改后游戏内无变化1. 找到的是显示值的副本或缓存2. 游戏服务器端验证在线游戏3. 地址不正确1. 尝试锁定该值看游戏行为是否异常如无法受伤2. 查找“是什么访问了该地址”看是否有其他代码在频繁写入1. 继续扫描或尝试找指针链2.对于在线游戏客户端修改几乎无效且违法请放弃WriteProcessMemory 失败错误代码5或61. 权限不足错误52. 句柄无效错误63. 地址不可写1. 以管理员身份运行你的修改器2. 检查OpenProcess是否成功3. 检查地址是否有效如是否在栈/堆上1. 管理员权限运行2. 确保OpenProcess请求了PROCESS_VM_WRITE权限3. 使用VirtualQueryEx检查内存区域属性游戏重启后之前找到的地址失效变量是动态分配的堆/栈地址随机化这是正常现象说明你找到的是动态地址必须进行指针扫描找到指向该动态地址的静态基址和偏移链Cheat Engine 被游戏检测并关闭游戏有反调试或反作弊保护如EAC, BattlEyeCE本身特征明显容易被检测1. 仅在单机、无保护的游戏或自己程序上练习2. 学习驱动级或更底层的隐藏调试技术高级话题风险高指针链复杂难以分析数据结构嵌套深涉及虚函数表等汇编不熟看不懂指令1. 使用CE的“指针扫描”功能生成可能指针地图2. 结合IDA Pro等静态分析工具辅助理解3. 从基础数据结构如数组、链表的逆向开始练习8. 最佳实践与工程建议如果你希望将这项技术用于合法的软件分析、游戏模组开发或安全研究请遵循以下建议法律与道德底线永远只在你自己拥有完全产权的软件或明确允许修改如提供官方Mod SDK的软件上进行操作。未经授权修改他人软件是明确的侵权行为对于在线游戏更是违法行为会导致封号甚至承担法律责任。环境隔离在虚拟机中进行所有逆向和分析工作。这可以保护你的宿主机系统安全也方便进行快照和回滚。从简单开始不要一开始就挑战大型商业游戏。从自己写的小程序、开源游戏或经典的、保护较弱的单机游戏如一些老游戏开始练习。系统化学习掌握CE只是第一步。要深入理解你需要学习x86/x64汇编语言这是读懂反汇编代码的基础。Windows PE文件格式理解.exe和.dll的结构。操作系统原理特别是进程、内存管理、API调用机制。使用专业工具IDA Pro静态分析、x64dbg/OllyDbg动态调试是行业标准。记录与分析在逆向过程中使用笔记软件详细记录你的发现重要的地址、函数、字符串、数据结构布局、指针链。绘制简单的内存布局图有助于理解。代码健壮性如果你要编写外部工具务必加入完善的错误处理检查每个API调用的返回值、日志记录。对于指针寻址要考虑模块基址重定位、地址有效性验证等问题。理解“为什么”不要满足于找到地址。多问为什么这个变量属于哪个对象谁创建了它谁在修改它它的生命周期是怎样的理解上下文比找到地址更重要。9. 总结与后续学习方向通过本文我们完成了一次完整的、针对C整数变量的逆向与修改实践。我们从最基础的概念——进程内存、变量存储、指针——出发使用Cheat Engine这个强大的工具经历了“附加进程-首次扫描-变化过滤-地址验证”的标准流程成功定位并修改了一个运行中程序的整数变量。更进一步我们探讨了动态地址与静态指针的核心难题并展示了如何用C编写一个简单的跨进程内存读写程序。本文的核心价值不在于教你制作外挂而在于为你打开一扇窗让你理解软件在运行时其状态是如何在内存中具象化表示的。调试器是如何通过内存扫描和监控来洞察程序行为的。数据在进程间的隔离并非绝对操作系统提供了可控的通道调试API。一个看似简单的“改数字”背后涉及地址空间、指针、反汇编等一系列计算机系统核心知识。如果你想继续深入可以沿着以下方向学习汇编语言与反汇编这是逆向工程的基石。推荐从Intel x86汇编开始理解寄存器、指令、栈帧调用约定如cdecl,stdcall。使用专业调试器尝试使用x64dbg或IDA Pro的调试功能它们比CE提供更底层的控制和分析能力。分析数据结构尝试逆向一个简单的C类对象找到它的成员变量和虚函数表。理解游戏引擎学习UnityC#或Unreal EngineC的基本内存管理这对于现代游戏逆向更有帮助。安全领域将这些技术应用于软件漏洞分析Fuzzing、恶意代码分析、软件保护与破解研究仅限合法授权范围。技术本身是中立的它既可以用于破坏也可以用于创造和保护。希望你能将这份好奇心与学习能力投入到软件调试、性能分析、安全研究或游戏模组开发等富有建设性的领域中去。真正的挑战和乐趣永远在于理解系统如何工作而非简单地改变一个数字。