第十八章:逆向 Unreal Engine 游戏

Unreal Engine,简称 UE,在游戏圈的地位不用我多说。从《绝地求生》到《堡垒之夜》,再到各种 3A 大作,UE 几乎无处不在。说实话,我第一次逆向 UE 游戏时,也被它那套复杂的对象系统搞得有点懵。但摸清楚套路后,你会发现——嗯,其实 UE 的结构比很多自研引擎要规整得多。

这一章,我们就来拆解 UE 游戏的核心结构。我会带你看看 GObjects、GNames 这些关键数据,再聊聊怎么用 UE4SS 这类工具快速上手。

UE 游戏的结构:从外到内

一个 UE 游戏跑起来,内存里大概长这样:

  • 引擎核心模块:负责渲染、物理、音频这些底层活儿
  • 游戏逻辑模块:你的角色、武器、技能,都在这里
  • 对象系统:UE 最骚的地方——所有东西都是 UObject
  • 反射系统:运行时能拿到类的信息,这给逆向开了扇窗

我个人习惯先把 UE 游戏看作一个巨大的对象池。池子里泡着成千上万个 UObject,每个都有名字、有类型、有属性。你想想看,只要能遍历这个池子,游戏里的一切不就尽在掌握了?

核心要点:UE 的反射机制是逆向的突破口。它把类的信息暴露在运行时,我们只需要找到这些信息的存储位置。

GObjects 与 GNames:UE 的命门

UE 游戏有两个全局变量,是所有逆向工程师的宝贝——GObjectsGNames

GObjects:对象大本营

GObjects 是一个数组,存着游戏中所有 UObject 的指针。说白了,这就是 UE 的对象管理器。每个 UObject 在数组里都有一个索引,通过索引就能拿到对象本身。

我曾在逆向一个 UE 射击游戏时,想找玩家的血量。直接搜内存?太慢了。我直接遍历 GObjects,找到玩家角色的 UObject,然后读它的属性——几秒钟就定位到了血量地址。

GObjects 的结构大概是这样:

// GObjects 本质上是一个 TUObjectArray
class FUObjectArray {
    TUObjectArray* ObjObjects;  // 对象数组
    int32 NumElements;          // 当前对象数量
    int32 MaxElements;          // 最大容量
};

// 每个 UObject 的开头
class UObject {
    void* VTable;               // 虚函数表
    int32 ObjectFlags;          // 对象标志
    int32 InternalIndex;        // 在 GObjects 中的索引
    UClass* ClassPrivate;       // 对象的类
    FName NamePrivate;          // 对象的名字
    UObject* OuterPrivate;      // 外部对象(通常是包)
};

小技巧:找 GObjects 地址,可以搜特征码 "48 8B 05 ? ? ? ? 48 8B 0C C8 48 8B 04 D1"(64位下)。这个模式在 UE 4.x 版本中很稳定。

GNames:名字的字典

GNames 是 UE 的名字系统。每个名字(比如 "PlayerState"、"Health")都有一个唯一的 ID,通过 ID 可以查到名字字符串。GNames 本质上是一个大数组,存着所有用到的名字。

为什么要搞这么复杂?因为 UE 里到处都要比较名字,用整数 ID 比字符串比较快得多。你想想看,每帧都要比较几万个名字,用字符串那还得了?

GNames 的结构:

// GNames 是一个 TNameEntryArray
class FNameEntry {
    int32 Index;                // 名字的索引
    FNameEntry* HashNext;       // 哈希链下一个
    int32 Len;                  // 字符串长度
    char Name[1024];            // 名字字符串(实际长度由 Len 决定)
};

// 通过 GNames 查名字
FNameEntry* Entry = GNames[Name.Index];
char* NameStr = Entry->Name;

注意:UE 4.0 到 4.25 之间,GNames 的结构变化了好几次。我曾经在逆向一个老版本 UE 游戏时,按新版本的偏移去读,结果读出来的全是乱码。所以,一定要先确认引擎版本。

使用 UE4SS 或类似工具进行逆向

手动找 GObjects、GNames 虽然能锻炼技术,但效率确实不高。好在社区有大佬做了 UE4SS 这样的工具,让我们能快速上手。

UE4SS 是什么?

UE4SS 是一个针对 UE 游戏的逆向辅助工具。它本质上是一个 DLL 注入器,注入后能提供:

  • 对象浏览器:可视化查看所有 UObject
  • 控制台:执行 Lua 脚本,调用游戏内部函数
  • Dump 工具:导出游戏的所有类信息
  • Live View:实时查看对象属性变化

我个人觉得 UE4SS 最香的功能是 Lua 脚本。你可以写脚本来自动化很多操作,比如批量修改属性、触发游戏事件、甚至写个简单的作弊菜单。

实战:用 UE4SS 找玩家坐标

假设我们要找一个 UE 游戏中玩家的坐标。用 UE4SS 怎么做?

  1. 注入 UE4SS 到游戏进程
  2. 打开对象浏览器,搜索 "Player" 相关的类
  3. 找到 APlayerControllerACharacter 的实例
  4. 查看它的属性,找到 RootComponentCharacterMovement
  5. USceneComponent 里找到 RelativeLocation

用 Lua 脚本实现:

-- 获取玩家控制器
local PlayerController = FindFirstOf("BP_PlayerController_C")
if PlayerController then
    -- 获取控制的 Pawn
    local Pawn = PlayerController:GetPawn()
    if Pawn then
        -- 获取位置
        local Location = Pawn.K2_GetActorLocation()
        print("玩家坐标: " .. Location.X .. ", " .. Location.Y .. ", " .. Location.Z)
    end
end

你看,几行代码就拿到了坐标。要是手动逆向,你得先找 GObjects,再遍历对象,再解析属性——至少多花半小时。

核心思路:UE4SS 帮我们封装了 GObjects 和 GNames 的访问逻辑。你不需要关心底层怎么找,只需要关心「我要操作哪个对象」。

UE4SS 的局限性

当然,UE4SS 也不是万能的。我遇到过几个坑:

  • 版本兼容:UE4SS 主要支持 UE 4.18 到 4.27,UE5 的支持还在完善中
  • 反作弊检测:有些游戏会检测 DLL 注入,UE4SS 可能被 ban
  • 性能开销:对象浏览器在对象数量多的时候会卡,我见过一个游戏有 10 万个 UObject,浏览器直接崩了

所以我的建议是:UE4SS 适合快速原型验证,但真要写稳定的工具,还是得自己实现 GObjects 遍历。

知识体系总览

下面这张图,是我梳理的 UE 逆向核心知识结构。你可以把它当作地图,随时回来看看自己走到哪了。

UE 逆向核心知识体系 Unreal Engine 游戏 GObjects 对象数组 GNames 名字系统 反射系统 GObjects 内容 • 所有 UObject 指针 • 通过索引访问对象 • 遍历可找到任何实例 GNames 内容 • 名字到 ID 的映射 • 整数 ID 比较更快 • 用于属性名、类名 反射系统内容 • 运行时类信息 • 属性偏移量 • 函数指针 工具:UE4SS / 手动特征码 / Lua 脚本 目标:读取/修改游戏数据

手动实现 GObjects 遍历

如果你不想依赖 UE4SS,或者想更深入地理解底层,那就得自己动手了。这里我给出一个手动遍历 GObjects 的流程:

  1. 定位 GObjects 地址:用 Cheat Engine 搜索特征码,或者通过分析引擎模块的导出表
  2. 解析对象数组:读取 GObjects 指向的 TUObjectArray,拿到对象指针数组
  3. 遍历对象:从索引 0 开始,逐个读取 UObject 指针
  4. 读取对象信息:通过 UObject 的 ClassPrivate 和 NamePrivate 获取类名和对象名
  5. 筛选目标:根据类名或对象名,找到我们感兴趣的对象

代码示例(伪代码,实际需要根据引擎版本调整偏移):

// 假设已经找到 GObjects 地址
uintptr_t GObjectsAddr = 0x12345678;

// 读取对象数组
FUObjectArray* ObjArray = *(FUObjectArray**)GObjectsAddr;
int32 NumObjects = ObjArray->NumElements;

// 遍历所有对象
for (int32 i = 0; i < NumObjects; i++) {
    UObject* Obj = ObjArray->ObjObjects->GetObjectPtr(i);
    if (Obj == nullptr) continue;
    
    // 读取对象名字
    FName Name = Obj->NamePrivate;
    char* NameStr = GetNameFromGNames(Name.Index);
    
    // 读取对象类名
    UClass* Class = Obj->ClassPrivate;
    FName ClassName = Class->NamePrivate;
    char* ClassNameStr = GetNameFromGNames(ClassName.Index);
    
    // 打印信息
    printf("[%d] Class: %s, Name: %s\n", i, ClassNameStr, NameStr);
}

避坑指南:我曾经在遍历 GObjects 时,没检查对象指针是否有效,结果读到了一个已经被销毁的对象,直接崩溃。所以,一定要加空指针检查,最好再验证一下对象的 VTable 是否在合理范围内。

总结

逆向 UE 游戏,说白了就是跟 GObjects 和 GNames 打交道。这两个数据结构是 UE 的命门,掌握了它们,你就能在游戏内存里自由穿梭。

UE4SS 是个好工具,能帮你快速上手。但我建议你也要理解底层原理,这样遇到 UE4SS 搞不定的情况时,你还能自己动手。

嗯,这一章的内容就到这里。记住,实践出真知——找个 UE 游戏练练手,比看十遍教程都有用。


公众号:蓝海资料掘金营,微信deep3321