UE4逆向实战:从GName定位到DumpName的完整操作指南(4.23以下版本)
如果你对虚幻引擎4(UE4)的逆向工程感兴趣,尤其是想了解游戏对象名称系统的内部工作原理,那么这篇文章就是为你准备的。在UE4游戏的安全分析、外挂开发或模组制作中,理解并能够提取游戏中的对象名称是至关重要的一步。今天,我将带你深入UE4 4.23以下版本的核心,手把手教你如何定位关键的GName全局变量,并实现DumpName操作。
与那些只讲理论的教程不同,这篇文章完全从实战出发。我会假设你已经有了一些基础的逆向知识,比如知道如何使用调试器和内存扫描工具,但即使你是初学者,跟着步骤走也能掌握核心技巧。我们将使用Cheat Engine(CE)作为主要工具,因为它免费、强大且社区支持完善。整个过程就像侦探破案一样,通过线索一步步找到目标,既有挑战性又有成就感。
1. 理解UE4的FName系统与GName全局变量
在开始动手之前,有必要先理解我们要找的是什么。UE4使用一种称为FName的高效字符串管理系统,它不像普通字符串那样直接存储文本内容,而是将字符串存储在全局的**名称池(Name Pool)**中,每个字符串分配一个唯一的索引。FName对象本身只存储这个索引,比较和传递时只操作索引,大大提升了性能。
这个全局名称池就是通过GName(或TNameEntryArray)这个全局变量来管理的。在UE4 4.23之前的版本中,GName是一个直接可访问的全局数组;而在4.23及之后版本中,它被封装在FNamePool结构中,访问方式有所不同。我们今天聚焦在4.23以下版本。
为什么GName如此重要?
- 它是所有游戏对象名称的中央存储库
- 通过它可以将FName索引转换为可读的字符串
- 是逆向分析中定位游戏对象、函数和属性的关键入口点
从源码层面看,FName的核心结构是这样的:
// 简化版的FName结构(基于UE4 4.22)
class FName
{
public:
// 获取名称字符串
FORCEINLINE FString ToString() const
{
return GetDisplayNameEntry()->GetPlainNameString();
}
private:
// 名称索引(核心数据)
FNameEntryId ComparisonIndex;
// 显示索引(可能不同)
FNameEntryId DisplayIndex;
// 编号(用于区分同名对象)
int32 Number;
};
而GName实际上是一个TNameEntryArray类型的全局数组,存储着所有的FNameEntry对象,每个FNameEntry包含实际的字符串数据。我们的目标就是找到这个全局数组在内存中的位置。
2. 准备工作与环境配置
在开始逆向之前,确保你准备好了以下工具和环境:
必需工具清单:
- Cheat Engine 7.4+ - 我们的主要逆向工具
- IDA Pro 或 Ghidra - 用于静态分析(可选但推荐)
- UE4 4.22或更早版本的游戏 - 作为分析目标
- Process Hacker 或 Process Explorer - 查看进程信息
- 文本编辑器 - 记录偏移和地址
目标游戏选择建议: 对于初学者,我推荐选择以下类型的游戏作为练习目标:
- 单机UE4游戏 - 没有反作弊,分析更简单
- 小体量独立游戏 - 加载快,结构相对简单
- 已知使用UE4 4.22或更早版本的游戏 - 确保方法适用
注意:请仅在你有合法权限的游戏上进行逆向分析,比如自己购买的游戏或明确允许修改的游戏。尊重开发者的劳动成果和版权。
Cheat Engine基础配置:
- 下载并安装最新版Cheat Engine
- 首次运行可能需要关闭一些杀毒软件的实时保护(CE有时会被误报)
- 熟悉CE的基本操作:附加进程、内存扫描、查看内存区域、下断点
实战前的心理准备: 逆向工程很少有一次成功的情况。你可能会遇到游戏更新导致偏移变化、反调试保护、或者自己理解错误的情况。保持耐心,把每次失败都当作学习的机会。我刚开始时,为了找一个简单的偏移花了整整两天时间,但现在回想起来,那些挫折让我对内存布局有了更深的理解。
3. 使用Cheat Engine定位GName的实战步骤
现在进入实战环节。我将以《BattleRoyaleTrainer》(一个UE4单机训练游戏)为例,展示完整的定位过程。你可以用任何UE4 4.23以下的游戏跟随操作。
3.1 第一步:定位关键字符串特征
UE4引擎在初始化时会注册大量硬编码的名称,如"None"、"ByteProperty"、"IntProperty"等。这些字符串在内存中的位置与GName有直接关系,是我们寻找GName的重要线索。
操作步骤:
- 启动游戏和Cheat Engine
- 在CE中选择游戏进程并附加
- 点击"Memory View"打开内存查看器
- 在内存查看器中右键选择"Search for → String references"
- 在弹出的对话框中输入"ByteProperty"(不含引号)
- 确保搜索选项设置为"Case sensitive"和"Entire block"
你会看到类似这样的结果:
地址 反汇编
00007FF6A1B3D120 push rbx
00007FF6A1B3D122 sub rsp, 20h
00007FF6A1B3D126 mov rbx, rcx
00007FF6A1B3D129 lea rcx, [00007FF6A3A8F2D0h] ; "ByteProperty"
00007FF6A1B3D130 call 00007FF6A1B3CF00
关键技巧:
- 不要只看第一个结果,往下滚动查找在代码段(.text)中的引用
- 注意那些在函数开头附近被引用的字符串,很可能是初始化函数
- 记录下字符串地址,比如上面的
00007FF6A3A8F2D0
3.2 第二步:分析字符串引用函数
找到字符串引用后,我们需要查看是哪个函数在使用它。在内存查看器中双击字符串地址,CE会跳转到引用该字符串的代码位置。
常见的GName相关函数特征:
- FName::ToString() - 将FName转换为字符串
- FName::GetDisplayNameEntry() - 获取名称条目
- 静态初始化函数 - 在游戏启动时注册硬编码名称
以"ByteProperty"为例,我们可能会看到这样的代码模式:
; 典型的GName初始化代码模式
mov rcx, [GName地址] ; 加载GName指针
lea rdx, [字符串地址] ; 加载"ByteProperty"地址
call RegisterNameFunction ; 调用注册函数
实际操作:
- 在字符串引用处按Ctrl+D查看反汇编
- 向上滚动查看函数开头(通常有
push rbp, mov rbp, rsp等序言) - 寻找对全局变量的访问指令,如
mov rax, [7FF6A3B45000h]
识别GName访问模式: GName通常以以下形式被访问:
- 直接内存访问:
mov rax, [GName地址] - 相对RIP访问:
lea rax, [rip+GName偏移] - 通过函数获取:
call GetGNameFunction
3.3 第三步:追踪GName的交叉引用
一旦找到可能初始化或使用GName的函数,我们需要追踪它的交叉引用。在CE中,你可以:
- 在函数内的GName访问指令上右键
- 选择"Find out what accesses this address"
- CE会记录所有访问该地址的代码
实际案例追踪: 假设我们在00007FF6A1B3D130处找到了对"ByteProperty"的引用,向上分析函数:
; 函数开头
00007FF6A1B3D120 push rbx
00007FF6A1B3D122 sub rsp, 20h
00007FF6A1B3D126 mov rbx, rcx
00007FF6A1B3D129 lea rcx, [00007FF6A3A8F2D0h] ; "ByteProperty"
00007FF6A1B3D130 call 00007FF6A1B3CF00 ; 注册函数
00007FF6A1B3D135 mov rcx, rbx
00007FF6A1B3D138 add rsp, 20h
00007FF6A1B3D13C pop rbx
00007FF6A1B3D13D ret
这个函数看起来是在注册名称。我们需要找到它被谁调用,以及GName在哪里被初始化。在函数开头00007FF6A1B3D120处右键选择"Find references to this address"。
如果幸运的话,你可能会找到一个像这样的初始化函数:
; GName初始化函数(简化版)
GName_Init:
push rbp
mov rbp, rsp
sub rsp, 40h
mov [rbp+var_18], rbx
mov [rbp+var_10], rsi
mov [rbp+var_8], rdi
; 初始化GName数组
lea rax, GName ; 关键!这里加载GName地址
mov [rax], 0
mov [rax+8], 0
...
; 注册硬编码名称
lea rcx, [Str_None]
call RegisterName
lea rcx, [Str_ByteProperty] ; 我们找到的字符串
call RegisterName
...
3.4 第四步:验证找到的GName地址
找到可能的GName地址后,我们需要验证它是否正确。GName在内存中通常表现为一个结构体数组,具有以下特征:
GName数组的典型内存布局:
地址 数据
GName+0x00: 指向第一个FNameEntry块的指针
GName+0x08: 指向第二个FNameEntry块的指针
GName+0x10: 指向第三个FNameEntry块的指针
...
验证方法:
- 在CE的十六进制查看器中转到疑似GName的地址
- 查看该地址存储的值(应该是一个指针)
- 跟随该指针(右键→"Follow in dump")
- 你应该看到类似这样的结构:
FNameEntry内存块示例:
偏移 数据 ASCII
0x00: 00 00 00 00 00 00 00 00 ........
0x08: 4E 6F 6E 65 00 00 00 00 None....
0x10: 42 79 74 65 50 72 6F 70 ByteProp
0x18: 65 72 74 79 00 00 00 00 erty....
快速验证脚本: 你可以在CE中编写一个简单的Lua脚本来验证GName:
function verifyGName(gnameAddr)
local blockPtr = readPointer(gnameAddr)
if blockPtr == nil or blockPtr == 0 then
print("无效的GName地址")
return false
end
-- 读取第一个名称(应该是"None")
local nameEntry = blockPtr + 0x8 -- 跳过头部信息
local name = readString(nameEntry, 50, true)
if name == "None" then
print(

&spm=1001.2101.3001.5002&articleId=152630255&d=1&t=3&u=a99d8c375321440e8ed8313562b2da9f)
1618

被折叠的 条评论
为什么被折叠?



