首页
社区
课程
招聘
[原创]UE4.27SDK-Dump
发表于: 2024-8-11 19:45 2670

[原创]UE4.27SDK-Dump

2024-8-11 19:45
2670

UE4.27 SDK Dump

近期学了学UE4相关知识,并且自己动手写了写SDK dump,特此记录一下。相关代码已传至github,使用了FridaTypeScript,解决了FridaJs下代码挤压在同一个文件的问题,同时使用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等信息

image-20240714172715346

UobjectBase

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

image-20240715111610611

其字段含义如下

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是基类也并无道理,因为继承自UObjectBaseUObject没有新增任何成员,从VS提供的内存视图可知,下文也会延续这一说法。

image-20240723095540028

FName

正是因为UE4中所有对象都继承自UObject,而UObject又有成员FName提供名字,因此UE4的反射系统才能方便得知一个对象的名字,甚至是字段、函数参数等名字。这也给逆向工作提供了可乘之机,那么就不得不研究一下UE4是怎么通过这个FName得到的名字的。

FNameEngine\Source\Runtime\Core\Public\UObject\NameTypes.h定义,这个类中有一个成员函数ToString(),还存储了字符串索引FNameEntryId ComparisonIndex;

image-20240715114813804

见名思义,调用FName::ToString即可得到名字,返回一个FString类型。

FString

查看FString类,在Engine\Source\Runtime\Core\Public\Containers\UnrealString.h定义

image-20240714173747698

可见字符串相关数据存储在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的值

image-20240714212028986

这个类主要就是存放字符串在GName中的索引

还有一些关键类会在后续边解析边提及。

解析ToString()

ToString

此函数实现于Engine\Source\Runtime\Core\Private\UObject\UnrealNames.cpp

image-20240729172018231

可知该函数先调用了GetDisplayNameEntry函数

GetDisplayNameEntry

GetDisplayNameEntry此函数实现就在ToString函数上方

image-20240729173300599

可知此函数调用了三个函数,依次查看

GetNamePool()

此函数在Engine\Source\Runtime\Core\Private\UObject\UnrealNames.cpp中定义

image-20240714205547097

这是一个单例模式的函数,即在So中存在唯一的全局静态变量NamePoolData,查看NamePoolData的类型FNamePool

  1. static bool bNamePoolInitialized;
    • 声明了一个静态布尔变量bNamePoolInitialized,用于跟踪FNamePool实例是否已经被初始化。
  2. alignas(FNamePool) static uint8 NamePoolData[sizeof(FNamePool)];
    • 声明了一个静态数组NamePoolData,其大小等于FNamePool类的大小,并使用alignas关键字确保这个数组按照FNamePool的内存对齐要求进行对齐。这个数组将用作FNamePool实例的内存空间。
  3. GetNamePool函数则是用于返回FNamePool的实例,如果未被初始化则初始化。

这个模式保证了即使在多线程环境下,FNamePool也只会被初始化一次。它不使用C++11中的魔法静态(magic statics)或单例模式中的锁,以减少运行时的开销。通过这种方式,FNamePool类的实例在第一次调用GetNamePool时被创建,并在随后的调用中直接返回,避免了重复初始化。

GetDisplayIndex()

此函数实现于Engine\Source\Runtime\Core\Public\UObject\NameTypes.h

image-20240729173437588

GetDisplayIndexFast()

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

image-20240729173455037

查看相关宏定义,

image-20240714211337676

由注释可知此宏只用于编辑器,而在实际运行中不会启用

因此GetDisplayIndexFast实际返回ComparisonIndexComparisonIndex有两处被定义,其中一处是作为FName的成员变量,FNameEntryId前文也有解析,它里面就只存储了uint32_t的值,作为字符串在GName中索引。

image-20240715111926364

如果还没忘记的话,FName是基类UObject的成员之一,即可以通过NamePrivate获得此Index

Resolve()

此函数定义于Engine\Source\Runtime\Core\Private\UObject\UnrealNames.cpp。在ToString函数中,Resolve参数是GetDisplayIndex函数的返回值,其类型是FNameEntryId

image-20240714183707060

而查看Resolve的定义

image-20240715112332385

其参数类型却是FNameEntryHandle,即说明FNameEntryHandle的构造函数存在相关转换

转至相关定义可以发现,确实有以FNameEntryId为参数的构造函数

image-20240714212916118

这里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相关实现

image-20240715112302267

这里的Blocks就是前文提及的重要四个变量之一,位于FNameEntrlAllocator下,FNameEntrlAllocator则是FNamePool唯一成员。

稍微总结一下,调用GetNamePool得到GName,调用GetDisplayIndex得到索引,调用ResolveGName上根据索引找到一个FNameEntry类型变量,最后返回此变量。

FNameEntry

查看GetDisplayNameEntry函数返回类型FNameEntry

image-20240715123755799

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

FNameEntryHeader

image-20240715124124435

这里就指示了是否为宽字符以及字符长度。

GetPlainNameString()

在调用完GetDisplayNameEntry就会接着调用GetPlainNameString,将FNameEntry类型转换为FString

image-20240729174905865

如下是GetPlainNameString函数定义

image-20240729174848325

这里的Header即为FNameEntryHeader 中的Header,他用于判断是否为宽字符。再返回对应的类型字符串。

GetUnterminatedName也仅仅只是返回FNameEntry中存储的字符串而已。

image-20240729175127534

总结

ToString函数实现会调用两个关键函数GetDisplayNameEntryGetPlainNameStringGetDisplayNameEntry又依次调用GetNamePoolGetDisplayIndexResolve三个函数。

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有多少个ActorsActor地址都在哪,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

image-20240729185341826

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 IdComparisonIndex转化为FNameEntryHandle

image-20240714212916118

1
2
3
4
5
var FNameBlockOffsetBits = 16
var FNameBlockOffsets = 65536
 
var Block = ComparisonIndex >> FNameBlockOffsetBits
var Offset = ComparisonIndex & (FNameBlockOffsets - 1)

前面已经得到FNameEntryAllocatorBlocks

1
2
var Blocks = GNames.add(0x40)
var FNameEntryAllocator = GNames 
Entries.Resolve实现

查看Resolve相关实现

image-20240715112302267

得到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只是存储了字符串是否为宽字符,长度为多少而已,真正字符串就在后面的AnsiNameWideName

1
2
3
4
5
6
7
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`)
}

整合一下就是

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`)
    }
}

效果

image-20240729194413303

FUObjectArray

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

image-20240729194922327

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 TUObjectArrayTUObjectArray类型就是FChunkedFixedUObjectArray

image-20240729195250191

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`等迭代方法
 
![image-20240723100142226](https://yring-me.oss-cn-beijing.aliyuncs.com/test/202408111922690.png)
 
### FField
 
在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;
};
 
#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;
};

image-20240723101628057

UClass

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

image-20240729203535618

回顾一下,从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中定义了许多具体的类型,如FInterfacePropertyFStructPropertyFEnumPropertyFIntProperty等,他们均继承自FField

FInterfaceProperty

image-20240729204945870

FStructProperty

image-20240729204757146

FEnumProperty

image-20240729205045137

这些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

image-20240729210426658

image-20240729210441118

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

image-20240729210637754

image-20240729210714428

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

image-20240729210856037

image-20240730092610660

而要在内存中区分这些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后,就能通过FFieldFFieldClass *ClassPrivate得到具体property类型,再强制转化为该property类型指针,这一强转正确性是由FProperty继承FField,而具体property又是继承FProperty,相当于把父类指针强转为了子类指针。

UEnum

UEnum继承自UFieldUField则是继承自UObject,相比于UObjectUField仅仅是多了一个UField* next指针用于迭代,而UEnum相比于UField则是多了几个成员,其中最重要的是TArray<TPair<FName, int64_t>> Names,这是一个键值对,用于记录名字和值的对应。

image-20240730094916263

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

image-20240730093944056

从内存试图可以比较清楚得知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: 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();

FMapProperty

对于一些符复合类型则可能需要走一下小递归,以MapProperty为例,他会有两个FProperty类型来分别描述keyvalue类型,这也就意味着如果需要知道keyvalue是什么类型,就必须再一次进行FProperty解析

1
2
3
4
5
6
7
8
// FMapProperty
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}; //[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继承自UStructUStruct继承于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型参数。

参考

  1. frida-ue4dumper
  2. ue4游戏逆向之GName内存解析(4.23版本及其以上)
  3. [原创] UnrealEngine4恢复符号过程分析

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2024-8-11 23:36 被yring编辑 ,原因:
收藏
免费 2
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//