ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C++游戏逆向实战:定位与读取内存中的人物属性与坐标

C++游戏逆向实战:定位与读取内存中的人物属性与坐标 最近在分析一些游戏客户端时经常需要定位和读取游戏内实体的关键数据比如人物的生命值、坐标位置等。无论是出于学习游戏引擎内存结构、开发辅助工具还是进行安全研究理解如何通过C和逆向工程技术来定位并解析这些数据都是一个非常核心且实用的技能。本文将以一个虚构的“王权与自由”游戏场景为例系统性地拆解从零开始分析人物属性和坐标的完整流程。我们将聚焦于Windows平台下的C编程与逆向分析基础涵盖从环境搭建、内存扫描、指针寻址到稳定读取的每一个步骤并提供可直接编译运行的代码示例。无论你是对游戏逆向感兴趣的初学者还是有基础想了解具体实现细节的开发者都能从中获得一套可复用的方法论。1. 核心概念与目标分析在开始动手之前我们必须明确几个核心概念以及本次实战的目标边界。这有助于我们理解每一步操作背后的原理而非盲目地复制代码。1.1 什么是游戏逆向分析游戏逆向分析通常指的是在不拥有源代码的情况下通过反汇编、调试、内存查看等技术手段分析游戏程序的运行逻辑、数据结构、通信协议等。其目的多种多样包括但不限于安全研究发现游戏漏洞评估其安全性。外挂/辅助开发自动化游戏操作或修改游戏数据请注意这可能违反游戏用户协议甚至法律。Mod模组开发为游戏增加新的内容或功能。学习与研究理解优秀游戏引擎的设计与实现。本文的定位是纯粹的技术研究与学习所有示例和代码仅用于演示在Windows环境下如何使用标准C和公开的API来读取另一个进程的内存数据。我们强烈反对并谴责任何利用此类技术破坏游戏公平性、侵犯他人权益或进行非法活动的行为。1.2 人物属性与坐标在内存中的形态游戏中的每一个实体玩家、怪物、NPC等通常都由一个或多个C对象或结构体在内存中表示。这些对象包含了该实体的所有状态信息。人物属性可能包括生命值HP、魔法值MP、等级、攻击力、防御力等。这些数据通常是整数int、浮点数float或布尔值bool存储在对象内存的特定偏移位置。坐标用于确定实体在游戏世界中的位置通常是一个包含X, Y, Z三个分量的向量Vector3。每个分量通常是一个4字节的单精度浮点数float。这个Vector3结构体很可能就是人物对象的一个成员变量。我们的核心目标就是找到并稳定地读取代表“本地玩家”或“特定玩家”的那个对象在内存中的地址然后根据已知或分析出的属性偏移量读出我们关心的数据。1.3 技术路线概览我们将遵循一个典型的逆向分析流程环境准备安装必要的开发与调试工具。信息收集确定目标进程并寻找内存数据的线索例如通过修改游戏内数值并用工具扫描内存变化。定位基址找到指向玩家对象的静态指针或通过多层指针链能稳定访问的地址。分析偏移确定生命值、坐标等属性在玩家对象内部的偏移量。编写读取器使用C编写程序通过进程句柄读取目标进程内存并解析出所需数据。稳定性与优化处理指针链、考虑地址随机化ASLR、优化读取频率等。2. 环境准备与工具链工欲善其事必先利其器。以下是完成本教程所需的软件和环境。2.1 开发环境配置操作系统Windows 10 或 Windows 11。大部分API和工具在此环境下工作。IDE/编译器推荐使用Visual Studio 2022社区版免费。确保安装时勾选“使用C的桌面开发”工作负载。它将提供完整的C编译、调试和MSVC工具链。C标准本项目主要使用Windows API和标准C对C标准版本要求不高C11或更高即可。在Visual Studio中创建项目时选择“控制台应用”模板。2.2 逆向分析辅助工具这些工具用于辅助我们分析游戏内存不直接用于我们最终的外挂程序。Cheat Engine (CE)功能极其强大的内存扫描与调试工具。我们将主要用它来扫描变化的数值、查找访问/改写地址的代码、分析指针结构。这是逆向初学者的神器。x64dbg / OllyDbg动态调试器用于在汇编指令层面单步跟踪程序执行深入分析代码逻辑。x64dbg对64位程序支持更好。Process Explorer微软出品的增强型任务管理器可以查看进程的详细信息、加载的DLL等比系统自带的任务管理器更强大。ReClass.NET一个交互式内存查看和结构体分析工具。当你找到一个可能是对象基址的指针时可以用它来“绘制”出这个对象在内存中的布局猜测各个偏移对应的数据类型。2.3 创建Visual Studio项目打开Visual Studio选择“创建新项目”。搜索并选择“控制台应用”项目模板名称可能显示为“Console App”确保语言是C。为项目命名例如GameMemoryReader选择保存位置。创建完成后你会在“解决方案资源管理器”中看到GameMemoryReader.cpp源文件。3. 核心原理进程内存读写API在Windows操作系统中每个进程都有自己独立的虚拟地址空间。一个进程不能直接访问另一个进程的内存。为了实现跨进程内存读写我们需要使用Windows提供的特定API并获取足够的权限。3.1 关键API函数介绍我们将使用以下几个位于windows.h头文件中的核心函数OpenProcess打开一个已存在进程对象并返回一个进程句柄。我们需要这个句柄来执行后续操作。HANDLE OpenProcess( DWORD dwDesiredAccess, // 请求的访问权限如 PROCESS_VM_READ BOOL bInheritHandle, // 句柄是否可继承 DWORD dwProcessId // 目标进程的PID );ReadProcessMemory从指定进程的指定地址读取一块内存数据到本进程的缓冲区。BOOL ReadProcessMemory( HANDLE hProcess, // 目标进程句柄由OpenProcess获得 LPCVOID lpBaseAddress, // 要读取的目标进程内存起始地址 LPVOID lpBuffer, // 接收数据缓冲区的指针本进程地址空间 SIZE_T nSize, // 要读取的字节数 SIZE_T *lpNumberOfBytesRead // 实际读取的字节数可设为NULL );WriteProcessMemory将本进程缓冲区的数据写入到指定进程的指定地址。警告修改游戏内存风险极高极易导致游戏崩溃或被检测本文仅演示读取不鼓励使用写入功能。BOOL WriteProcessMemory( HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesWritten );CloseHandle关闭打开的句柄对象。这是一个良好的编程习惯避免资源泄漏。BOOL CloseHandle(HANDLE hObject);3.2 权限与错误处理访问权限为了读取内存我们至少需要PROCESS_VM_READ权限。通常我们会请求PROCESS_VM_READ | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION等组合权限以提高成功率。在打开进程前我们的程序可能需要以管理员权限运行特别是对于某些受保护的游戏进程。错误处理这些API调用可能会失败。我们必须检查返回值。ReadProcessMemory成功返回非零值失败返回0。失败后可以调用GetLastError()获取错误代码帮助排查问题例如权限不足、地址无效、进程已退出。4. 实战定位并读取人物生命值我们假设“王权与自由”游戏中本地玩家的生命值HP可以通过一个多级指针链访问。这是现代游戏保护机制的常见情况基址本身是随机的但偏移关系是固定的。4.1 使用Cheat Engine进行初步扫描启动游戏和Cheat Engine运行“王权与自由”游戏并登录到角色。然后以管理员身份运行Cheat Engine。附加进程在Cheat Engine中点击左上角的电脑图标在进程列表中找到游戏进程例如GameClient.exe并双击打开。首次扫描假设你当前角色满血是1000。在CE的“数值”输入框输入“1000”扫描类型选择“精确数值”数值类型选择“4字节”因为HP通常是int或float。点击“首次扫描”。改变数值在游戏中让角色受到伤害假设血量变为850。再次扫描回到CE在数值框输入新的血量“850”点击“再次扫描”。筛选地址重复步骤4和5直到左侧的地址列表减少到几个或几十个。尝试将这些地址添加到下方的地址列表中。确认地址在地址列表中对某个地址双击“值”进行修改观察游戏内血量是否同步变化。如果变化说明找到了正确的动态地址。但请注意这个地址每次游戏启动都会变化。4.2 分析指针链找到动态地址后下一步是找到指向它的静态指针或指针链。找出是什么访问了这个地址在CE的地址列表中右键点击你找到的动态地址选择“找出是什么访问了这个地址”。然后回到游戏进行一些操作如攻击、被攻击CE会列出所有读取或写入该地址的汇编指令。查看指令上下文在列出的指令上双击CE会显示反汇编窗口并高亮该指令。通常指令会是类似mov eax, [ebx10]或movss xmm0, [rcx34]的形式。[ ]内的内容就是地址的计算方式。分析指针偏移例如如果指令是mov eax, [GameClient.exe109A24C]那么GameClient.exe109A24C就是一个模块基址偏移形式的静态地址。如果指令是mov eax, [esi10]而esi寄存器的值来自另一个地址那么我们就需要一层层往上找。手动添加指针在CE地址列表下方点击“手动添加地址”。勾选“指针”在地址栏输入你分析出的最外层静态地址例如GameClient.exe109A24C。然后点击“添加偏移”输入第一层偏移例如0。如果后面还有偏移就继续添加。最后在指针链的末尾输入你最初找到的属性偏移例如生命值偏移0x10。如果计算出的地址值和你最初找到的动态地址一致并且重启游戏后指针链的基址部分可能变化但偏移不变依然能正确指向当前血量那么这条指针链就是有效的。假设我们最终分析出的指针链是GameClient.exe 0x039A24C- 偏移0x20- 偏移0x8- 生命值偏移0x104.3 编写C内存读取代码现在我们有了指针链信息可以开始编写稳定的读取程序。首先在Visual Studio项目中确保包含了必要的头文件并链接了必要的库通常不需要额外链接。// GameMemoryReader.cpp #include iostream #include windows.h #include tlhelp32.h // 用于进程快照 #include vector // 一个简单的函数根据进程名获取进程ID DWORD GetProcessIdByName(const wchar_t* processName) { DWORD processId 0; HANDLE snapshot CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot ! INVALID_HANDLE_VALUE) { PROCESSENTRY32W processEntry; processEntry.dwSize sizeof(PROCESSENTRY32W); if (Process32FirstW(snapshot, processEntry)) { do { if (_wcsicmp(processEntry.szExeFile, processName) 0) { processId processEntry.th32ProcessID; break; } } while (Process32NextW(snapshot, processEntry)); } CloseHandle(snapshot); } return processId; } // 根据模块名获取模块在目标进程中的基址 uintptr_t GetModuleBaseAddress(DWORD procId, const wchar_t* modName) { uintptr_t modBaseAddr 0; HANDLE snapshot CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, procId); if (snapshot ! INVALID_HANDLE_VALUE) { MODULEENTRY32W modEntry; modEntry.dwSize sizeof(MODULEENTRY32W); if (Module32FirstW(snapshot, modEntry)) { do { if (_wcsicmp(modEntry.szModule, modName) 0) { modBaseAddr (uintptr_t)modEntry.modBaseAddr; break; } } while (Module32NextW(snapshot, modEntry)); } CloseHandle(snapshot); } return modBaseAddr; } // 核心函数通过多级指针读取最终值 templatetypename T T ReadMultiLevelPointer(HANDLE hProcess, uintptr_t basePtr, const std::vectoruintptr_t offsets) { uintptr_t addr basePtr; uintptr_t readAddr 0; // 逐级读取指针 for (size_t i 0; i offsets.size() - 1; i) { if (!ReadProcessMemory(hProcess, (LPCVOID)addr, readAddr, sizeof(uintptr_t), nullptr)) { std::cerr 读取指针链第 i 级失败! 错误码: GetLastError() std::endl; return T(); } if (readAddr 0) { std::cerr 指针链第 i 级为空指针! std::endl; return T(); } addr readAddr offsets[i]; } // 读取最终的值 T value; if (!ReadProcessMemory(hProcess, (LPCVOID)addr, value, sizeof(T), nullptr)) { std::cerr 读取最终值失败! 错误码: GetLastError() std::endl; return T(); } return value; } int main() { // 1. 获取目标进程ID和句柄 const wchar_t* targetProcess LGameClient.exe; // 替换为你的游戏进程名 DWORD procId GetProcessIdByName(targetProcess); if (procId 0) { std::cout 未找到进程: targetProcess std::endl; system(pause); return 1; } std::cout 找到进程 targetProcess , PID: procId std::endl; HANDLE hProcess OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, procId); if (hProcess NULL || hProcess INVALID_HANDLE_VALUE) { std::cerr 打开进程失败! 错误码: GetLastError() std::endl; std::cout 请尝试以管理员身份运行本程序。 std::endl; system(pause); return 1; } // 2. 获取游戏主模块基址 const wchar_t* moduleName LGameClient.exe; uintptr_t gameBaseAddr GetModuleBaseAddress(procId, moduleName); if (gameBaseAddr 0) { std::cerr 获取模块基址失败! std::endl; CloseHandle(hProcess); system(pause); return 1; } std::cout 模块 moduleName 基址: 0x std::hex gameBaseAddr std::dec std::endl; // 3. 定义我们分析出的指针链和偏移 // 假设指针链为GameClient.exe 0x039A24C - 0x20 - 0x8 - HP偏移 0x10 uintptr_t staticOffsetFromBase 0x039A24C; // 静态偏移 std::vectoruintptr_t hpOffsets { 0x20, 0x8, 0x10 }; // 多级偏移最后一个0x10是HP在对象内的偏移 // 计算第一级地址静态地址 uintptr_t basePointer gameBaseAddr staticOffsetFromBase; // 4. 循环读取生命值 std::cout 开始读取生命值... (按CtrlC退出) std::endl; while (true) { int currentHP ReadMultiLevelPointerint(hProcess, basePointer, hpOffsets); if (currentHP 0) { // 简单的有效性检查 std::cout 当前生命值: currentHP std::endl; } Sleep(1000); // 每秒读取一次避免过高CPU占用 } // 清理实际上上面的循环是无限的这里仅作示范 CloseHandle(hProcess); return 0; }4.4 编译与运行将上述代码复制到你的GameMemoryReader.cpp中替换原有内容。在Visual Studio中确保编译配置为Debug或Release平台为x86或x64。这非常重要你必须知道你的目标游戏是32位x86还是64位x64的。大多数现代游戏是64位的。你可以在任务管理器的“详细信息”选项卡中查看进程的“平台”列。根据游戏平台在VS中设置对应的解决方案平台。按CtrlShiftB编译项目。先运行游戏并进入有角色的场景。以管理员身份运行编译生成的GameMemoryReader.exe。如果权限不足OpenProcess会失败。如果一切正确控制台会输出进程信息、模块基址并开始每秒打印一次读取到的生命值。5. 扩展读取三维坐标读取坐标的原理与读取生命值完全相同区别在于坐标通常是一个由三个float组成的结构体Vector3。我们需要一次性读取12个字节3 * 4 bytes。5.1 分析坐标偏移使用Cheat Engine的相同方法但这次扫描类型选择“浮点数”。在游戏中移动角色扫描变化的X、Y、Z坐标值。找到动态地址后同样分析出指向它的指针链。假设我们分析出坐标的指针链为GameClient.exe 0x03A1B00- 偏移0x30- 坐标偏移0x40注意0x40这个偏移处存放的是一个Vector3结构体的起始地址。5.2 定义结构体并读取我们在C代码中定义一个Vector3结构体并修改读取逻辑。// 在文件顶部添加结构体定义 struct Vector3 { float x; float y; float z; // 为了方便输出可以重载 运算符 friend std::ostream operator(std::ostream os, const Vector3 vec) { os ( vec.x , vec.y , vec.z ); return os; } }; // 在main函数中定义坐标的指针链 uintptr_t coordStaticOffsetFromBase 0x03A1B00; std::vectoruintptr_t coordOffsets { 0x30, 0x40 }; // 注意0x40是Vector3的起始偏移 uintptr_t coordBasePointer gameBaseAddr coordStaticOffsetFromBase; // 在循环中读取坐标 Vector3 currentPos ReadMultiLevelPointerVector3(hProcess, coordBasePointer, coordOffsets); std::cout 当前位置: currentPos std::endl;注意ReadMultiLevelPointer模板函数需要能处理Vector3这样的自定义类型。我们之前定义的函数已经可以因为它使用sizeof(T)来读取内存。确保Vector3是标准布局类型POD没有虚函数这样内存布局才是可预测的。6. 常见问题与排查思路在开发过程中你几乎一定会遇到各种问题。下面是一个快速排查指南。问题现象可能原因解决思路OpenProcess失败错误码5拒绝访问权限不足。游戏进程可能受保护或你的程序未以管理员权限运行。1.以管理员身份运行你的读取器程序。2. 检查游戏是否有反作弊系统如EasyAntiCheat, BattlEye这类系统会阻止外部进程访问。学习环境建议在单机、无保护的游戏或特定测试程序上进行。ReadProcessMemory失败错误码299仅部分完成或无效地址指针链中的某一级地址无效为0或不可读。1. 检查指针链是否正确。用Cheat Engine的“手动添加地址”功能验证指针链在当前游戏运行实例下是否有效。2. 目标对象可能还未被创建如角色未加载。添加延迟或等待逻辑。3. 游戏可能是64位但你用32位程序去读64位指针。确保你的程序平台x86/x64与游戏一致。读取到的数值是乱码或固定不变1. 偏移量错误。2. 数据类型错误如把float当int读。3. 基址因游戏更新而改变。1. 用Cheat Engine重新确认偏移量和数据类型。2. 检查ReadMultiLevelPointer中读取uintptr_t的步骤64位程序要用uintptr_t8字节32位用DWORD4字节。我们的模板函数使用了uintptr_t在64位编译下是合适的。3. 游戏更新后静态偏移如GameClient.exe0x039A24C很可能发生变化需要重新分析。程序编译错误uintptr_t未定义编译器版本或设置问题。确保包含了cstdint或stdint.h头文件。在Visual Studio中通常windows.h已间接包含。如果报错可在文件顶部添加#include cstdint。程序一运行游戏就崩溃你的读取操作可能触发了游戏的反调试或内存保护机制。极其危险立即停止。在具有反作弊系统的在线游戏中进行此类操作很可能导致账号被封禁。仅在用于学习的、无保护的离线环境或特制测试程序中进行实践。7. 最佳实践与高级话题当你掌握了基础的内存读取后下面是一些提升代码健壮性和效率的建议。7.1 稳定性与健壮性错误处理如示例所示每一个API调用后都应检查返回值并使用GetLastError()获取详细信息。有效性验证读取到的指针值应检查是否为NULL0。读取到的生命值、坐标等也应进行合理性检查如HP是否在0-最大值之间。缓存与重试不要过于频繁地读取内存例如每毫秒一次。这会造成不必要的CPU开销也可能增加被检测的风险。可以缓存一些不常变化的数据如玩家最大生命值。对于可能暂时无效的指针如角色死亡时实现重试逻辑而非直接崩溃。模块基址的动态获取我们的示例使用了GetModuleBaseAddress函数它会在每次需要时获取基址。更好的做法是在初始化时获取并缓存因为模块基址在进程生命周期内通常不变除非发生模块卸载/重载。7.2 应对地址随机化ASLR现代操作系统和编译器默认启用ASLR这意味着每次启动游戏GameClient.exe在内存中的加载基址都是随机的。我们之前使用的GameClient.exe0x039A24C中的GameClient.exe代表的是本次运行的模块基址。我们的代码通过GetModuleBaseAddress动态获取了这个值因此天然地兼容了ASLR。0x039A24C这个偏移是相对于模块基址的相对偏移RVA它通常是固定的除非游戏版本更新。7.3 关于多级指针的通用化我们编写的ReadMultiLevelPointer函数是一个通用模板可以处理任意层级的指针链和最终数据类型。在实际项目中你可以将它封装在一个独立的Memory工具类中。7.4 安全与法律警示再次强调仅用于学习与研究本文所有技术知识旨在帮助开发者理解Windows内存管理、进程间通信和软件逆向基础。尊重知识产权与用户协议对商业软件进行逆向工程可能违反其最终用户许可协议EULA。绝不用于在线游戏在具有反作弊系统的多人在线游戏中使用内存修改或读取技术几乎必然会导致账号永久封禁。反作弊系统会检测异常的内存访问模式、加载的DLL、运行的进程等。测试环境建议在虚拟机中针对自己编写的简单测试程序或明确声明允许修改的单机游戏中进行练习。通过本教程你应该已经掌握了使用C和Windows API进行跨进程内存读取的基本流程并理解了从逆向分析到代码实现的全链路。关键在于耐心使用Cheat Engine等工具进行分析并仔细处理编程中的边界情况和错误。这项技能是许多底层系统编程、安全分析和调试工作的基础希望你能将其用于正当且有价值的学习和开发之中。如果在实践中遇到具体问题多查阅Microsoft Docs关于Windows API的官方文档以及Cheat Engine论坛上的相关教程社区通常有丰富的资源可供参考。
返回列表