diff --git a/shared/sdk/FName.cpp b/shared/sdk/FName.cpp index 3d13fc54..7847a5f0 100644 --- a/shared/sdk/FName.cpp +++ b/shared/sdk/FName.cpp @@ -163,6 +163,90 @@ std::optional FName::get_constructor() { return result; } +std::optional FName::get_to_string() { + static auto result = []() -> std::optional { + SPDLOG_INFO("FName::get_to_string"); + + const auto module = sdk::get_ue_module(L"Engine"); + + if (module == nullptr) { + SPDLOG_ERROR("FName::get_to_string: Failed to get module"); + return std::nullopt; + } + + const auto str_data = utility::scan_string(module, L"TAutoWeakObjectPtr<%s%s>"); + if (!str_data) { + SPDLOG_ERROR("FName::get_to_string: Failed to get string data"); + return std::nullopt; + } + + SPDLOG_INFO("FName::get_to_string: str_data={:x}", *str_data); + + const auto str_ref = utility::scan_displacement_reference(module, *str_data); + + if (!str_ref) { + SPDLOG_ERROR("FName::get_to_string: Failed to get string reference"); + return std::nullopt; + } + + SPDLOG_INFO("FName::get_to_string: str_ref={:x}", *str_ref); + + const auto containing_func = utility::find_function_start(*str_ref); + + if (!containing_func) { + SPDLOG_ERROR("FName::get_to_string: Failed to find containing function"); + return std::nullopt; + } + + SPDLOG_INFO("FName::get_to_string: containing_func={:x}", *containing_func); + + std::optional last_direct_call{}; + std::optional result{}; + + // It's known that FName::ToString precedes a string reference to TAutoWeakObjectPtr<%s%s>. + // So we must keep track of each direct call that precedes it + // because there's an indirect call that also precedes it. + // TODO: Possibly fix this for modular builds. + utility::exhaustive_decode((uint8_t*)*containing_func, 100, [&](INSTRUX& ix, uintptr_t ip) -> utility::ExhaustionResult { + if (result) { + return utility::ExhaustionResult::BREAK; + } + + if (std::string_view{ix.Mnemonic}.starts_with("CALL") && !ix.BranchInfo.IsIndirect) { + last_direct_call = utility::calculate_absolute((ip + ix.Length) - 4); + SPDLOG_INFO("Found a direct call to {:x}", *last_direct_call); + } + + if (std::string_view{ix.Mnemonic}.starts_with("CALL")) { + return utility::ExhaustionResult::STEP_OVER; + } + + const auto displacement = utility::resolve_displacement(ip); + + if (displacement && *displacement == str_data) { + if (last_direct_call) { + result = (FName::ToStringFn)*last_direct_call; + } + + return utility::ExhaustionResult::BREAK; + } + + return utility::ExhaustionResult::CONTINUE; + }); + + if (!result) { + SPDLOG_ERROR("FName::get_to_string: Failed to find ToString function"); + return std::nullopt; + } + + SPDLOG_INFO("FName::get_to_string: result={:x}", (uintptr_t)*result); + + return result; + }(); + + return result; +} + FName::FName(std::wstring_view name, EFindName find_type) { const auto constructor = get_constructor(); @@ -174,4 +258,19 @@ FName::FName(std::wstring_view name, EFindName find_type) { fn(this, name.data(), static_cast(find_type)); } + +std::wstring FName::to_string() const { + const auto to_string = get_to_string(); + + if (!to_string) { + return L""; + } + + const auto fn = *to_string; + TArray buffer{}; + + fn(this, &buffer); + + return buffer.data; +} } \ No newline at end of file diff --git a/shared/sdk/FName.hpp b/shared/sdk/FName.hpp index 1bebdf82..59dc2e8a 100644 --- a/shared/sdk/FName.hpp +++ b/shared/sdk/FName.hpp @@ -4,6 +4,8 @@ #include #include +#include "TArray.hpp" + namespace sdk { enum EFindName { Find, @@ -14,7 +16,11 @@ struct FName { using ConstructorFn = void* (*)(FName*, const wchar_t*, uint32_t); static std::optional get_constructor(); + using ToStringFn = TArray* (*)(const FName*, TArray*); + static std::optional get_to_string(); + FName(std::wstring_view name, EFindName find_type = EFindName::Add); + std::wstring to_string() const; int32_t a1{0}; int32_t a2{0}; diff --git a/shared/sdk/UObjectArray.cpp b/shared/sdk/UObjectArray.cpp new file mode 100644 index 00000000..bb771497 --- /dev/null +++ b/shared/sdk/UObjectArray.cpp @@ -0,0 +1,171 @@ +#include +#include +#include + +#include "EngineModule.hpp" + +#include "FName.hpp" +#include "UObjectArray.hpp" + +namespace sdk { +FUObjectArray* FUObjectArray::get() { + static auto result = []() -> FUObjectArray* { + SPDLOG_INFO("[FUObjectArray::get] Searching for FUObjectArray..."); + + const auto engine = sdk::get_ue_module(L"Engine"); + + if (engine == nullptr) { + return nullptr; + } + + const auto object_base_init_fn = utility::find_function_with_string_refs(engine, L"gc.MaxObjectsNotConsideredByGC", + L"/Script/Engine.GarbageCollectionSettings"); + + if (!object_base_init_fn) { + SPDLOG_ERROR("[FUObjectArray::get] Failed to find object base init function"); + return nullptr; + } + + SPDLOG_INFO("[FUObjectArray::get] Found object base init function at 0x{:x}", *object_base_init_fn); + + // Now, at this point we're in a bit of a predicament + // There's a function that takes a reference to the GUObjectArray as a parameter within this function + // but in some circumstances, it can become inlined + FUObjectArray* result{nullptr}; + + utility::exhaustive_decode((uint8_t*)*object_base_init_fn, 1024, [&](INSTRUX& ix, uintptr_t ip) -> utility::ExhaustionResult { + if (result != nullptr) { + return utility::ExhaustionResult::BREAK; + } + + if (std::string_view{ix.Mnemonic}.starts_with("CALL")) { + return utility::ExhaustionResult::STEP_OVER; + } + + const auto displacement = utility::resolve_displacement(ip); + + if (!displacement) { + return utility::ExhaustionResult::CONTINUE; + } + + if (IsBadReadPtr((void*)*displacement, 0x30)) { + return utility::ExhaustionResult::CONTINUE; + } + + // Now, we need to analyze this "structure" to see if it's actually a GUObjectArray + // We need to make sure the integer values look integer-like, and if casting them + // to 8 bytes, they must point to bad memory + // also, the actual pointer values should be pointing to good memory + int32_t& potential_obj_first_gc_index = *(int32_t*)(*displacement + 0x0); + int32_t& potential_obj_last_non_gc_index = *(int32_t*)(*displacement + 0x4); + int32_t& potential_obj_max_objects_not_consid_by_gc = *(int32_t*)(*displacement + 0x8); + bool& potential_obj_is_open_for_disregard_for_gc = *(bool*)(*displacement + 0xC); + + // array of structures, unknown size + void*& potential_obj_obj_objects = *(void**)(*displacement + 0x10); + int32_t& potential_obj_obj_max = *(int32_t*)(*displacement + 0x18); + int32_t& potential_obj_obj_count = *(int32_t*)(*displacement + 0x1C); + + // FChunkedFixedUObjectArray + // this is a bit tricky and not necessarily needed to identify the GUObjectArray + // however, it will be necessary to determine how to traverse the GUObjectArray + void*& potential_pre_allocated_objects = *(void**)(*displacement + 0x18); + int32_t& potential_chunked_obj_max = *(int32_t*)(*displacement + 0x20); + int32_t& potential_chunked_obj_count = *(int32_t*)(*displacement + 0x24); + int32_t& potential_max_chunks = *(int32_t*)(*displacement + 0x28); + int32_t& potential_num_chunks = *(int32_t*)(*displacement + 0x2C); + + if (potential_obj_first_gc_index < 0 || potential_obj_last_non_gc_index < 0 || potential_obj_max_objects_not_consid_by_gc < 0) { + SPDLOG_INFO("Skipping potential GUObjectArray at 0x{:x} due to negative values", *displacement); + return utility::ExhaustionResult::CONTINUE; + } + + if (!IsBadReadPtr(*(void**)&potential_obj_first_gc_index, sizeof(void*))) { + SPDLOG_INFO("Skipping potential GUObjectArray at 0x{:x} due to valid pointer", *displacement); + return utility::ExhaustionResult::CONTINUE; + } + + if (!IsBadReadPtr(*(void**)&potential_obj_max_objects_not_consid_by_gc, sizeof(void*))) { + SPDLOG_INFO("Skipping potential GUObjectArray at 0x{:x} due to valid pointer", *displacement); + return utility::ExhaustionResult::CONTINUE; + } + + if (*(uint8_t*)&potential_obj_is_open_for_disregard_for_gc > 1) { + SPDLOG_INFO("Skipping potential GUObjectArray at 0x{:x} due to invalid boolean", *displacement); + return utility::ExhaustionResult::CONTINUE; + } + + if (potential_obj_obj_objects == nullptr || IsBadReadPtr(potential_obj_obj_objects, sizeof(void*))) { + SPDLOG_INFO("Skipping potential GUObjectArray at 0x{:x} due to invalid pointer", *displacement); + return utility::ExhaustionResult::CONTINUE; + } + + // At this point we've found it, check if it's a chunked array or not, and set a static variable + if (IsBadReadPtr(*(void**)&potential_max_chunks, sizeof(void*)) && potential_max_chunks > 0 && potential_num_chunks > 0) { + try { + SPDLOG_INFO("max_chunks: {}, num_chunks: {}", potential_max_chunks, potential_num_chunks); + + bool any_failed = false; + + for (auto i = 0; i < potential_num_chunks; ++i) { + auto potential_chunked_obj = *(void**)((uintptr_t)potential_obj_obj_objects + (i * sizeof(void*))); + + if (potential_chunked_obj == nullptr || IsBadReadPtr(potential_chunked_obj, sizeof(void*))) { + SPDLOG_INFO("GUObjectArray failed to pass the chunked test"); + any_failed = true; + break; + } + } + + if (!any_failed) { + SPDLOG_INFO("GUObjectArray appears to be chunked"); + s_is_chunked = true; + } else { + SPDLOG_INFO("GUObjectArray appears to not be chunked (max_chunks: {}, num_chunks: {})", potential_max_chunks, potential_num_chunks); + } + } catch(...) { + SPDLOG_ERROR("Failed to check if GUObjectArray is chunked"); + } + } else { + SPDLOG_INFO("GUObjectArray appears to not be chunked (max_chunks: {}, num_chunks: {})", potential_max_chunks, potential_num_chunks); + } + + SPDLOG_INFO("Found GUObjectArray at 0x{:x}", *displacement); + result = (FUObjectArray*)*displacement; + return utility::ExhaustionResult::BREAK; + }); + + if (result == nullptr) { + SPDLOG_ERROR("[FUObjectArray::get] Failed to find GUObjectArray"); + return nullptr; + } + + // do an initial first pass as a test + /*for (auto i = 0; i < result->get_object_count(); ++i) { + auto item = result->get_object(i); + if (item == nullptr) { + continue; + } + + auto obj = *(void**)item; + + if (obj == nullptr || IsBadReadPtr(obj, sizeof(void*))) { + continue; + } + + FName* fname = (FName*)((uintptr_t)obj + 0x18); + try { + const auto name = fname->to_string(); + + SPDLOG_INFO("{} {}", i, utility::narrow(name)); + } catch(...) { + + } + }*/ + + return result; + }(); + + return result; +} +} \ No newline at end of file diff --git a/shared/sdk/UObjectArray.hpp b/shared/sdk/UObjectArray.hpp new file mode 100644 index 00000000..604a3bb0 --- /dev/null +++ b/shared/sdk/UObjectArray.hpp @@ -0,0 +1,57 @@ +#pragma once + +namespace sdk { +class UObjectBase; + +struct FUObjectItem { + UObjectBase* object{nullptr}; + int32_t flags{0}; + int32_t cluster_index{0}; + int32_t serial_number{0}; +}; + +struct FUObjectArray { + static FUObjectArray* get(); + + static bool is_chunked() { + return s_is_chunked; + } + + int32_t get_object_count() { + if (s_is_chunked) { + return *(int32_t*)((uintptr_t)this + OBJECTS_OFFSET + sizeof(void*) + sizeof(void*) + 0x4); + } + + return *(int32_t*)((uintptr_t)this + OBJECTS_OFFSET + 0x8); + } + + FUObjectItem* get_object(int32_t index) { + if (index < 0) { + return nullptr; + } + + if (s_is_chunked) { + const auto chunk_index = index / OBJECTS_PER_CHUNK; + const auto chunk_offset = index % OBJECTS_PER_CHUNK; + const auto chunk = *(void**)((uintptr_t)get_objects_ptr() + (chunk_index * sizeof(void*))); + + if (chunk == nullptr) { + return nullptr; + } + + return (FUObjectItem*)((uintptr_t)chunk + (chunk_offset * sizeof(FUObjectItem))); + } + + return (FUObjectItem*)((uintptr_t)get_objects_ptr() + (index * sizeof(FUObjectItem))); + } + + void* get_objects_ptr() { + return *(void**)((uintptr_t)this + OBJECTS_OFFSET); + } + +private: + constexpr static inline auto OBJECTS_PER_CHUNK = 64 * 1024; + constexpr static inline auto OBJECTS_OFFSET = 0x10; + static inline bool s_is_chunked{false}; +}; +} \ No newline at end of file