UnrealEngine POLYGON 全逆向笔记
2024-7-24 17:43:48 Author: mp.weixin.qq.com(查看原文) 阅读量:3 收藏

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


准备阶段

游戏环境

1.在Steam下载并安装POLYGON
2.找到游戏目录POLYGON\POLYGON\Binaries\Win64
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

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

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

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

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

3、最终选用

[!NOTE]

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

GName

[!IMPORTANT]

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

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

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

[!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:%s\n", 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

看雪ID:Euarno

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

*本文为看雪论坛优秀文章,由 Euarno 原创,转载请注明来自看雪社区

# 往期推荐

1、Alt-Tab Terminator注册算法逆向

2、恶意木马历险记

3、VMP源码分析:反调试与绕过方法

4、Chrome V8 issue 1486342浅析

5、Cython逆向-语言特性分析

球分享

球点赞

球在看

点击阅读原文查看更多


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458564856&idx=1&sn=904dd1ab3f274c3482d65d70601ba81b&chksm=b18d887286fa0164bae684f6bdda0e79672b9286baa1348fce6b41319876e12439c134c0bc5c&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh