📓

원본 기록 01 — FName·SDK Generator 분석 (2024 당시)

 

1주차 부터 이어지는 내실 다지기 IDA 버전

IDA 학습 보고서
notion image
notion image
일단 기본적인 내실을 다지기 위한 IDA로 리버싱 문제 풀기
 

분석 시작 ( 시행 착오 )

 
이번주에 pdb블럭을 직접 생성해보고 싶어서 노트북으로 직접 epic 허브를 깔고 언리얼엔진을 다운로드 받은 언리얼엔진에서 컴파일 시도 → 너무 오래걸림
 
피씨방에서 가서 다시 다운로드 받은뒤 시도 → pc방 용량이 부족하다면서
notion image
한번 팅기고 다시 용량 확보후 다시 시도
 
notion image
crash 발생 pdb 만들기 보류
 
 

Fname 을 찾아서

 
pdb가 존재 하지 않는 데모게임을 분석하려고 했으나 아무리 찾아도 Fname을 찾을 수 없었습니다. 미숙해서 그런지 몰라도
시도한 방법
1 . crtl+ 1 Strings 에서 찾기 → 없음
2 . function에서 찾기 → 없음
 
따라서 방향석을 잃고 Dumper와 sdk generator를 이용해보기로 결심

SDK Generator

UE4SDKGenerator
s4m33r89
UnrealEngineSDKGenerator
polivilas
SDK Generator 를 통해 한번 분석을 해보려고 했습니다.
sdk를 계속 알아보니 unreal engine4 sdk generator 가 유명하다는 것을 알게 되었고 이를 사용해 보려 다운로드 해보았으나 원인 모를 오류발생 빌드도 불가
 

Dumper

UnrealDumper-4.25
guttir14
사용하려고 하니 언리얼엔진4 까지 지원한다고
notion image
 
→ 뭔가 이상함의 기운을 감지했습니다. 잘못하고 있다.
 
기본으로 돌아가기

언리얼 엔진 작동 방식( 다소 부정확할 수 있음 추후 수정 )

 
  1. 언리얼엔진으로 프로젝트를 만든다.
  1. 프로젝트를 패키징한 이를 통해 오디오 , 모델링 등 리소스 파일들은 Cook 과정을 거친다.
  1. 이제 해당 엔진코드 들이 동적라이브러리로 변환(?) 된다.
 

그렇다면 FNAME와 같은 구조체는 어떻게 처리될까?

 
언리얼엔진 오픈소스 허가받고 git clone으로 내려받기 (한 1시간 걸린듯)
 
이후 setup.bat 파일과 GenerateProjectFiles.bat 파일을 실행 ( 한 30분 소요)
이제 그러면 .sln 파일을 얻을 수 있었고 비주얼 스튜디오로 열 수 있다. 이제 분석을 해보면
class FName { public: #if UE_FNAME_OUTLINE_NUMBER CORE_API FNameEntryId GetComparisonIndex() const; CORE_API FNameEntryId GetDisplayIndex() const; #else // UE_FNAME_OUTLINE_NUMBER FORCEINLINE FNameEntryId GetComparisonIndex() const { checkName(IsWithinBounds(ComparisonIndex)); return ComparisonIndex; } ...
 
드디어 Fname 이 뭔지 찾을 수 있었다.
Fname은 클래스 였고 아래를 더 살펴 보니 드디어 GetPlainNameString 이 뭔지 찾을 수 있었다. → 클래스 멤버 함수 였다.
/** Get name without number part as a dynamically allocated string */ CORE_API FString GetPlainNameString() const; /** Convert name without number part into TCHAR buffer and returns string length. Doesn't allocate. */ CORE_API uint32 GetPlainNameString(TCHAR(&OutName)[NAME_SIZE]) const;
CORE_API는 Unreal Engine에서 특정 함수나 클래스가 다른 모듈에서도 접근 가능하도록 하는 매크로라고 합니다.
 
 

실질적인 함수는 ? → unrealNames.cpp

 
GetPlainNameString 가 어떻게 구성되는지 보고 싶어서 찾아보았습니다.
unrealNames.cpp 에서 찾을 수 있었으며
uint32 FName::GetPlainNameString(TCHAR(&OutName)[NAME_SIZE]) const { const FNameEntry& Entry = *GetDisplayNameEntry(); Entry.GetName(OutName); return Entry.GetNameLength(); } FString FName::GetPlainNameString() const { return GetDisplayNameEntry()->GetPlainNameString(); }
 
 

GetDisplayNameEntry 가 뭘까?

const FNameEntry* FName::GetDisplayNameEntry() const { return ResolveEntryRecursive(GetDisplayIndexInternal()); }
FNameEntry 포인터를 반환한다.
 
GetName은 문자열을 반환해준다.
 
void FNameEntry::GetName(TCHAR(&OutName)[NAME_SIZE]) const { CopyAndConvertUnterminatedName(OutName); OutName[GetNameLength()] = TEXT('\0'); }
 
CopyAndConvertUnterminatedName
void FNameEntry::CopyAndConvertUnterminatedName(TCHAR* OutName) const { if (sizeof(TCHAR) < sizeof(WIDECHAR) && IsWide()) // Normally compiled out { FNameBuffer Temp; CopyUnterminatedName(Temp.WideName); ConvertInPlace<WIDECHAR, TCHAR>(Temp.WideName, Header.Len); FPlatformMemory::Memcpy(OutName, Temp.AnsiName, Header.Len * sizeof(TCHAR)); } else if (IsWide()) { CopyUnterminatedName((WIDECHAR*)OutName); ConvertInPlace<WIDECHAR, TCHAR>((WIDECHAR*)OutName, Header.Len); } else { CopyUnterminatedName((ANSICHAR*)OutName); ConvertInPlace<ANSICHAR, TCHAR>((ANSICHAR*)OutName, Header.Len); } }

FNameEntry

/** * A global deduplicated name stored in the global name table. */ struct FNameEntry { private: #if WITH_CASE_PRESERVING_NAME FNameEntryId ComparisonId; #endif FNameEntryHeader Header; // Unaligned to reduce alignment waste for non-numbered entries struct FNumberedData { #if UE_FNAME_OUTLINE_NUMBER #if WITH_CASE_PRESERVING_NAME // ComparisonId is 4B-aligned, 4B-align Id/Number by 2B pad after 2B Header uint8 Pad[sizeof(Header) % alignof(decltype(ComparisonId))]; #endif uint8 Id[sizeof(FNameEntryId)]; uint8 Number[sizeof(uint32)]; #endif // UE_FNAME_OUTLINE_NUMBER }; ... /** Copy null-terminated name to TCHAR buffer without allocating. */ CORE_API void GetName(TCHAR(&OutName)[NAME_SIZE]) const;
 
Nametypes.h 파일에서 GetName이 FNameEntry에 존재하는 것을 확인
 
 
해당 함수는 다음과 같이 구성되어있습니다.
FString FNameEntry::GetPlainNameString() const { checkName(Header.Len != 0); FNameBuffer Temp; if (Header.bIsWide) { return FString::ConstructFromPtrSize(GetUnterminatedName(Temp.WideName), Header.Len); } else { return FString::ConstructFromPtrSize(GetUnterminatedName(Temp.AnsiName), Header.Len); } }
 
 

본론 : FName 을 사용하면 어떻게 되나?

 
FName 사용방법
FName DefaultName; FName PlayerName(TEXT("Player")); FName PlayerNameWithNumber(TEXT("Player"), 1); 등등 수많은 방법 존재
 
그렇다면 FName 생성자는?
 

FName 생성자

FName::FName(const WIDECHAR* Name, EFindName FindType) : FName(FNameHelper::MakeDetectNumber(MakeUnconvertedView(Name), FindType)) {} FName::FName(const ANSICHAR* Name, EFindName FindType) : FName(FNameHelper::MakeDetectNumber(MakeUnconvertedView(Name), FindType)) {} FName::FName(const UTF8CHAR* Name, EFindName FindType) : FName(FNameHelper::MakeDetectNumber(MakeUnconvertedView(Name), FindType)) {} FName::FName(int32 Len, const WIDECHAR* Name, EFindName FindType) : FName(FNameHelper::MakeDetectNumber(MakeUnconvertedView(Name, Len), FindType)) {} FName::FName(int32 Len, const ANSICHAR* Name, EFindName FindType) : FName(FNameHelper::MakeDetectNumber(MakeUnconvertedView(Name, Len), FindType)) {} FName::FName(int32 Len, const UTF8CHAR* Name, EFindName FindType) : FName(FNameHelper::MakeDetectNumber(MakeUnconvertedView(Name, Len), FindType)) {} FName::FName(const WIDECHAR* Name, int32 InNumber) : FName(FNameHelper::MakeWithNumber(MakeUnconvertedView(Name), FNAME_Add, InNumber)) {} FName::FName(const ANSICHAR* Name, int32 InNumber) : FName(FNameHelper::MakeWithNumber(MakeUnconvertedView(Name), FNAME_Add, InNumber)) {} FName::FName(const UTF8CHAR* Name, int32 InNumber) : FName(FNameHelper::MakeWithNumber(MakeUnconvertedView(Name), FNAME_Add, InNumber)) {} FName::FName(int32 Len, const WIDECHAR* Name, int32 InNumber) : FName(InNumber != NAME_NO_NUMBER_INTERNAL ? FNameHelper::MakeWithNumber(MakeUnconvertedView(Name, Len), FNAME_Add, InNumber) : FNameHelper::MakeDetectNumber(MakeUnconvertedView(Name, Len), FNAME_Add)) {} FName::FName(int32 Len, const ANSICHAR* Name, int32 InNumber) : FName(InNumber != NAME_NO_NUMBER_INTERNAL ? FNameHelper::MakeWithNumber(MakeUnconvertedView(Name, Len), FNAME_Add, InNumber) : FNameHelper::MakeDetectNumber(MakeUnconvertedView(Name, Len), FNAME_Add)) {} FName::FName(int32 Len, const UTF8CHAR* Name, int32 InNumber) : FName(InNumber != NAME_NO_NUMBER_INTERNAL ? FNameHelper::MakeWithNumber(MakeUnconvertedView(Name, Len), FNAME_Add, InNumber) : FNameHelper::MakeDetectNumber(MakeUnconvertedView(Name, Len), FNAME_Add)) {} FName::FName(const TCHAR* Name, int32 InNumber, bool bSplitName) : FName(InNumber == NAME_NO_NUMBER_INTERNAL && bSplitName ? FNameHelper::MakeDetectNumber(MakeUnconvertedView(Name), FNAME_Add) : FNameHelper::MakeWithNumber(MakeUnconvertedView(Name), FNAME_Add, InNumber)) {} FName::FName(const FNameEntrySerialized& LoadedEntry) : FName(FNameHelper::MakeFromLoaded(LoadedEntry)) {}
 

FNameHelper 구조체임

 
/** Templated implementations of non-templated member functions, helps keep header clean */ struct FNameHelper { template<typename ViewType> static FName MakeDetectNumber(ViewType View, EFindName FindType) { if (View.Len == 0) { return FName(); } uint32 InternalNumber = ParseNumber(View.Str, /* may be shortened */ View.Len); return MakeWithNumber(View, FindType, InternalNumber); } static FName MakeWithNumber(FNameAnsiStringView View, EFindName FindType, int32 InternalNumber) { // Ignore the supplied number if the name string is empty // to keep the semantics of the old FName implementation if (View.Len == 0) { return FName(); } --- 여러함수 존재
 
 

FLOW

MakeDetectNumber
static FName MakeDetectNumber(ViewType View, EFindName FindType) { if (View.Len == 0) { return FName(); } uint32 InternalNumber = ParseNumber(View.Str, /* may be shortened */ View.Len); return MakeWithNumber(View, FindType, InternalNumber); }
숫자를 Parse 한뒤
MakeDetectNumber → MakeWithNumber
 
MakeWithNumber
static FName MakeWithNumber(FNameAnsiStringView View, EFindName FindType, int32 InternalNumber) { // Ignore the supplied number if the name string is empty // to keep the semantics of the old FName implementation if (View.Len == 0) { return FName(); } return MakeInternal(FNameStringView(View.Str, View.Len), FindType, InternalNumber); }
문자열 길이 검사한뒤
MakeWithNumber → MakeInternal
 
MakeInternal
static FName MakeInternal(FNameStringView View, EFindName FindType, int32 InternalNumber) { FNameEntryIds Ids; UE_AUTORTFM_OPEN({ Ids = FindOrStoreString(View, FindType); }); #if UE_FNAME_OUTLINE_NUMBER if (FindType == FNAME_Find && !Ids.DisplayId) { // We need to disambiguate here between "we found 'None'" and "we didn't find what we were looking for" if (View.IsNoneString()) { return MakeWithNumber(Ids, FindType, InternalNumber); // 'None' with suffix } else { return FName(); // Not found } } #endif return MakeWithNumber(Ids, FindType, InternalNumber); }
 
Ids = FindOrStoreString(View, FindType);
이부분에서 FindType에 따라 결정
if ( FindType이 FNAME_Add 라면 )
해당 문자열을 저장하고 저장된 ids 를 가져온다.
 
if ( FindType이 FNAME_Find 라면 )
해당 문자열을 찾아서 저장되어있는 ids를 가져온다.
 
FindOrStoreString 코드
static FNameEntryIds FindOrStoreString(FNameStringView View, EFindName FindType) { FNameEntryIds Result{}; UE_AUTORTFM_OPEN( { if (View.Len >= NAME_SIZE) { // If we're doing a find, and the string is too long, then clearly we didn't find it if (FindType == FNAME_Find) { Result = FNameEntryIds{}; } else { checkf(false, TEXT("FName's %d max length exceeded. Got %d characters excluding null-terminator:\n%.*s"), NAME_SIZE - 1, View.Len, NAME_SIZE, View.IsAnsi() ? ANSI_TO_TCHAR(View.Ansi) : View.Wide) const ANSICHAR* ErrorString = "ERROR_NAME_SIZE_EXCEEDED"; Result = FindOrStoreString(FNameStringView(ErrorString, FCStringAnsi::Strlen(ErrorString), false), FNAME_Add); } } else { FNamePool& Pool = GetNamePool(); if (FindType == FNAME_Add) { FNameEntryId DisplayId = Pool.Store(View); Result = FNameEntryIds{ ResolveComparisonId(DisplayId), DisplayId }; } else { check(FindType == FNAME_Find); FNameEntryId DisplayId = Pool.Find(View); Result = FNameEntryIds{ ResolveComparisonId(DisplayId), DisplayId }; } } }); return Result; }
if (FindType == FNAME_Find && !Ids.DisplayId) FindType == FNAME_Find 이라는 것은 사용자가 문자열을 저장한다는게 아니라 찾을 때 Type
 
MakeInternal → MakeWithNumber(IDS , ~ , ~ )
 
static FName MakeWithNumber(FNameEntryIds BaseIds, EFindName FindType, int32 InternalNumber) { #if UE_FNAME_OUTLINE_NUMBER #if WITH_CASE_PRESERVING_NAME // Advanced users must pass in matching display & comparison ids checkName(FName::ResolveEntry(BaseIds.DisplayId)->ComparisonId == BaseIds.ComparisonId); #endif // If BaseIds are NAME_None, we want to produce a numbered suffix of None. // If anything searches for BaseIds they need to validate whether they want to pass NAME_None on or not before calling this function. if (InternalNumber == NAME_NO_NUMBER_INTERNAL) { return FinalConstruct(BaseIds); } FNamePool& Pool = GetNamePool(); if (FindType == FNAME_Add) { FNameEntryId DisplayId; UE_AUTORTFM_OPEN({ DisplayId = Pool.StoreWithNumber(BaseIds, InternalNumber); }); return FinalConstruct(FNameEntryIds{ ResolveComparisonId(DisplayId), DisplayId }); } else { check(FindType == FNAME_Find); FNameEntryId DisplayId = Pool.FindWithNumber(BaseIds.DisplayId, InternalNumber); if (DisplayId) { return FinalConstruct(FNameEntryIds{ ResolveComparisonId(DisplayId), DisplayId }); } else { // Not found return FName(); } } #else // Number is just stored in the FName pass it straight on return FinalConstruct(BaseIds, InternalNumber); #endif } template<typename CharType> static bool EqualsString(FName Name, const CharType* Str, int32 Len) { check(Len >= 0); // Make NAME_None == TEXTVIEW("") or TEXTVIEW(nullptr) consistent with NAME_None == FName(TEXT("")) or FName(nullptr) if (Len == 0) { return Name.IsNone(); } // "_[num]" is considered "None" to keep semantics of old FName implementation if (Name.IsNone() & (Str[0] == '_')) { ParseNumber(Str, /* in-out */ Len); return Len == 0; } const FNameEntry* Entry = FName::ResolveEntry(Name.GetComparisonIndexInternal()); #if UE_FNAME_OUTLINE_NUMBER int32 Number = Entry->IsNumbered() ? Entry->GetNumber() : NAME_NO_NUMBER_INTERNAL; Entry = Entry->IsNumbered() ? FName::ResolveEntry(Entry->GetNumberedName().Id) : Entry; check(!Entry->IsNumbered()); #else int32 Number = Name.GetNumber(); #endif // Check _[number] suffix first and shorten Len if it matches if (Number != NAME_NO_NUMBER_INTERNAL && Number != ParseNumber(Str, /* in-out */ Len)) { return false; } if (Len != Entry->GetNameLength()) { return false; } FNameBuffer Temp; return Entry->IsWide() ? !FPlatformString::Strnicmp(Str, Entry->GetUnterminatedName(Temp.WideName), Len) : !FPlatformString::Strnicmp(Str, Entry->GetUnterminatedName(Temp.AnsiName), Len); } template<typename CharType> static uint32 ParseNumber(const CharType* Name, int32& InOutLen) { const int32 Len = InOutLen; int32 Digits = 0; for (const CharType* It = Name + Len - 1; It >= Name && *It >= '0' && *It <= '9'; --It) { ++Digits; } const CharType* FirstDigit = Name + Len - Digits; static constexpr int32 MaxDigitsInt32 = 10; if (Digits && Digits < Len && *(FirstDigit - 1) == '_' && Digits <= MaxDigitsInt32) { // check for the case where there are multiple digits after the _ and the first one // is a 0 ("Rocket_04"). Can't split this case. (So, we check if the first char // is not 0 or the length of the number is 1 (since ROcket_0 is valid) if (Digits == 1 || *FirstDigit != '0') { int64 Number = Atoi64(Name + Len - Digits, Digits); if (Number < MAX_int32) { InOutLen -= 1 + Digits; return static_cast<uint32>(NAME_EXTERNAL_TO_INTERNAL(Number)); } } } return NAME_NO_NUMBER_INTERNAL; }
 
요약 Flag와 인자에 따라 여러가지 함수제공
 
MakeWithNumber(IDS , ~ , ~ ) → FinalConstruct
 
static FName FinalConstruct(FNameEntryIds Ids, int32 InternalNumber) #endif // UE_FNAME_OUTLINE_NUMBER { FName Out; Out.ComparisonIndex = Ids.ComparisonId; #if WITH_CASE_PRESERVING_NAME Out.DisplayIndex = Ids.DisplayId; #endif #if !UE_FNAME_OUTLINE_NUMBER Out.Number = InternalNumber; #endif return Out; }
단순히 만들기
 
 
 
일단 여기까지 오픈소스 분석이었습니다.

+추가 치트 엔진 튜토리얼 밀기

 
notion image
 
치트엔진 튜토리얼은 8레벨 까지 밀었습니다.