UE4逆向实战:手动定位GName与DumpName技术详解 1. 项目概述为什么我们要动UE4的GName如果你正在研究UE4游戏无论是为了制作Mod、分析游戏逻辑还是进行安全研究那么“GName”和“DumpName”这两个词对你来说绝对不陌生。它们就像是进入UE4引擎内部世界的大门钥匙。简单来说GName是虚幻引擎内部管理所有字符串名称比如类名、函数名、属性名、资源路径的核心全局表而DumpName就是我们通过技术手段把这个表里的所有字符串名称“倾倒”出来形成一个可读的列表文件的过程。为什么这件事如此重要想象一下你面对一个编译后的UE4游戏所有的C类名、蓝图节点名在二进制文件里都变成了晦涩的内存地址和索引。没有这些字符串符号逆向工程就像在黑暗的迷宫里摸索。一旦你拿到了GName表就等于获得了一张标注了所有房间名称的地图。你可以通过一个函数地址反向查找到它的函数名可以通过一个对象的虚表知道它属于哪个类。这对于分析游戏对象结构、定位关键函数、甚至是制作外接设备映射比如把方向盘、飞行摇杆映射到游戏操作都至关重要。网上有很多现成的工具和脚本号称能一键Dump但在实战中尤其是在对付一些做过保护的、或者非标准编译的UE4游戏时它们经常失灵。这时候掌握手动定位GName的方法就成了解决问题的终极底牌。今天我就以Unreal Engine 4.23版本及以下为例手把手带你用最经典的逆向工具Cheat EngineCE像侦探一样一步步在内存中“搜”出GName的地址并完成Dump。这个方法不依赖特定游戏版本通用性强是每个想深入UE4逆向的开发者都应该掌握的硬核技能。2. 核心思路与工具准备逆向不是瞎蒙在开始“搜内存”之前我们必须理解背后的原理。盲目搜索只会浪费大量时间。UE4的GName通常由一个名为FNamePool或TNameEntryArray的结构管理不同版本有差异4.23及以下常用的是TNameEntryArray结构的GNames。它是一个存储了所有FNameEntry名称条目的数组。我们的目标就是找到这个全局数组的地址。我们的核心思路是利用FName结构本身的特性。在UE4中一个FName并不直接存储字符串而是存储了一个索引FNameEntryId通过这个索引去GNames数组中查找对应的字符串内容。同一个字符串在全游戏中有唯一的FNameEntryId。我们可以利用这个“唯一索引”的特性通过CE进行对比查找。所需工具清单Cheat Engine (CE) 主力的内存扫描和调试工具。建议使用7.4或7.5版本稳定性好。目标UE4游戏 一个使用UE4 4.23或更早版本开发的游戏进程。为了演示你可以用UE4 4.23自己打包一个空白项目这样地址偏移更标准。一个调试器可选但推荐 如x64dbg或Immunity Debugger。当CE的扫描遇到复杂情况时调试器可以用于深入分析。文本编辑器 用于记录和整理找到的地址和偏移。注意本文所有技术仅用于学习、研究和安全测试目的请务必在合法授权的环境下进行操作切勿用于破坏游戏平衡或侵犯他人权益。前期重要准备关闭所有不必要的程序减少内存干扰。以管理员身份运行CE确保有足够的权限访问目标进程内存。熟悉CE的基本操作 如何附加进程、首次扫描、再次扫描、查看内存区域、找出是什么访问了该地址等。3. 实战第一步定位GName的“线索”——FName的实例我们不可能直接去搜“GName”这个字符串因为它是一个符号在发布版本中不存在。我们需要找到一个“跳板”这个跳板就是游戏中实实在在存在的FName对象。3.1 寻找稳定的FName字符串引用在游戏中有一些字符串是绝对稳定、从一开始就加载并且不会改变的。例如核心的UObject类名Object,Actor,Pawn,Character。引擎内部模块名CoreUObject,Engine。一些非常基础的属性名RootComponent,PlayerController。我们的策略是先找到这些字符串在内存中的地址然后回溯引用它的FName对象最后从FName对象中提取出索引再通过索引去定位存储所有字符串的数组GName。操作步骤用CE附加到目标游戏进程。选择扫描类型为“字符串”并勾选“Unicode”或“UTF-8”UE4内部字符串通常是UTF-16或ANSI但存储的FName字符串一般是ANSI。为了稳妥可以先从“字符串”类型开始。在数值框里输入一个确定的字符串比如Object然后进行首次扫描。你会得到大量包含Object这个字符串的地址。这很正常因为内存中可能有很多地方存了这个字符串比如动态生成的、资源里的。我们需要找到那个属于FNamePool/NameEntry的稳定实例。通常这些字符串在内存中会连续排列你可以通过查看内存区域观察附近是否有其他熟悉的类名如Actor,Pawn等来辅助判断。找到一个你认为可能是FNameEntry字符串的地址记下来假设为String_Object_Addr。3.2 找出访问该字符串的代码光有字符串地址还不够我们需要知道是哪个FName的索引在用它。在CE的地址列表中右键点击你找到的String_Object_Addr选择“找出是什么访问了这个地址”。回到游戏里进行一些操作比如移动、打开菜单让游戏逻辑运行起来。此时CE的调试窗口会列出所有读取或写入该字符串地址的汇编指令及其所属的模块地址。我们需要寻找的是那种通过一个基地址加偏移来读取字符串的指令。例如你会看到类似这样的指令mov rcx, [rax0x10] ; rcx 现在指向字符串或者更关键的寻找一个将索引通常是一个整数作为参数传入某个函数的调用。比如call UE4Module.dllXXXXX ; FName::ToString 之类的函数 ; 调用前ecx/rdx或栈上可能存放着一个代表索引的整数记录下这些指令的地址。我们的目标是找到那个作为索引的整数值被加载的地方。你可能需要在这些指令上下断点然后观察寄存器和栈的变化来确认哪个值是FName的索引。4. 关键推导从FName索引到GNames数组假设经过上一步的调试我们确定了一个事实当游戏要显示Object这个名称时它会使用一个索引值比如我们观察到是0x0C十进制12。同时我们通过调试发现有一个全局的数组指针我们称之为GNames游戏代码会这样使用GNames[Index]来获取字符串地址。4.1 通过两个已知字符串确定数组结构仅凭一个索引我们无法确定GNames的地址和元素大小。这里需要一个经典的“两点确定一条直线”的方法。重复第3步再找一个稳定的字符串比如Actor。通过同样的“找出是什么访问了这个地址”的方法确定它的FName索引。假设我们找到Actor的索引是0x2A十进制42。现在我们有两个关键数据对索引0x0C- 字符串地址String_Object_Addr索引0x2A- 字符串地址String_Actor_Addr如果GNames是一个简单的指针数组每个元素是一个FNameEntry*占8字节那么GNames (Index * 8) 字符串指针地址我们可以列出方程来解GNames的基地址。但UE4的TNameEntryArray结构可能更复杂它可能包含一个块分配器。不过对于4.23及以下的版本一个常见的模式是GNames是一个二级指针指向一个FNameEntry**的数组。4.2 使用Cheat Engine进行指针扫描Pointer Scan这是CE里非常强大的一个功能可以帮我们系统地找到指向某个地址的所有可能指针链。我们已经有了String_Object_Addr和String_Actor_Addr。在CE中打开“指针扫描”工具。首先对String_Object_Addr进行指针扫描。设置一个合理的最大偏移比如0~1000和深度3~5。扫描会得到一系列可能指向该字符串地址的指针路径例如[Game.exe0x123456] - 0x20 - 0x8 字符串地址这表示在Game.exe0x123456这个地址存储的值加上0x20偏移再取该地址的值加上0x8偏移最终指向我们的字符串。对String_Actor_Addr也进行同样的指针扫描。关键比对对比两次指针扫描的结果寻找共同的基地址模块和相似的偏移模式。因为Object和Actor都来自同一个GNames数组所以指向它们的指针链很可能共享一个顶层的基地址即GNames数组本身的地址只是中间的索引偏移不同。例如你可能会发现两条链链A (Object):[UE4Core.dll0xABCD00] - 0x0 - [0x60] - 0x0 String_Object_Addr链B (Actor):[UE4Core.dll0xABCD00] - 0x0 - [0x60] - 0x18 String_Actor_Addr注意UE4Core.dll0xABCD00是共同的- 0x0 - [0x60]这部分也是共同的最后的偏移0x0和0x18不同。这个共同的UE4Core.dll0xABCD00就极有可能是GNames相关的全局指针。而最后的偏移差0x18 - 0x0 0x18十进制24正好等于我们两个索引的差(0x2A - 0x0C) 0x1E十进制30乘以每个元素的大小这里需要计算验证。如果每个元素是8字节指针30*80xF0对不上0x18。这说明数组结构可能不是简单的指针数组。4.3 分析内存验证GNames结构找到可疑的地址比如上面的UE4Core.dll0xABCD00后在CE的内存浏览器中查看它。跳转到该地址。它可能存储着另一个地址一个指针我们称之为GNamesArrayPtr。跳转到GNamesArrayPtr。你应该能看到一个巨大的数组。数组的每个元素是什么在GNamesArrayPtr (Index * ElementSize)的位置存储的应该是对应FNameEntry的指针或直接是结构体。对于4.23版本常见的TNameEntryArray结构其GNames变量可能直接就是一个FNameEntry**的数组。也就是说GNames本身就是一个指针指向一个FNameEntry*数组。GNames[0]就是第一个FNameEntry的地址GNames[1]是第二个以此类推。为了验证用我们找到的索引去计算计算GNamesArrayPtr (Index_Object * 8)查看该地址存储的值是否等于String_Object_Addr。计算GNamesArrayPtr (Index_Actor * 8)查看该地址存储的值是否等于String_Actor_Addr。 如果都匹配那么恭喜你GNamesArrayPtr就是我们要找的GNames数组的地址而UE4Core.dll0xABCD00就是指向它的全局指针。实操心得这个过程可能需要反复尝试和验证。不要只依赖两个字符串用第三个如Pawn甚至第四个字符串进行交叉验证能极大地提高准确性。有时候游戏会有多个FName相关的全局变量如GNames用于静态名称GEngineNames等需要根据上下文判断哪个是我们需要的。5. 编写脚本与DumpName将内存数据变为文本一旦我们确定了GNames数组的地址假设我们最终找到的静态地址是GNamesPtr Game.exe 0x1234560并且知道它是一个FNameEntry*的数组每个元素8字节那么Dump就变成了一个简单的遍历和读取字符串的过程。5.1 使用Cheat Engine的Auto Assembler功能CE内置的Auto Assembler允许我们编写简单的汇编脚本来操作内存。在CE中点击“内存查看窗口”的“工具”菜单选择“Auto Assembler”。点击“模板”选择“代码注入”。这会生成一个框架。我们需要编写一段脚本遍历GNames数组读取每个FNameEntry*然后解析出字符串并写入文件。由于CE的AA脚本处理文件I/O比较麻烦一个更简单的方法是将字符串地址和内容输出到CE的日志窗口然后从日志中复制保存。下面是一个概念性的脚本框架注意这是伪代码逻辑实际需要根据确切的内存结构调整[ENABLE] // 假设 // GNamesArray 地址已经找到放在一个变量里例如 alloc(GNamesBase, 8) // 我们知道数组的大概大小或者可以通过遍历直到遇到空指针来判断结束。FNameEntry的数量可能成千上万。 // FNameEntry 的结构通常前2字节是字符串长度可能不包含结尾null然后是ANSI字符串。 alloc(dumpName, 1024) alloc(currentIndex, 4) alloc(stringAddr, 8) alloc(logBuffer, 256) registersymbol(dumpName) dumpName: // 这里应该是你的汇编代码实现以下逻辑 // for (int i0; iMaxCount; i) { // FNameEntry* entry *(FNameEntry***)(GNamesBase i*8); // if (entry null) continue; // short len *(short*)entry; // 读取长度 // char* str (char*)(entry 2); // 字符串起始位置 // // 将 i 和 str 格式化输出到某个地方如写入一个内存区域或调用OutputDebugString // } // 由于汇编较复杂更实际的做法是... [DISABLE] // 清理代码 dealloc(dumpName) unregistersymbol(dumpName)5.2 更实用的方法使用Lua脚本对于复杂的逻辑和文件操作CE的Lua脚本引擎是更好的选择。你可以通过CE的Lua接口来读取内存、计算、并直接写入文件。在CE中打开“表格”菜单选择“显示Cheat Table Lua脚本”。在Lua脚本窗口中你可以编写这样的脚本local gnamesPtr 0x1234560 -- 替换为你找到的GNames静态地址相对于模块基址的偏移 local moduleBase getAddress(Game.exe) -- 获取模块基址 local gnamesBase readPointer(moduleBase gnamesPtr) -- 读取GNames数组的基地址 local outputFile io.open(DumpedNames.txt, w) local index 0 local maxEntries 2000000 -- 设置一个足够大的上限防止无限循环 while index maxEntries do -- 计算当前FNameEntry*的地址 local entryPtrAddr gnamesBase (index * 8) local entryPtr readPointer(entryPtrAddr) if entryPtr nil or entryPtr 0 then -- 空指针可能是数组末尾但也可能是间隙我们选择跳过 index index 1 goto continue end -- 读取FNameEntry假设结构int16长度 ANSI字符串 local len readShort(entryPtr) -- 读取字符串长度2字节 if len 0 or len 1024 then -- 简单的合理性检查 index index 1 goto continue end local str readString(entryPtr 2, len, true) -- 从长度后开始读取字符串true表示ANSI if str then outputFile:write(string.format([%06d] %s\n, index, str)) end ::continue:: index index 1 end outputFile:close() print(Dump完成)运行这个Lua脚本它就会遍历GNames数组将索引和对应的名称字符串写入到DumpedNames.txt文件中。注意事项上述Lua脚本中的内存结构FNameEntry开头2字节是长度是UE4早期版本的一种常见布局但并非绝对。在4.23中FNameEntry可能是一个更复杂的结构体包含比较计数、哈希值等。你可能需要根据实际情况调整读取偏移。最可靠的方法是在内存浏览器中手动分析几个已知的FNameEntry地址观察其内存布局确定长度和字符串的准确位置。6. 常见问题排查与高阶技巧在实际操作中你几乎一定会遇到各种问题。这里记录一些典型的坑和解决方法。6.1 扫描不到字符串或字符串地址不稳定原因1字符串编码。UE4可能使用宽字符UTF-16。在CE首次扫描时尝试选择“字符串”类型下的“Unicode”选项。原因2字符串被压缩或混淆。一些安全措施会加密字符串。你需要先找到解密函数或者在内存中寻找解密后的瞬间。可以尝试扫描字符串的哈希值如CRC32、FNV而不是明文。原因3FName使用哈希直接比较很少访问字符串本身。这种情况下“找出是什么访问了这个地址”可能收效甚微。你需要寻找调用FName::ToString或FName::GetDisplayNameEntry()等函数的地方在这些函数入口下断点来捕获索引。6.2 指针扫描结果太多无法找到共同模式策略增加指针扫描的“深度”和“偏移”范围但不要太大否则结果爆炸。更有效的方法是先通过调试找到至少一条确定的、从GNames到具体字符串的访问指令链然后根据这条链的特征如固定的模块偏移去过滤指针扫描的结果。使用“指针映射”功能CE的指针扫描器可以生成一个.map文件然后用“指针扫描”对话框中的“指针映射”功能来筛选只显示与特定模块相关的指针路径。6.3 确定GNames地址后Dump出来的名字是乱码或不全原因1FNameEntry结构分析错误。这是最常见的问题。FNameEntry在4.23版本可能不是简单的长度字符串。它可能有一个头部结构包含// 一种可能的结构 struct FNameEntry { int32 ComparisonId; // 或 Hash int32 Index; char Name[1]; // 柔性数组实际长度由分配决定 };你需要用已知的字符串地址在内存浏览器中向前多看几十个字节观察规律。看看字符串前面是否有固定的模式如重复的魔术字、长度值、索引值。原因2字符串非ANSI。可能是UTF-8。尝试用readString(..., ..., false)最后一个参数false表示UTF-8来读取。原因3GNames数组不是单层。可能存在分块Chunk结构。GNames指向一个TNameEntryArray它内部有多个FNameEntry*的块Blocks。你需要遍历所有块。这时代码会更复杂需要先读取TNameEntryArray的结构体获取Blocks指针和BlockSize等信息。6.4 针对不同UE4版本的调整4.20以下GNames的结构相对简单更接近传统的TArrayFNameEntry*。4.20 ~ 4.25引入了FNamePool的雏形或变体结构可能开始复杂化。重点寻找FNamePool::Get()或FName::GetDisplayNameEntry()函数的实现逆向这些函数是找到GNames关键地址的捷径。通用思路无论版本如何变化核心目标不变——找到将FName索引解析为字符串的代码。在IDA或GHIDRA中静态分析游戏主模块或UE4核心模块如CoreUObject-*.dll搜索字符串Accessing FName with invalid index或Invalid FName index等错误信息这些信息通常就在FName::ToString函数附近是极好的切入点。6.5 利用IDAPython或GHIDRA脚本进行自动化分析对于经常需要分析不同UE4游戏的同学手动CE扫描毕竟效率较低。更进阶的做法是用IDA Pro或GHIDRA加载游戏模块。通过特征码或字符串引用定位到FName::ToString或FNamePool::Resolve函数。编写IDAPython或GHIDRA脚本分析该函数的交叉引用找到读取全局GNames或FNamePool指针的指令。脚本自动计算并输出这个全局指针的地址偏移。 这种方法一旦成型对于同引擎版本的游戏几乎可以做到秒级定位。手动用CE定位GName的过程本质上是一次对程序内存布局和数据结构的深度侦查。它要求你不仅会使用工具更要理解工具背后的原理和引擎的运行机制。每一次成功的定位都是对逆向思维和耐心的一次锤炼。当你面对一个全新的、没有符号的UE4游戏能够凭借这套方法独立挖出GName时那种成就感远非使用现成工具可比。这套方法的价值在于其通用性和底层性它不依赖于任何外部签名或版本库是真正意义上的“硬碰硬”技术。掌握了它你就拥有了解开绝大多数UE4游戏名称迷雾的万能钥匙。