UE4游戏逆向:PUBGM SDK生成原理与GObjects遍历实战

发布时间:2026/9/14 1:40:09
UE4游戏逆向:PUBGM SDK生成原理与GObjects遍历实战
简介面向游戏开发与逆向分析人群的PUBG Mobile SDK v1.2.0资料包以C头文件与源码文件为主可用于研究游戏逻辑、网络通信与UI框架辅助开发调试或协议分析。包内包含611个hpp定义文件、204个cpp实现文件、2个txt转储文件及1个log日志共818个文件压缩包仅4.52MB目录结构清晰便于按功能模块检索。文件类型中hpp提供类与接口声明cpp展示具体实现逻辑txt可能导出游戏对象结构或名称映射log则记录生成过程信息对理解SDK内部运作和游戏数据模型有直接帮助。已有569人学习下载适合具备C基础、对游戏逆向与安全研究感兴趣的开发者参考但使用时应遵守游戏开发者协议与相关法律法规避免用于破坏游戏平衡或绕过限制的非法行为。1. 一套解包 UE4 引擎的 SDKPUBGM 项目代号 ShadowTracker 的产物PUBGM SDK v1.2.0 这套东西说白了就是一份把 Unreal Engine 4 运行时反射信息完整导出的 C 头文件包。PUBG Mobile国际服的项目代号是 ShadowTrackerExtra所以你会看到 SDK 解出来的核心类全部带着ShadowTrackerExtra_前缀。SDK 在游戏开发里是什么含义很多刚接触的人理解成官方提供的一堆 API 文档但这份不是它属于“由内存快照反向生成”的引擎数据描述包含 SDK.hpp、ObjectsDump.txt、NamesDump.txt 和 Generator.log。它解决的核心问题很直接当你面对一个没有源码的 UE4 游戏怎么在一小时内知道类的继承关系、属性偏移、函数在哪个 UFunction 里而不是对着十六进制逐字节猜。这套包适合 UE4 框架扩展开发、游戏安全研究和反作弊分析的人看也适合想搞明白 GObjects 和 FName 机制的人当解剖样本。至于下载说明里常写的 make hack 和 bypass我建议把技术原理和实际使用分开对待原理可以研究往线上游戏注入就属于另一回事了后文会单独说合规边界。2. 生成原理GObjects 全局表与 FName 池如何被“翻译”成 C 头文件2.1 为什么 SDK 生成依赖 UE4 反射系统而不是反汇编UE4 和很多自研引擎最大的差异是它有完整的反射结构。编辑器里看到的 UCLASS、UPROPERTY、UFUNCTION 宏在编译后会以UClass、UProperty、UFunction等运行时对象形态保留在内存里。这意味着哪怕没有源码只要有办法遍历到这些对象就能拿到类名、父类、属性名、属性偏移量、函数参数布局等信息。SDK 生成器做的事情本质上就是“把反射数据翻译成 C 声明”。所以 SDK 工具链第一个要解决的不是反编译指令而是定位两个全局符号GObjects和GNames。GObjects 是 UE4 全局的对象数组所有 UObject 实例都注册在这个数组里GNames 是全局名字表保存了所有 FName 对应的字符串。两个符号在游戏进程里都以全局数据形式存在但发布版二进制不带符号表常见做法是用特征码AOB Pattern在 so 或 exe 的内存区段里扫描定位。提示那个年代的 UE4 版本里 GObjects 通常是一个FUObjectArray的静态实例内部有 ObjFirstGCIndex、ObjLastNonGCIndex 这类字段定位后直接强转成FUObjectArray*就能用。2.2 从内存快照到四个落盘文件拿到 GObjects 和 GNames 的地址后生成器一般会跑这么一圈流程遍历 GNames把索引和字符串导出成NamesDump.txt作用类似一张字符串常量表遍历 GObjects把每个对象的 ClassName、Name、Outer 链、Index 导出成ObjectsDump.txt可以理解为对象全景快照进一步递归解析每个 UClass 的继承链、属性列表、函数列表据此生成SDK.hpp过程中把跳过的对象、解析失败的属性、重复的命名写进Generator.log方便排查。ObjectsDump.txt和NamesDump.txt是给人和程序看的中间产物。实际排查问题时它们比 SDK.hpp 更接近引擎原始状态。举个真实场景你想确认某个 Actor 类型是否存在于当前版本里直接从 ObjectsDump 过滤比翻阅上千行的 SDK.hpp 快得多。import re from collections import Counter # 统计 ObjectsDump.txt 中各类对象的出现次数 class_counter Counter() with open(ObjectsDump.txt, r, encodingutf-8, errorsignore) as f: for line in f: # 常见格式: [Class] [ObjectName] [Index] [Outer] m re.match(r\s*\[\s*(\S)\s*\]\s(\S), line) if m: class_counter[m.group(1)] 1 for name, count in class_counter.most_common(40): print(f{count:6d} {name})这段脚本逻辑很简单逐行读取用正则提取类名和对象名再用 Counter 做频次统计。参数上encodingutf-8, errorsignore很重要因为 NamesDump 和 ObjectsDump 里可能混有非 UTF-8 的二进制残留不加errorsignore可能在读一半时直接抛异常。如果你的文件里对象名本身包含了空格正则里(\S)只取第一段后面内容会丢这时要把正则放宽成r\s*\[\s*(\S)\s*\]\s(.*?)\s*$。2.3 Generator.log 在这套工具链里的角色Generator.log 不是普通日志它是判断 SDK 有没有生成完整的“体检报告”。最常见的三类记录分别是找不到 FName 索引而丢弃的属性、重复命名的类和函数、Outer 为空导致无法建立继承挂载的对象。拿到 SDK 后第一件事不是打开 SDK.hpp而是先看 Generator.log 的 WARNING 数量和分布。如果一个 SDK 的 WARNING 超过几十条类里可能存在大量空指针或错误的偏移量。pubgm_sdk_v1.2.0 这套包里 Generator.log 就记录了生成器从 ShadowTrackerExtra 主模块读取反射数据时的边界情况。我的经验是WARNING 集中在个别非游戏模块比如 UMG 或 TweenMaker时问题不大但如果高频出现在 Engine 和 Gameplay 核心模块那这套 SDK 的可用性要打问号后续编译时大概率一堆 unresolved external。2.4 官方 SDK 与内存导出的 SDK 有什么不同官方 SDK例如 Android SDK、视频直播 SDK是厂商按稳定接口发布的开发套件有文档、有版本语义、有兼容性承诺。而 PUBGM SDK 这种由内存导出的 SDK本质是一个“一次性快照”它准确反映生成那一刻的游戏内存布局但不具备向后兼容性。游戏更新后偏移量全部失效需要重新生成。理解了这一点你就明白为什么拿到一个 SDK 包不能直接无脑用必须把它当作一个“数据快照”来校验版本对应关系。这点和做 Windows dump 一样当前进程的 PEB 指针不能带到下一个进程里用。3. SDK.hpp 结构拆解从 UObject 到 UWorld 的继承链与偏移量表3.1 SDK.hpp 顶层布局不只是一个大头文件SDK.hpp 看起来像单个几千行的头文件但它内部是有分区的。多数 UE4 dumper 生成的版本都会包含这几块开头是#pragma once和基础类型别名比如用int8_t/uint8_t重映射bool/uint8之类接着是一组枚举定义例如武器类型、动画状态、伤害类型然后才是核心的类声明区按继承深度从 UObject 逐层往下展开。最后会附带一组全局函数和全局对象的声明比如UWorld** World、UGameEngine** Engine方便使用者直接引用而不用自己扫描。初看 SDK.hpp 容易懵因为类太多而且每个类里都夹带着// 0x0228(0x0C)这样的偏移注释。我的习惯是先从 UObject、UField、UFunction 三个根类看起。这三个类的布局是否正确决定了整棵继承树是否可靠。3.2 UObject 的头部字段决定了解析基石UObject 是所有 UE4 对象的基类在内存里的头几个字段基本是固定的字段偏移示例类型说明VTablePtr0x0000void*虚函数表指针对象首地址即 vtableObjectFlags0x0008int32EObjectFlags标记对象状态InternalIndex0x000Cint32在 GObjects 中的索引ClassPrivate0x0010UClass*指向该对象的 UClassNamePrivate0x0018FName对象名字OuterPrivate0x0028UObject*对象的 Outer通常是包或作用域对象这些字段的名字在 SDK.hpp 里可能带前缀或后缀但语义一致。注意表中偏移只是示例各引擎版本会有差异真正生成时dumper 是从引擎的 UObject 静态布局里直接读出来的所以 SDK.hpp 里注释的偏移才是权威不要拿这个表去套别的版本。一个值得关注的细节ClassPrivate与NamePrivate之间的字段在部分版本里会有FName的内存池指针如果 SDK.hpp 里没有体现说明生成时跳过了这在读对象名字时可能产生一个空的GetName()。我一般会额外在调试器里核对一个已知类对象的这两个字段确保头文件与实际内存一致再继续。3.3 属性偏移量的生成UProperty 与 Offset 的映射关系SDK.hpp 里每个成员变量注释的// 0x0228(0x0C)前面的值是相对当前类起始地址的偏移括号里是属性大小。这个偏移不是生成器猜的而是来自UProperty::Offset_Internal。UE4 在运行时为每个属性维护偏移和大小dumper 遍历 UClass 的ChildProperties链表就能拿到整个输出。这里也是坑最密集的区域。如果 UClass 的父类解析有误子类的偏移就会整体偏移最终表现为“能读到对象但所有属性值都是错的”。所以使用 SDK.hpp 时我会用两个方法验证是否正常一是对比UObject::GetFullName()的输出是否与实际游戏内对得上二是取一个已知长度的 TArray 属性检查内存里读到的Num/Data是否符合逻辑范围如果 Num 是个天文数字说明当前偏移已经偏离真实布局。3.4 UFunction 与 ProcessEvent 在头文件里的呈现UFunction 在 SDK.hpp 里会被声明为类方法但实际游戏调用走的是一条更底层路径ProcessEvent(UFunction*, void* Params)。SDK.hpp 里通常为每个 UFunction 生成一个参数结构体比如FStartFire_Parms字段依次对齐到0x10边界。如果你的代码需要调用某函数严格来说是把参数结构体填好然后调用目标对象的 ProcessEvent。// SDK.hpp 中典型的 UFunction 参数结构体 struct FStartFire_Parms { uint8_t Pad[0xC]; // 对齐填充保证 Params 尺寸一致 class AActor* Target; // 0x0010 uint8_t bFromFirstPerson; // 0x0018 }; // Size: 0x20注意填充字节不是随便写的它决定了参数在栈上或寄存器里的偏移。如果 SDK.hpp 里没有填充直接拿看起来更紧凑的自定义结构体替代ProcessEvent 很可能把后面的参数读错。我一般不会手动改 UFunction 参数结构体除非通过 Generator.log 确认某个函数解析失败。这里就是“看起来能用”和“真的能用”的分水岭。3.5 FName、TArray、FString 三个基础类型的内存表达SDK.hpp 里所有字符串和数组都是按引擎内部布局展开的不是 C 标准库类型。FName 通常是两个 uint32一个比较索引一个显示索引FString 是 TArraywchar_t的包装TArray 则是 Data/Num/Max 三个字段。类型内存布局32位视角注意事项FNameint32 ComparisonIndex int32 DisplayIndex读写要查 GNamesFStringwchar_t* Data int32 Num int32 Max要按宽字符处理TArrayT* Data int32 Num int32 Max可能是数组也可能是容器在 SDK.hpp 里你看到TArrayclass AActor* Actors;时它的底层就是{AActor** Data; int Num; int Max;}。取值时必须通过 Data 再解引用一次直接拿对象地址去读会读到指针本身。这些类型不区分是移动端还是 PC 端只要是 UE4 都通用。4. 把 SDK 接进 Visual Studio从 GObjects 遍历到调用一个函数4.1 工程准备x64 控制台工程与 SDK.hpp 的放置拿到一套完整可编译的 SDK 后第一步是建一个干净的 Visual Studio C 空项目。配置上建议选 x64 Debug字符集用 UnicodeC 标准选 C17 或更高如果游戏模块是 ARM64也可以在本机用 ARM64 编译链但验证时仍然需要目标设备或模拟器。SDK.hpp 直接放到工程根目录把其它 dump 文件放在一份独立目录里避免 IDE 索引大量文本卡顿。#include cstdio #include SDK.hpp int main() { if (!SDK::Init()) // 内部会定位 GObjects 等全局数据 { printf([!] SDK init failed\n); return -1; } UWorld* World SDK::GetWorld(); if (!World || !World-PersistentLevel) { printf([!] world invalid\n); return -1; } int ActorCount World-PersistentLevel-Actors.Num(); printf([*] Actor count: %d\n, ActorCount); return 0; }这里的SDK::Init()是常见 dumper 生成时会附带的一个初始化入口作用是把自己扫描到的全局指针注册到 SDK 内部。不同生成器导出函数名不完全一样以你拿到的 SDK.hpp 实际声明为准。上面代码验证的是“GObjects 定位 UWorld 访问 TArray 长度读取”这条链路是否通畅凡是这三步能跑通说明 SDK 基址、对象指针和数组布局都没问题。4.2 遍历 GObjects 并过滤指定类GObjects 是全局对象数组遍历它是分析运行时对象格局的基本功。一个典型的遍历加过滤的写法如下for (int32_t i 0; i SDK::GObjects-GetNum(); i) { UObject* Obj SDK::GObjects-GetByIndex(i); if (!Obj || !Obj-IsA(UPlayer::StaticClass())) continue; printf([%05d] %s\n, i, Obj-GetFullName().c_str()); }GetByIndex的作用是从对象数组里按索引取对象IsA判断继承关系UPlayer::StaticClass()来自 SDK.hpp 的 UClass 声明。这里的GetFullName()返回的字符串形如PlayerController /Game/Map/Player_0可以用来快速核对当前类名与对象实例是否对应。之所以建议先打印全名而不是直接提取属性是因为全名组合了 ClassName、Outer 和 Name一旦 SDK 的 Outer 链有问题这里就会立刻暴露出来。4.3 调用 UFunctionProcessEvent 的参数与返回方式当你要触发一个游戏内技能或交互逻辑时直接调用成员函数并不可靠正确路径是拿到 UFunction 并调用 ProcessEvent。先把参数结构体归零再填充需要的字段最后调用static UFunction* FireFunc UObject::FindObjectUFunction(LFunction ShadowTrackerExtra.WeaponAnim.Fire); if (!FireFunc) return; FStartFire_Parms Params {}; Params.Target SomeActor; Params.bFromFirstPerson true; SomeActor-ProcessEvent(FireFunc, Params);这里有两个关键点。第一个是FStartFire_Parms的字节布局必须与 SDK.hpp 里定义的完全一致不能为了省事用简化结构体第二个是返回值只在ProcessEvent的执行结果里体现如果函数有返回值它通常被放在 Params 结构体的最后一个字段上按引用写入。FindObject的作用是用完整路径在对象表里查找 UFunction如果没找到要判断是名字路径拼错还是函数未生成到 SDK 中前者查ObjectsDump.txt后者查Generator.log。注意调用游戏内函数前先确认当前环境是否允许这样做。如果目标是分析专用测试包或离线模拟器那没问题线上游戏环境做这类操作轻则封号重则涉及破坏计算机信息系统相关法律问题。4.4 常见崩溃为什么 ProcessEvent 一调用就栈溢出ProcessEvent 调用崩溃九成都是参数结构体尺寸不对。UE4 的 ProcessEvent 内部会分配一块栈帧按参数结构体大小来拷贝如果你传的结构体比真实的小后续的寄存器或栈取值就越界如果比真实的大多出来的部分可能覆盖返回地址。调试时可以用 Visual Studio 的异常设置打开“访问冲突”断点然后查看崩溃栈上Params地址附近的十六进制内容对照 SDK.hpp 中该 Params 的 Size 值。另外要确认你拿到的 UFunction 指针是不是空的。游戏更新后字符串路径中 ShadowTrackerExtra 前缀可能变化FindObject 找不到就会静默返回 nullptr不检查直接调用就是空指针崩溃。我给项目加函数调用之前总会先在 GObjects 遍历里把目标 UFunction 完整路径打印一遍确认名字和路径都对再写正式调用代码。5. 我在 1.2.0 版本上实际踩过的一组坑5.1 Generator.log 里的 WARNING 要按严重程度分类看很多人拿到 SDK 看到几十条 WARNING 就慌了其实要按类型分。我见过比较多的是这几种警告内容摘要实际影响建议处理属性指针解析失败该属性偏移为 0读取时拿到空数据不要直接访问该属性改用对象报告或其它途径FName 索引越界NamesDump 中没有对应字符串如果只出现在个别 UI 图标资源上忽略不影响核心逻辑重复的类名被重命名SDK.hpp 中类名可能与游戏内不完全一致以 Generator.log 里重命名后的名字为准否则 FindObject 失败UFunction 函数参数解析失败参数结构体缺失ProcessEvent 无法调用跳过该函数或者手动在 SDK.hpp 里补全并自测判断逻辑很简单核心游戏模块的警告优先排查UI 和动画模块的相对影响小。例如 PUBGM SDK 里 TweenMaker 和 UMG 的文件生成警告多数不影响世界观对象的遍历但 Engine 的 GObjects 相关警告出现了就要留意。5.2 ObjectsDump 与 SDK.hpp 里的对象对不上这种情况大多是生成时游戏内场景还在加载比如你在匹配大厅里跑生成器这时候场景 Actor 还没全部创建ObjectsDump 里自然没有对局中的物品种类。解决方式不是改生成器参数而是把游戏停在你要分析的固定场景再重新 dump。如果只在冻结场景下生成的 SDK对动态加载的 Asset 支持会弱一些分析时要清楚这个来源限制。5.3 游戏更新后哪些东西最容易失效UE4 游戏每次更新属性偏移的变动几乎不可避免。1.2.0 是 PUBGM 在 ShadowTrackerExtra 代号下相对早期的 SDK 版本后续版本里 UWorld、APlayerController 这些大类的偏移经常整体变化。失效优先级可以这样看先失效的是 UFunction 的字符串路径然后是 UProperty 的偏移量最后才是类的继承结构。所以拿到新版本的 dump我会做一次快速注册具体做法是比较两次 SDK.hpp 中 UWorld 类的开头注释偏移如果偏移变了就不要继续沿用旧 SDK 里的 World-OwningGameInstance 访问链。5.4 一个实用的验证技巧文本归一化 diffGit 不熟没关系用命令行就能完成新旧 SDK 的差异评估。关键是不能直接 diff 整个文件因为生成器的小改动会导致大量行号变化比较结果没法看。我会用 grep 过滤出类声明、偏移注释、结构体大小注释再做统一 diffdiff (grep -E class |struct |// 0x SDK_v1.1.hpp) \ (grep -E class |struct |// 0x SDK_v1.2.0.hpp) sdk_changes.diff这个命令的作用是只保留类名、结构体名和带偏移注释的行再对比两版差异。( )是进程替换能让两侧内容像文件一样参与 diff不会产生临时文件。输出里的// 0x偏移行的增减就是这次更新影响到的具体属性如果某个之前一直在用的类和函数对应的偏移行大量出现就要针对它做回归验证。这是我拿到任何新生成 SDK 后做的第一件事比直接打开头文件翻高效得多。本文还有配套的精品资源点击获取