虚幻引擎逆向一、查找GName\GWorld

  本文是逆向学习的记录笔记

一、从源码分析内存结构

  Unreal的对象继承结构是UObject继承UObjectBaseUtility继承UObjectBase。

//UObjectBaseUtility.h
FORCEINLINE FString GetName() const
{
    return GetFName().ToString();
}
//UObjectBase.h
FORCEINLINE FName GetFName() const
{
    return NamePrivate;
}

//对象布局:
EObjectFlags                    ObjectFlags;//4Bytes
int32                           InternalIndex;//4Bytes
ObjectPtr_Private::TNonAccessTrackedObjectPtr<UClass>                           ClassPrivate;//指针8Bytes
FName                           NamePrivate;

  可以看到上述UObjectBase中,FName在16字节偏移处,但UObjectBase有虚函数,对象最前面还会有8字节的虚函数表指针,所以是24字节偏移,写成16进制就是0x18
  FName转换FString(删了一些检查代码):

FString FName::ToString() const
{
    return GetDisplayNameEntry()->GetPlainNameString();
}

const FNameEntry* FName::GetDisplayNameEntry() const
{
    return ResolveEntryRecursive(GetDisplayIndexInternal());
}

FORCEINLINE FNameEntryId FName::GetDisplayIndexInternal() const
{
#if WITH_CASE_PRESERVING_NAME
    return DisplayIndex;
#else // WITH_CASE_PRESERVING_NAME
    return ComparisonIndex;
#endif // WITH_CASE_PRESERVING_NAME
}

const FNameEntry* FName::ResolveEntry(FNameEntryId LookupId)
{
    return &GetNamePool().Resolve(LookupId);
}

const FNameEntry* FName::ResolveEntryRecursive(FNameEntryId LookupId)
{
    const FNameEntry* Entry = ResolveEntry(LookupId);
#if UE_FNAME_OUTLINE_NUMBER
    if (Entry->Header.Len == 0)
    {
        return ResolveEntry(Entry->GetNumberedName().Id); // Should only ever recurse one level
    }
    else
#endif
    {
        return Entry;
    }
}

class FName
{
//...
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

  可以发现,上述代码中,FName本质是存储一个ID,到全局的NamePool(GName)中取得FNameEntry再转换成FString。

class FNamePool
{
public:
    FNameEntry&     Resolve(FNameEntryHandle Handle) const { return Entries.Resolve(Handle); }
private:
    FNameEntryAllocator Entries;
}

class FNameEntryAllocator
{
public:
FNameEntry& Resolve(FNameEntryHandle Handle) const
{
    return *reinterpret_cast<FNameEntry*>(Blocks[Handle.Block] + Stride * Handle.Offset);
}

static constexpr uint32 FNameBlockOffsetBits = 16;
static constexpr uint32 FNameBlockOffsets = 1 << FNameBlockOffsetBits;
struct FNameEntryHandle
{
    uint32 Block = 0;
    uint32 Offset = 0;

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

struct FNameEntryId
{
private:
    uint32 Value;
}

  上述分析发现,FNameEntryId本质是一个uint32,上述resolve时,高16Byte视为Block,低16Byte视为Offset,到FNamePool中的FNameEntryAllocator中的Blocks取FNameEntry。
  有一点坑要注意,Resolve时用到的Stride,定义如下:

enum { Stride = alignof(FNameEntry) };

  在IDE中看可能是4,但是看定义如下:

struct FNameEntry
{
private:
#if WITH_CASE_PRESERVING_NAME
    FNameEntryId ComparisonId;
#endif
    FNameEntryHeader Header;

    // Unaligned to reduce alignment waste for non-numbered entries
    struct FNumberedData
    {
#if UE_FNAME_OUTLINE_NUMBER 
#if WITH_CASE_PRESERVING_NAME // ComparisonId is 4B-aligned, 4B-align Id/Number by 2B pad after 2B Header
        uint8 Pad[sizeof(Header) % alignof(decltype(ComparisonId))]; 
#endif                      
        uint8 Id[sizeof(FNameEntryId)];
        uint8 Number[sizeof(uint32)];
#endif // UE_FNAME_OUTLINE_NUMBER   
    };

    union
    {
        ANSICHAR            AnsiName[NAME_SIZE];
        WIDECHAR            WideName[NAME_SIZE];
        FNumberedData       NumberedName;
    };

  其中WITH_CASE_PRESERVING_NAME宏只在编辑器生效,因此实际的Stride值应该是2。

二、逆向

1. 找到FNamePool的全局静态变量地址

1.1 IDA静态查找方法

  逆向过程中,先用ida找到FNamePool的构造函数调用,找到FNamePool的全局静态变量地址:

static bool bNamePoolInitialized;
alignas(FNamePool) static uint8 NamePoolData[sizeof(FNamePool)];

static FNamePool& GetNamePool()
{
    if (bNamePoolInitialized)
    {
        return *(FNamePool*)NamePoolData;
    }

    FNamePool* Singleton = nullptr;
    
    UE_AUTORTFM_OPEN
    {
        Singleton = new (NamePoolData) FNamePool;
        bNamePoolInitialized = true;
        LLM(FLowLevelMemTracker::Get().FinishInitialise());
    };
    
    return *Singleton;
}

  如何找FNamePool构造函数地址,可以用Shift+F12或Names窗口,搜索ByteProperty,这个是构造函数中默认添加的硬编码Name:

FNamePool::FNamePool()
{
    // Register all hardcoded names
#define REGISTER_NAME(num, name) ENameToEntry[num] = Store(FNameStringView(#name, FCStringAnsi::Strlen(#name)));
#include "UObject/UnrealNames.inl"
#undef REGISTER_NAME
// UnrealNames.inl
// Special zero value, meaning no name.
REGISTER_NAME(0,None)

// Class property types (name indices are significant for serialization).
REGISTER_NAME(1,ByteProperty)
REGISTER_NAME(2,IntProperty)
REGISTER_NAME(3,BoolProperty)
REGISTER_NAME(4,FloatProperty)
//...

  找到后选择ByteProperty的全局静态变量,按X,用xrefs找到调用这个变量的地方,既是FNamePool的构造函数,可以用F5将汇编转为C代码。



  对这个构造函数按X,用xrefs查找构造函数调用处(或者View>Open subviews>Functions),可能结果有多个,可以随意进入一个,能发现类似这样的代码:

    if ( byte_144A3AA64 )
    {
      v8 = &stru_144A56400;
    }
    else
    {
      v8 = (RTL_SRWLOCK *)sub_141055BD0((__int64)&stru_144A56400);
      byte_144A3AA64 = 1;
    }

  对比UE代码能猜测到,这是一个只初始化一次的写法,在上述例子中,byte_144A3AA64bNamePoolInitialized,用来判断是否已初始化;sub_141055BD0是构造函数;stru_144A56400就是全局静态内存的位置,既alignas(FNamePool) static uint8 NamePoolData[sizeof(FNamePool)];
  还需要计算这个地址相对于程序集的偏移,用IDA的Edit>Segments>Rebase program可以获得基址:


  计算0x144A56400-0x140000000=0x4A56400
  可以用CE验证一下,因为我换了个游戏版本,假设上述获取的偏移地址为0x431BAC0,添加地址"exe名称"+0x431BAC0,这个地址是上面刚算的偏移,获取的是FNamePool,也就是GName的地址,将其数值加10(16进制),写成"exe名称"+0x431BAD0,即可偏移到Blocks的内容,因为FNamePool首个成员就是FNameEntryAllocator对象Entries。
  FNameEntryAllocator需要偏移16字节(0x10)获得Blocks的地址:

class FNameEntryAllocator
{
    mutable FRWLock Lock;//8
    uint32 CurrentBlock = 0;//4
    uint32 CurrentByteCursor = 0;//4
    uint8* Blocks[FNameMaxBlocks] = {};

  获取地址注意要用8字节,默认4字节获得的地址是错误的:



  用内存视图可以右键Display Type改成8byte,然后找到加0x10后的内存,右键值,点击Follow。


  即可看到熟悉的内容,这个Blocks中,第一个Block的字符串池:


  None前面的两字节搞不懂的话,可以看FNameEntry的定义,用位域的语法,用16bit存了bIsWideLowercaseProbeHashLen三个变量。Block里存的不是字符串而是FNameEntry,这个不要搞混了。

1.2 CheatEngine动态查找方法:

  上一节是用CE验证方法的正确性,这一节是直接用CE找NamePool的地址
  CE附加游戏,全局搜索ByteProperty,类型字符串,可能搜到好几十个。对每个右键>Browse this memory region(浏览相关内存区域),观察MemoryView中,内存是否像上文中静态查找的内存区域,比如前面是否有None字符串,后面是否有IntProperty等。


  找到后,根据上一节的经验,找到*.None(要从None往前两个字节)右键goto address获取当前地址,得到地址后,全局搜索8Bytes的这个地址261F9F30000,于是得到"exe名称"+431BAD0,这个结果和上一节相同,431BAD0是Blocks的地址,将其减0x10就是NamePool的地址。

2. 查找GWorld的方法

  引擎中代码如下:

UWorld* UEngine::GetWorldFromContextObject(const UObject* Object, EGetWorldErrorMode ErrorMode) const
{
    if (Object == nullptr)
    {
        switch (ErrorMode)
        {
        case EGetWorldErrorMode::Assert:
            check(Object);
            break;
        case EGetWorldErrorMode::LogAndReturnNull:
            FFrame::KismetExecutionMessage(TEXT("A null object was passed as a world context object to UEngine::GetWorldFromContextObject()."), ELogVerbosity::Warning);
            //UE_LOG(LogEngine, Warning, TEXT("UEngine::GetWorldFromContextObject() passed a nullptr"));
            break;
        case EGetWorldErrorMode::ReturnNull:
            break;
        }
        return nullptr;
    }

    bool bSupported = true;
    UWorld* World = (ErrorMode == EGetWorldErrorMode::Assert) ? Object->GetWorldChecked(/*out*/ bSupported) : Object->GetWorld();
    if (bSupported && (World == nullptr) && (ErrorMode == EGetWorldErrorMode::LogAndReturnNull))
    {
        FFrame::KismetExecutionMessage(*FString::Printf(TEXT("No world was found for object (%s) passed in to UEngine::GetWorldFromContextObject()."), *GetPathNameSafe(Object)), ELogVerbosity::Warning);
    }
    return (bSupported ? World : GWorld);
}

  在Names视图中搜索ANullObjectWas可定位到变量,或者在Strings视图搜索,不过要注意,A null object was……字符串被TEXT宏包围,意味着是宽字符,需要在Strings中任意右键Setup,在string types中勾选Unicode C-style (16-bits)
  用xrefs定位引用到:

__int64 __fastcall sub_1427F1530(__int64 a1, __int64 a2, int a3)
{
  __int64 v4; // rsi
  __int64 v6; // rax
  __int64 v7; // rdi
  const wchar_t *v8; // rbx
  const wchar_t *v9; // r8
  __int64 v10; // rdx
  const wchar_t *v11; // [rsp+20h] [rbp-28h] BYREF
  int v12; // [rsp+28h] [rbp-20h]
  const wchar_t *v13; // [rsp+30h] [rbp-18h] BYREF
  int v14; // [rsp+38h] [rbp-10h]
  __int64 v15; // [rsp+58h] [rbp+10h] BYREF
  __int64 v16; // [rsp+68h] [rbp+20h]

  v4 = a2;
  if ( a2 )
  {
    LOBYTE(v15) = 1;
    if ( a3 == 2 )
      v6 = sub_140F63220(a2, &v15);
    else
      v6 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)a2 + 344i64))(a2);
    v7 = v6;
    if ( !(_BYTE)v15 )
      return qword_14446D5E0;
    if ( !v6 && a3 == 1 )
    {
      v16 = 0i64;
      sub_140FBBA70(v4, &v13, 0i64);
      v8 = &String;
      v9 = &String;
      if ( v14 != (_DWORD)v7 )
        v9 = v13;
      sub_140C9D710(&v11, L"No world was found for object (%s) passed in to UEngine::GetWorldFromContextObject().", v9);
      LOBYTE(v10) = 3;
      if ( v12 != (_DWORD)v7 )
        v8 = v11;
      sub_140FA1A50(v8, v10, v7);
      if ( v11 )
        sub_140CDC2B0();
      if ( v13 )
        sub_140CDC2B0();
    }
    if ( !(_BYTE)v15 )
      return qword_14446D5E0;
    return v7;
  }
  else
  {
    if ( a3 == 1 )
    {
      v15 = 0i64;
      LOBYTE(a2) = 3;
      sub_140FA1A50(
        L"A null object was passed as a world context object to UEngine::GetWorldFromContextObject().",
        a2,
        0i64);
    }
    return 0i64;
  }
}

  对比代码,上述的qword_14446D5E0就是GWorld,用GName同样方法减去基址就能得到偏移hex(0x14446D5E0-0x140000000)=0x446d5e0

2.1 DumpSDK

  利用工具将引擎中的数据结构dump出来帮助分析,可以用Dumper-7,编译dll,注入UE游戏中,然后就能在C:\Dumper-7\<游戏目录>\CppSDK找到dump出的文件了。
  例如能看到ULevel* PersistentLevelUWorld中的偏移在0x30处:

// Class Engine.World
// 0x0760 (0x0788 - 0x0028)
class UWorld final : public UObject
{
public:
    uint8                                         Pad_28[0x8];                                       // 0x0028(0x0008)
    class ULevel*                                 PersistentLevel;                                   // 0x0030(0x0008)

  AActor的数组在0x98的位置:

// Class Engine.Level
// 0x0270 (0x0298 - 0x0028)
class ULevel final : public UObject
{
public:
    uint8                                         Pad_28[0x70];                                      // 0x0028(0x0070)
    class TArray<class AActor*>                   Actors;                                            // 0x0098(0x0010)

  TArray的结构是指针、当前大小、总体容量:

template<typename ArrayElementType>
class TArray
{
protected:
    ArrayElementType* Data;
    int32 NumElements;
    int32 MaxElements;

  在下一节中,我们利用CE来验证这个结果。

2.2 CE分析

  CE附加到游戏中,添加地址"游戏.exe"+0x446d5e0,值是UWorld*,所以设定值为8字节,展示为16进制。
  将指针指向的值复制出来,到MemoryView中,使用Tools>Dissect data/structures分析结构和数据,Group中拷贝刚刚的值,Sturctures>Define new structure填入4096。
  注:有可能无法自动分析出结构,需要自己在指定偏移处右键ChangeType。


  上图可以看到,在UWorld偏移0x30处得到ULevel,再通过0x98得到TArray<AActor>,可以看到在这个开始页面,AActor有49个,容量最高是88。
  根据SDK,AActor的0x130的偏移是USceneComponent*,可获取到偏移数据FTransform,但实际上Dumper未分析出来这个数据的偏移是多少。
  我们可以先定位到AActor的首地址,按空格在Memory Browse中跳转位置,然后在Memory Browse中将显示值的类型改为Float(注:新的UE版本可能是Double类型的),查看哪里数据比较像FTransform:

struct FTransform
{
public:
    struct FQuat                                  Rotation;                                          // 0x0000(0x0010)
    struct FVector                                Translation;                                       // 0x0010(0x000C)
    uint8                                         Pad_1C[0x4];                                       // 0x001C(0x0004)
    struct FVector                                Scale3D;                                           // 0x0020(0x000C)
    uint8                                         Pad_2C[0x4];                                       // 0x002C(0x0004)
};

  可以看到前四个字节是四元数,16字节偏移处是世界坐标,根据UnrealEngine的尺度,单位是1cm,因此很容易出现成千上万的高数值,32字节偏移处是缩放,一般都是1,1,1,根据这个特征,在我的游戏中能看到FTransform偏移是在0x1C0处。
  SDK中我可以手动修复结构:

//uint8                                       Pad_152[0xA6];                                     // 0x0152(0x00A6)(Fixing Struct Size After Last Property [ Dumper-7 ])
uint8                                         Pad_152[0x6E];                                     // 0x0152(0x006E)
FTransform                                    ComponentToWorld;                                  // 0x01C0(0x0030)
uint8                                         Pad_1F0[0x8];                                      // 0x01F0(0x0008)

三、程序获取

3.1 用上述获取的信息编写方法:
#include "engine.h"
#include "SDK/Engine_classes.hpp"//Dumper-7导出的文件
#include <Windows.h>

namespace Engine
{
    uint8_t* GameBase = (uint8_t*)GetModuleHandleA(0);
    uint8_t** FNamePool = (uint8_t**)(GameBase + 0x431BAC0);
    SDK::UWorld** GWorld = (SDK::UWorld**)(GameBase + 0x446d5e0);

    struct FNameEntry {
        uint16_t bIsWide : 1;
        uint16_t LowercaseProbeHash : 5;
        uint16_t Len : 10;

        union {
            char AnsiName[1024];
            wchar_t WideName[1024];
        };
    };
    };

    std::string GetName(uint32_t Id) {
        uint32_t Block = Id >> 16;
        uint32_t Offset = Id & 65535;

        // "2 + "是因为GName是uint8_t*的数组,一个偏移8Byte,两个偏移0x10
        FNameEntry* Info = (FNameEntry*)(FNamePool[2 + Block] + 2 * Offset);
        return std::string(Info->AnsiName, Info->Len);
    }

    SDK::UWorld* GetWorld( ) {
        return *GWorld;
    }
}

3.2 用IMGUI显示

  推荐找一个IMGUI的通用hook框架,我用的是UniversalHookX
  menu.cppMenu::Render方法主要用来编写显示逻辑。
  获取UWorld:

void Render( ) {
    if (!bShowMenu)
        return;

    if (ImGui::Begin("逆向工具 - AActor 查看器", &bShowMenu)) {
        // 显示基本信息
        SDK::UWorld* World = Engine::GetWorld( );
        if (World == nullptr || World->PersistentLevel == nullptr) {
            ImGui::Text("无法获取 World 或 PersistentLevel");
            ImGui::End( );
            return;
        }

  添加一些选项:

SDK::TArray<SDK::AActor*>& Actors = World->PersistentLevel->Actors;
ImGui::Text("当前场景中的 Actor 数量: %d", Actors.Num( ));

// 添加一些控制选项
static bool bShowOnlyValidActors = true;
ImGui::Checkbox("仅显示有效 Actor", &bShowOnlyValidActors);

static bool bShowRelativeTransforms = true;
ImGui::Checkbox("显示相对变换", &bShowRelativeTransforms);

static bool bShowWorldTransforms = true;
ImGui::Checkbox("显示世界变换", &bShowWorldTransforms);

  遍历AActor列表:

if (ImGui::BeginChild("ActorList", ImVec2(0, 0), true)) {
    for (size_t i = 0; i < Actors.Num( ); ++i) {
        SDK::AActor* actor = Actors[i];

  显示Actor名称:

// 获取 Actor 名称
SDK::FName fname = actor->Name;
std::string actorName = Engine::GetName(fname.ComparisonIndex);

// 显示折叠菜单
if (ImGui::TreeNodeEx((void*)actor, 0, "[%d] %s", i, actorName.c_str( )))
{

  显示Transform信息:

if (bShowWorldTransforms) {
    SDK::FTransform& worldTransform = actor->RootComponent->ComponentToWorld;
    SDK::FVector worldLocation = worldTransform.Translation;
    SDK::FRotator worldRotation = QuatToEulerDeg(worldTransform.Rotation);
    SDK::FVector worldScale = worldTransform.Scale3D;

    ImGui::Separator( );
    ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.8f, 1.0f), "世界变换:");
    ImGui::Text("位置: X: %.2f, Y: %.2f, Z: %.2f",
                worldLocation.X, worldLocation.Y, worldLocation.Z);
    ImGui::Text("旋转: Pitch: %.2f, Yaw: %.2f, Roll: %.2f",
                worldRotation.Pitch, worldRotation.Yaw, worldRotation.Roll);
    ImGui::Text("缩放: X: %.2f, Y: %.2f, Z: %.2f",
                worldScale.X, worldScale.Y, worldScale.Z);
}

  上述UI逻辑编写完后,将其编译成dll注入到游戏中,即可看到UI菜单:


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容