初探UE4引擎逆向

前言:在现代游戏开发中,尤其是在像虚幻引擎(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里

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类型

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>
    {

搜索模板

重点类型

TArray{
    wchar_t* data;
    int num; //有多长
    int maxnum ;//最大长度
 }

所以说FString 返回的是TArray类

再看下GetFname

    FORCEINLINE FName GetFName() const
    {
        return NamePrivate;
    }

我们看下NamePrivate的类型,发现就是FName

FName NamePrivate;

这个FName在class COREUOBJECT_API UObjectBase 里,这里学习下计算偏移获取FName的位置

计算偏移方法用类首地址+虚表 函数 函数指针在64位下占8个字节,成员属性变量占具体属性的长度

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(); 跟进去

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()

const FNameEntry* FName::GetDisplayNameEntry() const
{
    return &GetNamePool().Resolve(GetDisplayIndex());
}

从GetDisplayIndex() 看

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()

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

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 去看下

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; 继续跟进

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,我们看下

class FNameEntryAllocator
{
public:
    enum { Stride = alignof(FNameEntry) };
    enum { BlockSizeBytes = Stride * FNameBlockOffsets };
}

发现Stride是通过alignof 枚举出FNameEntry,我们跟进去FNameEntry

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++程序计算如下:

#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()

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

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); 我们继续跟进这个函数

void FNameEntry::CopyUnterminatedName(WIDECHAR* Out) const
{
    FPlatformMemory::Memcpy(Out, WideName, sizeof(WIDECHAR) * Header.Len);
    Decode(Out, Header.Len);
}

发现就是赋值的操作,具体没多少用处这里,这里理解下就可以了,至此我们已经分析了GetName加密算法的整个流程

填一下之前的坑,找到FNamePool 也就是GName

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里

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 定位即可

发现成功定位到了FNamePool::FNamePool()

紧接着,去交叉引用它,看下调用到它的static FNamePool& GetNamePool()

在第四个位置成功找到了它

为什么确定是它呢,我们看下源码

在源码当中它返回了new的FNamePool函数指针,所以这里很明显是它

我们计算一下它的偏移

在程序当中的偏移为0x144A99140

程序基地址查看方式 在ida--->options--->General

可以看到程序基地址是0x140000000

然后偏移就是0x144A99140-0x140000000,再用动态基地址+这个偏移 就是真正的FNamePool(GName)了

二、过反调试

DeathlyStillnessGame有个反调试,这里我们用ida patch掉,在ida当中查询FindWindow

在ida当中看到,它对WinDbgFrameClass OLLYDBG 进行了检测,这种情况绕过的方法,就是直接把这个函数patch掉,让这个函数直接返回ret 不让它执行下面的代码就可以了,patch如下

然后apply下就可以了,我们发现已经成功打开了olldbg

三、加解密算法实现

这里来实现下加解密的实现用来完成字符串提取的工作,下面我们来分析下各个加密的关键部分

1. FNameEntryHandle 结构体

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 类

class FNameEntry
{
public:
    static FString EncryptName(const FString& InName);
    static FString DecryptName(const FString& InName);
};

3. FName 类

class FName
{
private:
    FString NamePrivate;

public:
    FName(const FString& InName) : NamePrivate(InName) {}

    FString GetName() const;
    FString ToString() const;
    const FNameEntry* GetDisplayNameEntry() const;
};

加密和解密算法

假设我们的加密算法是简单的字符移位。根据我们的需求,我们将使用上述结构体和类实现加密和解密逻辑。

FName.h

#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

#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

#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

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
  • 阅读 ( 3544 )
  • 分类:代码审计

0 条评论

请先 登录 后评论
Azd
Azd

4 篇文章

站长统计