【软件逆向-UE4.27SDK-Dump【详细】】此文章归类为:软件逆向。
近期学了学UE4相关知识,并且自己动手写了写SDK dump,特此记录一下。相关代码已传至github,使用了Frida与TypeScript,解决了Frida在Js下代码挤压在同一个文件的问题,同时使用C++去写结构体并得到偏移,能够更好的维护。
在阅读代码之前,就必须去了解一下UE4的命名约定,具体的自己去查看官网文档,下面是一些基本需要知道的:
在UE4引擎中全局定义了UWorldProxy对象,此对象在Engine\Source\Runtime\Engine\Classes\Engine\World.h定义,这个类中有一个变量UWorld* World;
在官网定义中可以看到,UWorld是地图或沙盒的顶级对象,其中会有Actor等信息

根据继承关系找到最顶级的类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提供的内存视图可知,下文也会延续这一说法。

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

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

可见字符串相关数据存储在TArray容器中,容器类型是TCHAR,而TCHAR类型就是wchar_t类型。
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函数是怎么实现的。这一点会在后文进行。
此类型在Engine\Source\Runtime\Core\Private\UObject\UnrealNames.cpp中定义,这里只关注第一个成员
1 2 3 4 5 6 | class FNamePool{public: FNameEntryAllocator Entries; …………}; |
这个类型定义了一个全局静态变量NamePoolData数组,这个NamePoolData就是常说的GName,因为它就存储了全局的字符串,相当于一个字符串池,后续的ToString函数也高度依赖这个数组。
此类型在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];}; |
此类型在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]; };}; |
此类型中,只有一个uint32的类型变量,其余均是函数,因此可简单理解为ComparisonIndex就是一个uint32的值

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

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

可知此函数调用了三个函数,依次查看
此函数在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实例的内存空间。FNamePool的实例,如果未被初始化则初始化。这个模式保证了即使在多线程环境下,FNamePool也只会被初始化一次。它不使用C++11中的魔法静态(magic statics)或单例模式中的锁,以减少运行时的开销。通过这种方式,FNamePool类的实例在第一次调用GetNamePool时被创建,并在随后的调用中直接返回,避免了重复初始化。
此函数实现于Engine\Source\Runtime\Core\Public\UObject\NameTypes.h

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

查看相关宏定义,

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

如果还没忘记的话,FName是基类UObject的成员之一,即可以通过NamePrivate获得此Index。
此函数定义于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; |
查看Resolve相关实现

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

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

这里就指示了是否为宽字符以及字符长度。
在调用完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字符串
在实际运行环境中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类
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需要通过其他手段得到)
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这一字段,通过偏移很容易得到
var FName_Offset = 0x18 var FName = actor.add(FName_Offset);
得到FName后就是实现那几个函数。这里稍微提一下为什么actor.add(FName_Offset)之后不用在readPointer,是因为UObject里面直接存放的就是FName这一结构,而不是FName*指针,所以不需要再readPointer
这一步有两种方式,第一种通过IDA等方式先找到全局变量GName,第二种通过特征码等方式在内存中找到。如何找不在这里展开,默认已经找到。找到之后需要使用的是FNamePool的第一个成员FNameEntryAllocator中的Blocks,
这里有一个点需要注意, FNameEntryAllocator第一个成员是Lock,这android平台对应的是pthread_rwlock_t类型,windows平台对应的是SRWLOCK类型,
在32位安卓平台上此成员大小应该是0x28,在64位安卓平台上应该是0x38,那么从FNamePool得到Blocks就是
var FNameEntryAllocator = GNames var Blocks = GNames.add(0x40)
这一步很简单
var ComparisonIndex = FName.add(0).readU32()
第一步是将FNameEntryId Id即ComparisonIndex转化为FNameEntryHandle

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

得到FNameEntry
var FNameEntry = Blocks.add(Block * 8).readPointer().add(Offset * 2)
得到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中
var FNameEntryHeader = FNameEntry.readU16()
var isWide = FNameEntryHeader & 1
var Len = FNameEntryHeader >> 6 //最大长度就是1024,所以需要右移6位
if (0 == isWide) {
console.log(`\x1b[32m[+] actor ${actor}: ${FNameEntry.add(2).readCString(Len)}\x1b[0m`)
}
整合一下就是
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`)
}
}
效果

名字解析只是最基本的,复杂的还是成员字段、函数签名等信息。在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

定义如下
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,定义也简单
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就是
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
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应该是很简单的事了·。
/**
*
* @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;
},
/**
*
* @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";
}
},
在进一步之前,还需要了解更多的关键类。
它继承自UObject,但仅仅只多一个UField* Next迭代指针,这个类主要是用于UClass等迭代方法

在UE4.25以后,使用UField子集FField来描述属性信息,且FField没有继承任何一个类,这样大幅度减少了属性对象的占用。
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 | class 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() |
这个类继承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() |
这个类比较重要,记录了成员、函数信息的指针,它继承于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;}; |

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

回顾一下,从GUObjectArray可以根据索引一步一步得到UObject,而UObject中又有UClass成员,它由继承自UStruct,那么从这个成员就可以得到UStruct,进一步得到 UField *Children; // 结构体中的方法和
FField *ChildProperties; // 结构体中的属性
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就可以根据具体的类型进行解析,在UE4中定义了许多具体的类型,如FInterfaceProperty,FStructProperty,FEnumProperty,FIntProperty等,他们均继承自FField
FInterfaceProperty

FStructProperty

FEnumProperty

这些Property自身可能还会有字段比如UStriptStruct* Struct来进一步描述信息,也有可能仅依赖于FProperty而不需要新字段去描述信息。
实际上,在逆向中根据一步一步偏移得到的FField就已经是具体的每一个Property了,即通过这个函数
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得到这个属性的名字
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();
}
};
在从UStruct得到FField后,就能通过FField中FFieldClass *ClassPrivate得到具体property类型,再强制转化为该property类型指针,这一强转正确性是由FProperty继承FField,而具体property又是继承自FProperty,相当于把父类指针强转为了子类指针。
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;}; |

从内存试图可以比较清楚得知FByteProperty仅仅只是比FProperty多一个UENum*指针,而关键也是要解析这个指针。第一步当然是要得到这个指针
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>大小进行遍历,而元素数量上则是由
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: UEnum
func = 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();
对于一些符复合类型则可能需要走一下小递归,以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来处理这种递归的解析
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解析的流程
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继承自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);
在此之后则是通过类名字进行判断是否为函数
if (className?.startsWith("Function") || className === "DelegateFunction")
如果是函数,则可以通过UStruct得到参数属性(UStruct继承自UField,故这里也是把UField给强转为了UStruct)
var funcParmsProperties: FFieldPointer = UStruct.getChildProperties(prop);
那么参数解析自然走得就是之前property的解析步骤了。
当然UE4还定义了一些属性flag,用于标识函数类型或者参数类型,比如native函数,out型参数。
更多【软件逆向-UE4.27SDK-Dump【详细】】相关视频教程:www.yxfzedu.com