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;
UClass* ClassPrivate;
FName NamePrivate;
UObject* OuterPrivate;
};
|
这里值得一提的是,普遍认为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
和一个联合体,这两个成员都会参与后续字符串转换。
这里就指示了是否为宽字符以及字符长度。
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 :
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 = 0x18
var 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 = 16
var FNameBlockOffsets = 65536
var Block = ComparisonIndex >> FNameBlockOffsetBits
var 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 & 1
var Len = FNameEntryHeader >> 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 ; }
var FNameEntryAllocator = GNames
var FName_Offset = 0x18
var FName = actor.add(FName_Offset);
var ComparisonIndex = FName.add(0).readU32()
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)
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,
};
FUObjectItem** Objects;
FUObjectItem* PreAllocatedObjects;
int32 MaxElements;
int32 NumElements;
int32 MaxChunks;
int32 NumChunks;
}
|
显然就存储了当前所有最大元素数量,当前存活的元素数量等信息
FUObjectItem
就是一个一个具体的Object
,定义也简单
1 2 3 4 5 6 7 8 9 | class FUObjectItem
{
class UObject *Object;
uint32_t Flags;
uint32_t ClusterRootIndex;
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();
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();
var UObjectBaseObject = FUObjectItemObjects.add(WithinChunkIndex).readPointer();
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 | 在进一步之前,还需要了解更多的关键类。
它继承自`UObject`,但仅仅只多一个`UField * Next `迭代指针,这个类主要是用于`UClass`等迭代方法
![image - 20240723100142226 ](https: / / yring - me.oss - cn - beijing.aliyuncs.com / test / 202408111922690.png )
在UE4. 25 以后,使用`UField`子集`FField`来描述属性信息,且`FField`没有继承任何一个类,这样大幅度减少了属性对象的占用。
```cpp
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;
};
class FField
{
public:
uint64_t VTable;
FFieldClass * ClassPrivate; / / 类名,用于区分FProperty类型
FFieldVariant Owner;
FField * Next ; / / 指向下一个FField
FName NamePrivate; / / 属性名称
uint32_t FlagsPrivate;
};
|
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;
EPropertyFlags PropertyFlags;
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 = {
getSuperClass: function (structz: UStructPointer) {
return structz.add(OFFSET.offset_UStruct_SuperStruct).readPointer()
},
getChildren: function (structz: UStructPointer) {
return structz.add(OFFSET.offset_UStruct_Children).readPointer();
},
getChildProperties: function (structz: UStructPointer) {
return structz.add(OFFSET.offset_UStruct_ChildProperties).readPointer();
},
getClassName: function (GName: NativePointer, clazz: UObjectPointer) {
return UObject.getName(GName, clazz);
},
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: 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) {
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 = {
getName: function (GName: NativePointer, FField: FFieldPointer) {
return getFNameFromID(GName, FField.add(OFFSET.offset_FField_Name).readU32());
},
getClassName: function (GName: NativePointer, FField: FFieldPointer) {
return getFNameFromID(GName, FField.add(OFFSET.offset_FField_Class).readPointer().readU32());
},
getNext: function (FField: FFieldPointer) {
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*
指针后就可以开始解析这个指针,也就是解析里面的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} {
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 = {
getNamesArray: function (en: UEnumPointer) {
return en.add(OFFSET.offset_UEnum_Names).readPointer();
},
getCount: function (en: UEnumPointer) {
return en.add(OFFSET.offset_UEnum_Count).readU32();
}
}
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();
|
FMapProperty
对于一些符复合类型则可能需要走一下小递归,以MapProperty
为例,他会有两个FProperty
类型来分别描述key
和value
类型,这也就意味着如果需要知道key
和value
是什么类型,就必须再一次进行FProperty
解析
1 2 3 4 5 6 7 8 | class 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};
}
|
可以看到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
型参数。
参考
- frida-ue4dumper
- ue4游戏逆向之GName内存解析(4.23版本及其以上)
- [原创] UnrealEngine4恢复符号过程分析
最后于 2小时前
被yring编辑
,原因: