首页
社区
课程
招聘
[原创] UnrealEngine4恢复符号过程分析
发表于: 2023-8-3 13:28 16650

[原创] UnrealEngine4恢复符号过程分析

2023-8-3 13:28
16650

之前看到过很多游戏都是用虚幻引擎写的,就萌生了研究它的兴趣,但是发现网上关于如何Dump出SDK没有很好的资料,正好研究具体原理并整理一下。本文基于UnrealEngine4.27版本,对于4.25之前的版本需要针对UProperty和FProperty的变动部分进行修改。

U/FProperty: 表示 C++ 中的属性,即类或结构体的成员变量。
UEnum: 表示 C++ 中的枚举,内部保存了一个 TMap,维护了 Name、Value、Index 三大信息的对应关系。
UStruct: 表示 C++ 中的复杂类型,包含函数、类、结构体三种。内部维护了所表示类型的所有 UProperty。
UFunction: 表示 C++ 中的函数,内部维护了函数指针、栈帧、参数返回值信息,还提供了反射执行所表示函数的方法。
UClass:表示 C++ 中的类,在 UStruct 的基础上扩展了 UFunction 的保存与查找方法。
UScriptStruct: 表示 C++ 中的结构体,只是在 UStruct 的基础上增加了一些工具方法而已。

可以发现即使能够直接通过UObject::FName Name来获取类/函数/属性的名称,解析FName同样不是一件简单的事情。因为FName实际并未存储实际的字符串,而是存储了字符串的引用。在虚幻引擎中,FName为静态字符串类型,类似于存储与.rodata段的const char*字符串。在FName中实际上存储的是一个Index,即FNameEntryId。
FName

通过FNameEntryId即可从FNamePool GName中查找到实际的字符串值。GName的具体逆向方法这里不做详细介绍,这里介绍如何通过GName和FNameEntryId来访问具体的字符串。查看FNamePool的定义,可以发现其中即FNameEntryAllocator,而其中存在这一个指针数组,数组中存储着指向FNameEntry的指针,名为Blocks。
FNamePool

查看FNameEntry可以发现其中存储了字符串本体,因此我们根据FNamePool和FName中的FNameEntryId查表即可获取到字符串本体。
FNameEntry

查表方式借鉴虚幻引擎FNameEntryAllocator下的Resolve方法,FNameEntryHandle其实是通过移位分裂FNameEntryId的方式获得的,代码如下。
FNameEntryAllocator
FNameEntryHandle

至此便可以通过FNamePool GName和对应的FName获得对象的名称了,具体的实现代码请查看源码Dump.cpp下的GetNameByIndex函数。
src

在了解了解析UClass等反射数据的方法和通过FName获取具体字符串的操作后,现在只需要获取到相应的类对象即可进行dump了。

#define CLASS_TYPENAME "Class /Script/CoreUObject.Class"
#define ENUM_TYPENAME "Class /Script/CoreUObject.Enum"
#define SCRIPTSTRUCT_TYPENAME "Class /Script/CoreUObject.ScriptStruct"

虽然上文将我们感兴趣的UObject分为了三类,但实际上需要处理的只有两类:UEnum和UStruct,因为UClass和UScriptStruct皆继承自UStruct,且仅通过UStruct结构就可提取出类的属性和方法等所有生成Sdk的信息。

U/FProperty: 表示 C++ 中的属性,即类或结构体的成员变量。
UEnum: 表示 C++ 中的枚举,内部保存了一个 TMap,维护了 Name、Value、Index 三大信息的对应关系。
UStruct: 表示 C++ 中的复杂类型,包含函数、类、结构体三种。内部维护了所表示类型的所有 UProperty。
UFunction: 表示 C++ 中的函数,内部维护了函数指针、栈帧、参数返回值信息,还提供了反射执行所表示函数的方法。
UClass:表示 C++ 中的类,在 UStruct 的基础上扩展了 UFunction 的保存与查找方法。
UScriptStruct: 表示 C++ 中的结构体,只是在 UStruct 的基础上增加了一些工具方法而已。

#define CLASS_TYPENAME "Class /Script/CoreUObject.Class"
#define ENUM_TYPENAME "Class /Script/CoreUObject.Enum"
#define SCRIPTSTRUCT_TYPENAME "Class /Script/CoreUObject.ScriptStruct"

  • 虚幻引擎是目前主流两大引擎之一,该引擎采用C++实现,相比于Unity3d,适用于开发大型3A游戏。在逆向方面,该引擎由于使用了C++来实现,因此本身是不会存在虚拟机的概念的,这点与Unity不同,因此理论上在逆向方面比Unity更加困难。但是虚幻引擎为了实现对象的管理,垃圾收集系统以及一些动态特性,采用了一种编译时期的反射系统。
  • 该反射机制在编译过程中发挥作用,预处理源码中需要参与反射机制的类,并为其生成反射数据,其中包括了类名,类的方法,类中的属性等相关信息,并将这些信息包装在数据结构中保存下来,这给了我们可乘之机,能够借助这些信息恢复出原有的结构体和类以及函数名等符号信息。
  • 在本节中,将给出虚幻引擎反射机制中比较重要的集中数据类型/类。虚幻引擎中所有对象的类都是UObject的派生类,其中为了描述反射获得的对象,虚幻引擎采用UField来实现(类似于Java,同样继承自UObject)。UField派生出了一系列的类用于描述数据结构。具体如下所示:

    U/FProperty: 表示 C++ 中的属性,即类或结构体的成员变量。
    UEnum: 表示 C++ 中的枚举,内部保存了一个 TMap,维护了 Name、Value、Index 三大信息的对应关系。
    UStruct: 表示 C++ 中的复杂类型,包含函数、类、结构体三种。内部维护了所表示类型的所有 UProperty。
    UFunction: 表示 C++ 中的函数,内部维护了函数指针、栈帧、参数返回值信息,还提供了反射执行所表示函数的方法。
    UClass:表示 C++ 中的类,在 UStruct 的基础上扩展了 UFunction 的保存与查找方法。
    UScriptStruct: 表示 C++ 中的结构体,只是在 UStruct 的基础上增加了一些工具方法而已。

  • 其中UStruct并不代表C++中的结构体,而是用于描述能够承载UProperty的数据结构,例如UClass,UScriptStruct中的属性以及UFunction中的函数参数。具体的继承图如下图所示:
  • 值得注意的是,上图描述的是虚幻引擎4.25之后版本的继承关系,4.25之前则是使用UProperty来描述属性,而UProperty继承自UField,这样带来了大量的花销,4.25之后的版本进行了效率优化,使用了UField的子集FField来描述Property,且不用继承自UObject,这样大幅度减少了属性对象的占用,在下文中将使用FProperty来指代UProperty,两者意思实际上是一样的。
  • 在本节中将给出上述数据结构/类的具体定义,代码来源自虚幻引擎官方代码,并进行了一系列的修正,从而正确的访问对应的数据。
    UOObject: 首先是最重要的UObject结构,其中由于虚幻引擎中存在Overide的情况,因此添加了一个VTable来描述虚表。
    UObject
    UField: UField较为简单,相比于UObject多了一个迭代指针,往往用于UClass等数据结构迭其下的方法。
    UField
    UEnum: UEnum维护了一个映射表,Dump时只需要关注这个表即可。
    Unum
    UStruct: 主要关注Children,SuperStruct和ChildProperties
    UStruct
    UFunction: 主要关注FunctionFlags,NumParms和Func,函数参数通过UStruct::ChildProperties来访问。
    UFunction
    FProperty: 该类继承自FField,主要用于描述结构体中的变量或者类中的属性。由于FField,FFieldClass和FProperty的唯一作用仅仅用于描述属性,因此这里一起放出。
    FProperty
  • 可以发现即使能够直接通过UObject::FName Name来获取类/函数/属性的名称,解析FName同样不是一件简单的事情。因为FName实际并未存储实际的字符串,而是存储了字符串的引用。在虚幻引擎中,FName为静态字符串类型,类似于存储与.rodata段的const char*字符串。在FName中实际上存储的是一个Index,即FNameEntryId。
    FName

  • 通过FNameEntryId即可从FNamePool GName中查找到实际的字符串值。GName的具体逆向方法这里不做详细介绍,这里介绍如何通过GName和FNameEntryId来访问具体的字符串。查看FNamePool的定义,可以发现其中即FNameEntryAllocator,而其中存在这一个指针数组,数组中存储着指向FNameEntry的指针,名为Blocks。
    FNamePool

  • 查看FNameEntry可以发现其中存储了字符串本体,因此我们根据FNamePool和FName中的FNameEntryId查表即可获取到字符串本体。
    FNameEntry

  • 查表方式借鉴虚幻引擎FNameEntryAllocator下的Resolve方法,FNameEntryHandle其实是通过移位分裂FNameEntryId的方式获得的,代码如下。
    FNameEntryAllocator
    FNameEntryHandle

  • 至此便可以通过FNamePool GName和对应的FName获得对象的名称了,具体的实现代码请查看源码Dump.cpp下的GetNameByIndex函数。
    src

  • 虚幻引擎中存在一个全局数组保存了所有的UObject,因此可以从这个变量入手,该变量是FUObjectArray类型,具体如何通过逆向找到它的技巧这里不做说明。
    GUObjectArray
  • FUObjectArray类比较复杂,其中包含了一个TUObjectArray ObjObjects.而TUObjectArray实际上就是FChunkedFixedUObjectArray类型,其下含有一个指针Objects,不出所料就是指向UObject的存储内存区域,FUObjectItem下的Object即UObject对象。通过FChunkedFixedUObjectArray下的NumElements即可获取当前UObject对象的数量。
    FUObjectArray
  • 然而UObject的储存并不是平坦的,而是分块的,这点从Chunked可以看出来,同样的可以查看虚幻引擎的源码(UObjectArray.h)来得知如何访问FChunkedFixedUObjectArray来获取UObject*。
    FChunkedFixedUObjectArray
  • 借鉴一下就能写出通过Index获取对应UObject*的代码。如下。
    GetObjectByIndex
  • 通过GetObjectByIndex方法即可遍历当前存在的所有UObject了。
  • UObject中只有UClass,UScriptStruct和UEnum是需要我们关注并进行解析Dump的。因此需要有一种办法来判断UObject是否是这些类。前文提到了UObject都存在Name,通过以下代码即可完整的Dump出UObject的名称,包括其所属的类名等。代码先获取了当前Object的类,然后再解析其名称,再获取当前Object的完整名称,包括承载该类对象的名称。
    GetFullName
  • 通过该方法我们便可以用一种类似反射的方式找到对应的类对象了,其中包括表示Class/Enum/ScriptStruct的UClass。例如下面定义的三个字符串就代表了描述Class/Enum/ScriptStruct的类对象的名称。

    #define CLASS_TYPENAME "Class /Script/CoreUObject.Class"
    #define ENUM_TYPENAME "Class /Script/CoreUObject.Enum"
    #define SCRIPTSTRUCT_TYPENAME "Class /Script/CoreUObject.ScriptStruct"

  • 因此我们只需要在开始就保存下上述三个字符串对应的对象即可,通过以下代码。
    FindObject
    Init
  • 在获取了这三种数据结构所对应的UClass后,只需要判断给定UObject的父类是否是这三种数据结构所对应的类派生出来的即可。在前文中提到了UClass是可以通过SuperStruct来获取其超类的,因此判定思路非常简单,获取需要判定的UObject的类对象并沿着继承链通过SuperStruct向上迭代,如果和某个UClass一样,则说明该对象是UClass的或者其派生类的对象。具体代码实现如下所示。
    Isa
  • 接下来就是对不同的UObject进行实际的分类,并分别进行处理。
  • 对于Enum的处理较为简单,只需要解析UEnum结构体,从中提取EnumName和Value的映射关系即可。值得注意的是Enum和Value以Pair的形式存储到TArray种。TPair的内存结构较为简单,就是两个模板类直接组合。TArray则是存在一个指针和两个描述数组大小的变量,指针指向一个连续的内存区域,用于存储ElementType类型的数据,这里也即TPair。解析TArray和TPair较为简单,不做赘述。
    SData
  • 下面代码通过访问UEnum下的Names变量,进行遍历,访问其中所存在的TPair,从而提取出对应的EnumName和Value。
    EnumProc
  • UStruct比UEnum更加复杂,其中存在着两种需要处理的数据leixing,分别为方法(UFunction)和属性(FProperty)。在UStruct下分别通过Children和ChildProperties可以开始访问,其中Children是UField,后者是FField,但实际上是一样的,都可以通过其中的Next指针获得下一个方法或者属性,类似于链表的组织方式。下面的代码给出了访问Property的方式,访问Function也是一样的。
    GetFproperty
  • 处理FProperty的方法无外乎直接从结构体中提取相应的数据,主要为属性本身的类型名,名称和偏移,这里不做赘述。
    Process1
  • UFunction使用了FProperty来代表参数,通过UFunction::ChildProperties来访问(其中包含函数返回值类型,需要单独处理),因此解析函数的参数同样需要上文的ProcessProperty函数,其他的数据直接从UFunction中读取即可,包括函数的地址,这些数据都能帮助我们恢复符号。具体代码如下。
    ProcssFunction
  • 到这里已经能够获取所有我们关心的对象并进行解析了,具体的SDK逻辑也就是一些字符串拼接的操作,可以直接查看源码,这里给出效果图。
    pic
  • 虚幻引擎是目前主流两大引擎之一,该引擎采用C++实现,相比于Unity3d,适用于开发大型3A游戏。在逆向方面,该引擎由于使用了C++来实现,因此本身是不会存在虚拟机的概念的,这点与Unity不同,因此理论上在逆向方面比Unity更加困难。但是虚幻引擎为了实现对象的管理,垃圾收集系统以及一些动态特性,采用了一种编译时期的反射系统。
  • 该反射机制在编译过程中发挥作用,预处理源码中需要参与反射机制的类,并为其生成反射数据,其中包括了类名,类的方法,类中的属性等相关信息,并将这些信息包装在数据结构中保存下来,这给了我们可乘之机,能够借助这些信息恢复出原有的结构体和类以及函数名等符号信息。
  • 在本节中,将给出虚幻引擎反射机制中比较重要的集中数据类型/类。虚幻引擎中所有对象的类都是UObject的派生类,其中为了描述反射获得的对象,虚幻引擎采用UField来实现(类似于Java,同样继承自UObject)。UField派生出了一系列的类用于描述数据结构。具体如下所示:

    U/FProperty: 表示 C++ 中的属性,即类或结构体的成员变量。
    UEnum: 表示 C++ 中的枚举,内部保存了一个 TMap,维护了 Name、Value、Index 三大信息的对应关系。
    UStruct: 表示 C++ 中的复杂类型,包含函数、类、结构体三种。内部维护了所表示类型的所有 UProperty。
    UFunction: 表示 C++ 中的函数,内部维护了函数指针、栈帧、参数返回值信息,还提供了反射执行所表示函数的方法。
    UClass:表示 C++ 中的类,在 UStruct 的基础上扩展了 UFunction 的保存与查找方法。
    UScriptStruct: 表示 C++ 中的结构体,只是在 UStruct 的基础上增加了一些工具方法而已。


  • [招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

    最后于 2023-8-4 09:20 被R1mao编辑 ,原因: 更新附件
    上传的附件:
    收藏
    免费 13
    支持
    分享
    最新回复 (17)
    雪    币: 99
    活跃值: (398)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    2
    感谢大佬 看雪因你而精彩
    2023-8-3 15:57
    1
    雪    币: 3570
    活跃值: (4709)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    3
    不错 很详细了
    2023-8-3 16:16
    0
    雪    币: 9014
    活跃值: (6240)
    能力值: ( LV3,RANK:20 )
    在线值:
    发帖
    回帖
    粉丝
    4
    感谢大佬分享
    2023-8-3 18:25
    0
    雪    币: 562
    活跃值: (4342)
    能力值: ( LV3,RANK:30 )
    在线值:
    发帖
    回帖
    粉丝
    5
    感谢大佬
    2023-8-3 23:33
    0
    雪    币: 202
    能力值: ( LV1,RANK:0 )
    在线值:
    发帖
    回帖
    粉丝
    6
    没记错的话好像只有netvar才能dump
    2023-8-4 06:32
    0
    雪    币: 3114
    活跃值: (3279)
    能力值: ( LV9,RANK:150 )
    在线值:
    发帖
    回帖
    粉丝
    7
    veryluckko 没记错的话好像只有netvar才能dump
    理论上是被UHT处理过的类和属性才能Dump
    2023-8-4 10:12
    0
    雪    币: 3317
    活跃值: (2512)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    8
    好好的教程。网上真的很少说这个的。谢谢了
    2023-8-4 11:14
    0
    雪    币: 3059
    活跃值: (30876)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    9
    感谢分享
    2023-8-4 11:18
    1
    雪    币: 4883
    活跃值: (18890)
    能力值: ( LV13,RANK:317 )
    在线值:
    发帖
    回帖
    粉丝
    10
    感谢分享
    2023-8-5 22:34
    0
    雪    币: 886
    活跃值: (2310)
    能力值: ( LV4,RANK:52 )
    在线值:
    发帖
    回帖
    粉丝
    11
    感谢分享
    2023-8-6 15:43
    0
    雪    币: 60
    能力值: ( LV1,RANK:0 )
    在线值:
    发帖
    回帖
    粉丝
    12
    非常感谢大佬的分享,网上UE资料感觉好少。资料真的宝贵。感谢!
    2023-8-8 15:57
    0
    雪    币: 3836
    活跃值: (4142)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    13
    感谢分享
    2023-8-26 20:27
    0
    雪    币: 3114
    活跃值: (3279)
    能力值: ( LV9,RANK:150 )
    在线值:
    发帖
    回帖
    粉丝
    14
    图怎么挂了?
    2023-9-4 20:18
    0
    雪    币: 500
    活跃值: (915)
    能力值: ( LV3,RANK:20 )
    在线值:
    发帖
    回帖
    粉丝
    15
    感谢大佬!666
    2023-9-13 09:57
    0
    雪    币: 198
    能力值: ( LV1,RANK:0 )
    在线值:
    发帖
    回帖
    粉丝
    16
    666
    2023-11-23 16:46
    0
    雪    币: 401
    活跃值: (1125)
    能力值: ( LV3,RANK:20 )
    在线值:
    发帖
    回帖
    粉丝
    17
    图片崩了?
    2023-11-29 12:04
    0
    雪    币: 223
    能力值: ( LV1,RANK:0 )
    在线值:
    发帖
    回帖
    粉丝
    18
    讲的真的很好很仔细
    2024-8-16 22:37
    0
    游客
    登录 | 注册 方可回帖
    返回
    //