UE4逆向工程:从GetName到DumpName的对象名称解析实战 1. 项目概述为什么UE4逆向绕不开GetName与DumpName如果你正在研究UE4游戏的安全、外挂检测或者想深入理解引擎内部的对象管理机制那么“GetName”和“DumpName”这两个函数绝对是你绕不开的坎。这不仅仅是两个简单的API调用它们构成了UE4逆向工程中从定位对象到解析其完整信息链路的基石。简单来说GetName是获取单个对象名称的“钥匙”而DumpName则是批量导出所有对象名称及其关联信息的“地图生成器”。掌握从GetName到DumpName的完整流程意味着你能够系统地遍历、识别和分类游戏运行时内存中的成千上万个UObjectUE4的基础对象这是进行任何深度分析如寻找特定Actor、分析游戏逻辑、制作辅助工具的前提。这个流程在不同版本的UE4引擎中并非一成不变尤其是4.23版本前后UE4在底层对象管理上做了一次重大的重构直接影响了我们逆向时寻找关键函数和偏移的方式。很多网上的老教程在4.23及之后的版本上会完全失效导致初学者卡在第一步。因此本文将不仅详细拆解从定位GetName到实现完整DumpName的每一步操作更会重点对比4.23版本的关键差异让你无论是面对老项目还是新游戏都能快速上手。整个过程会涉及IDA Pro静态分析、x64dbg动态调试、内存结构解读和简单的C代码编写目标是给你一套可复现、可验证的实战方法。2. 核心思路与版本差异总览在深入代码之前我们必须先理清核心目标和技术路线。我们的最终目的是实现一个“DumpName”功能它能遍历游戏内存中的GObject全局对象数组对其中每一个有效的UObject调用其GetName()方法获取其名称FName并转换为可读字符串最终输出一份包含对象地址、内部索引和名称的列表。2.1 技术路线图整个流程可以分解为以下几个关键步骤它们环环相扣定位GObject和GNames这是所有UE4逆向的起点。GObject或GUObjectArray是存储所有UObject的全局数组GNames是存储所有FName字符串的全局表。我们需要在游戏二进制文件中找到它们的地址。分析UObject和FName结构理解内存中UObject和FName是如何布局的特别是GetName()函数如何从UObject内部找到对应的FName索引再通过GNames表解析出字符串。定位并分析GetName函数找到UObject::GetName()或FName::ToString()等关键函数的地址通过反汇编理解其内部逻辑这是手动实现DumpName或编写外部工具调用它的基础。遍历GObject数组编写逻辑安全地遍历GObject数组过滤掉无效或已销毁的对象。实现名称获取与输出对每个有效UObject调用或模拟GetName()逻辑将FName索引转换为字符串并格式化输出。处理版本差异重点解决4.23版本因“FUObjectArray”重构带来的GObject查找方式、UObject内部偏移等变化。2.2 4.23版本前后的关键差异对比这是本教程最具价值的部分。UE4 4.23版本对对象管理系统进行了大规模重构许多旧的签名和偏移彻底改变。特性4.22及之前版本 (传统方式)4.23及之后版本 (新方式)对逆向的影响全局对象数组通常是一个简单的TArrayUObject*全局符号可能是GObject或UObject::GObjects。引入FUObjectArray类进行封装。全局变量是GUObjectArray一个FUObjectArray实例。核心数据在FUObjectArray::ObjObjects中。查找模式完全不同。旧方法搜索GObject字符串或特定模式可能失效。需要寻找GUObjectArray或分析FUObjectArray构造函数引用。UObject内部索引UObject内部有一个InternalIndex成员直接对应在GObject数组中的索引。InternalIndex依然存在但含义和偏移可能发生变化。更重要的是对象数组现在是一个两级结构分块索引计算更复杂。遍历数组时不能简单地将索引作为GObject[i]来访问。需要调用FUObjectArray::IndexToObject等函数或理解其分块布局。GetName相关函数GetName()函数实现相对直接通常内部调用FName::ToString()。函数签名稳定。函数逻辑核心未变但因其依赖的FName内部也可能有调整且编译器优化可能导致内联直接定位函数入口可能稍难。定位方法从搜索字符串“GetName”可能变为分析FName相关操作或使用IDA的交叉引用。GNames全局表GNames通常是一个TArrayTArrayFNameEntry*结构相对容易通过字符串引用定位。结构基本保持不变但FNameEntry本身可能为了内存优化而有调整。定位GNames的方法如搜索“InvalidName”字符串依然有效。影响较小是流程中相对稳定的部分。注意版本差异是动态的小版本号如4.26, 4.27, 5.0之间也可能有微调。本文以4.23这个分水岭为例提供的是方法论。实际操作中务必结合目标游戏的具体版本来验证偏移和签名。3. 实战环境准备与工具链选择工欲善其事必先利其器。UE4逆向对工具链有一定要求以下是我经过多个项目验证后推荐的组合兼顾了效率和稳定性。3.1 核心工具清单静态分析工具IDA Pro (7.7)作用反汇编游戏主模块如.exe或.dll进行代码流分析、结构体定义、字符串查找、交叉引用追踪。这是理解引擎逻辑的“地图”。为什么是IDA其对C RTTI运行时类型信息的支持、强大的反编译器Hex-Rays和成熟的插件生态在分析大型项目如UE4时无可替代。免费的Ghidra也可用但在用户体验和社区资源上略逊一筹。关键插件准备确保安装并配置好Hex-Rays反编译器这是将汇编代码转为伪C的关键。动态调试工具x64dbg作用附加到运行中的游戏进程下断点、单步执行、查看和修改寄存器/内存实时验证静态分析的结论。为什么选x64dbg对Windows平台支持极好免费、开源、社区活跃。相比OllyDbg它对64位程序的支持更成熟界面也更现代。在追踪GetName函数调用栈、验证GObject地址时必不可少。内存查看/编辑工具Cheat Engine作用快速扫描内存、查找指针链、查看和编辑内存数据。在初步探索GObject、GNames地址时非常高效。使用场景我们可以利用UE4对象名称字符串是只读的常量这一特点在Cheat Engine中搜索一个已知的游戏内对象名称如“PlayerController”通过查找访问该地址的代码反向定位到GNames相关的函数或地址。开发环境Visual Studio 2019/2022作用编写我们的外部Dump工具。我们将编写一个DLL注入到游戏进程或者编写一个独立的外部读取器需处理内存读取。配置使用C语言需要熟悉基本的Win32 API如ReadProcessMemory或使用像minhook这样的库进行内部调用。3.2 目标样本与符号获取目标选择为了教程的通用性建议使用UE4引擎自带的空白项目或官方示例项目如ShooterGame打包出的独立游戏进行练习。这样可以确保我们分析的代码是“纯净”的引擎代码不受复杂游戏逻辑的过度干扰。调试符号.pdb文件这是“开挂”级别的资源。如果游戏开发者在发布时未剥离调试符号一些独立游戏或开发版可能保留那么IDA可以直接加载.pdb文件所有函数、全局变量都会拥有原始名称如UObject::GetName、GUObjectArray。但绝大多数商业游戏都会剥离符号因此我们的教程将基于“无符号”这一最普遍的情况进行这也是逆向的真正挑战所在。实操心得在开始逆向一个未知游戏前先用strings工具或IDA的字符串视图快速浏览一下二进制文件。如果能看到大量清晰的UE4内部字符串如“UObject”、“Actor”、“Component”那说明引擎代码剥离得不彻底逆向难度会降低。如果字符串都被混淆或加密了那就要做好打硬仗的准备。4. 第一步定位核心全局变量GObject/GNames在没有符号的情况下我们需要通过特征码和内存访问模式来定位这些全局变量。4.1 定位GNames相对稳定GNames存储所有字符串其结构稳定且通过字符串常量反向查找是经典方法。在IDA中搜索字符串打开IDA加载游戏主模块切换到字符串视图ShiftF12。搜索一个UE4内部常用的、独特的字符串例如None或ByteProperty。None是FName的默认空名几乎必然存在。查找引用在找到的None字符串地址上点击右键选择List cross-references to...。你会看到哪些代码引用了这个字符串。分析引用函数跳转到引用处通常你会在一个函数里看到类似lea rcx, [模块基址XXXXXX]这样的指令其中加载的地址就是GNames中某个FNameEntry的指针。通过上下分析找到加载GNames数组基地址的代码。GNames通常是一个二级指针数组其本身的位置在代码中会以全局变量的形式被引用如mov rax, cs:qword_7FF6A3CF8E40。特征码定位法备选如果字符串搜索不顺利可以尝试搜索特征码。GNames的类型通常是TStaticIndirectArrayThreadSafeReadFNameEntry*, 2。在反汇编中访问它的代码模式常常是mov rax, [巨大的地址]-mov rcx, [rax索引*8]-mov rdx, [rcx...]用于获取字符串指针。找到那个“巨大的地址”它很可能就是GNames。4.2 定位GObject / GUObjectArray版本敏感这是4.23版本差异的关键体现。对于4.22及之前版本通过对象迭代器寻找UE4有一个全局函数ForEachObject它会遍历所有UObject。这个函数内部一定会访问GObject数组。在IDA中可以搜索字符串“UObjectArray”或“Bad Name Index”等找到相关函数然后分析其汇编看它从哪里读取对象数组的基地址。通过UObject构造函数寻找UObject在创建时会被加入到全局数组中。查找UObject静态构造函数或StaticConstructor的引用跟踪其调用链最终也能找到写入对象数组的地方。对于4.23及之后版本搜索字符串“GUObjectArray”新版本中这个全局变量名称有时会保留在字符串表中。这是最直接的方法。分析FUObjectArray构造函数如果字符串被剥离需要寻找FUObjectArray::FUObjectArray这个构造函数。它会在引擎初始化时被调用并且会设置GUObjectArray这个全局实例。在IDA中你可以寻找一个函数其内部初始化了一个很大的结构体分配内存、设置函数指针等并且这个结构体的地址被存储到一个全局变量中。通过交叉引用找到这个全局变量就是GUObjectArray。使用参考模式已知GUObjectArray的类型是FUObjectArray。你可以从UE4开源代码中例如4.27版本找到FUObjectArray类的内存布局然后在IDA中定义一个相似的结构体在疑似地址上应用该结构体观察其字段是否与预期匹配例如是否有指向对象块ObjObjects的指针。注意事项找到的地址是模块内的偏移地址如0x123456。在实际工具中我们需要的是运行时地址即模块基地址 偏移地址。模块基地址可以通过GetModuleHandle(NULL)或枚举进程模块获得。5. 第二步逆向GetName函数与FName结构找到全局变量后我们需要理解如何从一个UObject得到它的名字。5.1 分析UObject基础结构一个最小的UObject在内存中大致包含以下部分简化class UObject { void** VfTable; // 虚函数表指针 int32 ObjectFlags; // 对象标志位 int32 InternalIndex; // 在全局对象数组中的索引 (重要!) UClass* ClassPrivate; // 指向该对象的UClass UObject* OuterPrivate; // 外部对象包组织 FName NamePrivate; // 对象的名字 (FName结构) // ... 其他成员 };我们需要关注的焦点是NamePrivate成员。GetName()函数的本质就是读取这个FName成员并将其转换为FString。5.2 解析FName结构FName在内存中不是一个字符串而是一个高效的索引结构struct FName { int32 ComparisonIndex; // 用于快速比较的索引 int32 DisplayIndex; // 用于显示的名称索引通常与ComparisonIndex相同 int32 Number; // 用于区分同名对象的编号如Player_0, Player_1 };GetName()函数的工作就是从UObject中取出FName主要是DisplayIndex。使用DisplayIndex作为索引去GNames全局表中查找对应的FNameEntry*。FNameEntry结构里存储了实际的字符串数据ANSI或WIDE。将字符串数据复制到FString中并返回。5.3 动态追踪GetName调用为了精确找到GetName的函数地址和内部逻辑动态调试是最佳手段。在x64dbg中附加游戏进程。触发一个已知对象的名称获取例如在游戏中打开控制台如果支持输入stat fps等命令这些命令通常会打印出一些对象的名称。或者你知道某个特定Actor的名字。在IDA中找到疑似字符串回到IDA在字符串视图找到你将要触发的那个对象名称字符串比如“HUD”。在x64dbg中对该字符串地址设置内存访问断点。触发操作在游戏中执行触发名称显示的操作。分析调用栈x64dbg会断在读取该字符串的代码处。此时查看调用栈Call Stack向上回溯。你会看到一系列函数调用。其中一个非常可能就是FName::ToString或UObject::GetName。记录下这个函数的地址。静态分析该函数回到IDA跳转到这个函数地址可能需要根据ASLR重新计算偏移。现在你就拥有了一个活生生的GetName函数进行反汇编分析。使用Hex-Rays反编译你可以清晰地看到其C伪代码逻辑验证我们之前对FName和GNames关系的理解。实操心得GetName函数可能很短甚至被编译器内联。如果动态断点直接断在了非常靠近字符串操作的代码没有明显的函数框架说明可能被内联了。这时你需要向上回溯更多层调用栈找到一个更“胖”的、包含循环或条件判断的函数那可能是调用GetName的父函数。通过分析父函数如何传递UObject的this指针也能反向推断出获取名称的逻辑。6. 第三步遍历GObject数组与实现Dump逻辑掌握了核心数据和函数后我们就可以编写代码来实现Dump了。6.1 理解对象数组遍历4.22及之前版本简单数组 假设我们找到了GObject它是一个TArrayUObject*。遍历伪代码如下TUObjectArray* ObjectArray (TUObjectArray*)(ModuleBase GObjectOffset); for (int i 0; i ObjectArray-Num(); i) { UObject* Obj ObjectArray-GetObjectPtr(i); if (Obj Obj-IsValid()) { // 需要验证对象有效性 FString Name Obj-GetName(); // 输出 Name... } }4.23及之后版本FUObjectArray 结构更复杂通常需要通过FUObjectArray的接口来安全遍历。核心是访问FUObjectArray::ObjObjects它是一个FChunkedFixedUObjectArray。遍历需要处理分块FUObjectArray* GUObjectArray (FUObjectArray*)(ModuleBase GUObjectArrayOffset); FChunkedFixedUObjectArray* ObjObjects GUObjectArray-ObjObjects; for (int i 0; i ObjObjects-Num(); i) { FUObjectItem* ObjectItem ObjObjects-GetObjectPtr(i); if (ObjectItem ObjectItem-Object) { UObject* Obj (UObject*)ObjectItem-Object; if (Obj-IsValid()) { FString Name Obj-GetName(); // 输出 Name... } } }这里FUObjectItem是一个包装可能包含对象指针、标志位等。IsValid()函数需要自己实现或逆向通常检查对象指针非空、特定标志位未设置如RF_PendingKill。6.2 编写Dump工具的关键代码我们将创建一个DLL使用MinHook等钩子库或者直接创建线程在游戏内执行Dump。这里展示核心逻辑片段外部读取器模式需使用ReadProcessMemory// 假设我们已经通过逆向获得了以下偏移地址相对于模块基址 uintptr_t ModuleBase GetModuleBase(LGame.exe); // 自定义函数获取模块基址 uintptr_t GNamesOffset 0x1234560; uintptr_t GUObjectArrayOffset 0xABCDEF0; // 4.23版本示例 uintptr_t GetNameFuncOffset 0x7890123; // 计算运行时绝对地址 uintptr_t GNamesPtr ModuleBase GNamesOffset; uintptr_t GUObjectArrayPtr ModuleBase GUObjectArrayOffset; using GetName_t FString*(__fastcall*)(UObject*, FString*); GetName_t GetName (GetName_t)(ModuleBase GetNameFuncOffset); void DumpAllObjectNames() { // 读取GNames和GUObjectArray的指针这里需要根据实际内存结构定义结构体 TNameEntryArray* GNames *(TNameEntryArray**)GNamesPtr; FUObjectArray* ObjArray (FUObjectArray*)GUObjectArrayPtr; FILE* pFile fopen(ObjectDump.txt, w); if (!pFile) return; FChunkedFixedUObjectArray* ObjObjects ObjArray-ObjObjects; int32_t TotalObjects ObjObjects-Num(); for (int32_t i 0; i TotalObjects; i) { FUObjectItem* Item ObjObjects-GetObjectPtr(i); if (!Item || !Item-Object) continue; UObject* Obj (UObject*)Item-Object; // 简单的有效性检查检查虚表指针是否在合理范围内ClassPrivate是否有效等 if (!IsValidUObject(Obj)) continue; FString OutString; GetName(Obj, OutString); // 调用逆向得到的GetName函数 // 将FString的宽字符转换为多字节字符输出 char AnsiName[1024]; WideCharToMultiByte(CP_UTF8, 0, OutString.c_str(), -1, AnsiName, sizeof(AnsiName), nullptr, nullptr); fprintf(pFile, [%06d] 0x%p - %s\n, i, (void*)Obj, AnsiName); } fclose(pFile); OutputDebugStringA([Dump] Object names saved to ObjectDump.txt); }注意上述代码是高度简化的概念代码。实际编写需要正确定义TNameEntryArray、FUObjectArray、FChunkedFixedUObjectArray、FUObjectItem、UObject、FString等复杂的内存结构体并且处理所有指针读取的安全性。IsValidUObject函数也需要根据逆向出的对象标志位来实现。7. 第四步版本差异的深度处理与适配让我们更深入地看看4.23版本变化带来的具体挑战和解决方案。7.1 FUObjectArray结构解析通过逆向开源版本或动态分析我们可以还原出近似结构struct FUObjectArray { int32_t ObjFirstGCIndex; // 与垃圾回收相关 int32_t ObjLastNonGCIndex; int32_t MaxObjectsNotConsideredByGC; int32_t OpenForDisregardForGC; FChunkedFixedUObjectArray ObjObjects; // **核心成员** // ... 其他同步原语等 }; struct FChunkedFixedUObjectArray { enum { NumElementsPerChunk 64*1024 }; // 每块65536个对象 FUObjectItem** Objects; // 指向指针数组的指针二级指针 FUObjectItem* PreAllocatedObjects; // 预分配块 int32_t MaxElements; // 最大元素数 int32_t NumElements; // 当前元素数 int32_t MaxChunks; // 最大块数 int32_t NumChunks; // 当前块数 // 关键函数通过索引获取FUObjectItem* FORCEINLINE FUObjectItem* GetObjectPtr(int32 Index) const { int32 ChunkIndex Index / NumElementsPerChunk; int32 WithinChunkIndex Index % NumElementsPerChunk; if (!Objects[ChunkIndex]) return nullptr; return Objects[ChunkIndex][WithinChunkIndex]; } }; struct FUObjectItem { UObject* Object; // 真正的UObject指针 int32 Flags; // 标志位如RF_PendingKill, RF_Reach... int32 ClusterRootIndex; int32 SerialNumber; };逆向要点你需要通过动态调试在找到的GUObjectArray地址处手动解析内存观察其布局是否与上述模式匹配。特别是ObjObjects的偏移量以及Objects二级指针的指向。7.2 GetName函数在4.23的潜在变化虽然函数核心逻辑不变但调用约定或内联策略可能改变。一种更稳健的方法是不直接调用引擎函数而是自己实现名称解析逻辑。既然我们已经知道了UObject中FName NamePrivate的偏移假设通过比较两个不同UObject的内存差异得出。GNames全局表的地址和FNameEntry的结构。我们可以绕过GetName直接实现自己的GetObjectNameFString GetObjectNameCustom(UObject* Obj) { if (!Obj || !GNames) return FString(LInvalid); // 1. 读取FName int32 NameIndex *(int32*)((uintptr_t)Obj NamePrivateOffset); // 假设DisplayIndex在FName的第一个int32 int32 InstanceNumber *(int32*)((uintptr_t)Obj NamePrivateOffset 4); // Number // 2. 从GNames获取字符串指针 // GNames通常是一个TArrayTArrayFNameEntry*第一级是FNamePool区块 int32 Block NameIndex / 16384; // 假设每块16384个条目 int32 Offset NameIndex % 16384; FNameEntry* NameEntry GNames-Get(Block, Offset); // 需要实现Get函数 if (!NameEntry) return FString(LUnknown); // 3. 读取字符串可能是ANSI或WIDE wchar_t WideName[1024]; if (NameEntry-IsWide()) { // 宽字符 wcscpy_s(WideName, NameEntry-GetWideName()); } else { // ANSI转宽字符 char AnsiName[1024]; strcpy_s(AnsiName, NameEntry-GetAnsiName()); MultiByteToWideChar(CP_UTF8, 0, AnsiName, -1, WideName, 1024); } // 4. 处理实例编号 FString Result WideName; if (InstanceNumber 0) { Result L_; Result std::to_wstring(InstanceNumber - 1); // UE4内部Number从1开始输出时通常减1 } return Result; }这种方法更底层不依赖特定的函数地址兼容性更好但需要更精确地逆向出FNameEntry的结构和GNames的访问方法。8. 常见问题、排查技巧与实战心得在实际操作中你一定会遇到各种问题。以下是我踩过坑后总结的排查清单。8.1 通用问题排查表问题现象可能原因排查步骤Dump出的名称全是乱码或空1.GNames地址错误。2.FName到字符串的转换逻辑错误。3.FNameEntry结构解析错误ANSI/WIDE判断。1. 动态调试在GetNameCustom函数中检查读取到的NameIndex值是否合理通常不是0。2. 手动计算GNames中某个已知索引如0对应“None”的地址用Cheat Engine查看该地址内存确认是否是有效的字符串。3. 检查FNameEntry的第一个字节可能是标志位位0表示是否宽字符。遍历时游戏崩溃1. 访问了无效的UObject指针。2.GObject数组边界计算错误。3. 多线程冲突在游戏运行时遍历。1. 加强IsValidUObject检查检查虚表指针、Class指针是否在可执行模块范围内。2. 确认Num()函数的返回值是否正确或者使用逆向出的数组最大尺寸。3. 尝试在游戏线程安全的时机如加载画面进行Dump或挂起其他线程。找到的GetName函数调用无效1. 函数签名错误调用约定。2.this指针传递错误。3. 函数已被内联找到的不是完整函数。1. 使用反编译器确认函数签名__fastcall,__thiscall。x64通常用__fastcall前两个参数在RCX, RDX。2. 确保调用时RCX寄存器或第一个参数是有效的UObject*。3. 考虑使用自定义的GetObjectNameCustom方案绕过函数调用。4.23版本下找不到GObject1. 仍在用旧字符串“GObject”搜索。2.GUObjectArray结构偏移计算错误。1. 改用“GUObjectArray”或分析FUObjectArray相关代码。2. 动态分配一个UObject如通过控制台命令在调试器中下断点跟踪它被添加到数组的过程从而定位数组指针。Dump出的对象数量远少于预期1. 遍历逻辑只遍历了活跃对象忽略了待销毁对象。2.FUObjectArray的分块遍历逻辑有误。1. 检查有效性过滤是否过于严格尝试不过滤RF_PendingKill标志的对象看看。2. 仔细核对FChunkedFixedUObjectArray::GetObjectPtr的实现确保索引计算正确。特别是ChunkIndex和WithinChunkIndex的计算。8.2 实战心得与技巧从已知推导未知始终从一个你绝对确定的点开始。比如你知道游戏里有一个叫“PlayerController”的对象。在调试器中找到这个字符串然后反向追踪是谁在访问它这是定位GNames和GetName逻辑最可靠的方法。善用IDA的Struct视图和重命名功能一旦你分析出一块内存是什么结构比如FUObjectItem立刻在IDA中创建一个对应的结构体ShiftF9并应用到相应的地址上。给全局变量、函数起一个有意义的名称如GUObjectArray,UObject_GetName。这能极大提升后续分析的效率。版本确认是第一步在开始逆向前尽可能确认游戏使用的UE4版本。可以通过分析二进制文件中的版本字符串如“4.27.2”或通过一些特征函数如特定版本的字符串哈希函数来判断。这能帮你快速选择正确的逆向路径。外部读取 vs 内部注入对于简单的分析编写一个外部进程使用ReadProcessMemory读取目标游戏内存是更安全的选择避免了注入DLL可能导致的崩溃或检测。但对于需要调用引擎内部函数如获取复杂的属性的复杂工具DLL注入是必要的。保持耐心多次验证逆向工程很少能一次成功。每得到一个关键地址或偏移都要通过多种方式验证如用不同对象测试、在游戏不同状态下测试。一个地址的错误可能会导致整个链条失效。最后我想强调的是从GetName到DumpName的流程是UE4逆向的“基本功”。它锻炼的是你定位全局数据、理解引擎核心结构、进行安全内存操作的能力。掌握了这套流程你就打开了分析UE4游戏内部世界的大门后续无论是分析Gameplay框架、寻找UFunction进行调用、还是解析网络数据包都将拥有坚实的基础。这个过程充满挑战但每一次成功的Dump看到屏幕上滚过成千上万个熟悉的引擎内部类名时那种成就感是无与伦比的。