Skip to content

Commit

Permalink
SDK: Add GUObjectArray and FName::ToString
Browse files Browse the repository at this point in the history
  • Loading branch information
praydog committed Jul 5, 2023
1 parent 254d37d commit 0f15e05
Show file tree
Hide file tree
Showing 4 changed files with 333 additions and 0 deletions.
99 changes: 99 additions & 0 deletions shared/sdk/FName.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,90 @@ std::optional<FName::ConstructorFn> FName::get_constructor() {
return result;
}

std::optional<FName::ToStringFn> FName::get_to_string() {
static auto result = []() -> std::optional<FName::ToStringFn> {
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<uintptr_t> last_direct_call{};
std::optional<FName::ToStringFn> 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();

Expand All @@ -174,4 +258,19 @@ FName::FName(std::wstring_view name, EFindName find_type) {

fn(this, name.data(), static_cast<uint32_t>(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<wchar_t> buffer{};

fn(this, &buffer);

return buffer.data;
}
}
6 changes: 6 additions & 0 deletions shared/sdk/FName.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <cstdint>
#include <string_view>

#include "TArray.hpp"

namespace sdk {
enum EFindName {
Find,
Expand All @@ -14,7 +16,11 @@ struct FName {
using ConstructorFn = void* (*)(FName*, const wchar_t*, uint32_t);
static std::optional<ConstructorFn> get_constructor();

using ToStringFn = TArray<wchar_t>* (*)(const FName*, TArray<wchar_t>*);
static std::optional<ToStringFn> 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};
Expand Down
171 changes: 171 additions & 0 deletions shared/sdk/UObjectArray.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#include <spdlog/spdlog.h>
#include <utility/Scan.hpp>
#include <utility/String.hpp>

#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;
}
}
57 changes: 57 additions & 0 deletions shared/sdk/UObjectArray.hpp
Original file line number Diff line number Diff line change
@@ -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};
};
}

0 comments on commit 0f15e05

Please sign in to comment.