首页
社区
课程
招聘
[原创]UnrealEngine 5 逆向之类名算法的分析
2023-8-3 15:48 4017

[原创]UnrealEngine 5 逆向之类名算法的分析

2023-8-3 15:48
4017

本文章所带来内容基于逆向分析虚幻引擎源代码,虚幻引擎是由Epic公司开发的新时代游戏引擎,其中虚幻三闭源,虚幻四、虚幻五均是开源引擎,在Github上由众多大佬们一起维护UnrealEngine(想在Github访问源代码需要加入Epic组织,也可以直接下载Epic客户端通过客户端下载引擎访问)。

Part:1

虚幻引擎的游戏应用:

虚幻四:大逃杀 和平等

虚幻五:堡垒 悟空等

Part:2 加入组织后如何获取Engine全部代码.

①下载.zip格式压缩包(可自选版本)

②下载后在目录运行Setup.bat,会在控制台显示下载进度,期间可以中断,注意分配足够储存空间(10G+)(也可以直接runtime源码分析,节省很多空间)

③操作全部正确后,在根目录会出现 "UE5.sln"("UE4.sln"),本文章使用 Version5.2进行分析.

Part:3 操练起来

①直接突击到"UObjectBase.h"(虚幻引擎万物继承UObject)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
EObjectFlags    ObjectFlags;
 
/** Index into GObjectArray...very private. */
int32       InternalIndex;
 
/** Class the object belongs to. */
UClass*     ClassPrivate;
 
/** Name of this object */
FName       NamePrivate;
 
/** Object this object resides in. */
UObject*    OuterPrivate;

FName就是我们要找的东西,这里可以计算一下他在Base类下的偏移:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
补充一下FName的结构,想要更详细了解请参阅引擎源码中"NameTypes.h"
这里要特别注意,不同引擎版本这三个的顺序不一样哦,有的版本(5.x),会把Number放在最后,但其实这三个影响不大,管他究竟是哪个,试三次总能对.(其实用到的永远是第一个,第一个位置不变)
*/
private:
FNameEntryId    ComparisonIndex;
#if !UE_FNAME_OUTLINE_NUMBER
uint32          Number;
#endif// ! //UE_FNAME_OUTLINE_NUMBER
#if WITH_CASE_PRESERVING_NAME
FNameEntryId    DisplayIndex;
#endif // WITH_CASE_PRESERVING_NAME
//记住啦,一会还要用到它!

pOffFName=

0x8(虚表 64 bits)+0x4(EObjectFlags枚举)+0x4(int32)+0x8(UClass指针 64 bits)=0x18

这已经能解释为什么在很多易语言大手子的源码里边要从对象往下读0x18获取FName,知其然要知其所以然.

②这一步比较麻烦,比较吃电脑性能,你在代码里翻来翻去会发现他都是这么获取类名的.

1
2
3
4
5
6
7
8
9
10
11
FORCEINLINE FString GetName() const
{
    if (this != nullptr)
    {
        return NamePrivate.ToString();
    }
    else
    {
        return TEXT("None");
    }
}

如果不告诉你自己翻得时候也是可以找到的,比如某个对象在获取自己类名的时候,就需要调用这个函数,但是Vs神奇的索引机制经常索引错,包括如果搜GetName会搜到大量重复的数据(千+),所以直接提供啦.

不难看出,下继承类从父类获取NamePrivate并调用方法FName::ToString来转换成FString(FString是虚幻引擎基于自己更为安全的数组TArray实现的一种类).

接着继续,这就是FName::ToString(Vs直接跳可能会跳错)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String FName::ToString() const
{
// With UE_FNAME_OUTLINE_NUMBER reading number isn't free so skip this check
#if !UE_FNAME_OUTLINE_NUMBER
    if (GetNumber() == NAME_NO_NUMBER_INTERNAL)
    {
        // Avoids some extra allocations in non-number case
        return GetDisplayNameEntry()->GetPlainNameString();
    }
#endif // !UE_FNAME_OUTLINE_NUMBER
     
    FString Out;   
    ToString(Out);
    return Out;
}

那就势必要看看

1
GetDisplayNameEntry()->GetPlainNameString();

其中

1
2
3
4
const FNameEntry* FName::GetDisplayNameEntry() const
{
    return ResolveEntryRecursive(GetDisplayIndexInternal());
}

再其中

1
2
3
4
5
6
7
8
    FORCEINLINE FNameEntryId GetDisplayIndexInternal() const
    {
#if WITH_CASE_PRESERVING_NAME
        return DisplayIndex;
#else // WITH_CASE_PRESERVING_NAME
        return ComparisonIndex;
#endif // WITH_CASE_PRESERVING_NAME
    }

看函数的名称应该标准返回是DisplayIndex?嘿嘿,用到的应该是ComparisonIndex,WITH_CASE_PRESERVING_NAME在编译器环境.

哥哥们要注意,虚幻引擎重写了这部分代码,老版本可能在GetDisplayNameEntry直接调用了NamePool,不急,我们跟一下就能看到.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//"UnrealNames.cpp"
const FNameEntry* FName::ResolveEntryRecursive(FNameEntryId LookupId)
{
    const FNameEntry* Entry = ResolveEntry(LookupId);
//下面都是错误处理,主要看上边这一行.
#if UE_FNAME_OUTLINE_NUMBER
    if (Entry->Header.Len == 0)
    {
        return ResolveEntry(Entry->NumberedName.Id);
    }
    else
#endif
    {
        return Entry;
    }
}

看样子需要找一下FName::ResolveEntry,如下.

1
2
3
4
const FNameEntry* FName::ResolveEntry(FNameEntryId LookupId)
{
    return &GetNamePool().Resolve(LookupId);
}
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
//再次分析之前需要看一下这个频繁出现的FNameEntryId究竟是什么
struct FNameEntryId{
private:
    uint32 Value;
}
//他真的只是存了一个ID(4bits uint32),那么FNameEntry呢
struct FNameEntry
{
private:
#if WITH_CASE_PRESERVING_NAME
    FNameEntryId ComparisonId;
#endif
    FNameEntryHeader Header;
    union
    {
        ANSICHAR    AnsiName[NAME_SIZE];
        WIDECHAR    WideName[NAME_SIZE];
#if UE_FNAME_OUTLINE_NUMBER
         
        struct
        {
            FNameEntryId    Id;
            uint32          Number;
        } NumberedName;
#endif // UE_FNAME_OUTLINE_NUMBER
    };
};

提到了ResolveEntry函数要调用GetNamePool.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static bool bNamePoolInitialized;
 
alignas(FNamePool) static uint8 NamePoolData[sizeof(FNamePool)];
 
static FNamePool& GetNamePool()
{
    if (bNamePoolInitialized)
    {
        return *(FNamePool*)NamePoolData;
    }
 
    FNamePool* Singleton = new (NamePoolData) FNamePool;
    bNamePoolInitialized = true;
    return *Singleton;
}

是的,NamePool就赤裸裸的放在这里做全局变量,它之中储存所有字符串,后续的文章会讲如何逆向寻找他.(4.x)可能会叫他GName.接着说.

1
2
3
4
5
6
return &GetNamePool().Resolve(LookupId);
//------------------------------------------
FNameEntry& Resolve(FNameEntryHandle Handle) const
{
    return Entries.Resolve(Handle);
}

聪明的你发现了,这tm类型也不匹配啊?不要急宝贝.

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
struct FNameEntryHandle
{
    uint32 Block = 0;
    uint32 Offset = 0;
 
    FNameEntryHandle(uint32 InBlock, uint32 InOffset)
        : Block(InBlock)
        , Offset(InOffset)
    {
        checkName(Block < FNameMaxBlocks);
        checkName(Offset < FNameBlockOffsets);
    }
 
    FNameEntryHandle(FNameEntryId Id)
        : Block(Id.ToUnstableInt() >> FNameBlockOffsetBits)
        , Offset(Id.ToUnstableInt() & (FNameBlockOffsets - 1))
    {
    }
 
    operator FNameEntryId() const
    {
        return FNameEntryId::FromUnstableInt(Block << FNameBlockOffsetBits | Offset);
    }
 
    explicit operator bool() const { return Block | Offset; }
};

看到第二个构造函数了吗,那是最核心的部分!解决完类型匹配的问题,Entries.Resolve(Handle)的转换是通过类FNameEntryAllocator实现的(Entries类型是FNameEntryAllocator).

接下来开始核心分析

1
2
3
4
5
FNameEntryHandle(FNameEntryId Id)
      : Block(Id.ToUnstableInt() >> FNameBlockOffsetBits)
      , Offset(Id.ToUnstableInt() & (FNameBlockOffsets - 1))
  {
  }

传入的Id就是FName下的成员ComparisonIndex

1
uint32 ToUnstableInt() const { return Value; }

Id.ToUnstableInt() 方法获取的值就是ComparisonIndex.Value

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
//可以轻松的在"UnrealName.cpp"找到这些,他们都放在一起哦
static constexpr uint32 FNameMaxBlockBits = 13;
static constexpr uint32 FNameBlockOffsetBits = 16;
static constexpr uint32 FNameMaxBlocks = 1 << FNameMaxBlockBits;
 
static constexpr uint32 FNameBlockOffsets = 1 << FNameBlockOffsetBits;
 
static constexpr uint32 FNameEntryIdBits = FNameBlockOffsetBits + FNameMaxBlockBits;
 
static constexpr uint32 FNameEntryIdMask = (1 << FNameEntryIdBits ) - 1;
 
我把他丢给了Claude:
 
好的,已知:
 
static constexpr uint32 FNameMaxBlockBits = 13;
 
static constexpr uint32 FNameBlockOffsetBits = 16;
 
根据这两个已知值,可以计算出剩余值:
 
static constexpr uint32 FNameMaxBlocks = 1 << FNameMaxBlockBits;
 
这里FNameMaxBlockBits是13,1左移13位结果是:8192
 
所以:
static constexpr uint32 FNameMaxBlocks = 8192;
 
static constexpr uint32 FNameBlockOffsets = 1 << FNameBlockOffsetBits;
 
这里FNameBlockOffsetBits是16,1左移16位结果是:65536
 
所以:
static constexpr uint32 FNameBlockOffsets = 65536;
 
static constexpr uint32 FNameEntryIdBits = FNameBlockOffsetBits + FNameMaxBlockBits;
 
FNameBlockOffsetBits + FNameMaxBlockBits = 16 + 13 = 29
 
所以:
static constexpr uint32 FNameEntryIdBits = 29;
 
static constexpr uint32 FNameEntryIdMask = (1 << FNameEntryIdBits) - 1;
 
1左移29位结果是:536870912,减1则是:536870911
 
所以:
static constexpr uint32 FNameEntryIdMask = 536870911;
 
请确认我的计算结果是否正确,如果有任何问题,请随时提出来讨论!
姑且就认为他的计算正确吧.

那个算法我们就可以改写了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
auto Id=ComparisonIndex;
Handle.Block=(Id.Value >> 16);
Handle.Offset=(Id.Value  & 65535);
 
//再加上一开始的
 
FNameEntry& Resolve(FNameEntryHandle Handle) const
{
return
*reinterpret_cast<FNameEntry*>(
Blocks[Handle.Block] + Stride * Handle.Offset
);
 
}
//Stride=4 因为 enum { Stride = alignof(FNameEntry) };
//(手动奸笑)

Block又是什么?

定义在FNameEntryAllocator下面(就是Resolve那里出现的)

uint8* Blocks[FNameMaxBlocks] = {};并初始化了

不感觉很熟悉吗?我们梳理下逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class FNameEntryAllocator
{  
mutable FRWLock Lock;
uint32 CurrentBlock = 0;
uint32 CurrentByteCursor = 0;
uint8* Blocks[FNameMaxBlocks] = {};
}
class FNamePool
{
private:
enum { MaxENames = 512 };
FNameEntryAllocator Entries;
};
 
alignas(FNamePool) static uint8 NamePoolData[sizeof(FNamePool)];
 
const FNameEntry* FName::ResolveEntry(FNameEntryId LookupId)
{
  return &GetNamePool().Resolve(LookupId);
}
1
2
3
4
5
6
7
8
9
10
//FRWLock占用8Bits 原因是:
mutable FRWLock Lock;
typedef FHoloLensRWLock FRWLock;
class FPThreadsRWLock
{
private:
  pthread_rwlock_t Mutex;//指针类型
};

也就是从全局NamePoolData--偏移0x4-->Entries--偏移0x10-->Blocks

这也解释了为什么易语言大手子要 GName+0x10

实际上很多情况下都直接找Entries地址,后边文章讲.

都找齐了吗???Stride=4???

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct FNameEntry
{
private:
#if WITH_CASE_PRESERVING_NAME
    FNameEntryId Compariso nId;
#endif
    FNameEntryHeader Header;
    union
    {
        ANSICHAR    AnsiName[NAME_SIZE];
        WIDECHAR    WideName[NAME_SIZE];
#if UE_FNAME_OUTLINE_NUMBER
        struct
        {
            FNameEntryId    Id;
            uint32          Number;
        } NumberedName;
#endif // UE_FNAME_OUTLINE_NUMBER
};
//好熟悉的#if WITH_CASE_PRESERVING_NAME

也就是实际编译出来的程序,没有FNameEntryId Compariso nId

Part:4

但是不论我在FNameEntry增加什么, Stride = alignof(FNameEntry)计算出来的值永远是4,alignof是计算对齐的,有大佬能解答我一下吗.

Part:5

①第一次写这么多,看雪这个网页编辑字一多就有点卡哦O.

②逆向路漫漫,道路很远,脚步更长.

③一个人之所以健全,主要的不是年龄,而是他的志向。过去是远了淡了的雾霭,未来是近了亮了的晨光,只有志向远大的人,才能眺望晨光,用全部热忱去迎接。

Hello 看雪!


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2023-8-3 16:04 被Uioa编辑 ,原因: 大佬Q我
收藏
点赞3
打赏
分享
最新回复 (5)
雪    币: 99
活跃值: (398)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Uioa 2023-8-10 21:06
2
0
文章留下的问题解决啦.最近就发出来
雪    币: 3685
活跃值: (3869)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
caolinkai 2023-8-10 21:59
3
0
感谢分享
雪    币: 51
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
333333 2023-8-10 23:17
4
0

111111

最后于 2023-8-10 23:18 被333333编辑 ,原因:
雪    币: 6124
活跃值: (4101)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
黑洛 1 2023-8-11 00:18
5
0
是不是搞遗迹2啊
雪    币: 19535
活跃值: (29224)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-8-11 09:36
6
1
感谢分享
游客
登录 | 注册 方可回帖
返回