From 39e0a2856a2e3873f0103040c1a4d129522dde1f Mon Sep 17 00:00:00 2001 From: SirCxyrtyx Date: Thu, 23 Jul 2026 22:29:02 -0700 Subject: [PATCH 1/2] PostLoad asis now run deterministically, immediately before WinMain --- bink2w64.vcxproj | 1 + bink2w64.vcxproj.filters | 1 + src/dllmain.cpp | 108 +++++++++++++++++++++------------------ src/drm.cpp | 105 +++++++++++++++++++++++++++++++++++++ src/drm.h | 73 ++++---------------------- src/modules/asi_loader.h | 6 --- 6 files changed, 175 insertions(+), 119 deletions(-) create mode 100644 src/drm.cpp diff --git a/bink2w64.vcxproj b/bink2w64.vcxproj index ba3b385..79093f0 100644 --- a/bink2w64.vcxproj +++ b/bink2w64.vcxproj @@ -100,6 +100,7 @@ + diff --git a/bink2w64.vcxproj.filters b/bink2w64.vcxproj.filters index ab80bf9..f265fd8 100644 --- a/bink2w64.vcxproj.filters +++ b/bink2w64.vcxproj.filters @@ -36,6 +36,7 @@ src + diff --git a/src/dllmain.cpp b/src/dllmain.cpp index f7afcfd..a59669b 100644 --- a/src/dllmain.cpp +++ b/src/dllmain.cpp @@ -1,5 +1,4 @@ #include "dllexports.h" -#define ASI_LOG_FNAME "bink2w64_proxy.log" #include #include @@ -125,7 +124,54 @@ void SetWorkingDirectory() { SetCurrentDirectoryW(exePath.parent_path().c_str()); } -void __stdcall OnAttach() +void __stdcall AsyncAttach() +{ + // Handle logic depending on the attached-to exe. + switch (GLEBinkProxy.Game) + { + case LEGameVersion::Launcher: + { + // Keep trying to find a pattern we are *almost* guaranteed to have until we find it. + DRM::WaitForDRMv3(); + + if (!GLEBinkProxy.LauncherArgs->Activate()) + { + GLogger.writeln(L"OnAttach: ERROR: handling of Launcher args failed, aborting!"); + } + + // Hook CreateProcessA so we can properly set the working directory when launching a game. + if (!GLEBinkProxy.LauncherFixer->Activate()) + { + GLogger.writeln(L"OnAttach: ERROR: error fixing launcher working directory issue!"); + } + + break; + } + default: + { + GLogger.writeln(L"OnAttach: unsupported game, bye!"); + return; + } + } + + return; +} + +using t_PostDRM_hook = void*(void*, char, size_t, void*); +t_PostDRM_hook* PostDRM_hook_orig = nullptr; +void* PostDRM_hook(void* param_1, char param_2, size_t param_3, void* param_4) +{ + void* ret = PostDRM_hook_orig(param_1, param_2, param_3, param_4); + // Unlock the console. + if (!GLEBinkProxy.ConsoleEnabler->Activate()) + { + GLogger.writeln(L"OnAttach: ERROR: console bypass installation failed"); + } + GLEBinkProxy.AsiLoader->PostLoad(GLEBinkProxy.SPI); + return ret; +} + +bool SyncAttach() { // Open console or log. Utils::SetupOutput(); @@ -147,7 +193,7 @@ void __stdcall OnAttach() if (mhStatus != MH_OK) { GLogger.writeln(L"OnAttach: ERROR: failed to initialize the hooking library (code = %d).", mhStatus); - return; + return false; } // Initialize global settings. @@ -170,6 +216,7 @@ void __stdcall OnAttach() if (!GLEBinkProxy.AsiLoader->Activate()) { GLogger.writeln(L"OnAttach: ERROR: loading of one or more ASI plugins failed!"); + return false; } GLogger.writeln(L"OnAttach: got to preload / 0x%p", GLEBinkProxy); @@ -178,57 +225,18 @@ void __stdcall OnAttach() // Post-drm mods are loaded in the switch below. GLEBinkProxy.AsiLoader->PreLoad(GLEBinkProxy.SPI); - // Prevent the compiler from *potentially* reordering instructions before and after. - MemoryBarrier(); - - // Handle logic depending on the attached-to exe. - switch (GLEBinkProxy.Game) - { - case LEGameVersion::LE1: - case LEGameVersion::LE2: - case LEGameVersion::LE3: - { - // Keep trying to find a pattern we are *almost* guaranteed to have until we find it. - DRM::WaitForDRMv3(); - - // Unlock the console. - if (!GLEBinkProxy.ConsoleEnabler->Activate()) - { - GLogger.writeln(L"OnAttach: ERROR: console bypass installation failed, aborting!"); - break; - } - - // Load all native mods that declare being post-drm. - GLEBinkProxy.AsiLoader->PostLoad(GLEBinkProxy.SPI); - - break; - } - case LEGameVersion::Launcher: + if (GLEBinkProxy.Game == LEGameVersion::LE1 + || GLEBinkProxy.Game == LEGameVersion::LE2 + || GLEBinkProxy.Game == LEGameVersion::LE3) { - // Keep trying to find a pattern we are *almost* guaranteed to have until we find it. - DRM::WaitForDRMv3(); - - if (!GLEBinkProxy.LauncherArgs->Activate()) - { - GLogger.writeln(L"OnAttach: ERROR: handling of Launcher args failed, aborting!"); - } - - // Hook CreateProcessA so we can properly set the working directory when launching a game. - if (!GLEBinkProxy.LauncherFixer->Activate()) - { - GLogger.writeln(L"OnAttach: ERROR: error fixing launcher working directory issue!"); - } - - break; + DRM::InstallDRMv4Hook(PostDRM_hook, reinterpret_cast(&PostDRM_hook_orig)); } - default: + else { - GLogger.writeln(L"OnAttach: unsupported game, bye!"); - return; - } + CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)AsyncAttach, nullptr, 0, nullptr); } - return; + return true; } void __stdcall OnDetach() @@ -252,7 +260,7 @@ void __stdcall OnDetach() BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved) { switch (dwReason) { case DLL_PROCESS_ATTACH: - CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)OnAttach, nullptr, 0, nullptr); + SyncAttach(); return TRUE; case DLL_PROCESS_DETACH: diff --git a/src/drm.cpp b/src/drm.cpp new file mode 100644 index 0000000..b813a8f --- /dev/null +++ b/src/drm.cpp @@ -0,0 +1,105 @@ +#include "drm.h" +#include +#include + +void DRM::WaitForDRMv2() +{ + GLogger.writeln(L"WaitForDRMv2: waiting for DRM..."); + + if (!DrmEvent->InError()) + { + auto rc = DrmEvent->WaitForIt(10000); // 10 seconds timeout should be more than enough + switch (rc) + { + case Utils::EventWaitValue::Signaled: + GLogger.writeln(L"WaitForDRMv2: event signaled!"); + delete DrmEvent; + DrmEvent = nullptr; + break; + default: + GLogger.writeln(L"WaitForDRMv2: event wait failed (EventWaitValue = %d)", (int)rc); + } + } +} + +void DRM::WaitForDRMv3() +{ + int iterations = 0; + bool foundPattern = false; + BYTE* offset = nullptr; + + do + { + switch (GLEBinkProxy.Game) + { + case LEGameVersion::LE1: + foundPattern = nullptr != Utils::ScanProcess(LE1_UFunctionBind_Pattern, LE1_UFunctionBind_Mask); + break; + case LEGameVersion::LE2: + foundPattern = nullptr != Utils::ScanProcess(LE2_UFunctionBind_Pattern, LE2_UFunctionBind_Mask); + break; + case LEGameVersion::LE3: + foundPattern = nullptr != Utils::ScanProcess(LE3_UFunctionBind_Pattern, LE3_UFunctionBind_Mask); + break; + case LEGameVersion::Launcher: + foundPattern = nullptr != Utils::ScanProcess(LEL_DRMTest_Pattern, LEL_DRMTest_Mask); + break; + default: + foundPattern = true; + } + + iterations++; + + } while (!foundPattern && iterations < 20000); + + if (!foundPattern) + { + GLogger.writeln(L"WaitForDRMv3 - FAILED TO FIND THE PATTERN in %d iteration(s), but still stopping the poll because YOLO.", iterations); + return; + } + + GLogger.writeln(L"WaitForDRMv3 - found the pattern in %d iteration(s), stopping the poll.", iterations); +} + +BYTE* GetActivatorModuleBase() { + DWORD const CurrentProcess = GetCurrentProcessId(); + HANDLE const ProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, CurrentProcess); + + BYTE* BaseAddress = nullptr; + + if (ProcessSnapshot != INVALID_HANDLE_VALUE) { + MODULEENTRY32 ModuleEntry{}; + ModuleEntry.dwSize = sizeof ModuleEntry; + + if (Module32First(ProcessSnapshot, &ModuleEntry)) { + do { + auto const* const ModuleName = ModuleEntry.szModule; + if (ModuleName == std::wcsstr(ModuleName, L"Activation64")) { + BaseAddress = ModuleEntry.modBaseAddr; + break; + } + } while (Module32Next(ProcessSnapshot, &ModuleEntry)); + } + + CloseHandle(ProcessSnapshot); + } + + return BaseAddress; +} + +void DRM::InstallDRMv4Hook(void* detour, void** original) +{ + auto const ActivatorBase = GetActivatorModuleBase(); + if (!ActivatorBase) { + GLogger.writeln(L"InstallDRMv4Hook: ERROR: failed to find Activation64.dll base address."); + return; + } + auto const Offset = ActivatorBase + 0x11480; + auto spiReturn = GLEBinkProxy.SPI->InstallHook("PostDRM_hook", Offset, detour, original); + if (spiReturn != SPIReturn::Success) + { + GLogger.writeln(L"InstallDRMv4Hook: ERROR: failed to install post-drm hook (code = %d).", static_cast(spiReturn)); + return; + } + GLogger.writeln(L"InstallDRMv4Hook: successfully installed hook on Activation64.dll."); +} \ No newline at end of file diff --git a/src/drm.h b/src/drm.h index 87ef783..2385c64 100644 --- a/src/drm.h +++ b/src/drm.h @@ -1,10 +1,10 @@ #pragma once -#include "gamever.h" #include "utils/io.h" #include "utils/event.h" -#include "utils/hook.h" #include "dllstruct.h" +#include +#include namespace DRM @@ -13,12 +13,12 @@ namespace DRM // Version 2 // ====================================================================== - static Utils::Event* DrmEvent = nullptr; - bool GameWindowCreated = false; + inline Utils::Event* DrmEvent = nullptr; + inline bool GameWindowCreated = false; typedef HWND(WINAPI* CREATEWINDOWEXW)(DWORD dwExStyle, LPCWSTR lpClassName, LPCWSTR lpWindowName, DWORD dwStyle, int X, int Y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam); - CREATEWINDOWEXW CreateWindowExW_orig = nullptr; - HWND WINAPI CreateWindowExW_hooked(DWORD dwExStyle, LPCWSTR lpClassName, LPCWSTR lpWindowName, DWORD dwStyle, int X, int Y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam) + inline CREATEWINDOWEXW CreateWindowExW_orig = nullptr; + inline HWND WINAPI CreateWindowExW_hooked(DWORD dwExStyle, LPCWSTR lpClassName, LPCWSTR lpWindowName, DWORD dwStyle, int X, int Y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam) { //GLogger.writeln(L"CreateWindowExW: lpWindowName = %s", lpWindowName); if (nullptr != lpWindowName @@ -34,70 +34,17 @@ namespace DRM return CreateWindowExW_orig(dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam); } - void InitializeDRMv2() + inline void InitializeDRMv2() { DrmEvent = new Utils::Event(L"drm_wait"); } - void WaitForDRMv2() - { - GLogger.writeln(L"WaitForDRMv2: waiting for DRM..."); - - if (!DrmEvent->InError()) - { - auto rc = DrmEvent->WaitForIt(10000); // 10 seconds timeout should be more than enough - switch (rc) - { - case Utils::EventWaitValue::Signaled: - GLogger.writeln(L"WaitForDRMv2: event signaled!"); - delete DrmEvent; - DrmEvent = nullptr; - break; - default: - GLogger.writeln(L"WaitForDRMv2: event wait failed (EventWaitValue = %d)", (int)rc); - } - } - } + void WaitForDRMv2(); // Version 3 // ====================================================================== - void WaitForDRMv3() - { - int iterations = 0; - bool foundPattern = false; - BYTE* offset = nullptr; - - do - { - switch (GLEBinkProxy.Game) - { - case LEGameVersion::LE1: - foundPattern = nullptr != Utils::ScanProcess(LE1_UFunctionBind_Pattern, LE1_UFunctionBind_Mask); - break; - case LEGameVersion::LE2: - foundPattern = nullptr != Utils::ScanProcess(LE2_UFunctionBind_Pattern, LE2_UFunctionBind_Mask); - break; - case LEGameVersion::LE3: - foundPattern = nullptr != Utils::ScanProcess(LE3_UFunctionBind_Pattern, LE3_UFunctionBind_Mask); - break; - case LEGameVersion::Launcher: - foundPattern = nullptr != Utils::ScanProcess(LEL_DRMTest_Pattern, LEL_DRMTest_Mask); - break; - default: - foundPattern = true; - } - - iterations++; - - } while (!foundPattern && iterations < 20000); + void WaitForDRMv3(); - if (!foundPattern) - { - GLogger.writeln(L"WaitForDRMv3 - FAILED TO FIND THE PATTERN in %d iteration(s), but still stopping the poll because YOLO.", iterations); - return; - } - - GLogger.writeln(L"WaitForDRMv3 - found the pattern in %d iteration(s), stopping the poll.", iterations); - } + void InstallDRMv4Hook(void* detour, void** original); } diff --git a/src/modules/asi_loader.h b/src/modules/asi_loader.h index 2056577..9825df0 100644 --- a/src/modules/asi_loader.h +++ b/src/modules/asi_loader.h @@ -453,9 +453,6 @@ class AsiLoaderModule } } - //// Give async plugins some time to do things. - Sleep(250); - return true; } bool PostLoad(ISharedProxyInterface* interfacePtr) @@ -473,9 +470,6 @@ class AsiLoaderModule } } - //// Give async plugins some time to do things. - Sleep(250); - return true; } }; From 5dcf31ce821e332182891d0c4e3feaf90b7f1552 Mon Sep 17 00:00:00 2001 From: SirCxyrtyx Date: Thu, 23 Jul 2026 22:29:20 -0700 Subject: [PATCH 2/2] Fix build --- src/dllstruct.h | 4 ++-- src/modules/launcher_working_dir_fix.h | 4 ++-- src/spi/implementation.h | 2 +- src/spi/interface.h | 2 +- src/ue_types.h | 14 +++++++------- src/utils/hook.h | 2 +- src/utils/io.h | 22 ++++++++++------------ src/utils/memory.h | 6 +++--- 8 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/dllstruct.h b/src/dllstruct.h index 0379f65..0100dfc 100644 --- a/src/dllstruct.h +++ b/src/dllstruct.h @@ -170,7 +170,7 @@ struct LEBinkProxy auto leargs = GetStringValueFromHKCU(L"SOFTWARE\\ME3Tweaks", L"LEAutobootArgs"); if (leargs.length() > 0) { - CmdLine = new wchar_t[leargs.length()]; + CmdLine = new wchar_t[leargs.length() + 1]; wcscpy(CmdLine, leargs.data()); DeleteValueFromHKCU(L"SOFTWARE\\ME3Tweaks", L"LEAutobootArgs"); } @@ -196,4 +196,4 @@ struct LEBinkProxy // Global instance. -static LEBinkProxy GLEBinkProxy; +inline LEBinkProxy GLEBinkProxy; diff --git a/src/modules/launcher_working_dir_fix.h b/src/modules/launcher_working_dir_fix.h index 2243a28..c95d9d0 100644 --- a/src/modules/launcher_working_dir_fix.h +++ b/src/modules/launcher_working_dir_fix.h @@ -12,8 +12,8 @@ // These have to list outside the class or the actual hooking signature won't be correct typedef int (WINAPI* CREATEPROCESSA)(LPCSTR, LPSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCSTR, LPSTARTUPINFOA, LPPROCESS_INFORMATION); -CREATEPROCESSA CreateProcessA_orig = NULL; -int WINAPI CreateProcessA_hook(LPCSTR lpApplicationName, LPSTR lpCommandLine, +inline CREATEPROCESSA CreateProcessA_orig = NULL; +inline int WINAPI CreateProcessA_hook(LPCSTR lpApplicationName, LPSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, diff --git a/src/spi/implementation.h b/src/spi/implementation.h index d042916..20a2be8 100644 --- a/src/spi/implementation.h +++ b/src/spi/implementation.h @@ -162,7 +162,7 @@ namespace SPI } *outGameVersion = static_cast(GLEBinkProxy.Game); - return SPIReturn::FailureUnsupportedYet; + return SPIReturn::Success; } SPIDEFN FindPattern(void** outOffsetPtr, char* combinedPattern) diff --git a/src/spi/interface.h b/src/spi/interface.h index 86df86e..5cc1183 100644 --- a/src/spi/interface.h +++ b/src/spi/interface.h @@ -78,7 +78,7 @@ enum class SPIReturn : short /// Get a string describing an SPIReturn code. /// /// A const wide string -const wchar_t* SPIReturnToString(SPIReturn code) +inline const wchar_t* SPIReturnToString(SPIReturn code) { switch (code) { diff --git a/src/ue_types.h b/src/ue_types.h index e4b2258..89cf528 100644 --- a/src/ue_types.h +++ b/src/ue_types.h @@ -31,12 +31,12 @@ namespace UE // A prototype of a function which LE1 uses to decode // a char name from a FNameEntry structure. typedef void* (__stdcall* tGetName)(void* name, wchar_t* outBuffer); - tGetName GetName = nullptr; + inline tGetName GetName = nullptr; // A prototype of a function which LE2/3 use to decode // a char name from a FNameEntry structure. typedef void* (__stdcall* tNewGetName)(void* name, wchar_t* outBuffer); - tNewGetName NewGetName = nullptr; + inline tNewGetName NewGetName = nullptr; // Note: // I've got a pretty solid way of getting names without hooking ToString (^), @@ -47,8 +47,8 @@ namespace UE // A prototype of UFunction::Bind method used to // bind UScript functions to native implementations. typedef void(__thiscall* tUFunctionBind)(void* pFunction); - tUFunctionBind UFunctionBind = nullptr; - tUFunctionBind UFunctionBind_orig = nullptr; + inline tUFunctionBind UFunctionBind = nullptr; + inline tUFunctionBind UFunctionBind_orig = nullptr; // A partial representation of a UObject class. @@ -131,7 +131,7 @@ namespace UE // A GNative function which takes no arguments and returns TRUE. - void AlwaysPositiveNative(UObjectPartial* pObject, void* pFrame, void* pResult) + inline void AlwaysPositiveNative(UObjectPartial* pObject, void* pFrame, void* pResult) { //GLogger.writeln(L"UE::AlwaysPositiveNative: called for %s.", pObject->GetName()); @@ -156,7 +156,7 @@ namespace UE } // A GNative function which takes no arguments and returns FALSE. - void AlwaysNegativeNative(UObjectPartial* pObject, void* pFrame, void* pResult) + inline void AlwaysNegativeNative(UObjectPartial* pObject, void* pFrame, void* pResult) { //GLogger.writeln(L"UE::AlwaysNegativeNative: called for %s.", pObject->GetName()); @@ -183,7 +183,7 @@ namespace UE // A hooked wrapper around UFunction::Bind which calls the original and then // binds IsShippingPCBuild and IsFinalReleaseDebugConsoleBuild to AlwaysPositiveNative. - void HookedUFunctionBind(UObjectPartial* pFunction) + inline void HookedUFunctionBind(UObjectPartial* pFunction) { UFunctionBind_orig(pFunction); diff --git a/src/utils/hook.h b/src/utils/hook.h index ee63e6e..b7962cc 100644 --- a/src/utils/hook.h +++ b/src/utils/hook.h @@ -58,4 +58,4 @@ namespace Utils // Global instance. -Utils::HookManager GHookManager; +inline Utils::HookManager GHookManager; diff --git a/src/utils/io.h b/src/utils/io.h index 69762a8..6033f06 100644 --- a/src/utils/io.h +++ b/src/utils/io.h @@ -11,20 +11,18 @@ #include -#ifndef ASI_LOG_FNAME -#error Must set ASI log filename! -#endif +#define ASI_LOG_FNAME "bink2w64_proxy.log" #define ASI_IO_LOCK(MUTEX) const std::lock_guard lock(MUTEX); namespace Utils { - FILE* FGLog = nullptr; - std::mutex GOpenConsoleMtx; - std::mutex GCloseConsoleMtx; - std::mutex GWriteLineMtx; + inline FILE* FGLog = nullptr; + inline std::mutex GOpenConsoleMtx; + inline std::mutex GCloseConsoleMtx; + inline std::mutex GWriteLineMtx; - void OpenConsole(FILE* out, FILE* err) + inline void OpenConsole(FILE* out, FILE* err) { ASI_IO_LOCK(GOpenConsoleMtx); @@ -38,7 +36,7 @@ namespace Utils GetConsoleScreenBufferInfo(console, &lpConsoleScreenBufferInfo); SetConsoleScreenBufferSize(console, { lpConsoleScreenBufferInfo.dwSize.X, 30000 }); } - void CloseConsole() + inline void CloseConsole() { ASI_IO_LOCK(GCloseConsoleMtx); FreeConsole(); @@ -46,7 +44,7 @@ namespace Utils // Open a console window and redirect output to it. // In release mode, redirect all output to a file. - void SetupOutput() + inline void SetupOutput() { #ifdef ASI_DEBUG #define ASIOUT stdout @@ -64,7 +62,7 @@ namespace Utils #endif } // Close the console window. - void TeardownOutput() + inline void TeardownOutput() { #ifdef ASI_DEBUG CloseConsole(); @@ -125,4 +123,4 @@ namespace Utils } // Global instance. -static Utils::RuntimeLogger GLogger; +inline Utils::RuntimeLogger GLogger; diff --git a/src/utils/memory.h b/src/utils/memory.h index f5326ed..4fdbd20 100644 --- a/src/utils/memory.h +++ b/src/utils/memory.h @@ -9,14 +9,14 @@ namespace Utils { - bool IsExecutableAddress(LPVOID pAddress) + inline bool IsExecutableAddress(LPVOID pAddress) { MEMORY_BASIC_INFORMATION mi; VirtualQuery(pAddress, &mi, sizeof(mi)); return (mi.State == MEM_COMMIT && (mi.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY))); } - HMODULE GetGameModuleRange(BYTE** pStart, BYTE** pEnd) + inline HMODULE GetGameModuleRange(BYTE** pStart, BYTE** pEnd) { HMODULE exeModule = GetModuleHandle(nullptr); MODULEINFO exeModuleInfo; @@ -34,7 +34,7 @@ namespace Utils /// Scan the game module for a sequence of bytes defined by a pattern and a mask. /// TODO: refactor it to use PEiD patterns. /// - BYTE* ScanProcess(BYTE* pattern, BYTE* mask) + inline BYTE* ScanProcess(BYTE* pattern, BYTE* mask) { size_t patternLength = strlen((char*)mask);