From 3a0c6d951f951e27a4813e7232f9fd5ca166ed47 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Fri, 30 May 2025 12:19:11 +1200 Subject: [PATCH 1/7] fix fframe step generating garbage suspect due to the call function hook being thiscall, ecx was always a valid pointer, even if it pointed at garbage --- src/unrealsdk/game/bl1/bl1.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/unrealsdk/game/bl1/bl1.cpp b/src/unrealsdk/game/bl1/bl1.cpp index c5e20dd..3d715ef 100644 --- a/src/unrealsdk/game/bl1/bl1.cpp +++ b/src/unrealsdk/game/bl1/bl1.cpp @@ -49,7 +49,7 @@ const constinit Pattern<11> GNATIVES_SIG{ }; // NOLINTNEXTLINE(modernize-use-using) -typedef void(__stdcall* fframe_step_func)(FFrame*, void*); +typedef void(__thiscall* fframe_step_func)(UObject*, FFrame*, void*); fframe_step_func** fframe_step_gnatives; } // namespace @@ -59,8 +59,8 @@ void BL1Hook::find_fframe_step(void) { LOG(MISC, "GNatives: {:p}", reinterpret_cast(fframe_step_gnatives)); } -void BL1Hook::fframe_step(unreal::FFrame* frame, unreal::UObject* /*obj*/, void* param) const { - ((*fframe_step_gnatives)[*frame->Code++])(frame, param); +void BL1Hook::fframe_step(FFrame* frame, UObject* obj, void* param) const { + ((*fframe_step_gnatives)[*frame->Code++])(obj, frame, param); } #pragma region FName::Init From 0f61f39653198486d6275bf820f7e02ea5f26cbf Mon Sep 17 00:00:00 2001 From: apple1417 Date: Fri, 30 May 2025 19:04:34 +1200 Subject: [PATCH 2/7] only check params properties when validating delegate --- src/unrealsdk/unreal/structs/fscriptdelegate.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/unrealsdk/unreal/structs/fscriptdelegate.cpp b/src/unrealsdk/unreal/structs/fscriptdelegate.cpp index 9cb3b71..8275a98 100644 --- a/src/unrealsdk/unreal/structs/fscriptdelegate.cpp +++ b/src/unrealsdk/unreal/structs/fscriptdelegate.cpp @@ -94,8 +94,12 @@ void FScriptDelegate::validate_signature(const std::optional& fun reasonably simple to implement. */ { - auto func_props = func->func->properties(); - auto sig_props = signature->properties(); + auto func_props = std::ranges::filter_view(func->func->properties(), [](UProperty* prop) { + return (prop->PropertyFlags() & UProperty::PROP_FLAG_PARAM) != 0; + }); + auto sig_props = std::ranges::filter_view(signature->properties(), [](UProperty* prop) { + return (prop->PropertyFlags() & UProperty::PROP_FLAG_PARAM) != 0; + }); auto [func_diff, sig_diff] = std::ranges::mismatch( func_props, sig_props, From b12be1932623144c47a05576c0c20154e9a9fe64 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Fri, 30 May 2025 22:00:42 +1200 Subject: [PATCH 3/7] fix __thiscall warning --- src/unrealsdk/game/bl1/bl1.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/unrealsdk/game/bl1/bl1.cpp b/src/unrealsdk/game/bl1/bl1.cpp index 3d715ef..31635dc 100644 --- a/src/unrealsdk/game/bl1/bl1.cpp +++ b/src/unrealsdk/game/bl1/bl1.cpp @@ -41,6 +41,11 @@ void BL1Hook::post_init(void) { namespace { +#if defined(__MINGW32__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wattributes" // thiscall on non-class +#endif + // FFrame::Step is inlined, so instead we manually re-implement it using GNatives. const constinit Pattern<11> GNATIVES_SIG{ "8B 14 95 {????????}" // mov edx, [edx*4+01F942C0] @@ -52,6 +57,10 @@ const constinit Pattern<11> GNATIVES_SIG{ typedef void(__thiscall* fframe_step_func)(UObject*, FFrame*, void*); fframe_step_func** fframe_step_gnatives; +#if defined(__MINGW32__) +#pragma GCC diagnostic pop +#endif + } // namespace void BL1Hook::find_fframe_step(void) { From 793b9ef4171321259407979559bedb116070e4b3 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Sun, 1 Jun 2025 14:01:00 +1200 Subject: [PATCH 4/7] fix tps class interfaces offset --- src/unrealsdk/game/tps/offsets.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unrealsdk/game/tps/offsets.h b/src/unrealsdk/game/tps/offsets.h index 8e38102..944143f 100644 --- a/src/unrealsdk/game/tps/offsets.h +++ b/src/unrealsdk/game/tps/offsets.h @@ -71,7 +71,7 @@ class UClass : public UStruct { UObject* ClassDefaultObject; private: - uint8_t UnknownData01[0x14]; + uint8_t UnknownData01[0x10]; public: unreal::TArray Interfaces; From 7c362832f758eee5aadb3b8dfbdd45701f768333 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Sun, 1 Jun 2025 17:08:11 +1200 Subject: [PATCH 5/7] handle steam drm encrypting bl1 exe --- src/unrealsdk/game/bl1/antidebug.cpp | 5 -- src/unrealsdk/game/bl1/bl1.cpp | 2 + src/unrealsdk/game/bl1/bl1.h | 10 ++- src/unrealsdk/game/bl1/steamdrm.cpp | 110 +++++++++++++++++++++++++++ src/unrealsdk/memory.cpp | 9 +++ src/unrealsdk/pch.h | 2 + src/unrealsdk/utils.cpp | 53 +++++++++++++ src/unrealsdk/utils.h | 14 ++++ 8 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 src/unrealsdk/game/bl1/steamdrm.cpp diff --git a/src/unrealsdk/game/bl1/antidebug.cpp b/src/unrealsdk/game/bl1/antidebug.cpp index c8db461..7753063 100644 --- a/src/unrealsdk/game/bl1/antidebug.cpp +++ b/src/unrealsdk/game/bl1/antidebug.cpp @@ -1,8 +1,3 @@ - -// - NOTE - -// Copied from bl2/antidebug.cpp -// - #include "unrealsdk/pch.h" #include "unrealsdk/game/bl1/bl1.h" diff --git a/src/unrealsdk/game/bl1/bl1.cpp b/src/unrealsdk/game/bl1/bl1.cpp index 31635dc..e824f5a 100644 --- a/src/unrealsdk/game/bl1/bl1.cpp +++ b/src/unrealsdk/game/bl1/bl1.cpp @@ -14,6 +14,8 @@ using namespace unrealsdk::unreal; namespace unrealsdk::game { void BL1Hook::hook(void) { + wait_for_steam_drm(); + hook_antidebug(); hook_process_event(); diff --git a/src/unrealsdk/game/bl1/bl1.h b/src/unrealsdk/game/bl1/bl1.h index 3fe259c..1724a20 100644 --- a/src/unrealsdk/game/bl1/bl1.h +++ b/src/unrealsdk/game/bl1/bl1.h @@ -12,6 +12,11 @@ namespace unrealsdk::game { class BL1Hook : public AbstractHook { protected: + /** + * @brief Blocking waits for the steam drm to finish unpacking the executable. + */ + static void wait_for_steam_drm(void); + /** * @brief Finds `FName::Init`, and sets up such that `fname_init` may be called. */ @@ -126,10 +131,11 @@ class BL1Hook : public AbstractHook { template <> struct GameTraits { - static constexpr auto NAME = "Borderlands"; + static constexpr auto NAME = "Borderlands 1"; static bool matches_executable(std::string_view executable) { - return executable == "Borderlands.exe" || executable == "borderlands.exe"; + return executable.starts_with("Borderlands.exe") + || executable.starts_with("borderlands.exe"); } }; diff --git a/src/unrealsdk/game/bl1/steamdrm.cpp b/src/unrealsdk/game/bl1/steamdrm.cpp new file mode 100644 index 0000000..4fe32ac --- /dev/null +++ b/src/unrealsdk/game/bl1/steamdrm.cpp @@ -0,0 +1,110 @@ +#include "unrealsdk/pch.h" +#include "unrealsdk/game/bl1/bl1.h" +#include "unrealsdk/memory.h" +#include "unrealsdk/utils.h" + +#if UNREALSDK_FLAVOUR == UNREALSDK_FLAVOUR_WILLOW && !defined(UNREALSDK_IMPORTING) + +using namespace unrealsdk::utils; +using namespace unrealsdk::memory; + +namespace unrealsdk::game { + +namespace { + +// This is ___tmainCRTStartup, so expect it's very stable +const constinit Pattern<14> UNPACKED_ENTRY_SIG{"6A 58 68 ?? ?? ?? ?? E8 ?? ?? ?? ?? 33 DB"}; + +std::atomic ready = false; +std::mutex ready_mutex; +std::condition_variable ready_cv; + +// This is probably orders of magnitude bigger than we need, but best be safe +const constexpr std::chrono::seconds FALLBACK_DELAY{5}; + +// NOLINTBEGIN(readability-identifier-naming) + +// NOLINTNEXTLINE(modernize-use-using) - need a typedef for calling conventions in msvc +typedef void(WINAPI* GetStatupInfoA_func)(LPSTARTUPINFOA); +GetStatupInfoA_func GetStatupInfoA_ptr; + +void GetStartupInfoA_hook(LPSTARTUPINFOA lpStartupInfo) { + GetStatupInfoA_ptr(lpStartupInfo); + + static_assert(decltype(ready)::is_always_lock_free, "need to lock on checking ready flag too"); + if (ready.load()) { + return; + } + + const std::lock_guard lock{ready_mutex}; + ready.store(true); + ready_cv.notify_all(); +} + +// NOLINTEND(readability-identifier-naming) + +} // namespace + +void BL1Hook::wait_for_steam_drm(void) { + { + // Immediately suspend the other threads + const ThreadSuspender suspend{}; + + if (UNPACKED_ENTRY_SIG.sigscan_nullable() != 0) { + // If we found a match, we're already unpacked + return; + } + + LOG(MISC, "Waiting for steam drm unpack"); + + // Set up a hook for GetStartupInfoA, which is the one of the first things the unpacked + // entry function calls, which we'll use to tell once it's been unpacked + MH_STATUS status = MH_OK; + + status = MH_CreateHook(reinterpret_cast(&GetStartupInfoA), + reinterpret_cast(&GetStartupInfoA_hook), + reinterpret_cast(&GetStatupInfoA_ptr)); + if (status != MH_OK) { + LOG(ERROR, "Failed to create GetStartupInfoA hook: {:x}", + static_cast(status)); + + LOG(ERROR, "Falling back to a static delay"); + std::this_thread::sleep_for(FALLBACK_DELAY); + return; + } + + status = MH_EnableHook(reinterpret_cast(&GetStartupInfoA)); + if (status != MH_OK) { + LOG(ERROR, "Failed to enable GetStartupInfoA hook: {:x}", + static_cast(status)); + + LOG(ERROR, "Falling back to a static delay"); + std::this_thread::sleep_for(FALLBACK_DELAY); + return; + } + + // Drop out of this scope and unsuspend the other threads, let the unpacker run + } + + std::unique_lock lock(ready_mutex); + ready_cv.wait(lock, [] { return ready.load(); }); + + MH_STATUS status = MH_OK; + status = MH_DisableHook(reinterpret_cast(&GetStartupInfoA)); + if (status != MH_OK) { + LOG(ERROR, "Failed to disable GetStartupInfoA hook: {:x}", static_cast(status)); + + // If it fails, there isn't really any harm in leaving it active, just return + return; + } + + status = MH_RemoveHook(reinterpret_cast(&GetStartupInfoA)); + if (status != MH_OK) { + LOG(ERROR, "Failed to remove GetStartupInfoA hook: {:x}", static_cast(status)); + return; + } +} + +} // namespace unrealsdk::game + +#endif diff --git a/src/unrealsdk/memory.cpp b/src/unrealsdk/memory.cpp index b294877..84e7972 100644 --- a/src/unrealsdk/memory.cpp +++ b/src/unrealsdk/memory.cpp @@ -28,6 +28,15 @@ std::pair get_exe_range(void) { auto module_length = nt_header->OptionalHeader.SizeOfImage; range = {reinterpret_cast(allocation_base), module_length}; + + if constexpr (sizeof(uintptr_t) == 4) { + LOG(MISC, "Executable memory range: {:08x}-{:08x}", range->first, + range->first + range->second); + } else { + LOG(MISC, "Executable memory range: {:012x}-{:012x}", range->first, + range->first + range->second); + } + return *range; } diff --git a/src/unrealsdk/pch.h b/src/unrealsdk/pch.h index 43259f7..6535c56 100644 --- a/src/unrealsdk/pch.h +++ b/src/unrealsdk/pch.h @@ -29,6 +29,8 @@ #define SetThreadDescription(x, y) #endif +#include + #include #ifdef __cplusplus diff --git a/src/unrealsdk/utils.cpp b/src/unrealsdk/utils.cpp index d060b52..9af332a 100644 --- a/src/unrealsdk/utils.cpp +++ b/src/unrealsdk/utils.cpp @@ -90,4 +90,57 @@ std::filesystem::path get_executable(void) { return path; } +namespace { + +/** + * @brief Suspends or resumes all other threads in the process. + * + * @param resume True if to resume, false if to suspend them. + */ +void adjust_thread_running_status(bool resume) { + HANDLE thread_snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (thread_snapshot == nullptr) { + CloseHandle(thread_snapshot); + return; + } + + THREADENTRY32 te32{}; + te32.dwSize = sizeof(THREADENTRY32); + + if (Thread32First(thread_snapshot, &te32) == 0) { + CloseHandle(thread_snapshot); + return; + } + + do { + if (te32.th32OwnerProcessID != GetCurrentProcessId() + || te32.th32ThreadID == GetCurrentThreadId()) { + continue; + } + + HANDLE thread = OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME, + 0, te32.th32ThreadID); + if (thread != nullptr) { + if (resume) { + ResumeThread(thread); + } else { + SuspendThread(thread); + } + + CloseHandle(thread); + } + } while (Thread32Next(thread_snapshot, &te32) != 0); + + CloseHandle(thread_snapshot); +} + +} // namespace + +ThreadSuspender::ThreadSuspender(void) { + adjust_thread_running_status(false); +} +ThreadSuspender::~ThreadSuspender() { + adjust_thread_running_status(true); +} + } // namespace unrealsdk::utils diff --git a/src/unrealsdk/utils.h b/src/unrealsdk/utils.h index b20a3de..1e0b531 100644 --- a/src/unrealsdk/utils.h +++ b/src/unrealsdk/utils.h @@ -131,6 +131,20 @@ struct DLLSafeCallback { R operator()(As... args) { return this->vftable->call(this, args...); } }; +/** + * @brief RAII class which suspends all other threads for it's lifespan. + */ +class ThreadSuspender { + public: + ThreadSuspender(void); + ~ThreadSuspender(); + + ThreadSuspender(const ThreadSuspender&) = delete; + ThreadSuspender(ThreadSuspender&&) = delete; + ThreadSuspender& operator=(const ThreadSuspender&) = delete; + ThreadSuspender& operator=(ThreadSuspender&&) = delete; +}; + namespace { template From 426a54ae6e1dcbe722cbd14a72f18a42633b2183 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Sun, 1 Jun 2025 17:08:33 +1200 Subject: [PATCH 6/7] swap to release with debug info been meaning to do this forever, kept forgetting --- CMakePresets.json | 2 +- common_cmake | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index 718359f..9d44578 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -144,7 +144,7 @@ "name": "_release", "hidden": true, "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "RelWithDebInfo" } }, { diff --git a/common_cmake b/common_cmake index 0b1c32d..17528aa 160000 --- a/common_cmake +++ b/common_cmake @@ -1 +1 @@ -Subproject commit 0b1c32de1ff98a7851f798d6b092edd1e89e5eba +Subproject commit 17528aa7461deea26f55b2d7dc9ede720feee4c3 From aaea6abf7c49ee6a29b498b794722c37c2049cbc Mon Sep 17 00:00:00 2001 From: apple1417 Date: Mon, 2 Jun 2025 18:56:49 +1200 Subject: [PATCH 7/7] fix spelling --- src/unrealsdk/game/bl1/steamdrm.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/unrealsdk/game/bl1/steamdrm.cpp b/src/unrealsdk/game/bl1/steamdrm.cpp index 4fe32ac..13d7ff7 100644 --- a/src/unrealsdk/game/bl1/steamdrm.cpp +++ b/src/unrealsdk/game/bl1/steamdrm.cpp @@ -25,11 +25,11 @@ const constexpr std::chrono::seconds FALLBACK_DELAY{5}; // NOLINTBEGIN(readability-identifier-naming) // NOLINTNEXTLINE(modernize-use-using) - need a typedef for calling conventions in msvc -typedef void(WINAPI* GetStatupInfoA_func)(LPSTARTUPINFOA); -GetStatupInfoA_func GetStatupInfoA_ptr; +typedef void(WINAPI* GetStartupInfoA_func)(LPSTARTUPINFOA); +GetStartupInfoA_func GetStartupInfoA_ptr; void GetStartupInfoA_hook(LPSTARTUPINFOA lpStartupInfo) { - GetStatupInfoA_ptr(lpStartupInfo); + GetStartupInfoA_ptr(lpStartupInfo); static_assert(decltype(ready)::is_always_lock_free, "need to lock on checking ready flag too"); if (ready.load()) { @@ -63,7 +63,7 @@ void BL1Hook::wait_for_steam_drm(void) { status = MH_CreateHook(reinterpret_cast(&GetStartupInfoA), reinterpret_cast(&GetStartupInfoA_hook), - reinterpret_cast(&GetStatupInfoA_ptr)); + reinterpret_cast(&GetStartupInfoA_ptr)); if (status != MH_OK) { LOG(ERROR, "Failed to create GetStartupInfoA hook: {:x}", static_cast(status));