UnrealEngine POLYGON 全逆向笔记

admin 2024年7月24日22:31:23评论15 views字数 10555阅读35分11秒阅读模式

本文仅用于交流学习请勿用于不法用途,如有侵权联系作者删除。


准备阶段

游戏环境

1.在Steam下载并安装POLYGON
2.找到游戏目录POLYGONPOLYGONBinariesWin64
3.找到游戏文件POLYGON-Win64-Shipping.exe
4.拖进IDA等待漫长分析过程

开发环境

1.Visual Studio 2022
2.C++20

逆向环境

1.Cheat Engine
2.IDA Pro
3.Inject Tool

[!TIP]

游戏有EasyAntiCheat保护


开始

Directx Virtual Table

1、直接打开CE添加这几个地址,确认游戏是基于Dx11

UnrealEngine POLYGON 全逆向笔记

(1)找到ImGui官方源代码,编译一份example_win32_directx11.exe,手动增加一下SwapChain地址输出.官方库地址:https://github.com/ocornut/imgui

(2)运行得到输出:g_pSwapChain:00007FFA24E17000

(3)在CE中搜索并进行指针扫描

UnrealEngine POLYGON 全逆向笔记

UnrealEngine POLYGON 全逆向笔记

2、重新开游戏简单筛选一下,两个应该是都可以用的。

UnrealEngine POLYGON 全逆向笔记

3、最终选用

UnrealEngine POLYGON 全逆向笔记

[!NOTE]

在同一台设备上,Dx虚表位置**“固定”**

GName

[!IMPORTANT]

有关GName的寻找原理请参见前文,本文不做赘述

(1)先通过字符串熟练地找到void __fastcall FNamePool_FNamePool(__int64 a1)

(2)分析交叉引用,如下表:

UnrealEngine POLYGON 全逆向笔记

[!NOTE]

Down表示当前函数调用了FNamePool_FNamePool函数。

Up表示FNamePool_FNamePool函数被当前函数调用。

(1)依次看看几个Up调用内容,不要找太长的函数,也尽量避开明显提到其他组件的函数,要时时刻刻切记我们寻找的只是简单的通过:

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

这种方式进行的构造函数调用,在为数不多的Up调用中寻找到以下符合要求的伪函数:

_DWORD *__fastcall sub_142B04A20(_DWORD *a1, _BYTE *a2){  bool v2; // zf  _BYTE *v3; // r8  __int64 v5; // rax  int v6; // eax  RTL_SRWLOCK *v7; // rax  _DWORD *result; // rax  const char *v9; // [rsp+20h] [rbp-18h] BYREF  int v10; // [rsp+28h] [rbp-10h]  char v11; // [rsp+2Ch] [rbp-Ch]  int v12; // [rsp+40h] [rbp+8h] BYREF  int v13; // [rsp+44h] [rbp+Ch]  v2 = *a2 == 0;  v3 = a2 + 2;  v9 = a2 + 2;  v5 = -1i64;  if ( v2 )  {    do      ++v5;    while ( v3[v5] );    v11 = 0;  }  else  {    do      ++v5;    while ( *(_WORD *)&v3[2 * v5] );    v11 = 1;  }  v10 = v5;  if ( (unsigned int)v5 < 0x400 )  {    if ( byte_148089CF9 )    {      v7 = &stru_1480AD880;    }    else    {      FNamePool_FNamePool((__int64)&stru_1480AD880);      byte_148089CF9 = 1;    }    sub_142B16A60(v7, &v12, &v9);    v13 = v12;    v6 = v12;  }  else  {    v10 = 24;    v9 = "ERROR_NAME_SIZE_EXCEEDED";    v11 = 0;    v6 = sub_142B0CCB0(&v9, 1i64);  }  *a1 = v6;  result = a1;  a1[1] = 0;  return result;}

(2)GName偏移通过计算0x1480AD880-0x140000000=0x80AD880得到,偏移为0x80AD880

(3)我们开CheatEngine简单检验一下,也确实是这个结果,我们有充分的理由认为,GName地址是“POLYGON-Win64-Shipping.exe”+0x80AD880

(4)通过以下代码验证GName正确性:

std::string GetName(uint32_t Id){    uint32_t Block = Id >> 16;    uint32_t Offset = Id & 65535;    uint8_t* GameBase = (uint8_t*)GetModuleHandleA("POLYGON-Win64-Shipping.exe");    uint8_t** GName = (uint8_t**)(GameBase + 0x80AD880);    FNameEntry* Info = (FNameEntry*)((GName)[2 + Block] + 2 * Offset);    return std::string(Info->AnsiName, Info->Len);} printf("Name:%sn", GetName(0).c_str());

得到输出:

Name:Non

GWorld

1、在UnrealEngine.cpp源代码中寻找如下函数:

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

它以UWorld*作为返回值,在函数中有大量明文字符串可用来作为特征寻找该函数。

[!NOTE]

如果你发现你在IDA中无法搜索到这些字符串,请设置一下识别的字符串风格,把unicode加进去

2、

.rdata:00000001470E6BE0 aANullObjectWas:                        ; DATA XREF: sub_144FEE480+1F↑o.rdata:00000001470E6BE0                 text "UTF-16LE", 'A null object was passed as a world context object '.rdata:00000001470E6C46                 text "UTF-16LE", 'to UEngine::GetWorldFromContextObject().',0
__int64 __fastcall sub_144FEE480(__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  v4 = a2;  if ( a2 )  {    LOBYTE(v15) = 1;    if ( a3 == 2 )      v6 = sub_142C63C80(a2, &v15);    else      v6 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)a2 + 392i64))(a2);    v7 = v6;    if ( !(_BYTE)v15 )      return qword_1482ACFD0;    if ( !v6 && a3 == 1 )    {      sub_142CD1520(v4, &v13, 0i64);      v8 = &chText;      v9 = &chText;      if ( v14 != (_DWORD)v7 )        v9 = v13;      sub_1429C9990(&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_142CAF290(v8, v10, 0i64);      if ( v11 )        sub_142A062C0();      if ( v13 )        sub_142A062C0();    }    if ( !(_BYTE)v15 )      return qword_1482ACFD0;    return v7;  }  else  {    if ( a3 == 1 )    {      v15 = 0i64;      LOBYTE(a2) = 3;      sub_142CAF290(        L"A null object was passed as a world context object to UEngine::GetWorldFromContextObject().",        a2,        0i64);    }    return 0i64;  }}

3、锁定return qword_1482ACFD0,直接计算0x1482ACFD0-0x0x140000000=0x82ACFD0,偏移为0x82ACFD0

GObject

(1)还是先看引擎源码,在UObjectHash.cpp,有GObject的定义:

// Global UObject array instanceFUObjectArray GUObjectArray;

(2)分析对GUObjectArray的引用,有很多含有字符串的函数可以作为寻找UObject的跳板,我选择了这个函数:

void UObjectBaseInit(){    SCOPED_BOOT_TIMING("UObjectBaseInit");    // Zero initialize and later on get value from .ini so it is overridable per game/ platform...    int32 MaxObjectsNotConsideredByGC = 0;    int32 SizeOfPermanentObjectPool = 0;    int32 MaxUObjects = 2 * 1024 * 1024; // Default to ~2M UObjects    bool bPreAllocateUObjectArray = false;     // To properly set MaxObjectsNotConsideredByGC look for "Log: XXX objects as part of root set at end of initial load."    // in your log file. This is being logged from LaunchEnglineLoop after objects have been added to the root set.    // Disregard for GC relies on seekfree loading for interaction with linkers. We also don't want to use it in the Editor, for which    // FPlatformProperties::RequiresCookedData() will be false. Please note that GIsEditor and FApp::IsGame() are not valid at this point.    if (FPlatformProperties::RequiresCookedData())    {        if (IsRunningCookOnTheFly())        {            GCreateGCClusters = false;        }        else        {            GConfig->GetInt(TEXT("/Script/Engine.GarbageCollectionSettings"), TEXT("gc.MaxObjectsNotConsideredByGC"), MaxObjectsNotConsideredByGC, GEngineIni);            // Not used on PC as in-place creation inside bigger pool interacts with the exit purge and deleting UObject directly.            GConfig->GetInt(TEXT("/Script/Engine.GarbageCollectionSettings"), TEXT("gc.SizeOfPermanentObjectPool"), SizeOfPermanentObjectPool, GEngineIni);        }        // Maximum number of UObjects in cooked game        GConfig->GetInt(TEXT("/Script/Engine.GarbageCollectionSettings"), TEXT("gc.MaxObjectsInGame"), MaxUObjects, GEngineIni);        // If true, the UObjectArray will pre-allocate all entries for UObject pointers        GConfig->GetBool(TEXT("/Script/Engine.GarbageCollectionSettings"), TEXT("gc.PreAllocateUObjectArray"), bPreAllocateUObjectArray, GEngineIni);    }    else    {#if IS_PROGRAM        // Maximum number of UObjects for programs can be low        MaxUObjects = 100000; // Default to 100K for programs        GConfig->GetInt(TEXT("/Script/Engine.GarbageCollectionSettings"), TEXT("gc.MaxObjectsInProgram"), MaxUObjects, GEngineIni);#else        // Maximum number of UObjects in the editor        GConfig->GetInt(TEXT("/Script/Engine.GarbageCollectionSettings"), TEXT("gc.MaxObjectsInEditor"), MaxUObjects, GEngineIni);#endif    }    if (MaxObjectsNotConsideredByGC <= 0 && SizeOfPermanentObjectPool > 0)    {        // If permanent object pool is enabled but disregard for GC is disabled, GC will mark permanent object pool objects        // as unreachable and may destroy them so disable permanent object pool too.        // An alternative would be to make GC not mark permanent object pool objects as unreachable but then they would have to        // be considered as root set objects because they could be referencing objects from outside of permanent object pool.        // This would be inconsistent and confusing and also counter productive (the more root set objects the more expensive MarkAsUnreachable phase is).        SizeOfPermanentObjectPool = 0;        UE_LOG(LogInit, Warning, TEXT("Disabling permanent object pool because disregard for GC is disabled (gc.MaxObjectsNotConsideredByGC=%d)."), MaxObjectsNotConsideredByGC);    }    // Log what we're doing to track down what really happens as log in LaunchEngineLoop doesn't report those settings in pristine form.    UE_LOG(LogInit, Log, TEXT("%s for max %d objects, including %i objects not considered by GC, pre-allocating %i bytes for permanent pool."),        bPreAllocateUObjectArray ? TEXT("Pre-allocating") : TEXT("Presizing"),        MaxUObjects, MaxObjectsNotConsideredByGC, SizeOfPermanentObjectPool);    GUObjectAllocator.AllocatePermanentObjectPool(SizeOfPermanentObjectPool);    GUObjectArray.AllocateObjectPool(MaxUObjects, MaxObjectsNotConsideredByGC, bPreAllocateUObjectArray);#if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE    UE::CoreUObject::Private::InitObjectHandles(GUObjectArray.GetObjectArrayCapacity());#endif    void InitGarbageElimination();    InitGarbageElimination();    void InitAsyncThread();    InitAsyncThread();    // Note initialized.    Internal::GetUObjectSubsystemInitialised() = true;    UObjectProcessRegistrants();}//上边的函数调用下边的函数void FUObjectArray::AllocateObjectPool(int32 InMaxUObjects, int32 InMaxObjectsNotConsideredByGC, bool bPreAllocateObjectArray){    check(IsInGameThread());    MaxObjectsNotConsideredByGC = InMaxObjectsNotConsideredByGC;    // GObjFirstGCIndex is the index at which the garbage collector will start for the mark phase.    // If disregard for GC is enabled this will be set to an invalid value so that later we    // know if disregard for GC pool has already been closed (at least once)    ObjFirstGCIndex = DisregardForGCEnabled() ? -1 : 0;    // Pre-size array.    check(ObjObjects.Num() == 0);    UE_CLOG(InMaxUObjects <= 0, LogUObjectArray, Fatal, TEXT("Max UObject count is invalid. It must be a number that is greater than 0."));    ObjObjects.PreAllocate(InMaxUObjects, bPreAllocateObjectArray);    if (MaxObjectsNotConsideredByGC > 0)    {        ObjObjects.AddRange(MaxObjectsNotConsideredByGC);    }}

(3)在IDA中定位到源码后,分析这部分:

if (MaxObjectsNotConsideredByGC <= 0 && SizeOfPermanentObjectPool > 0){    // If permanent object pool is enabled but disregard for GC is disabled, GC will mark permanent object pool objects    // as unreachable and may destroy them so disable permanent object pool too.    // An alternative would be to make GC not mark permanent object pool objects as unreachable but then they would have to    // be considered as root set objects because they could be referencing objects from outside of permanent object pool.    // This would be inconsistent and confusing and also counter productive (the more root set objects the more expensive MarkAsUnreachable phase is).    SizeOfPermanentObjectPool = 0;    UE_LOG(LogInit, Warning, TEXT("Disabling permanent object pool because disregard for GC is disabled (gc.MaxObjectsNotConsideredByGC=%d)."), MaxObjectsNotConsideredByGC);}//对应下面的伪代码 ReName了变量 if ( MaxObjectsNotConsideredByGC <= 0 && SizeOfPermanentObjectPool > 0 ) {   v1 = 0;   SizeOfPermanentObjectPool = 0;   if ( (unsigned __int8)byte_14807DBE0 >= 3u )   {     sub_142A654A0(&byte_14807DBE0, &off_146698318);     v1 = SizeOfPermanentObjectPool;   }}

(4)在MaxObjectsNotConsideredByGC = InMaxObjectsNotConsideredByGC时,类成员变量被赋值,对应伪代码:

dword_148153F28 = MaxObjectsNotConsideredByGC;//以下是类成员分布//  /** First index into objects array taken into account for GC.                           *///  int32 ObjFirstGCIndex;//  /** Index pointing to last object created in range disregarded for GC.                  *///  int32 ObjLastNonGCIndex;//  /** Maximum number of objects in the disregard for GC Pool *///  int32 MaxObjectsNotConsideredByGC;

(5)定位类索引首地址,就是类全局变量的地址,用0x148153F28-0x8-0x140000000=0x8153F20

UnrealEngine POLYGON 全逆向笔记

看雪ID:Euarno

https://bbs.kanxue.com/user-home-1001108.htm

*本文为看雪论坛优秀文章,由 Euarno 原创,转载请注明来自看雪社区
UnrealEngine POLYGON 全逆向笔记

原文始发于微信公众号(看雪学苑):UnrealEngine POLYGON 全逆向笔记

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月24日22:31:23
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   UnrealEngine POLYGON 全逆向笔记https://cn-sec.com/archives/2995677.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息