📓

원본 기록 02 — GObject·UWorld 문서화 (2024 당시)

문서화 파트와 안티치트 제작 팀에서 맡고 먼저 문서화를 시작하였다
 
namedump 문서화 버젼
 
 
 
 

Gobject 보고서 작성

언리얼 엔진에서 내부 해킹을 시도할 때 GObject를 덤프하는 것은 매우 중요한 과정입니다. UObject와 GObject의 관계에 대해 설명하겠습니다. UObject는 개별 객체이며, GObject는 이러한 객체들을 추적하는 글로벌 배열입니다. GObject 배열은 모든 UObject 인스턴스를 포함하고 있으며, 이를 통해 엔진은 객체의 생명 주기와 메모리 사용을 효율적으로 관리할 수 있습니다. 또한 GObject는 모든 UObject 인스턴스의 중앙 저장소 역할을 하여 객체를 쉽게 검색하고 조작할 수 있습니다. 언리얼 엔진은 다양한 게임 객체를 메모리에 저장하고 관리합니다. 여기에는 캐릭터, 무기, 환경 요소 등이 포함되며, GObject는 이러한 객체들을 추적하는 데 사용되는 글로벌 객체 배열입니다. 따라서 GObject를 덤프하면 게임의 모든 객체를 탐색하고 식별할 수 있습니다. 이를 통해 특정 객체를 찾아내고, 그 객체의 메모리 주소를 얻어 원하는 조작을 할 수 있습니다. 또한, 언리얼 엔진의 많은 기능은 객체 메서드와 데이터 구조에 의존하기 때문에, GObject 덤프를 통해 이러한 메서드와 구조를 식별할 수 있습니다. 이는 특정 기능을 활성화하거나 비활성화하고, 게임 데이터를 수정하는 등의 작업을 가능하게 합니다. 유의할 점은 객체의 메모리 레이아웃이 버전마다 달라질 수 있다는 것입니다. 따라서 GObject 덤프를 통해 현재 게임 버전에서 객체들의 메모리 오프셋을 정확히 파악하여 특정 객체의 올바른 필드를 조작할 수 있습니다. 초기, Fnamedump를 시도하며 UObject에 대한 정보를 얻게 되었습니다. 이 Uobject에 접근하기 위해서는 시작점이 필요했습니다.
 
처음 접근은 Unreal Engine 5 공식문서로 시작했습니다.
notion image
 
공식문서에 따르면 Uobject를 GUObjectArray로 관리한다고 되어 있습니다. 따라서 우리의 시작점은
GUObjectArray으로 정했습니다.
 

FUObjectArray

notion image
 
우리의 타겟인 GUObjectArray 의 타입은 FUobjectArray
 
 
notion image
FUobjectArray에는 FChunkedFixedUObjectArray 타입의 TUObjectArray 로 선언하고
 
notion image
 
TUObjectArray 타입의 objobjects 를 선언합니다

FChunkedFixedUObjectArray

notion image
 
FUObjectItem 타입의 더블 포인트 Objects를 선언합니다. 즉, FUObjectItem 형식으로 Objects를 관리함을 의미합니다

FUObjectItem

notion image
FUObjectItem는 구조체 였고
 
class UObjectBase* Object;
실제 할당된 오브겍트를 가르키는 UObjectBase 타입의 Object 가 선언되어 있습니다
 
 
notion image
 
Windbg로 UObjectBase 뭔지 한번 분석해보았습니다

Uobjectbase의 구조

notion image
 
 
여기 까지 하면 Reclass로 FUObjectArray 오프셋을 구해서 내부 값들을 실제로 확인할 수 있습니다.
우리는 총 windbg , IDA , 안티치트 이 3가지 종류로 구했고 그중 Windbg를 통해 간단히 설명하자면
 
notion image
 
GuobjectArray의 위치를 찾고 메인 모듈에서 서로 빼면 됩니다
0x00007ff7192283e0 - 0x00007ff707380000 = 0x11EA83E0
즉, 프로세스의 시작으로 부터 GuobjectArray 까지의 오프셋은 0x11EA83E0입니다
이제 Reclass로 분석해보면
notion image
 
UobjectBase 안에있는 요소들의 값을 알 수 있습니다.
 

UobjectBase에 관한 분석

 
UobjectBase에는 다음과 같은 정보들이 있습니다.
EObjectFlags ObjectFlags; int32 InternalIndex; ObjectPtr_Private::TNonAccessTrackedObjectPtr<UClass> ClassPrivate; FName NamePrivate; ObjectPtr_Private::TNonAccessTrackedObjectPtr<UObject> OuterPrivate;
ObjectFlags 객체의 상태를 나타내는 플래그
InternalIndex 객체배열에서의 index
ClassPrivate 객체의 클래스 타입을 나타내며 ( 어떤 클래스인지 ) ,클래스 관련 메타데이터를 포함
NamePrivate 객체의 이름
OuterPrivate 객체가 포함된 외부 객체를 나타냄
 
 
notion image
classPrivate는 추가적으로 분석해보면 안에는 Objectptr . 즉, 실제 오브젝트의 포인터가 있음을 알수 있습니다.
 
notion image
 
Fname ( NamePrivate) 은 다음과 같이 구성되어 있습니다.
처음 4바이트는 ComparisonIndex 값이 들어가 있다. 즉 이값을 통해 index를 얻을 수 있는것입니다.
 
참고로 여기서 ComparisonIndex는 1,2,3,4,5 … 이런식으로 순차적인게 아닌 우리가 아는 0 , 3, 10 이런 FNameDump의 인덱스입니다.
 

Uworld 분석

공식문서에 따르면 Uworld는 가장 높은 수준의 오브젝트이며  Actors와 Components 가 존재하고 랜더링되는 곳입니다.
 
notion image
 
이말은 즉, Uworld에 접근할 수 있다면 게임 월드에 접근할 수 있다는 뜻이다. 게임핵을 만들기 위해 장악해야 하는 필수적인 곳입니다.
 
소스코드 분석을 통해 flow를 따라가 보니
 

UObjectBase → UObjectBaseUtility 상속

notion image
 

UObjectBaseUtility → UObject 상속

notion image
 

UObject → Uworld 상속

notion image
 
즉, Uworld 또한 하나의 객체이며 이 게임 세상속에 들어가 있는 여러 다른 객체와 설정 값 그리고 flag등을 조정합니다.
 

GWorld

 
Uworld는 하나의 큰 class 였습니다. 그렇다면 Gworld?
Gworld는 현재 활성화된 게임 월드를 말합니다. 즉, 실질적인 현재 게임 상태의 월드에 있는 오브젝트들한테 접근 하려면 GWorld를 통해 접근 할 수 있습니다.
 
GWorld 소스코드
notion image
 
Gworld는 UworldProxy 형태로 되어있었습니다.
 

UworldProxy

 
/** Proxy class that allows verification on GWorld accesses. */ class UWorldProxy { public: UWorldProxy() : World(NULL) {} inline UWorld* operator->() { // GWorld is changed often on the game thread when in PIE, accessing on any other thread is going to be a race condition // In general, the rendering thread should not dereference UObjects, unless there is a mechanism in place to make it safe checkSlow(IsInGameThread()); return World; } inline const UWorld* operator->() const { checkSlow(IsInGameThread()); return World; } inline UWorld& operator*() { checkSlow(IsInGameThread()); return *World; } inline const UWorld& operator*() const { checkSlow(IsInGameThread()); return *World; } inline UWorldProxy& operator=(UWorld* InWorld) { World = InWorld; return *this; } inline UWorldProxy& operator=(const UWorldProxy& InProxy) { World = InProxy.World; return *this; } inline bool operator==(const UWorldProxy& Other) const { return World == Other.World; } inline operator UWorld*() const { checkSlow(IsInGameThread()); return World; } inline UWorld* GetReference() { checkSlow(IsInGameThread()); return World; } private: UWorld* World; };
notion image
 
inline 으로 여러가지 함수를 구현하고 있습니다. inline 형식은 컴파일러가 빠르고 최적화되게 컴파일이 될 수 있도록 하는 형식입니다.
 
가장 중요한 부분은
UWorld* World; 입니다.
 
즉, UworldProxy 은 UWorld 객체의 포인터를 가지고 있다는 것입니다.
 

UworldProxy

 
/** Proxy class that allows verification on GWorld accesses. */ class UWorldProxy { public: UWorldProxy() : World(NULL) {} inline UWorld* operator->() { // GWorld is changed often on the game thread when in PIE, accessing on any other thread is going to be a race condition // In general, the rendering thread should not dereference UObjects, unless there is a mechanism in place to make it safe checkSlow(IsInGameThread()); return World; } inline const UWorld* operator->() const { checkSlow(IsInGameThread()); return World; } inline UWorld& operator*() { checkSlow(IsInGameThread()); return *World; } inline const UWorld& operator*() const { checkSlow(IsInGameThread()); return *World; } inline UWorldProxy& operator=(UWorld* InWorld) { World = InWorld; return *this; } inline UWorldProxy& operator=(const UWorldProxy& InProxy) { World = InProxy.World; return *this; } inline bool operator==(const UWorldProxy& Other) const { return World == Other.World; } inline operator UWorld*() const { checkSlow(IsInGameThread()); return World; } inline UWorld* GetReference() { checkSlow(IsInGameThread()); return World; } private: UWorld* World; };
notion image
 
inline 으로 여러가지 함수를 구현하고 있습니다. inline 형식은 컴파일러가 빠르고 최적화되게 컴파일이 될 수 있도록 하는 형식입니다.
 
가장 중요한 부분은
UWorld* World; 입니다.
 
즉, UworldProxy 은 UWorld 객체의 포인터를 가지고 있습니다.
 
 

Uworld 구조

notion image
 
windbg로 보았을때 persistentlevel 이라는 변수의 오프셋은 0x30
형식은 TObjectPtr 으로 단일 포인터를 나타냅니다.
따라서 LEVEL_OFFSET = 0x030
 
이제 해당 메모리에서 포인터를 읽으면 ULevel의 위치를 읽을 수 있습니다.
 
Level.h
notion image
 
ULevel은 클래스 였다. Actor와 Model을 다루고 있습니다.
dbg 로 보았을때
notion image
 
+0x0a0 Actors : TArray<TObjectPtr<AActor>,TSizedDefaultAllocator<32> >
 
와 같은 부분을 볼 수 있다. AActor들의 포인터 배열( TArray 형식 )을 찾을 수 있었습니다.
 

TArray란?

 
TArray는 Unreal에서 자체제작한 자료 구조로 c++에서 자주 쓰이는 템플릿 클래스입니다.
 
notion image
다음과 같이 Array.h에 저장되어 있습니다.
 
이제 구조를 알아 보기 위해 Windbg를 써봤는데
 
notion image
 
맨처음에는 다음과 같이 찾을 수 없다고 나온다 그 이유는 TArray가 템플릿 클래스여서 그 아래와 같이 명시적으로 어떤 타입인지 지정을 해줘야 한다. 따라서 AActor 타입으로 지정을 해주니
 
+0x000 AllocatorInstance : TSizedHeapAllocator<32,FMemory>::ForElementType<TObjectPtr<AActor> > +0x008 ArrayNum : Int4B +0x00c ArrayMax : Int4B
 
 
다음과 같이
처음 8 바이트는 힙주소 ( 배열의 시작 지점 )
그 이후에는 각각 4바이트 int의 배열 현재 크기와 최대크기에 대한 정보가 들어가 있었습니다.
 
이를 통해 우리는 최종적인 dump코드를 작성할 수 있었습니다.
 
 

Dumper7

 
 
notion image
다운을 받고 Release 모드와 x64로 변경후
 
 
 
notion image
빌드를 실시하였습니다.
 
notion image
그럼 다음과 같은 dll이 나오게 되는데 이를 Dll injection 하기 위해 도구가 필요합니다.
 

Dll injection

 
따라서 DLL injector을 git에서 다운받았다.
 
 
notion image
 
다음과 같은 명령어로 실행을 해주니
notion image
 
다음과 같이 SDK 가 생성이 되었습니다. 이제 우리는 SDK 에 대한 이해를 완료하였고 핵개발로 넘어 갈 수 있었습니다.