问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
初探UE4引擎逆向
漏洞分析
CTF
前言:在现代游戏开发中,尤其是在像虚幻引擎(Unreal Engine)这样复杂而强大的引擎中,名称管理是一个至关重要的部分。名称不仅用于标识游戏中的各种对象,还用于调试、日志记录和资源管理等多个方面。虚幻引擎中的FName系统是一种高效的名称管理机制,能够快速查找和处理名称字符串。
前言:在现代游戏开发中,尤其是在像虚幻引擎(Unreal Engine)这样复杂而强大的引擎中,名称管理是一个至关重要的部分。名称不仅用于标识游戏中的各种对象,还用于调试、日志记录和资源管理等多个方面。虚幻引擎中的FName系统是一种高效的名称管理机制,能够快速查找和处理名称字符串。 本项目的目标是基于UE4.27.2源码,独立实现一个GName加密算法。通过分析UE4源码中的FName和FNameEntry结构,我们将重构并实现一个简化版本的名称管理系统,同时添加名称的加密和解密功能。这不仅有助于理解虚幻引擎的内部工作机制,还能提供一个独立的、可用于学习和参考的名称管理和加密系统。 加密算法在游戏开发中的应用非常广泛,例如保护敏感数据、防止作弊和提高安全性。本项目将展示如何在名称管理系统中集成简单的加密和解密逻辑,并确保这些操作的高效性和可靠性。 通过这个项目,我们希望能够提供一个易于理解且实用的示例,帮助开发者更好地掌握名称管理和加密算法的实现细节。同时,这也为进一步探索和扩展虚幻引擎中的其他复杂机制提供了一个良好的起点。 一、GetName加密算法 ------------- 为什么要进行加密算法的逆向分析呢? 答案就是为了后面实现加密算法做铺垫包括dump sdk等等。。。 主要目的是通过ID 获取字符串 GName ID: 110024 ====字符串 使用ue源码 把game dump下来 GetName() ctrl+f 查找GetName() 默认位置在Source-》Runtime-》Engine 在UObjectBaseUtillty.h里 ```c FString GetFullGroupName( bool bStartWithOuter ) const; /** * Returns the name of this object (with no path information) * * @return Name of the object. */ FORCEINLINE FString GetName() const { return GetFName().ToString(); } /** Optimized version of GetName that overwrites an existing string */ FORCEINLINE void GetName(FString &ResultString) const { GetFName().ToString(ResultString); } ``` FORCEINLINE 强制内联 FString 返回值 是个class CORE\_API FString 类 DataType Data; //只有一个成员变量 Data 是TArray类型 ```c class CORE_API FString { private: friend struct TContainerTraits; //友元 /** Array holding the character data */ typedef TArray DataType; DataType Data; //只有一个成员变量 template //模板 using TRangeElementType = typename TRemoveCV()))>::Type>::Type; template struct TIsRangeOfCharType : TIsCharType> { }; template struct TIsRangeOfTCHAR : TIsSame> { ``` 搜索模板 重点类型 ```c TArray{ wchar_t* data; int num; //有多长 int maxnum ;//最大长度 } ``` 所以说FString 返回的是TArray类 再看下GetFname ```c FORCEINLINE FName GetFName() const { return NamePrivate; } ``` 我们看下NamePrivate的类型,发现就是FName FName NamePrivate; 这个FName在class COREUOBJECT\_API UObjectBase 里,这里学习下计算偏移获取FName的位置 计算偏移方法用类首地址+虚表 函数 函数指针在64位下占8个字节,成员属性变量占具体属性的长度 ```c class COREUOBJECT_API UObjectBase { ............. virtual void DeferredRegister(UClass *UClassStaticClass,const TCHAR* PackageName,const TCHAR* Name); private: /** Flags used to track and report various object states. This needs to be 8 byte aligned on 32-bit platforms to reduce memory waste */ 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; friend class FBlueprintCompileReinstancer; friend class FContextObjectManager; } ``` virtual void DeferredRegister 是个虚表调用 +8 EObjectFlags enum遍历 +8(4) int32 InternalIndex; 是个typedef FPlatformTypes::int32 +4字节 UClass\* ClassPrivate; 是个class类的函数指针 +8 所以FName 在 类首地址+8+8+4+8 的位置处 接着分析GetFName().ToString(); 跟进去 ```c FString FName::ToString() const { if (GetNumber() == NAME_NO_NUMBER_INTERNAL) { // Avoids some extra allocations in non-number case return GetDisplayNameEntry()->GetPlainNameString(); } FString Out; ToString(Out); return Out; } ``` 看下GetDisplayNameEntry() ```c const FNameEntry* FName::GetDisplayNameEntry() const { return &GetNamePool().Resolve(GetDisplayIndex()); } ``` 从GetDisplayIndex() 看 ```c class CORE_API FName { public: FORCEINLINE FNameEntryId GetDisplayIndex() const { const FNameEntryId Index = GetDisplayIndexFast(); checkName(IsWithinBounds(Index)); return Index; } } GetDisplayIndexFast: FORCEINLINE FNameEntryId GetDisplayIndexFast() const { #if WITH_CASE_PRESERVING_NAME return DisplayIndex; #else return ComparisonIndex; //FNameEntryId ComparisonIndex; #endif } struct FNameEntryId { FNameEntryId() : Value(0) {} ...... private: uint32 Value; 直接返回的ID } ``` 通过GetDisplayIndex分析看,直接返回的ID给Index 然后return 接着分析&GetNamePool() ```c static bool bNamePoolInitialized; alignas(FNamePool) static uint8 NamePoolData[sizeof(FNamePool)]; // Only call this once per public FName function called // // Not using magic statics to run as little code as possible static FNamePool& GetNamePool() { if (bNamePoolInitialized) { return *(FNamePool*)NamePoolData; } FNamePool* Singleton = new (NamePoolData) FNamePool; bNamePoolInitialized = true; return *Singleton; } ``` 发现,这里NamePoolData是个全局变量,这里在ue4.23之前的版本里是GName,这里是4.23之后了,所以是NamePoolData,要想实现整个加密算法,必须通过IDA去找,这里先不介绍挖个坑,放到后面去说,这个主要功能是new了FNamePool 给Singleton 然后返回个指针 看下FNamePool 发现是个类class FNamePool ```c class FNamePool { public: FNamePool(); void Reserve(uint32 NumBlocks, uint32 NumEntries); FNameEntryId Store(FNameStringView View); FNameEntryId Find(FNameStringView View) const; FNameEntryId Find(EName Ename) const; const EName* FindEName(FNameEntryId Id) const; /** @pre !!Handle */ FNameEntry& Resolve(FNameEntryHandle Handle) const { return Entries.Resolve(Handle); } bool IsValid(FNameEntryHandle Handle) const; FNameEntryId StoreValue(const FNameComparisonValue& Value); void StoreBatch(uint32 ShardIdx, TArrayView Batch) { ComparisonShards[ShardIdx].InsertBatch(Batch); } #if WITH_CASE_PRESERVING_NAME FNameEntryId StoreValue(const FNameDisplayValue& Value, bool bReuseComparisonId); void StoreBatch(uint32 ShardIdx, TArrayView Batch) { DisplayShards[ShardIdx].InsertBatch(Batch); } bool ReuseComparisonEntry(bool bAddedComparisonEntry, const FNameDisplayValue& DisplayValue); #endif /// Stats and debug related functions /// uint32 NumEntries() const; uint32 NumAnsiEntries() const; uint32 NumWideEntries() const; uint32 NumBlocks() const { return Entries.NumBlocks(); } uint32 NumSlots() const; void LogStats(FOutputDevice& Ar) const; uint8** GetBlocksForDebugVisualizer() { return Entries.GetBlocksForDebugVisualizer(); } TArray DebugDump() const; private: enum { MaxENames = 512 }; FNameEntryAllocator Entries; #if WITH_CASE_PRESERVING_NAME FNamePoolShard DisplayShards[FNamePoolShards]; #endif FNamePoolShard ComparisonShards[FNamePoolShards]; // Put constant lookup on separate cache line to avoid it being constantly invalidated by insertion alignas(PLATFORM_CACHE_LINE_SIZE) FNameEntryId ENameToEntry[NAME_MaxHardcodedNameIndex] = {}; uint32 LargestEnameUnstableId; TMap> EntryToEName; }; ``` 从上面代码看到,类里集成很多方法 属性等,然后调用了里面的Resolve() 继续跟进 FNameEntry& Resolve(FNameEntryHandle Handle) const { return Entries.Resolve(Handle); 是个FNameEntry 类型 ,我们先去 FNameEntryHandle 去看下 ```c struct FNameEntryHandle { uint32 Block = 0; uint32 Offset = 0; FNameEntryHandle(uint32 InBlock, uint32 InOffset) : Block(InBlock) , Offset(InOffset) {} FNameEntryHandle(FNameEntryId Id) : Block(Id.ToUnstableInt() >> FNameBlockOffsetBits) , Offset(Id.ToUnstableInt() & (FNameBlockOffsets - 1)) {} } static constexpr uint32 FNameBlockOffsetBits = 16; static constexpr uint32 FNameBlockOffsets = 1 << FNameBlockOffsetBits; //1<<16 FNameEntryHandle(FNameEntryId Id) : Block(Id.ToUnstableInt() >> FNameBlockOffsetBits) , Offset(Id.ToUnstableInt() & (FNameBlockOffsets - 1)) {} ``` 找到了关键性的加密算法,Id.ToUnstableInt() 就是跟进ID返回的value 它的值, uint32 ToUnstableInt() const { return Value; },将它的值进行加密 这里先记下,到后面写解密算法的时候用得到,然后我们看下Entries.Resolve(Handle); 先跟到Entries里,发现是个FNameEntryAllocator Entries; 继续跟进 ```c class FNameEntryAllocator { public: enum { Stride = alignof(FNameEntry) }; enum { BlockSizeBytes = Stride * FNameBlockOffsets }; /** Initializes all member variables. */ FNameEntryAllocator() { LLM_SCOPE(ELLMTag::FName); Blocks[0] = (uint8*)FMemory::MallocPersistentAuxiliary(BlockSizeBytes, FNAME_BLOCK_ALIGNMENT); } ~FNameEntryAllocator() { for (uint32 Index = 0; Index <= CurrentBlock; ++Index) { FMemory::Free(Blocks[Index]); } } ........................ FNameEntry& Resolve(FNameEntryHandle Handle) const { // Lock not needed return *reinterpret_cast(Blocks[Handle.Block] + Stride * Handle.Offset); } } ``` 在类里我们找到了Resolve,这里是最终的加密算法,这里我们先找下Stride的值,发现是在class FNameEntryAllocator,我们看下 ```c class FNameEntryAllocator { public: enum { Stride = alignof(FNameEntry) }; enum { BlockSizeBytes = Stride * FNameBlockOffsets }; } ``` 发现Stride是通过alignof 枚举出FNameEntry,我们跟进去FNameEntry ```c struct FNameEntry { private: #if WITH_CASE_PRESERVING_NAME FNameEntryId ComparisonId; #endif FNameEntryHeader Header; union { ANSICHAR AnsiName[NAME_SIZE]; WIDECHAR WideName[NAME_SIZE]; }; FNameEntry(const FNameEntry&) = delete; FNameEntry(FNameEntry&&) = delete; FNameEntry& operator=(const FNameEntry&) = delete; FNameEntry& operator=(FNameEntry&&) = delete; ``` 其实这里我们可以写个c++程序去计算下,c++程序计算如下: ```c #include #include struct FNameEntryHeader { uint16_t bIsWide : 1; static constexpr uint32_t ProbeHashBits = 5; uint16_t LowercaseProbeHash : ProbeHashBits; uint16_t Len : 10; }; struct FNameEntry { private: FNameEntryHeader Header; union { char AnsiName[1024]; wchar_t WideName[1024]; }; }; int main() { std::cout << "Stride:"<(Blocks[Handle.Block] + Stride * Handle.Offset); } 还原后: FNameEntry& Resolve(FNameEntryHandle Handle) const { // Lock not needed return *reinterpret_cast(Blocks[ID>>16] + 2 * (ID&(i<<16-1)); } ``` 至此GetDisplayNameEntry()->GetPlainNameString()的GetDisplayNameEntry() 分析完成 下面我们接着分析GetPlainNameString() ```c FString FNameEntry::GetPlainNameString() const { FNameBuffer Temp; if (Header.bIsWide) { return FString(Header.Len, GetUnterminatedName(Temp.WideName)); } else { return FString(Header.Len, GetUnterminatedName(Temp.AnsiName)); } } ``` 我们继续查看GetUnterminatedName ```c FORCEINLINE const WIDECHAR* FNameEntry::GetUnterminatedName(WIDECHAR(&OptionalDecodeBuffer)[NAME_SIZE]) const { #ifdef WITH_CUSTOM_NAME_ENCODING CopyUnterminatedName(OptionalDecodeBuffer); return OptionalDecodeBuffer; #else return WideName; #endif } ``` 在编译器里执行了CopyUnterminatedName(OptionalDecodeBuffer); 我们继续跟进这个函数 ```c void FNameEntry::CopyUnterminatedName(WIDECHAR* Out) const { FPlatformMemory::Memcpy(Out, WideName, sizeof(WIDECHAR) * Header.Len); Decode(Out, Header.Len); } ``` 发现就是赋值的操作,具体没多少用处这里,这里理解下就可以了,至此我们已经分析了GetName加密算法的整个流程 填一下之前的坑,找到FNamePool 也就是GName ```c static FNamePool& GetNamePool() { if (bNamePoolInitialized) { return *(FNamePool*)NamePoolData; } FNamePool* Singleton = new (NamePoolData) FNamePool; bNamePoolInitialized = true; return *Singleton; } ``` D:\\steam\\steamapps\\common\\DeathlyStillnessGame\\DeathlyStillnessGame\\Binaries\\Win64 用IDA打开它,结合源码定位 进入到FNamePool里 ```c FNamePool::FNamePool() { for (FNamePoolShardBase& Shard : ComparisonShards) { Shard.Initialize(Entries); } #if WITH_CASE_PRESERVING_NAME for (FNamePoolShardBase& Shard : DisplayShards) { Shard.Initialize(Entries); } #endif // Register all hardcoded names #define REGISTER_NAME(num, name) ENameToEntry[num] = Store(FNameStringView(#name, FCStringAnsi::Strlen(#name))); #include "UObject/UnrealNames.inl" #undef REGISTER_NAME ``` 查看UObject/UnrealNames.inl 里,根据字符串定位ByteProperty ida 按shift+f12 然后搜索ByteProperty 定位即可 ![](https://cdn.nlark.com/yuque/0/2024/png/22994699/1722823369982-1cb59b0f-3924-459b-9a7c-57b40b325f45.png) 发现成功定位到了FNamePool::FNamePool() 紧接着,去交叉引用它,看下调用到它的static FNamePool& GetNamePool() ![](https://cdn.nlark.com/yuque/0/2024/png/22994699/1722823483400-5506c1fc-1f1a-4d2d-aacf-6219e8262078.png) 在第四个位置成功找到了它 ![](https://cdn.nlark.com/yuque/0/2024/png/22994699/1722823516640-c759d398-2fa4-41bc-bd12-86bff0a28b9d.png) 为什么确定是它呢,我们看下源码 ![](https://cdn.nlark.com/yuque/0/2024/png/22994699/1722823541180-1f6afd2b-d138-46a4-a7bc-b2679827d896.png) 在源码当中它返回了new的FNamePool函数指针,所以这里很明显是它 我们计算一下它的偏移 在程序当中的偏移为0x144A99140 程序基地址查看方式 在ida--->options--->General ![](https://cdn.nlark.com/yuque/0/2024/png/22994699/1722823681178-ffa5d271-7df0-431a-b09f-440a6517d0cb.png) 可以看到程序基地址是0x140000000 然后偏移就是0x144A99140-0x140000000,再用动态基地址+这个偏移 就是真正的FNamePool(GName)了 二、过反调试 ------ DeathlyStillnessGame有个反调试,这里我们用ida patch掉,在ida当中查询FindWindow ![](https://cdn.nlark.com/yuque/0/2024/png/22994699/1722827805911-81bb470b-f0ca-498b-9a0f-0d4550acd00a.png) 在ida当中看到,它对WinDbgFrameClass OLLYDBG 进行了检测,这种情况绕过的方法,就是直接把这个函数patch掉,让这个函数直接返回ret 不让它执行下面的代码就可以了,patch如下 ![](https://cdn.nlark.com/yuque/0/2024/png/22994699/1722828252328-d7e48338-10dd-4ea8-9298-33cf36e0c13d.png) 然后apply下就可以了,我们发现已经成功打开了olldbg ![](https://cdn.nlark.com/yuque/0/2024/png/22994699/1723085412550-9e76d8b1-7c46-4ba6-a2f1-0d7e20df17a0.png) 三、加解密算法实现 --------- 这里来实现下加解密的实现用来完成字符串提取的工作,下面我们来分析下各个加密的关键部分 1\. FNameEntryHandle 结构体 ```c struct FNameEntryHandle { uint32 Block = 0; uint32 Offset = 0; FNameEntryHandle(uint32 InBlock, uint32 InOffset) : Block(InBlock) , Offset(InOffset) {} FNameEntryHandle(FNameEntryId Id) : Block(Id.ToUnstableInt() >> FNameBlockOffsetBits) , Offset(Id.ToUnstableInt() & (FNameBlockOffsets - 1)) {} }; ``` 2\. FNameEntry 类 ```c class FNameEntry { public: static FString EncryptName(const FString& InName); static FString DecryptName(const FString& InName); }; ``` 3\. FName 类 ```c class FName { private: FString NamePrivate; public: FName(const FString& InName) : NamePrivate(InName) {} FString GetName() const; FString ToString() const; const FNameEntry* GetDisplayNameEntry() const; }; ``` 加密和解密算法 假设我们的加密算法是简单的字符移位。根据我们的需求,我们将使用上述结构体和类实现加密和解密逻辑。 FName.h ```c #pragma once #include <string> #include <vector> class FString : public std::string { public: using std::string::string; int32_t Len() const { return static_cast<int32_t>(this->length()); } }; class FNameEntry { public: static FString EncryptName(const FString& InName); static FString DecryptName(const FString& InName); }; struct FNameEntryId { uint32_t Id; FNameEntryId(uint32_t InId) : Id(InId) {} uint32_t ToUnstableInt() const { return Id; } }; struct FNameEntryHandle { uint32_t Block = 0; uint32_t Offset = 0; FNameEntryHandle(uint32_t InBlock, uint32_t InOffset) : Block(InBlock) , Offset(InOffset) {} FNameEntryHandle(FNameEntryId Id) : Block(Id.ToUnstableInt() >> 16) , Offset(Id.ToUnstableInt() & ((1 << 16) - 1)) {} }; class FNamePool { private: std::vector<std::vector<uint8_t*>> Blocks; static constexpr uint32_t Stride = 2; public: FNameEntry& Resolve(FNameEntryHandle Handle) const { return *reinterpret_cast<FNameEntry*>(Blocks[Handle.Block] + Stride * Handle.Offset); } }; class FName { private: FString NamePrivate; public: FName(const FString& InName) : NamePrivate(InName) {} FString GetName() const { return GetFName().ToString(); } FName GetFName() const { return *this; } FString ToString() const { return NamePrivate; } const FNameEntry* GetDisplayNameEntry() const { // Simplified implementation, as we don't have the full name pool logic static FNameEntry Entry; return &Entry; } }; ``` FName.cpp ```c #include "FName.h" FString FNameEntry::EncryptName(const FString& InName) { FString EncryptedName = InName; for (int32_t i = 0; i < InName.Len(); ++i) { EncryptedName[i] = InName[i] + 1; } return EncryptedName; } FString FNameEntry::DecryptName(const FString& InName) { FString DecryptedName = InName; for (int32_t i = 0; i < InName.Len(); ++i) { DecryptedName[i] = InName[i] - 1; } return DecryptedName; } ``` Main.cpp ```c #include "FName.h" #include <iostream> int main() { FString OriginalName = "ExampleName"; FName Name(OriginalName); FString EncryptedName = FNameEntry::EncryptName(Name.ToString()); FString DecryptedName = FNameEntry::DecryptName(EncryptedName); std::cout << "Original Name: " << OriginalName << std::endl; std::cout << "Encrypted Name: " << EncryptedName << std::endl; std::cout << "Decrypted Name: " << DecryptedName << std::endl; return 0; } ``` CMakeLists.txt ```c cmake_minimum_required(VERSION 3.10) project(GNameEncryptor) set(CMAKE_CXX_STANDARD 17) add_executable(GNameEncryptor main.cpp FName.cpp) ``` **分析** 1.FNameEntryHandle: 根据给定的FNameEntryId计算Block和Offset。 2.FNamePool::Resolve: 解析FNameEntryHandle以获得对应的FNameEntry。 3.FNameEntry::EncryptName和FNameEntry::DecryptName: 实现了简单的字符移位加密和解密算法。 4.FName类: 包含名称管理逻辑,主要用于演示如何获取加密和解密后的名称。 自此整个GName加密算法分析完成 总结:本次任务是基于UE4.27.2源码中的FName和FNameEntry结构,实现一个独立的GName加密算法项目。我们通过分析源码,提取关键部分,重构并实现了加密和解密算法。
发表于 2024-08-28 09:00:00
阅读 ( 1559 )
分类:
代码审计
1 推荐
收藏
0 条评论
请先
登录
后评论
Azd
4 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!