본문 바로가기

캡스톤 디자인/정보보호 프로젝트실습

[팰월드 안티치트 프로젝트] #1 - 언리얼 엔진 5 게임은 어떻게 분석되는가?

📌 이 글은 팰월드 안티치트 프로젝트의 학습 기록 시리즈입니다. 안티치트를 만들기 위해 먼저 "공격자가 게임을 어떻게 분석하는지" 이해하는 과정을 정리합니다

 

팰월드(Palworld)는 언리얼 엔진 5(UE5)로 만들어진 게임이다. 안티치트를 개발하려면 치터가 게임을 어떤 방식으로 분석하고 공격하는지 먼저 이해해야 한다. 이 글에서는 UE5 게임의 내부 구조를 분석하는 전체 과정을 다룬다.

이 글은 Open Cheat Tables의 Do0ks가 작성한 튜토리얼 "Reverse Unreal Engine Games Using IDA Pro + Other Tools"을 기반으로 학습한 내용을 정리한 것이다.


배경 지식: 언리얼 엔진 5의 핵심 구조

본격적인 분석에 앞서, UE5 게임을 리버싱할 때 반드시 알아야 하는 개념들을 짚고 넘어가자.

UObject — 모든 것의 시작

UE5에서 캐릭터, 무기, 아이템, 심지어 게임 모드까지 거의 모든 것은 UObject라는 하나의 부모 클래스를 상속받는다. 팰월드로 예를 들면 내 캐릭터도, 잡은 팰도, 인벤토리에 있는 아이템도 전부 UObject의 자식이다.

이게 리버싱에서 중요한 이유는, UObject가 리플렉션이라는 기능을 제공하기 때문이다. 리플렉션 덕분에 런타임에 "이 오브젝트의 클래스 이름이 뭐지?", "Health라는 변수가 어디에 있지?" 같은 정보를 코드에서 조회할 수 있다. 치터 입장에서는 이 기능을 역으로 이용해서 게임 내부 구조를 파악하는 것이고, 안티치트 입장에서는 이 접근을 어떻게 막을지 고민해야 하는 부분이다.

세 가지 핵심 글로벌 테이블

UE5 게임을 리버싱할 때 가장 먼저 찾아야 하는 것이 세 가지 글로벌 테이블이다. 이 세 가지만 찾으면 게임 내부의 거의 모든 데이터에 접근할 수 있는 길이 열린다.

GWorld는 현재 게임 월드를 가리키는 포인터다. 여기를 시작점으로 포인터 체인을 따라가면 플레이어 캐릭터, 팰, 아이템 등 월드에 존재하는 모든 오브젝트에 도달할 수 있다.

GNames는 엔진이 사용하는 모든 이름을 모아둔 테이블이다. UE5는 "PlayerCharacter"같은 문자열을 매번 저장하는 대신 번호로 관리하는데, GNames가 그 번호와 실제 이름을 연결해주는 사전 역할을 한다.

GObjects는 현재 메모리에 로드된 모든 UObject 인스턴스의 목록이다. 이걸 덤프하면 게임이 지금 어떤 오브젝트들을 들고 있는지 한눈에 볼 수 있다.

이 튜토리얼의 핵심 목표가 바로 이 세 가지의 메모리 주소(오프셋)를 찾아내는 것이다.

 

필요한 도구:

도구 용도
IDA Pro 디스어셈블러/디컴파일러. EXE를 분석하여 어셈블리/의사 코드로 변환
SigMaker IDA 플러그인. 함수의 AOB(바이트 패턴)를 자동 생성
UEDumper 오프셋을 입력하면 게임의 전체 SDK를 덤프하는 도구
Cheat Engine 메모리 스캐너. 실행 중인 게임의 메모리 값을 검색/수정
언리얼 엔진 (분석 대상과 동일 버전) 참조 게임 제작용

 

대상 게임의 UE 버전 확인: 

분석의 첫 단계는 대상 게임이 사용하는 UE 버전을 정확히 파악하는 것이다.

1단계: Steam 라이브러리에서 Palworld를 우클릭한 뒤, 관리 → 로컬 파일 탐색을 클릭하면 게임이 설치된 폴더가 자동으로 열린다.

2단계: EXE 파일 찾기 : 열린 폴더에서 Palworld.exe 파일을 찾는다.

3단계: 속성에서 버전 확인 : Palworld.exe를 우클릭 → 속성 → 자세히 탭을 선택하면 다음 정보를 확인할 수 있다.

제품 버전 항목에 ++UE5+Release-5.1-CL-0이라고 표시되어 있다. 이를 통해 팰월드는 언리얼 엔진 5.1 버전을 사용하고 있음을 확인할 수 있다.

 

참조 게임 제작을 위한 언리얼 엔진 설치: 

왜 이 작업을 하는가? 팰월드의 UE 버전이 5.1임을 확인했다. 분석 대상 게임과 동일한 엔진 버전으로 참조 게임을 만들어야 엔진 코드의 바이트 패턴이 일치하기 때문에, 같은 버전의 언리얼 엔진을 설치해야 한다.

 

설치 과정: 

먼저 에픽게임즈 공식 사이트에서 에픽게임즈 런처를 다운로드하여 설치한다.

런처를 실행한 뒤 왼쪽 메뉴에서 언리얼 엔진을 클릭하고, 상단 탭에서 라이브러리를 선택한다. 여기서 엔진 버전 옆에 있는 + 버튼을 누르면 새로운 엔진 버전을 추가할 수 있다. 팰월드와 동일한 5.1 버전을 선택하고 설치를 진행한다.

 

⚠️ 반드시 팰월드와 동일한 5.1 버전을 설치해야 한다. 다른 버전(예: 5.3, 5.4)으로 설치하면 엔진 내부 코드가 달라져서 바이트 패턴 비교가 정확하지 않다.

 

참조 게임 프로젝트 생성: 

설치가 완료되면 해당 버전의 실행 버튼을 클릭한다. 새 프로젝트 화면에서 3인칭(또는 1인칭) 블루프린트 템플릿을 선택하고, 프로젝트 저장 경로와 이름을 지정한 뒤 프로젝트 생성을 클릭하면 된다.

프로젝트 생성이 완료되었다. 이제 이 프로젝트를 빌드하여 PDB 파일을 얻어야 한다.

PDB 파일이란?
PDB(Program Database)는 프로그램을 빌드할 때 자동으로 생성되는 디버깅 정보 파일이다. EXE가 컴퓨터가 실행하는 기계어 코드라면, PDB는 그 기계어가 원래 소스코드에서 어떤 함수와 변수였는지 알려주는 "번역표" 역할을 한다.
PDB 없이 IDA Pro에서 EXE를 열면 sub_18BBF60처럼 의미 없는 이름만 보이지만, PDB를 함께 로드하면 같은 위치가 GWorld나 FSeamlessTravelHandler::Tick처럼 실제 이름으로 표시된다.

 

왜 이 작업을 하는가?

Palworld.exe 파일에는 PDB가 포함되어 있지 않다. 개발사가 배포할 때 제거하기 때문이다. 하지만 우리가 방금 만든 참조 게임은 직접 빌드하는 것이기 때문에 PDB를 포함시킬 수 있다. 같은 UE 5.1 엔진을 사용하므로 엔진 부분의 바이트 패턴이 동일하고, 참조 게임의 PDB로 확인한 함수 이름을 팰월드에도 그대로 적용할 수 있다.

 

빌드 방법:

프로젝트가 열린 상태에서 상단 메뉴의 플랫폼 → 패키징 설정에 들어간다. 설정 항목이 많기 때문에 검색창에 debug를 입력하면 빠르게 찾을 수 있다. 여기서 출시 빌드의 디버그 파일 포함 옵션을 활성화한 뒤 빌드를 진행하면, EXE 파일과 함께 .pdb 파일이 생성된다.

빌드가 완료되면 지정한 폴더 안에 Windows 폴더가 생성된다. 해당 폴더에 들어가면 Engine, UE5 폴더와 함께 UE5.exe 파일이 생성된 것을 확인할 수 있다. 이 EXE 파일과 함께 생성된 PDB 파일을 IDA Pro에서 열어 분석에 사용하게 된다.

 

IDA Pro에서 분석 준비: 

빌드가 완료되었으니 이제 IDA Pro를 사용하여 본격적인 분석을 시작한다. 참조 게임과 팰월드를 각각 IDA에서 열어 나란히 비교할 것이다.

 

분석할 파일 위치: 

먼저 두 개의 EXE 파일 위치를 확인한다.

참조 게임 EXE: 빌드 결과물이 저장된 폴더에서 UE5/Windows/Engine/Binaries/Win64/UnrealGame.exe 경로에 있다. 같은 폴더에 UnrealGame.pdb 파일도 함께 있어야 한다.

팰월드 EXE: Steam에서 팰월드를 우클릭 → 관리 → 로컬 파일 탐색으로 게임 폴더를 열고, Pal/Binaries/Win64/Palworld-Win64-Shipping.exe 경로에서 찾을 수 있다.

 

IDA에서 파일 열기:

IDA 창을 두 개 띄워야 한다. 하나는 참조 게임용, 하나는 팰월드용이다.

참조 게임 열기: 첫 번째 IDA 창에서 UnrealGame.exe를 드래그 앤 드롭하거나 File → Open으로 연다. 파일 형식을 묻는 창이 뜨면 Portable executable for AMD64 (PE) [pe64.dll]이 선택된 상태로 OK를 누른다. 로딩 중에 오디오 DLL 파일을 찾는 창이 뜰 수 있는데, 분석에 필요 없으므로 취소를 누르면 된다. 로딩이 완료되면 PDB를 로드할 것인지 물어보는데, 이때 "예"를 선택한다. 이 PDB 덕분에 엔진의 모든 함수 이름을 확인할 수 있게 된다.

팰월드 열기: 두 번째 IDA 창에서 Palworld-Win64-Shipping.exe를 같은 방식으로 연다. 마찬가지로 파일 형식은 기본값 그대로 OK를 누르고, PDB를 묻는 창이 뜨면 이번에는 "아니요"를 선택한다. 팰월드에는 PDB가 없기 때문이다. 로딩이 완료되면 편집 → 세그먼트 → 리베이스 프로그램에서 값을 0으로 설정한다. 이렇게 하면 불필요한 기본 주소가 제거되어 깔끔한 오프셋 값을 얻을 수 있다.

 

💡 두 파일 모두 용량이 크기 때문에 IDA에서 로딩하는 데 상당한 시간이 걸릴 수 있다. 참조 게임 EXE가 약 235MB, 팰월드 EXE가 약 151MB이므로 여유를 갖고 기다리자.


GWorld 찾기

IDA에서 두 게임을 모두 열었으니 이제 본격적으로 GWorld의 오프셋을 찾아볼 차례다.

 

IDA 설정:

첫 번째는 오프코드 바이트 표시다. 옵션 → 일반에서 "Number of opcode bytes (graph)"를 10으로 설정하고, "Function offsets"를 체크한다. 필요하다면 "Auto comments"도 체크하면 어셈블리 코드에 자동 주석이 달려서 가독성이 좋아진다. 기본 상태에서는 어셈블리 코드만 보이지만, 이 설정을 켜면 각 명령어 왼쪽에 실제 바이트(hex 값)가 함께 표시된다. 나중에 두 게임의 바이트 패턴을 비교할 때 이 바이트가 보여야 "여기가 같은 코드다"라고 확인할 수 있기 때문이다.

 

두 번째는 문자열 뷰어다. Shift+F12를 눌러 문자열 뷰어를 열고, 뷰어 안에서 우클릭 → 설정에 들어가 C 스타일, 유니코드 C 스타일, C 스타일(32비트)을 전부 체크한다. 게임 EXE 안에는 다양한 방식으로 저장된 텍스트 문자열이 있는데, 세 가지를 모두 켜야 빠짐없이 검색할 수 있다. GWorld를 찾는 핵심 방법이 문자열 검색이기 때문에 이 설정이 필수다.

 

GWorld를 찾는 원리: 

GWorld를 직접 검색할 수는 없다. 팰월드 EXE에는 PDB가 없으니 "GWorld"라는 이름 자체가 존재하지 않기 때문이다. 대신 우회 전략을 사용한다.

UE5 엔진 코드에는 SeamlessTravel FlushLevelStreaming이라는 문자열이 포함되어 있고, 이 문자열은 GWorld를 참조하는 함수 근처에 위치한다. 이 문자열은 UE4부터 UE5까지 일관되게 존재하기 때문에, 어떤 언리얼 엔진 게임에서든 이 문자열을 검색하면 GWorld 근처로 이동할 수 있다.

 

검색 및 비교: 

양쪽 IDA의 문자열 뷰어에서 SeamlessTravel FlushLevelStreaming을 검색하고 양쪽을 비교해보면 흥미로운 점이 있다. 참조 게임(왼쪽)에서는 DATA XREF: FSeamlessTravelHandler::Tick(void)+1MFo 처럼 PDB 덕분에 실제 함수 이름이 표시되지만, 팰월드(오른쪽)에서는 DATA XREF: sub_553F30+19172o 처럼 의미 없는 이름으로 표시된다. 하지만 그 아래의 문자열 내용을 보면 양쪽 다 동일한 텍스트가 나열되어 있는 것을 확인할 수 있다. 결국 다른 것은 PDB에 의한 함수 이름뿐이고, 코드의 구조 자체는 같다는 것을 알 수 있다.

 

다음으로 위쪽의 함수 이름을 더블클릭하면 해당 코드 영역으로 이동한다.

 

여기서 F5를 눌러 의사 코드(pseudocode)를 생성하면 사람이 읽기 쉬운 C 코드 형태로 변환된다. 다만 GWorld는 SeamlessTravel FlushLevelStreaming 문자열보다 위쪽에 위치해 있기 때문에, 의사 코드 창에서 위로 스크롤하면서 찾아야 한다.

참조 게임 쪽에서는 PDB 덕분에 위로 올리다 보면 GWorld = 0LL;이라는 코드를 발견할 수 있다. GWorld라는 변수 이름이 그대로 표시되기 때문에 바로 알아볼 수 있다. 팰월드 쪽에서도 같은 위치까지 올리면 동일한 구조의 코드가 보이지만, PDB가 없기 때문에 qword_90D3030 = 0LL;처럼 변수 이름 대신 메모리 주소로 표시된다. 코드 구조가 같고 위치도 같으므로, qword_90D3030이 팰월드의 GWorld라고 확정할 수 있다. 이 90D3030 값이 바로 GWorld의 오프셋이며, 이후 UEDumper에 입력할 값이 된다.


GName 찾기

GWorld를 찾았으니 다음은 GNames를 찾을 차례다. 팰월드는 UE 5.1을 사용하기 때문에 FName::Pool 방식으로 찾아야 한다. 

 

참조 게임에서 FName::Pool 주소 확인:

먼저 참조 게임(언리얼) 쪽 IDA의 함수 목록에서 FName::ToString을 검색하여 해당 함수로 이동한다. F5를 눌러 의사 코드로 변환하면 함수의 내부 구조를 확인할 수 있다.

v11 = &stru_14DAB7540;
...
v12 = FNamePool::FNamePool((FNamePool *)&stru_14DAB7540);

여기서 주목할 부분은 24, 28번 줄이다. FNamePool이라는 이름이 PDB 덕분에 보이고, &stru_14DAB7540이 바로 FNamePool의 주소다. 이것이 UE 5.1에서의 GNames에 해당한다.

 

SigMaker로 패턴 추출: 

FName::ToString에서 SigMaker를 돌려 AOB 패턴을 추출한다. 참조 게임의 IDA View-A에서 인접 함수의 아무 줄이나 클릭하고 Ctrl+Alt+S로 SigMaker를 실행한다.

SigMaker 란?
선택한 함수의 AOB(Array of Bytes) 패턴을 자동으로 생성해주는 도구다. 함수의 바이트 코드를 분석하여 해당 함수만 고유하게 식별할 수 있는 바이트 패턴을 만들어준다. 이 패턴을 다른 게임에서 검색하면 같은 함수를 찾을 수 있다. 수동으로 바이트를 하나하나 확인하는 것보다 훨씬 빠르고 정확하다.

 

팰월드에서 검색: 

SigMaker로 뽑은 AOB 패턴을 복사하여 팰월드 쪽 IDA에서 Search → Sequence of bytes에 붙여넣고 검색한다.

 

결과가 나오면 더블클릭하여 해당 위치로 이동하고, F5를 눌러 의사 코드로 변환한다. 참조 게임의 의사 코드와 나란히 비교해보면, PDB가 없어서 함수 이름은 다르지만 코드 구조가 동일한 것을 확인할 수 있다. 이를 통해 해당 위치가 GName(FNamePool)라는 것을 알 수 있다.

 

최종적으로 팰월드의 GNames 오프셋은 0x8EC5100으로 확인되었다.


GObjects 찾기

수동 분석의 한계:

튜토리얼에서는 함수 검색에서 getobjectcluster를 검색하면 GObjects를 찾을 수 있다고 설명하고 있다. 하지만 실제로 팰월드에서 시도해본 결과, 함수를 통해 GObjects의 오프셋을 찾는 과정이 쉽지 않았다. UE 5.1에서는 컴파일러 최적화 등의 이유로 코드 구조가 튜토리얼의 예시와 다르게 나타나는 경우가 있기 때문이다.

리버싱은 항상 계획대로 되지 않는다. 한 가지 방법이 막히면 다른 도구나 접근법으로 전환하는 것도 중요한 판단이다.

 

여기서 사용한 것이 Dumper-7이라는 도구다.

Dumper-7이란?
Dumper-7은 언리얼 엔진(UE4/UE5) 게임에서 런타임 SDK를 자동으로 생성해주는 오픈소스 도구다. DLL 인젝션 방식으로 실행 중인 게임의 메모리를 분석하여, GWorld, GNames, GObjects 오프셋과 SDK 파일을 자동으로 추출해준다. UE4 부터 UE5까지 폭넓게 지원한다.

 

https://github.com/Encryqed/Dumper-7

 

GitHub - Encryqed/Dumper-7: Unreal Engine SDK Generator

Unreal Engine SDK Generator. Contribute to Encryqed/Dumper-7 development by creating an account on GitHub.

github.com

 

사전 준비:

Dumper-7을 사용하려면 세 가지가 필요하다.

Visual Studio는 Dumper-7이 소스코드 형태로 배포되기 때문에 직접 빌드해야 해서 필요하다. Community 버전(무료)을 설치할 때 "C++를 사용한 데스크톱 개발" 워크로드를 선택하면 된다.

Xenos Injector는 빌드한 DLL을 게임 프로세스에 넣어주는 도구다. GitHub(https://github.com/DarthTon/Xenos/releases)에서 최신 버전을 다운로드하고 압축을 풀면 Xenos64.exe를 바로 사용할 수 있다.

Windows Defender 실시간 보호는 일시적으로 꺼야 한다. DLL 인젝터는 Defender가 악성코드로 오탐지하는 경우가 많기 때문이다. 설정 → 개인 정보 및 보안 → Windows 보안 → 바이러스 및 위협 방지 → 설정 관리에서 실시간 보호를 끄면 된다. 작업이 끝나면 반드시 다시 켜야 한다. 

 

Dumper-7 빌드:

GitHub에서 소스코드를 다운로드(Code → Download ZIP)하고 압축을 해제한다. Dumper-7.sln 파일을 더블클릭하면 Visual Studio가 열리는데, 설정 도우미가 뜨면 적용을 눌러 빌드 도구 버전을 맞춘다. 상단 드롭다운에서 Releasex64를 선택한 뒤 Ctrl+B로 빌드하면 x64/Release/ 폴더에 Dumper-7.dll이 생성된다.

 

DLL 인젝션: 

먼저 대상 게임을 실행하고 메인 메뉴까지 완전히 로딩될 때까지 기다린다. 게임이 완전히 로딩되어야 엔진 구조체가 메모리에 올라가기 때문이다.

Xenos64.exe를 관리자 권한으로 실행하고 Add 버튼으로 빌드한 Dumper-7.dll을 선택한다. Process 드롭다운에서 게임 프로세스를 선택하는데, 언리얼 엔진 게임의 경우 Palworld-Win64-Shipping.exe처럼 -Win64-Shipping.exe로 끝나는 프로세스가 실제 게임 본체이므로 이것을 선택해야 한다. Inject를 클릭하면 인젝션이 진행된다.

 

인젝션이 성공하면 검은 콘솔 창이 뜨면서 다음과 같은 정보가 출력된다.

 

마무리

이번 글에서는 팰월드 안티치트 프로젝트의 첫 단계로, UE5 게임의 내부 구조를 이해하고 실제로 팰월드를 분석하는 과정을 진행했다.

먼저 UE5의 핵심 구조인 UObject, GWorld, GNames, GObjects가 무엇인지 알아보고, 팰월드의 UE 버전이 5.1임을 확인한 뒤 동일한 버전으로 참조 게임을 빌드하여 PDB 파일을 확보했다. 이후 IDA Pro에서 참조 게임과 팰월드를 나란히 비교하면서 GWorld와 GNames의 오프셋을 수동으로 찾아냈고, GObjects는 Dumper-7 도구를 활용하여 추출했다.

최종적으로 확보한 팰월드의 핵심 오프셋은 다음과 같다.

GWorld:   0x90D3030
GNames:   0x8EC5100
GObjects: 0x8F64810

 

이 세 가지 오프셋만 있으면 게임 메모리의 거의 모든 데이터에 접근할 수 있는 길이 열린다. 이것이 바로 치터가 게임을 공격하는 출발점이며, 안티치트는 이 접근을 어떻게 방해하고 감지할 것인지를 고민해야 한다.

다음 글에서는 이 오프셋들을 활용하여 UEDumper로 팰월드의 전체 SDK를 덤프하고, 라이브 에디터를 통해 실제 게임 메모리를 탐색하는 과정을 다룰 예정이다.