UE4.27 SDK Dump
近期学了学UE4相关知识,并且自己动手写了写SDK dump,特此记录一下。相关代码已传至github,使用了Frida与TypeScript,解决了Frida在Js下代码挤压在同一个文件的问题,同时使用C++去写结构体并得到偏移,能够更好的维护。
命名约定
在阅读代码之前,就必须去了解一下UE4的命名约定,具体的自己去查看官网文档,下面是一些基本需要知道的:
- 模版类以T作为前缀,比如TArray,TMap,TSet UObject派生类都以U前缀
- AActor派生类都以A前缀
- SWidget派生类都以S前缀
- 抽象接口以I前缀
- 枚举以E开头
- bool变量以b前缀,如bPendingDestruction
- 其他的大部分以F开头,如FString,FName
- typedef的以原型名前缀为准,如typedef TArray FArrayOfMyTypes;
关键类
UWorld
在UE4引擎中全局定义了UWorldProxy对象,此对象在Engine\Source\Runtime\Engine\Classes\Engine\World.h定义,这个类中有一个变量UWorld* World;
在官网定义中可以看到,UWorld是地图或沙盒的顶级对象,其中会有Actor等信息

UobjectBase
根据继承关系找到最顶级的类UobjectBase,此类在Engine\Source\Runtime\CoreUObject\Public\UObject\UObjectBase.h定义,在这个类中有一个成员变量FName,由注释可知此变量代表对象的名称。

其字段含义如下
1 2 3 4 5 6 7 8 9 | class UObjectBase{public: EObjectFlags ObjectFlags; //对象属性 int32_t InternalIndex; //对象GUObjectArray序号 UClass* ClassPrivate; //对象的类 FName NamePrivate; //对象的名字 UObject* OuterPrivate; //对象所在UPackage}; |
这里值得一提的是,普遍认为UObject是是UE4中所有对象的基类,但是从这里可以发现真正的基类是UObjectBase。认为UObject是基类也并无道理,因为继承自UObjectBase的UObject没有新增任何成员,从VS提供的内存视图可知,下文也会延续这一说法。

FName
正是因为UE4中所有对象都继承自UObject,而UObject又有成员FName提供名字,因此UE4的反射系统才能方便得知一个对象的名字,甚至是字段、函数参数等名字。这也给逆向工作提供了可乘之机,那么就不得不研究一下UE4是怎么通过这个FName得到的名字的。
FName在Engine\Source\Runtime\Core\Public\UObject\NameTypes.h定义,这个类中有一个成员函数ToString(),还存储了字符串索引FNameEntryId ComparisonIndex;

见名思义,调用FName::ToString即可得到名字,返回一个FString类型。
FString
查看FString类,在Engine\Source\Runtime\Core\Public\Containers\UnrealString.h定义

可见字符串相关数据存储在TArray容器中,容器类型是TCHAR,而TCHAR类型就是wchar_t类型。
TArray
TArray是一个模板类,定义也比较简单
1 2 3 4 5 6 7 8 | template <typename ElementType>class TArray{public: ElementType *Allocator; int32_t ArrayNum; int32_t ArrayMax;}; |
由此逻辑已经清晰,要在UE4中获得一个对象的名称,即该对象的字符串,只需要访问UObjectBase的成员变量NamePrivate,调用该变量的ToString函数,该函数返回一个FSting类型,这个类型里面就有所要字符串的地址。
以上过程是正向开发的调用过程,但在逆向的时候情况就不会有这么简单,当然也可以直接查找调用ToString函数偏移来得到对象名字,这一步可以通过IDA等手段完成。必要地,还是要看一下ToString函数是怎么实现的。这一点会在后文进行。
FNamePool
此类型在Engine\Source\Runtime\Core\Private\UObject\UnrealNames.cpp中定义,这里只关注第一个成员
1 2 3 4 5 6 | class FNamePool{public: FNameEntryAllocator Entries; …………}; |
这个类型定义了一个全局静态变量NamePoolData数组,这个NamePoolData就是常说的GName,因为它就存储了全局的字符串,相当于一个字符串池,后续的ToString函数也高度依赖这个数组。
FNameEntryAllocator
此类型在Engine\Source\Runtime\Core\Private\UObject\UnrealNames.cpp中定义,这类中有四个相当重要的成员变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class FNameEntryAllocator {public: enum { Stride = alignof(FNameEntry) }; enum { BlockSizeByte = Stride * FNameBlockOffsets }; mutable PVOID Lock; uint32_t CurrenBlock; uint32_t CurrentByteCursor; FNameEntry *Blocks[FNameMaxBlocks];}; |
FNameEntry
此类型在Engine\Source\Runtime\Core\Public\UObject\NameTypes.h定义,用于存储实际的字符串。FNameEntryHeader结构比较简单,后面会有提及。
1 2 3 4 5 6 7 8 9 10 | class FNameEntry{public: FNameEntryHeader Header; union { char AnsiName[NAME_SIZE]; wchar_t WideName[NAME_SIZE]; };}; |
FNameEntryId
此类型中,只有一个uint32的类型变量,其余均是函数,因此可简单理解为ComparisonIndex就是一个uint32的值

这个类主要就是存放字符串在GName中的索引
还有一些关键类会在后续边解析边提及。
解析ToString()
ToString
此函数实现于Engine\Source\Runtime\Core\Private\UObject\UnrealNames.cpp中

可知该函数先调用了GetDisplayNameEntry函数
GetDisplayNameEntry
GetDisplayNameEntry此函数实现就在ToString函数上方

可知此函数调用了三个函数,依次查看
GetNamePool()
此函数在Engine\Source\Runtime\Core\Private\UObject\UnrealNames.cpp中定义

这是一个单例模式的函数,即在So中存在唯一的全局静态变量NamePoolData,查看NamePoolData的类型FNamePool。
static bool bNamePoolInitialized;- 声明了一个静态布尔变量
bNamePoolInitialized,用于跟踪FNamePool实例是否已经被初始化。
- 声明了一个静态布尔变量
alignas(FNamePool) static uint8 NamePoolData[sizeof(FNamePool)];- 声明了一个静态数组
NamePoolData,其大小等于FNamePool类的大小,并使用alignas关键字确保这个数组按照FNamePool的内存对齐要求进行对齐。这个数组将用作FNamePool实例的内存空间。
- 声明了一个静态数组
- GetNamePool函数则是用于返回
FNamePool的实例,如果未被初始化则初始化。
这个模式保证了即使在多线程环境下,FNamePool也只会被初始化一次。它不使用C++11中的魔法静态(magic statics)或单例模式中的锁,以减少运行时的开销。通过这种方式,FNamePool类的实例在第一次调用GetNamePool时被创建,并在随后的调用中直接返回,避免了重复初始化。
GetDisplayIndex()
此函数实现于Engine\Source\Runtime\Core\Public\UObject\NameTypes.h

GetDisplayIndexFast()
进入GetDisplayIndexFast函数,此函数定义在相同目录下

查看相关宏定义,

由注释可知此宏只用于编辑器,而在实际运行中不会启用
因此GetDisplayIndexFast实际返回ComparisonIndex,ComparisonIndex有两处被定义,其中一处是作为FName的成员变量,FNameEntryId前文也有解析,它里面就只存储了uint32_t的值,作为字符串在GName中索引。

如果还没忘记的话,FName是基类UObject的成员之一,即可以通过NamePrivate获得此Index。
Resolve()
此函数定义于Engine\Source\Runtime\Core\Private\UObject\UnrealNames.cpp。在ToString函数中,Resolve参数是GetDisplayIndex函数的返回值,其类型是FNameEntryId

而查看Resolve的定义

其参数类型却是FNameEntryHandle,即说明FNameEntryHandle的构造函数存在相关转换
转至相关定义可以发现,确实有以FNameEntryId为参数的构造函数

这里FNameBlockOffsetBits在上方也有定义,是常量
1 2 3 4 | static constexpr uint32 FNameMaxBlockBits = 13;static constexpr uint32 FNameBlockOffsetBits = 16;static constexpr uint32 FNameMaxBlocks = 1 << FNameMaxBlockBits;static constexpr uint32 FNameBlockOffsets = 1 << FNameBlockOffsetBits; |
Entries.Resolve()
查看Resolve相关实现

这里的Blocks就是前文提及的重要四个变量之一,位于FNameEntrlAllocator下,FNameEntrlAllocator则是FNamePool唯一成员。
稍微总结一下,调用GetNamePool得到GName,调用GetDisplayIndex得到索引,调用Resolve在GName上根据索引找到一个FNameEntry类型变量,最后返回此变量。
FNameEntry
查看GetDisplayNameEntry函数返回类型FNameEntry

里同样有一个只在编译运行时才有效的宏,因此这里的成员只有FNameEntryHeader Header和一个联合体,这两个成员都会参与后续字符串转换。
FNameEntryHeader

这里就指示了是否为宽字符以及字符长度。
GetPlainNameString()
在调用完GetDisplayNameEntry就会接着调用GetPlainNameString,将FNameEntry类型转换为FString

如下是GetPlainNameString函数定义

这里的Header即为FNameEntryHeader 中的Header,他用于判断是否为宽字符。再返回对应的类型字符串。
GetUnterminatedName也仅仅只是返回FNameEntry中存储的字符串而已。

总结
ToString函数实现会调用两个关键函数GetDisplayNameEntry和GetPlainNameString,GetDisplayNameEntry又依次调用GetNamePool,GetDisplayIndex,Resolve三个函数。
GetNamePool函数主要用于初始化和返回FNamePool NamePoolData,其中FNamePool类的构造函数中会注册一大堆硬编码字符串,这里可通过搜索字符串并交叉引用的方式查找NamePoolData
GetDisplayIndex主要用于返回对象名称的索引,其返回类型是FNameEntryId,它的成员变量只有一个uint32 Value。此函数会继续调用GetDisplayIndexFast来得到ComparisonIndex,此变量也在FName中有存储。即可以通过UobjectBase下的FName NamePrivate成员来得到此Index。
Resolve根据返回的索引继续返回名称目录项,它的返回类型是FNameEntry,这个类有一个比特位域用于判断是否为宽字符,有一个联合体用于名称字符串。
最终调用Resolve返回类型FNameEntry的方法GetPlainNameString来返回FString字符串
利用Frida打印所有Actor的名字
UWorld解析
在实际运行环境中UWorld自己的第一个成员就是ULevel* PersistentLevel,可表示成这样
1 2 3 4 5 6 7 8 9 10 11 | class FNetworkNotify{ uint64_t VTable;};class UWorld : public UObject, public FNetworkNotify{public: /** Persistent level containing the world info, default brush and actors spawned during gameplay among other things */ ULevel *PersistentLevel; …………}; |
根据官方注释可知,PersistentLevel存储了world信息
ULevel
继续跟进ULevel类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class IInterface_AssetUserData{ uint64_t dummy;};template <typename ElementType>class TArray{public: ElementType *Allocator; int32_t ArrayNum; int32_t ArrayMax;};class ULevel : public UObject, public IInterface_AssetUserData{public: char dummyURL[104]; TArray<AActor *> Actors; …………}; |
此时就可以看到存储了所有Actors的数组,根据以上关系就可以轻松得知当前Level有多少个Actors,Actor地址都在哪,Frida代码如下(GWorld需要通过其他手段得到)
1 2 3 4 5 6 7 8 9 10 11 | var Level = GWorld.add(OFFSET.offset_UWorld_PersistentLevel).readPointer()console.log("Level :", Level)var Actors = Level.add(OFFSET.offset_ULevel_Actors).readPointer()console.log("Actors Array :", Actors)var Actors_Num = Level.add(OFFSET.offset_ULevel_Actors).add(8).readU32()console.log("Actors_num :", Actors_Num)var Actors_Max = Level.add(OFFSET.offset_ULevel_Actors).add(0xc).readU32()console.log("Actors_Max :", Actors_Max) |
在得到Actor之后,自然就是要从Actor开始解析,这也简单,因为Actor也是继承自UObject

而UObject中就存放了FName NamePrivate这一字段,通过他就可以得到名字,接下来要做的就是手动实现ToString这一函数。
首先需要获得FName NamePrivate这一字段,通过偏移很容易得到
1 2 | var FName_Offset = 0x18var FName = actor.add(FName_Offset); |
得到FName后就是实现那几个函数。这里稍微提一下为什么actor.add(FName_Offset)之后不用在readPointer,是因为UObject里面直接存放的就是FName这一结构,而不是FName*指针,所以不需要再readPointer
GetNamePool实现
这一步有两种方式,第一种通过IDA等方式先找到全局变量GName,第二种通过特征码等方式在内存中找到。如何找不在这里展开,默认已经找到。找到之后需要使用的是FNamePool的第一个成员FNameEntryAllocator中的Blocks,
这里有一个点需要注意, FNameEntryAllocator第一个成员是Lock,这android平台对应的是pthread_rwlock_t类型,windows平台对应的是SRWLOCK类型,
在32位安卓平台上此成员大小应该是0x28,在64位安卓平台上应该是0x38,那么从FNamePool得到Blocks就是
1 2 | var FNameEntryAllocator = GNames var Blocks = GNames.add(0x40) |
GetDisplayIndex实现
这一步很简单
1 | var ComparisonIndex = FName.add(0).readU32() |
Resolve实现
第一步是将FNameEntryId Id即ComparisonIndex转化为FNameEntryHandle

1 2 3 4 5 | var FNameBlockOffsetBits = 16var FNameBlockOffsets = 65536var Block = ComparisonIndex >> FNameBlockOffsetBitsvar Offset = ComparisonIndex & (FNameBlockOffsets - 1) |
前面已经得到FNameEntryAllocator的Blocks
1 2 | var Blocks = GNames.add(0x40)var FNameEntryAllocator = GNames |
Entries.Resolve实现
查看Resolve相关实现

得到FNameEntry
1 | var FNameEntry = Blocks.add(Block * 8).readPointer().add(Offset * 2) |
GetPlayNameString实现
得到FNameEntry后,就相当于得到了真正的字符串
1 2 3 4 5 6 7 8 9 10 | class FNameEntry{public: FNameEntryHeader Header; union { char AnsiName[NAME_SIZE]; wchar_t WideName[NAME_SIZE]; };}; |
FNameEntryHeader只是存储了字符串是否为宽字符,长度为多少而已,真正字符串就在后面的AnsiName或WideName中
1 2 3 4 5 6 7 | var FNameEntryHeader = FNameEntry.readU16()var isWide = FNameEntryHeader & 1var Len = FNameEntryHeader >> 6 //最大长度就是1024,所以需要右移6位if (0 == isWide) { console.log(`\x1b[32m[+] actor ${actor}: ${FNameEntry.add(2).readCString(Len)}\x1b[0m`)} |
整合一下就是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | export function dumpActorName(GWorld: NativePointer, GNames: NativePointer) { var Level = GWorld.add(OFFSET.offset_UWorld_PersistentLevel).readPointer() console.log("Level :", Level) var Actors = Level.add(OFFSET.offset_ULevel_Actors).readPointer() console.log("Actors Array :", Actors) var Actors_Num = Level.add(OFFSET.offset_ULevel_Actors).add(8).readU32() console.log("Actors_num :", Actors_Num) var Actors_Max = Level.add(OFFSET.offset_ULevel_Actors).add(0xc).readU32() console.log("Actors_Max :", Actors_Max) for (var index = 0; index < Actors_Num; index++) { var actor = Actors.add(index * 8).readPointer() if (actor == NULL) { continue; } //console.log("actor", actor) //通过角色actor获取其成员变量FName var FNameEntryAllocator = GNames var FName_Offset = 0x18 var FName = actor.add(FName_Offset); var ComparisonIndex = FName.add(0).readU32() // console.log("ComparisonIndex:", ComparisonIndex); var FNameBlockOffsetBits = 16 var FNameBlockOffsets = 65536 var Block = ComparisonIndex >> FNameBlockOffsetBits var Offset = ComparisonIndex & (FNameBlockOffsets - 1) var Blocks_Offset = 0x40 var Blocks = FNameEntryAllocator.add(Blocks_Offset) var FNameEntry = Blocks.add(Block * 8).readPointer().add(Offset * 2) // console.log("FNameEntry:", FNameEntry) var FNameEntryHeader = FNameEntry.readU16() var isWide = FNameEntryHeader & 1 var Len = FNameEntryHeader >> 6 if (0 == isWide) { console.log(`\x1b[32m[+] actor ${actor}: ${FNameEntry.add(2).readCString(Len)}\x1b[0m`) }} |
效果

FUObjectArray
名字解析只是最基本的,复杂的还是成员字段、函数签名等信息。在UE4中,还有一个关键全局变量GUObjectArray,这个全局变量存储了当前所有的对象

FUObjectArray部分定义如下
1 2 3 4 5 6 7 8 9 10 | class FUObjectArray{public: uint32_t ObjFirstGCIndex; uint32_t ObjLastNonGCIndex; uint32_t MaxObjectsNotConsideredByGC; bool OpenForDisregardForGC; TUObjectArray TUObjectArray; …………}; |
这里面重要的就是TUObjectArray TUObjectArray,TUObjectArray类型就是FChunkedFixedUObjectArray

TUObjectArray /FChunkedFixedUObjectArray
定义如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class FChunkedFixedUObjectArray{ enum { NumElementsPerChunk = 64 * 1024, }; /** Master table to chunks of pointers **/ FUObjectItem** Objects; /** If requested, a contiguous memory where all objects are allocated **/ FUObjectItem* PreAllocatedObjects; /** Maximum number of elements **/ int32 MaxElements; /** Number of elements we currently have **/ int32 NumElements; /** Maximum number of chunks **/ int32 MaxChunks; /** Number of chunks we currently have **/ int32 NumChunks;} |
显然就存储了当前所有最大元素数量,当前存活的元素数量等信息
FUObjectItem就是一个一个具体的Object,定义也简单
1 2 3 4 5 6 7 8 9 | class FUObjectItem{ class UObject *Object; uint32_t Flags; // UObject Owner Cluster Index uint32_t ClusterRootIndex; // Weak Object Pointer Serial number associated with the object uint32_t SerialNumber;}; |
从GUOjbectArray得到MaxElements就是
1 2 3 4 5 | export function getObjectCount(GUObjectArray: NativePointer) { var GUObjectElementCount = GUObjectArray.add(OFFSET.GAME_FUObjectArray_TUObjectArray_OFFSET).add(OFFSET.GAME_TUObjectArray_NumElements_OFFSET).readU32() console.log(`\x1b[32m[+] GUObjectElementCount: ${GUObjectElementCount}\x1b[0m`) return GUObjectElementCount;} |
然而UObject的储存并不是平坦的,而是分块的,这点从Chunked可以看出来,同样的可以查看虚幻引擎的源码(UObjectArray.h)来得知如何访问FChunkedFixedUObjectArray来获取UObject*
1 2 3 4 5 6 7 | FORCEINLINE_DEBUGGABLE FUObjectItem const* GetObjectPtr(int32 Index) const TSAN_SAFE{ const int32 ChunkIndex = Index / NumElementsPerChunk; const int32 WithinChunkIndex = Index % NumElementsPerChunk; FUObjectItem* Chunk = Objects[ChunkIndex]; return Chunk + WithinChunkIndex; } |
转成Frida
1 2 3 4 5 6 7 8 9 10 11 12 | export function getUObjectBaseObjectFromId(GUObjectArray: NativePointer, index: number): UObjectPointer { var FUObjectItem = GUObjectArray.add(OFFSET.GAME_FUObjectArray_TUObjectArray_OFFSET).readPointer();//定位到第一个chunk var chunkIndex = Math.floor(index / 0x10000) * Process.pointerSize; var WithinChunkIndex = (index % 0x10000) * OFFSET.GAME_FUOBJECT_ITEM_SIZE; var chunk = FUObjectItem.add(chunkIndex); var FUObjectItemObjects = chunk.readPointer();//定位到当前chunk的第一个位置 var UObjectBaseObject = FUObjectItemObjects.add(WithinChunkIndex).readPointer(); //这里直接返回Uobject return UObjectBaseObject;} |
在得到UObejct后就可以进一步开始解析,比如先解析得到所属类,这个对象名字。解析FName应该是很简单的事了·。
``is
/**
*
* @param obj
* @returns UClass* Returns the UClass that defines the fields of this object
*/
getClass: function (obj: UObjectPointer) {
var classPrivate = obj.add(OFFSET.offset_UObject_ClassPrivate).readPointer(); //得到所属类
// console.log(classPrivate: ${classPrivate});
return classPrivate;
},
1 2 3 4 5 6 7 8 9 10 11 12 13 | /** * * @param GName * @param obj * @returns string Returns the logical name of this object */getName: function (GName: NativePointer, obj: UObjectPointer) { if (this.isValid(obj)) { return getFNameFromID(GName, this.getNameId(obj)); } else { return "None"; }}, |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 在进一步之前,还需要了解更多的关键类。## dumpSdk所需类### UField它继承自`UObject`,但仅仅只多一个`UField* Next`迭代指针,这个类主要是用于`UClass`等迭代方法### FField在UE4.25以后,使用`UField`子集`FField`来描述属性信息,且`FField`没有继承任何一个类,这样大幅度减少了属性对象的占用。```cppclass FFieldClass{public: FName Name; // uint64_t Id; uint64_t CastFlags; uint64_t ClassFlags; FFieldClass *SuperClass; FField *DefaultObject;};class FFieldVariant{public: union FFieldObjectUnion { FField *Field; UObject *Object; } Container; uint64_t bIsUObject;};#pragma pack(4)class FField{public: uint64_t VTable; FFieldClass *ClassPrivate; // 类名,用于区分FProperty类型 FFieldVariant Owner; FField *Next; // 指向下一个FField FName NamePrivate; // 属性名称 uint32_t FlagsPrivate;};#pragma pack() |
FProperty
这个类继承FField,有更详细的信息描述
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class TEnumAsByte{public: uint8_t Value;};#pragma pack(8)class FProperty : public FField{public: int32_t ArrayDim; // 属性的维数 int32_t ElementSize; // 属性元素大小(属性整体大小为ElementSize*ArrayDim) EPropertyFlags PropertyFlags; // 属性Flag uint16_t RepIndex; TEnumAsByte BlueprintReplicationCondition; int32_t Offset_Internal; // 属性在结构体中的偏移 FName RepNotifyFunc; FProperty *PropertyLinkNext; FProperty *NextRef; FProperty *DestructorLinkNext; FProperty *PostConstructLinkNext;};#pragma pack() |
UStruct
这个类比较重要,记录了成员、函数信息的指针,它继承于UField
主要关注Children,SuperStruct和ChildProperties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class UStruct : public UField, private FStructBaseChain{public: UStruct *SuperStruct; // 该结构体的超类 UField *Children; // 结构体中的方法 FField *ChildProperties; // 结构体中的属性 int32_t PropertiesSize; // 属性占用大小 int32_t MinAlignment; TArray<PVOID> Script; FProperty *PropertyLink; FProperty *RefLink; FProperty *DestructorLink; FProperty *PostConstructLink; TArray<PVOID> ScriptAndPropertyObjectReferences; TArray<PVOID> *UnresolvedScriptProperties; PVOID unknown;}; |

UClass
这个类就是UObject中的成员,它继承UStruct,新增很多成员,不过实际使用的还是UStruct中已经定义的成员

回顾一下,从GUObjectArray可以根据索引一步一步得到UObject,而UObject中又有UClass成员,它由继承自UStruct,那么从这个成员就可以得到UStruct,进一步得到 UField *Children; // 结构体中的方法和
FField *ChildProperties; // 结构体中的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | export var UStruct = { /** * Struct this inherits from, may be null * @param structz * @returns UStruct* * */ getSuperClass: function (structz: UStructPointer) {//UStruct* // console.log(`UStruct.getSuperClass structz: ${structz}`); return structz.add(OFFSET.offset_UStruct_SuperStruct).readPointer() }, /** * * @param structz * @returns UField* Children:该结构体的方法 * */ getChildren: function (structz: UStructPointer) {//UField* // console.log(`UStruct.getChildren structz: ${structz}`); return structz.add(OFFSET.offset_UStruct_Children).readPointer(); }, /** * * @param structz * @returns FField* ChildProperties:该结构体的属性 */ getChildProperties: function (structz: UStructPointer) {//FField* // console.log(`UStruct.getChildProperties structz: ${structz}`); return structz.add(OFFSET.offset_UStruct_ChildProperties).readPointer(); }, getClassName: function (GName: NativePointer, clazz: UObjectPointer) { return UObject.getName(GName, clazz); }, /** * getStructClassPath:获取当前所属类的完全类限定符 * */ getClassPath: function (GName: NativePointer, object: UObjectPointer) { var clazz = UObject.getClass(object); var classname = UObject.getName(GName, clazz); var superclass = this.getSuperClass(clazz); while (UObject.isValid(superclass)) { if (classname == null) break; classname += "."; classname += UObject.getName(GName, superclass); superclass = this.getSuperClass(superclass); } return classname; }, /** * getStructClassPath:获取当前的类完全类限定符 * */ getStructClassPath: function (GName: NativePointer, clazz: UObjectPointer) { var classname = UObject.getName(GName, clazz); var superclass = this.getSuperClass(clazz); while (UObject.isValid(superclass)) { if (classname == null) break; classname += "."; classname += UObject.getName(GName, superclass); superclass = this.getSuperClass(superclass); } return classname; }} |
FField *ChildProperties
得到 FField *ChildProperties就可以根据具体的类型进行解析,在UE4中定义了许多具体的类型,如FInterfaceProperty,FStructProperty,FEnumProperty,FIntProperty等,他们均继承自FField
FInterfaceProperty

FStructProperty

FEnumProperty

这些Property自身可能还会有字段比如UStriptStruct* Struct来进一步描述信息,也有可能仅依赖于FProperty而不需要新字段去描述信息。
实际上,在逆向中根据一步一步偏移得到的FField就已经是具体的每一个Property了,即通过这个函数
1 2 3 4 | getChildProperties: function (structz: UStructPointer) {//FField* // console.log(`UStruct.getChildProperties structz: ${structz}`); return structz.add(OFFSET.offset_UStruct_ChildProperties).readPointer();}, |
得到的FField,要么已经是FInterfaceProperty,要么就是FStructProperty,总之是一种具体的类型。因此会在得到此指针后再增加一个sizeof_FProperty用于得到真正的xproperty


一些特殊property还多加了一个指针偏移,比如FMapProperty,则是有指针存储了不同信息


FEnumProperty则多存储了一个UnderlyingProp指针,需要加上这个指针大小才能指向UEnum


而要在内存中区分这些Property,则依赖于FField中的字段 FFieldClass *ClassPrivate; // 类名,用于区分FProperty类型。同样也能通过字段FName NamePrivate得到这个属性的名字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | export var FField = { /** * * @param GName * @param FField * @returns Name of this field */ getName: function (GName: NativePointer, FField: FFieldPointer) { return getFNameFromID(GName, FField.add(OFFSET.offset_FField_Name).readU32()); }, /** * * @param GName * @param FField * @returns Name of the class object representing the type of this FField */ getClassName: function (GName: NativePointer, FField: FFieldPointer) { return getFNameFromID(GName, FField.add(OFFSET.offset_FField_Class).readPointer().readU32()); }, /** * * @param FField * @returns UField* Next Field in the linked list */ getNext: function (FField: FFieldPointer) {//UField* return FField.add(OFFSET.offset_FField_Next).readPointer(); }}; |
property解析
在从UStruct得到FField后,就能通过FField中FFieldClass *ClassPrivate得到具体property类型,再强制转化为该property类型指针,这一强转正确性是由FProperty继承FField,而具体property又是继承自FProperty,相当于把父类指针强转为了子类指针。
UEnum
UEnum继承自UField,UField则是继承自UObject,相比于UObject,UField仅仅是多了一个UField* next指针用于迭代,而UEnum相比于UField则是多了几个成员,其中最重要的是TArray<TPair<FName, int64_t>> Names,这是一个键值对,用于记录名字和值的对应。

TArray是模板类,第一个成员是模板指针,第二第三个成员则分别描述当前有几个这样的模板指针,最大有多少个这样的模板指针。而这里的模板则是一个键值对。
1 2 3 4 5 6 7 8 9 10 11 12 13 | template <typename KeyType, typename ValueType>struct TPair{ KeyType Key; ValueType Value;};class UEnum : public FField{public: FString CppType; TArray<TPair<FName, int64_t>> Names;}; |
ByteProperty

从内存试图可以比较清楚得知FByteProperty仅仅只是比FProperty多一个UENum*指针,而关键也是要解析这个指针。第一步当然是要得到这个指针
1 2 3 4 5 6 7 8 9 10 11 12 | var enumObj = UByteProperty.getEnum(prop);export var UByteProperty = { getEnum: function (prop: FFieldPointer) { return prop.add(OFFSET.offset_FProperty_size).readPointer(); }, //得到类名字 getName: function (Gname: NativePointer, prop: FFieldPointer) { return UObject.getName(Gname, this.getEnum(prop)); }}//UEnum最终继承自UObject,自然是调用UObject的getName函数来解析名字 |
在得到UEnum*指针后就可以开始解析这个指针,也就是解析里面的TArray模板类,这也很简单。这里再贴出TArray定义
1 2 3 4 5 6 7 8 | template <typename ElementType>class TArray{public: ElementType *Allocator; int32_t ArrayNum; int32_t ArrayMax;}; |
大体步骤就是先得到TArray<TPair<FName, int64_t>> Names这个指针,读出他指向的数组起始地址,也就是一个个TPair组成的数组。通过数组元素大小即TPair<FName, int64_t>大小进行遍历,而元素数量上则是由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | var enumName = UByteProperty.getName(GName, prop); file.write(`\tenum ${enumName} ${thisFieldName} { //[Offset: ${ptr(FProperty.getOffset(prop))}, Size: ${FProperty.getElementSize(prop)}]\n`); for (var count = 0; count < UEnum.getCount(enumObj); count++) { var index = UEnum.getNamesArray(enumObj).add(count * OFFSET.enumItemSize).readU32(); var value = UEnum.getNamesArray(enumObj).add(count * OFFSET.enumItemSize + OFFSET.FName_Size).readU64(); file.write(`\t\t${(getFNameFromID(GName, index) as string).replace(enumName + "::", "")} = ${value}\n`) } file.write("\t};\n")export var UEnum = { /** * * @param en UEnumPointer * @returns TArray<TPair<FName, int64_t>> Names */ getNamesArray: function (en: UEnumPointer) { return en.add(OFFSET.offset_UEnum_Names).readPointer(); }, getCount: function (en: UEnumPointer) { return en.add(OFFSET.offset_UEnum_Count).readU32(); }}//Class: UEnumfunc = offset_so.findExportByName("get_UEnum_Names_Offset") as NativePointer;getOffsets = new NativeFunction(func, 'int', []);export var offset_UEnum_Names = getOffsets();export var offset_UEnum_Count = offset_UEnum_Names + Process.pointerSize;export var offset_UEnum_Max = offset_UEnum_Count + 4;func = offset_so.findExportByName("get_UEnum_Names_Size") as NativePointer;getOffsets = new NativeFunction(func, 'int', []);export var enumItemSize = getOffsets(); |
FMapProperty
对于一些符复合类型则可能需要走一下小递归,以MapProperty为例,他会有两个FProperty类型来分别描述key和value类型,这也就意味着如果需要知道key和value是什么类型,就必须再一次进行FProperty解析
1 2 3 4 5 6 7 8 | // FMapPropertyclass FMapProperty : public FProperty{public: FProperty *KeyProp; FProperty *ValueProp; …………}; |
这里以resolveProp来处理这种递归的解析
1 2 3 | else if (className === "MapProperty") { file.write(`\t<${resolveProp(GName, recurrce, UMapProperty.getKeyProp(prop))}, ${resolveProp(GName, recurrce, UMapProperty.getValueProp(prop))}> ${thiFieldName}; //[Offset: ${ptr(FProperty.getOffset(prop))}, Size: ${FProperty.getElementSize(prop)}]\n`); } |
可以看到resolveProp(GName, recurrce, UMapProperty.getKeyProp(prop))就负责解析key的类型。而resolveProp大体上也是Property解析的流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | export function resolveProp(GName: NativePointer, recurrce: UStructPointer[], prop: FPropertyPointer): string { if (prop == null) return "None"; var className = FField.getClassName(GName, prop) as string; if (UObjectPropertyList.includes(className)) { var propertyClass = UObjectProperty.getPropertyClass(prop); recurrce.push(...[propertyClass]); return UObject.getName(GName, propertyClass) + "*"; } else if (MetaClassList.includes(className)) { var metaClass = UClassProperty.getMetaClass(prop); recurrce.push(...[metaClass]); return "class" + UObject.getName(GName, metaClass); } else if (className === "InterfaceProperty") { var interfaceClass = UInterfaceProperty.getInterfaceClass(prop); recurrce.push(...[interfaceClass]); return "interface class" + UObject.getName(GName, interfaceClass); } else if (className === "StructProperty") { var structClass = UStructProperty.getStruct(prop); recurrce.push(...[structClass]); return "struct" + UObject.getName(GName, structClass); } else if (className === "ArrayProperty") { return resolveProp(GName, recurrce, UArrayProperty.getInner(prop)) + "[]"; } else if (className === "SetProperty") { return "<" + resolveProp(GName, recurrce, USetProperty.getElementProp(prop)) + ">"; } else if (className === "MapProperty") { return "<" + resolveProp(GName, recurrce, UMapProperty.getKeyProp(prop)) + ", " + resolveProp(GName, recurrce, UMapProperty.getValueProp(prop)) + ">"; } else if (className === "BoolProperty") { return "bool"; } else if (className === "UByteProperty") { var enumObj = UByteProperty.getEnum(prop); return resolveProp_writeByteEnum(GName, enumObj); } else if (className === "IntProperty") { return "int"; } else if (className === "Int8Property") { return "int8"; } else if (className === "Int16Property") { return "int16"; } else if (className === "Int64Property") { return "int64"; } else if (className === "UInt16Property") { return "uint16"; } else if (className === "UInt32Property") { return "uint32"; } else if (className === "UInt64Property") { return "uint64"; } else if (className === "FloatProperty") { return "float"; } else if (className === "DoubleProperty") { return "double"; } else if (className === "EnumProperty") { return resolveProp_writeEnumProperty(GName, prop); } else if (className === "StrProperty") { return "FString"; } else if (className === "TextProperty") { return "FText"; } else if (className === "NameProperty") { return "FName"; } else if (className === "DelegateProperty" || className === "MulticastDelegateProperty") { return "delegate"; } else { return FField.getName(GName, prop) + "(" + className + ")"; }} |
其余的Property也是类似这样去解析即可。
UFunction解析
除了类属性之外,还有类成员函数需要解析,这一步会简单许多,UFunction继承自UStruct,UStruct继承于UField,也就是说UFunction实际是继承于UField,这点与property继承于FField有所不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class UFunction : public UStruct{public: EFunctionFlags FunctionFlags; // 函数属性 uint16_t NumParms; // 参数数量 uint16_t ParmsSize; // 参数大小 uint16_t ReturnValueOffset; uint16_t RPCId; uint16_t RPCResponseId; FProperty *FirstPropertyToInit; UFunction *EventGraphFunction; int32_t EventGraphCallOffset; PVOID Func; // 函数指针}; |
在UStruct中,也有字段存储UField
1 2 3 4 5 6 7 | class UStruct : public UField, private FStructBaseChain{public: UStruct *SuperStruct; // 该结构体的超类 UField *Children; // 结构体中的方法 …………}; |
自然也是通过UField强转得到UFuncion指针。
既然UFunction最终继承自UObject,那么也自然是通过UObject得到函数名字
var thisFieldName = UObject.getName(GName, prop); var className = UObject.getClassName(GName, prop);
在此之后则是通过类名字进行判断是否为函数
1 | if (className?.startsWith("Function") || className === "DelegateFunction") |
如果是函数,则可以通过UStruct得到参数属性(UStruct继承自UField,故这里也是把UField给强转为了UStruct)
1 | var funcParmsProperties: FFieldPointer = UStruct.getChildProperties(prop); |
那么参数解析自然走得就是之前property的解析步骤了。
当然UE4还定义了一些属性flag,用于标识函数类型或者参数类型,比如native函数,out型参数。