Reverse Engineering for Unreal Engine
How to find GName, GUObjectArray, GWorld, and generate SDK on latest
Introduction
언리얼 엔진은 게임 개발에 사용되는 일반적으로 알려진 유니티 엔진과 다르게 좀 더 고성능 엔진으로 여겨지며 그만큼 바이너리를 리버싱하기 어렵다.
이번에는 최신 언리얼 엔진에 대하여 리버싱하기 위하여 필요한 각 오프셋 GName
, GUObejctArray
, GWorld
를 찾는 과정을 작성해보려고 한다. 또한 나아가 해당 오프셋을 기반으로 SDK 를 덤프하는 것을 진행해보려고 한다.
해당 블로그에서 사용한 언리얼 엔진은 5.5 - 5.6 을 기반으로 진행한다.
해당 문서는 학문적인 연구 목적으로 작성하였으며 사회에 반하는 행위에 대한 책임은 개인에게 있으며 악용될 우려가 있으므로 자세한 내용은 생략하여 작성합니다.
What is that
먼저 찾으려고 하는 것들에 대해서 간략하게 설명하면서 넘어가려고 한다.
GName
같은 경우 NamePool
data 전역변수로 게임 내 모든 오브젝트에 대하여 할당된 이름들을 가지고 있다. GUObectArray
같은 경우 오브젝트에 관한 정보가 들어있다고 한다. GWorld
같은 게임 레벨이나 부가적인 현금 가치들을 찾아볼 수 있다.
먼저 들어가기 전에 오프셋은 모바일 엔진에서 찾는 과정을 기록하려고 한다. 윈도우 환경 같은 경우 오프셋들이 바로 보여서 찾는 의미가 없기 때문에 모바일 엔진에서 오프셋을 찾는 방법을 시도하였다. 모바일 엔진 같은 경우 심볼들이 지워지고 이름들이 지워져서 오프셋 찾는데 꽤 고생하였다.
그러나 dump 는 PC 환경에서의 dump 를 진행했다. 맞는 환경에서 올바르게 덤프할 수 있는 도구들이 많이 없었고 이를 가장 올바르게 해줄 수 있는 도구는 윈도우 PC 환경이 유일헀다.
이때 SDK 를 덤프하면 Unreal Engine 이 어떻게 동작하는지, 어떤 클래스와 메소드가 있는지 볼 수 있다.
Understanding the Core Issue
이제부터 본격적으로 오프셋 찾는 여정을 시작해보려고 한다.
사실 처음 시작하면 굉장히 많이 고생할 수 있지만 현재 글을 쓰고 있는 필자도 누군가의 도움으로 결국 성공할 수 있었기 때문에 이 글이 누군가의 긴 새벽에 진주가 되길 바라는 마음에 시작한다. 하지만 맨 처음 작성하였는데 악용될 소지가 있어서 자세한 과정은 몇몇 생략하였다.
먼저 문자열 찾는데 쉽게 접근하려면 String view 에서 unicode C-style (16 bit)
를 선택한다. IDA Pro 를 사용했는데 분석 도구에 원리만 안다면 Ghidra 나 그 외에 도구를 사용해도 무방할 것 같다.
Version
버전 같은 경우 문자열 윈도우에서 손쉽게 release
라는 검색을 통해 아래와 같이 찾을 수 있다.
1
2
3
4
const __int16 *sub_166D098()
{
return L"++UE5+Release-5.5-CL-40574608";
}
현재 Unreal Engine 5.5 버전을 대상으로 진행하고 있다.
또는 Unreal Engine 대상이 Android Application 같은 경우 Androidmanifest 를 확인하면 볼 수 있다.
1
2
3
4
5
6
7
<meta-data
android:name="com.epicgames.unreal.GameActivity.EngineVersion"
android:value="5.5.4"/>
<meta-data
android:name="com.epicgames.unreal.GameActivity.EngineBranch"
android:value="++UE5+Release-5.5"/>
<meta-data
위에서 볼 수 있는 것처럼 엔진 버전을 확인할 수 있다.
GName
다음은 NamePool 을 찾아볼 것이다.
여기서부터는 엔진 코드를 같이 보면서 접근해야 한다.
원본 빌드를 볼 수 있지만 원본 코드를 통해 접근하는 것이 오프셋 찾는 데 좀 더 수월하게 접근할 수 있기 때문이다.
Unreal Engine 을 클론할 경우 공식 사이트에서 에픽게임즈 멤버로 초대되어야 한다.
이는 본 글에서 다루지 않겠다.
원본 코드는 여기를 참고하면 된다. 앤진 코드를 클론 받으면 아래와 같이 FNamePool 함수를 찾아볼 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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
// Make reverse mapping
LargestEnameUnstableId = 0;
for (uint32 ENameIndex = 0; ENameIndex < (uint32)EName::MaxHardcodedNameIndex; ++ENameIndex)
{
if (ENameIndex == (uint32)NAME_None || ENameToEntry[ENameIndex])
{
EntryToEName.Add(ENameToEntry[ENameIndex], (EName)ENameIndex);
LargestEnameUnstableId = FMath::Max(LargestEnameUnstableId, ENameToEntry[ENameIndex].ToUnstableInt());
}
}
// Verify all ENames are unique
if (NumAnsiEntries() != EntryToEName.Num())
{
// we can't print out here because there may be no log yet if this happens before main starts
if (FPlatformMisc::IsDebuggerPresent())
{
UE_DEBUG_BREAK();
}
else
{
FPlatformMisc::PromptForRemoteDebugging(false);
FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "DuplicatedHardcodedName", "Duplicate hardcoded name"));
FPlatformMisc::RequestExit(false, TEXT("FNamePool.DuplicateHardcodedName"));
}
}
}
이를 바이너리에서 찾아보면 아래와 같은 함수로 볼 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
__int64 __fastcall sub_1861BD8(__int64 a1)
{
{ . . . }
while ( v4 );
*v2 = sub_1863438(a1, "None", 4LL);
v2[1] = sub_1863438(a1, "ByteProperty", 12LL);
v2[2] = sub_1863438(a1, "IntProperty", 11LL);
v2[3] = sub_1863438(a1, "BoolProperty", 12LL);
v2[4] = sub_1863438(a1, "FloatProperty", 13LL);
v2[5] = sub_1863438(a1, "ObjectProperty", 14LL);
v2[6] = sub_1863438(a1, "NameProperty", 12LL);
v2[7] = sub_1863438(a1, "DelegateProperty", 16LL);
v2[8] = sub_1863438(a1, "DoubleProperty", 14LL);
v2[9] = sub_1863438(a1, "ArrayProperty", 13LL);
v2[10] = sub_1863438(a1, "StructProperty", 14LL);
v2[11] = sub_1863438(a1, "VectorProperty", 14LL);
v2[12] = sub_1863438(a1, "RotatorProperty", 15LL);
{ . . . }
if ( v13 - v18 != v2[2754] - v2[2777] )
{
sub_1762BE8(&v21, L"UnrealEd", 8LL);
sub_1762BE8(v20, L"DuplicatedHardcodedName", 23LL);
sub_174085C(&v22, &v21, v20, L"Duplicate hardcoded name");
sub_1817C40(0LL, &v22);
if ( v22 )
(*(void (__fastcall **)(unsigned int *))(*(_QWORD *)v22 + 24LL))(v22);
return sub_166D208(0LL, "F");
}
return result;
}
이때 v2[6] = sub_1863438(a1, "NameProperty", 12LL);
를 예로 들어본다면 맨 앞에 오는 매개변수가 NamePool 에 대한 포인터이다.
이를 호출하는 곳으로 찾아가면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 __fastcall sub_18638B0(int a1, int a2)
{
{ . . . }
v28 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
if ( (byte_888DA69 & 1) == 0 )
{
sub_1861BD8((__int64)&unk_888DA80); // Here call stack :)
byte_888DA69 = 1;
}
v4 = (unsigned __int16 *)(qword_888DAC0[HIWORD(a1)] + ((2 * a1) & 0x1FFFE));
v5 = (unsigned __int16 *)(qword_888DAC0[HIWORD(a2)] + ((2 * a2) & 0x1FFFE));
{ . . . }
}
sub_1861BD8
에 대하여 호출하는 곳을 확인해보면 unk_888DA80
를 볼 수 있는데 888DA80 가 오프셋이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
bool __fastcall sub_18654A8(unsigned __int64 a1, unsigned __int16 *a2, signed int a3)
{
{ . . . }
v8 = HIDWORD(a1);
v9 = (unsigned int)a1;
if ( (byte_888DA69 & 1) == 0 )
{
sub_1861BD8((__int64)&unk_888DA80); // Here another call stack
byte_888DA69 = 1;
}
{ . . . }
}
다른 호출부를 보면 먼저 접근 방식과 동일하게 오프셋을 확인할 수 있다.
GObject (GUObjectArray)
이번에는 GUObjectArray
를 찾아본다, GObject
도 비슷한 방법으로 찾으면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int32 FEngineLoop::PreInitPostStartupScreen(const TCHAR* CmdLine){
{ . . . }
#else // !WITH_ENGINE
#if WITH_COREUOBJECT
// Initialize the PackageResourceManager, which is needed to load any (non-script) Packages.
IPackageResourceManager::Initialize();
#endif
#endif // WITH_ENGINE
#if WITH_COREUOBJECT
if (GUObjectArray.IsOpenForDisregardForGC())
{
SCOPED_BOOT_TIMING("CloseDisregardForGC");
GUObjectArray.CloseDisregardForGC();
}
NotifyRegistrationComplete();
FReferencerFinder::NotifyRegistrationComplete();
#endif // WITH_COREUOBJECT
#if WITH_ENGINE
if (UOnlineEngineInterface::Get()->IsLoaded())
{
SetIsServerForOnlineSubsystemsDelegate(FQueryIsRunningServer::CreateStatic(&IsServerDelegateForOSS));
}
SlowTask.EnterProgressFrame(5);
{ . . . }
}
위 코드를 보면 GUObjectArray.CloseDisregardForGC();
와 같은 코드가 있는데 이를 바이너리 쪽에서 보면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
__int64 __fastcall sub_48642C0(__int64 a1)
{
{ . . . }
v130 = sub_17F5A44(&v236, "LoadStartupModules");
v131 = sub_48663EC(v130);
v132 = ((__int64 (__fastcall *)(__int16 **))sub_166D1F8)(&v236);
if ( (v131 & 1) == 0 )
goto LABEL_137;
if ( byte_88E67C4 )
{
sub_17F5A44(&v236, "CloseDisregardForGC");
sub_1AAC81C(&dword_88E67B8); // Here GUObjectArray
v132 = ((__int64 (__fastcall *)(__int16 **))sub_166D1F8)(&v236);
}
v133 = sub_1925D10(v132);
v134 = sub_1A83C84(v133);
v135 = sub_4197048(v134);
if ( ((*(__int64 (__fastcall **)(__int64, _QWORD))(*(_QWORD *)v135 + 720LL))(v135, 0LL) & 1) != 0 )
{
v234 = 0LL;
v235 = 0;
v136 = (_QWORD *)sub_16BDCF8(32LL, &v234);
*v136 = off_771AA28;
v137 = sub_16BDD50();
{ . . . }
}
sub_1AAC81C
에 넘어가는 dword_88E67B8
이 GUObjectArray
에 대한 8E67B8
오프셋이다.
아래와 같은 방법으로도 접근할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
}
}
AllocateObjectPool
를 역으로 호출하는 곳을 찾는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void UObjectBaseInit(){
{ . . . }
// 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."),
bPreAllocateUObjectArray ? TEXT("Pre-allocating") : TEXT("Presizing"), MaxUObjects, MaxObjectsNotConsideredByGC);
GUObjectArray.AllocateObjectPool(MaxUObjects, MaxObjectsNotConsideredByGC, bPreAllocateUObjectArray);
#if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE
UE::CoreUObject::Private::InitObjectHandles(GUObjectArray.GetObjectArrayCapacity());
#endif
{ . . . }
}
GUObjectArray.AllocateObjectPool
이렇게 호출되는데 바이너리쪽에서 보면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
__int64 __fastcall sub_1AAC578(__int64 a1, int a2, int a3, char a4) // call stack
{
int v7; // w8
__int64 result; // x0
int v9; // w20
int v10; // w21
int v11; // w22
int v12; // w8
int v13; // w24
int v14; // w22
__int64 v15; // x25
_QWORD *v16; // x0
unsigned __int64 v17; // x21
if ( a3 <= 0 )
v7 = 0;
else
v7 = -1;
*(_DWORD *)(a1 + 8) = a3;
*(_DWORD *)a1 = v7;
if ( a2 <= 0 )
sub_17B1018(
"./Runtime/CoreUObject/Private/UObject/UObjectArray.cpp",
124LL,
L"Max UObject count is invalid. It must be a number that is greater than 0.");
result = sub_1AAC6E0(a1 + 16, (unsigned int)a2, a4 & 1);
v9 = *(_DWORD *)(a1 + 8);
if ( v9 >= 1 )
{
v10 = *(_DWORD *)(a1 + 32);
v11 = *(_DWORD *)(a1 + 36) + v9;
if ( v11 > v10 )
{
if ( dword_88E5A20 )
sub_1AAE74C(&dword_88E67B8);
result = sub_17B1018(
"./Runtime/CoreUObject/Private/UObject/UObjectArray.cpp",
612LL,
L"Maximum number of UObjects (%d) exceeded when trying to add %d object(s), make sure you update MaxObjec"
"tsInGame/MaxObjectsInEditor/MaxObjectsInProgram in project settings.",
(unsigned int)v10,
(unsigned int)v9);
}
v12 = v11 + 65534;
v13 = *(_DWORD *)(a1 + 44);
if ( v11 - 1 >= 0 )
v12 = v11 - 1;
v14 = v12 >> 16;
while ( v14 >= v13 )
{
v15 = *(_QWORD *)(a1 + 16);
v16 = (_QWORD *)operator new(0x180008uLL);
*v16 = 0x10000LL;
v17 = (unsigned __int64)(v16 + 1);
memset(v16 + 1, 0, 0x180000uLL);
result = _aarch64_cas8_acq_rel(0LL, v17, (atomic_ullong *)(v15 + 8LL * v13));
v13 = *(_DWORD *)(a1 + 44);
if ( !result )
*(_DWORD *)(a1 + 44) = ++v13;
}
*(_DWORD *)(a1 + 36) += v9;
}
return result;
}
아래 함수에서 위의 sub_1AAC578
가 호출되는 곳을 찾아보는데
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__n128 sub_1AB08C0()
{
{ . . . }
sub_17D24A4(qword_8875430, "/", L"gc.MaxObjectsInGame", &v12, &qword_887FFE8);
sub_17B73E8(qword_8875430, "/", L"gc.PreAllocateUObjectArray", v11, &qword_887FFE8);
if ( !v13 )
byte_88E5A01 = 1;
v1 = sub_1AAC578((__int64)&dword_88E67B8, v12, v13, v11[0]); // Here
v2 = sub_1A16E58(v1);
sub_1924D88(v2);
byte_88E5A11 = 1;
sub_17F5A44(v17, "UObjectProcessRegistrants");
v3 = (_QWORD *)qword_88E5A70;
v15 = 0LL;
v16 = 0LL;
qword_88E5A68 = 0LL;
qword_88E5A70 = 0LL;
{ . . . }
}
맨 첫 매개변수 dword_88E67B8
가 GUObejctArray
에 대한 오프셋으로 동일하게 나온 것을 볼 수 있다.
GWORLD
GWorld
같은 경우 찾기가 쉽지 않는데
여러 코드를 분석한 결과 아래와 같은 함수를 찾을 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{ . . . }
if ( v163 == 3 ) // if (WorldContext.WorldType == EWorldType::PIE)
{
if ( v159 || (v164 = v249, *(_BYTE *)(v249 + 314) == 2) ) // if (bPackageAlreadyLoaded || NewWorld->WorldType == EWorldType::Editor)
{
if ( *((_DWORD *)a2 + 136) == -1 ) // if (WorldContext.PIEInstance == -1)
*((_DWORD *)a2 + 136) = 0;
v164 = (*(__int64 (__fastcall **)(_BYTE *, unsigned __int8 *, __int64, __int64))(*(_QWORD *)a1 + 1088LL))(
a1,
a2,
v249,
v8);
v249 = v164;
byte_888010C = 1;
}
else if ( *((_DWORD *)a2 + 136) != -1 ) // else if (WorldContext.PIEInstance != -1)
{
Java_com_epicgames_unreal_NativeCalls_HandleCustomTouchEvent(v249, v161, v162);
v164 = v249;
}
(*(void (__fastcall **)(_BYTE *, __int64))(*(_QWORD *)a1 + 1048LL))(a1, v164);
}
goto LABEL_357;
}
}
else
{
v249 = sub_461FF2C(v250, 0LL);
if ( !v249 )
goto LABEL_359;
v250 = ((__int64 (*)(void))sub_1AA8A70)();
if ( !v249 )
goto LABEL_359;
v159 = 1;
sub_40B4A94(*(_QWORD *)(v249 + 48));
v163 = *a2;
if ( v163 != 1 )
goto LABEL_258;
}
if ( (*(_DWORD *)(v249 + 315) & 0x40) != 0 && *((_DWORD *)a2 + 136) != -1 ) // if (NewWorld->bIsWorldInitialized && WorldContext.PIEInstance != -1)
{
v249 = sub_459E16C(a2, v8, &v250); // NewWorld = CreatePIEWorldByLoadingFromPackage(WorldContext, URL.Map, WorldPackage);
if ( !v249 ) // if (NewWorld == nullptr)
{ . . . }
위에서 좀 더 살펴보면 GWorld
에 해당하는 오프셋을 찾아볼 수 있다.
SDK Generation
해당 정보를 토대로 dump 를 추출할 수 있다.
그러나 앞서 언급했듯이 SDK 를 덤프하는 것은 PC 환경을 통해 진행하려고 한다.
PC 에서는 오프셋이 바로 보여서 찾는 수고가 없으며 반대로 모바일에서는 찾는 게 난해하여 오프셋이 안 보인다. 따라서 오프셋 찾는 과정은 모바일을 기반으로, dump 과정은 pc 를 기반으로 진행한다.
dump 를 진행할 때 사용할 수 있는 오픈소스 프로젝트가 있다.
dll 인젝션 방식을 사용하며 구현할 수 있으며 버전마다 호환성을 자동으로 잡아주는 아주 편리한 친구이다.
인젝션은 여러 방법이 있는데 코드로 구현했다.
인젝션할 수 있는 코드는 여기서 설명하지 않는다. 코드가 완성됐다고 가정하고 실행하면 아래와 같이 인젝션할 수 있는 대화상자가 나타난다.
1
2
3
4
5
6
=== Simple DLL Injector ===
Enter target process name (e.g., notepad.exe): Redacted.exe
Enter DLL path: C:\Users\root\inject.dll
Found process ID: 91984
DLL injected successfully!
Injection completed successfully!
그리고 인젝션되고서 오프셋을 찾고 로드된 엔진을 메모리 덤프한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
Started Generation
Searching for GObjects...
Found FChunkedFixedUObjectArray GObjects at offset 0xREDACTED
Off::InSDK::ObjArray::FUObjectItemSize: 18
Found 'FNamePool GNames' at offset 0xREDACTED
Found FName::AppendString at Offset 0xREDACTED
Off::UObject::Flags: 0x8
Off::UObject::Index: 0xC
Off::UObject::Class: 0x10
Off::UObject::Outer: 0x20
Off::UObject::Name: 0x18
Off::UClass::CastFlags: 0xD8
Off::UStruct::Children: 0x48
Off::UField::Next: 0x28
Off::UStruct::SuperStruct: 0x40
Off::UStruct::Size: 0x58
Off::UStruct::MinAlignment: 0x5C
Off::UClass::CastFlags: 0xD8
Game uses FProperty system
Off::UStruct::ChildProperties: 0x50
Applaying fix to hardcoded offsets
Off::FField::Next: 0x18
Off::FField::Class: 0x8
Off::FField::Name: 0x20
Off::FField::Flags: 0x24
Off::UClass::ClassDefaultObject: 0x110
Off::UClass::ImplementedInterfaces: 0x1D8
Off::UEnum::Names: 0x40
Off::UFunction::FunctionFlags: 0xB0
Off::UFunction::ExecFunction: 0xD8
Off::Property::ElementSize: 0x34
Off::Property::ArrayDim: 0x30
Off::Property::Offset_Internal: 0x44
Off::Property::PropertyFlags: 0x38
UBoolProperty::Base: 0x70
Off::EnumProperty::Base: 0x70
UPropertySize: 0x70
Off::ObjectProperty::PropertyClass: 0x70
Off::ByteProperty::Enum: 0x70
Off::StructProperty::Struct: 0x70
Off::DelegateProperty::SignatureFunction: 0x70
Off::ArrayProperty::Inner: 0x78
Off::SetProperty::ElementProp: 0x70
Off::MapProperty::Base: 0x70
Off::InSDK::ULevel::Actors: 0xA0
Off::InSDK::UDataTable::RowMap: 0x30
PE-Offset: 0xREDACTED
PE-Index: 0x4F
GWorld-Offset: 0xREDACTED
Off::InSDK::Text::TextSize: 0x10
Off::InSDK::Text::TextDatOffset: 0x0
Off::InSDK::Text::InTextDataStringOffset: 0x18
info: bIsObjPtrInsteadOfFieldPathProperty = false
info: bUseUint8ArrayDim = false
GameName: Redacted-5+Release-5.5
Generating SDK took (5720.89ms)
덤프한 결과를 code 에서는 다음과 같이 볼 수 있다.
덤프를 진행하면 위와 같이 덤프된 SDK 를 확인할 수 있다.
Conclusion
여기까지 오프셋을 찾아봤는데 Unreal Engine 에 대한 리버싱 과정을 정리하였다. 모바일에서도 자동으로 오프셋과 덤프를 할 수 있는 방법을 모색해볼 수 있을 것으로 예상된다. 시간이 가능하면 모바일에서도 자동으로 덤프해주는 도구를 만들어볼까 생각해본다.
If you find any errors, please let me know by comment or email. Thank you.