diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index b26252dd..d038adc1 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -156,7 +156,7 @@ jobs: run: sudo chmod 77 package_linux.sh && sudo ./package_linux.sh release ### upload artifact/package to github Actions (release mode) - - name: Upload build pacakge (release) + - name: Upload build package (release) uses: actions/upload-artifact@v4 with: name: "build-linux-release-${{ github.sha }}" @@ -178,7 +178,7 @@ jobs: run: sudo chmod 77 package_linux.sh && sudo ./package_linux.sh debug ### upload artifact/package to github Actions (debug mode) - - name: Upload build pacakge (debug) + - name: Upload build package (debug) uses: actions/upload-artifact@v4 with: name: "build-linux-debug-${{ github.sha }}" diff --git a/.github/workflows/build-win.yml b/.github/workflows/build-win.yml index a2e6f49a..cd8ae4b3 100644 --- a/.github/workflows/build-win.yml +++ b/.github/workflows/build-win.yml @@ -157,7 +157,7 @@ jobs: run: package_win.bat release ### upload artifact/package to github Actions (release mode) - - name: Upload build pacakge (release) + - name: Upload build package (release) uses: actions/upload-artifact@v4 with: name: "build-win-release-${{ github.sha }}" @@ -179,7 +179,7 @@ jobs: run: package_win.bat debug ### upload artifact/package to github Actions (debug mode) - - name: Upload build pacakge (debug) + - name: Upload build package (debug) uses: actions/upload-artifact@v4 with: name: "build-win-debug-${{ github.sha }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bdccb73..30e7cf91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 2024/2/7 + +* new persistent modes for cold client loader, mode 2 is a more accurate simulation and allows launching apps from their .exe + +--- + +* **[Breaking]** changed the ini sections of the cold client loader + +--- + ## 2024/1/26 * **[Detanup01]** added a new command line option for the tool `generate_emu_config` to disable the generation of `disable_xxx.txt` files, diff --git a/README.md b/README.md index dc955b6d..45bd06ae 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ This will: Additional arguments you can pass to this script: * `-j `: build with `` parallel jobs, by default 70% of the available threads * `-verbose`: output compiler/linker commands used by `CMAKE` -* `-packages_only`: install the required Linux pacakges via `apt isntall` and exit (don't rebuild) +* `-packages_only`: install the required Linux packages via `apt isntall` and exit (don't rebuild) --- diff --git a/post_build/README.experimental_steamclient.md b/post_build/README.experimental_steamclient.md index 4ae7db48..098c7a4b 100644 --- a/post_build/README.experimental_steamclient.md +++ b/post_build/README.experimental_steamclient.md @@ -34,6 +34,12 @@ You do not need to create a `steam_interfaces.txt` file for the `steamclient` ve * `IgnoreLoaderArchDifference`: don't display an error message if the architecture of the loader is different from the app. this will result in a silent failure if a dll injection didn't succeed. both the loader and the app must have the same arch for the injection to work + * `Mode` (in `[Persistence]` section): + - 0 = turned off + - 1 = loader will spawn the exe and keep hanging in the background until you press "OK" + - 2 = loader will NOT spawn exe, it will just setup the required environemnt and keep hanging + in the background until you run the exe manually, and press "OK" when you've finished playing. + This should help in scenarios where an external app has to launch the game, it is also recommended to run the loader with admin rights in this mode **Note** that any arguments passed to `steamclient_loader.exe` via command line will be passed to the target `.exe`. diff --git a/tools/steamclient_loader/win/ColdClientLoader.cpp b/tools/steamclient_loader/win/ColdClientLoader.cpp index ec3f40bc..5508a7f7 100644 --- a/tools/steamclient_loader/win/ColdClientLoader.cpp +++ b/tools/steamclient_loader/win/ColdClientLoader.cpp @@ -13,11 +13,29 @@ #include #include #include +#include static const std::wstring IniFile = pe_helpers::get_current_exe_path_w() + L"ColdClientLoader.ini"; static const std::wstring dbg_file = pe_helpers::get_current_exe_path_w() + L"COLD_LDR_LOG.txt"; constexpr static const char STEAM_UNIVERSE[] = "Public"; +constexpr static const char STEAM_URL_PROTOCOL[] = "URL:steam protocol"; + +const bool loader_is_32 = pe_helpers::is_module_32(GetModuleHandleW(NULL)); + +const std::wstring hklm_path(loader_is_32 ? L"SOFTWARE\\Valve\\Steam" : L"SOFTWARE\\WOW6432Node\\Valve\\Steam"); + +// Declare some variables to be used for Steam registry. +const DWORD UserId = 0x03100004771F810D & 0xffffffff; +const DWORD ProcessID = GetCurrentProcessId(); + +std::wstring ClientPath{}; +std::wstring Client64Path{}; + +std::vector dlls_to_inject{}; + + + std::wstring get_ini_value(LPCWSTR section, LPCWSTR key, LPCWSTR default_val = NULL) { @@ -144,6 +162,175 @@ static void to_bool_ini_val(std::wstring &val) } } + +bool orig_steam_hkcu = false; +WCHAR OrgSteamCDir_hkcu[8192] = { 0 }; +DWORD Size1_hkcu = _countof(OrgSteamCDir_hkcu); +WCHAR OrgSteamCDir64_hkcu[8192] = { 0 }; +DWORD Size2_hkcu = _countof(OrgSteamCDir64_hkcu); +bool patch_registry_hkcu() +{ + HKEY Registrykey = { 0 }; + if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam\\ActiveProcess", 0, KEY_ALL_ACCESS, &Registrykey) == ERROR_SUCCESS) { + orig_steam_hkcu = true; + // Get original values to restore later. + DWORD keyType = REG_SZ; + RegQueryValueExW(Registrykey, L"SteamClientDll", 0, &keyType, (LPBYTE)&OrgSteamCDir_hkcu, &Size1_hkcu); + RegQueryValueExW(Registrykey, L"SteamClientDll64", 0, &keyType, (LPBYTE)&OrgSteamCDir64_hkcu, &Size2_hkcu); + dbg_log::write("Found previous registry entry (HKCU) for Steam"); + } else if (RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam\\ActiveProcess", 0, 0, REG_OPTION_NON_VOLATILE, + KEY_ALL_ACCESS, NULL, &Registrykey, NULL) == ERROR_SUCCESS) { + dbg_log::write("Created new registry entry (HKCU) for Steam"); + } else { + dbg_log::write("Unable to patch Registry (HKCU), error = " + std::to_string(GetLastError())); + return false; + } + + RegSetValueExA(Registrykey, "ActiveUser", NULL, REG_DWORD, (const BYTE *)&UserId, sizeof(DWORD)); + RegSetValueExA(Registrykey, "pid", NULL, REG_DWORD, (const BYTE *)&ProcessID, sizeof(DWORD)); + RegSetValueExW(Registrykey, L"SteamClientDll", NULL, REG_SZ, (const BYTE *)ClientPath.c_str(), (ClientPath.size() + 1) * sizeof(ClientPath[0])); + RegSetValueExW(Registrykey, L"SteamClientDll64", NULL, REG_SZ, (const BYTE *)Client64Path.c_str(), (Client64Path.size() + 1) * sizeof(Client64Path[0])); + RegSetValueExA(Registrykey, "Universe", NULL, REG_SZ, (const BYTE *)STEAM_UNIVERSE, (DWORD)sizeof(STEAM_UNIVERSE)); + RegCloseKey(Registrykey); + return true; +} + +void cleanup_registry_hkcu() +{ + if (!orig_steam_hkcu) return; + + dbg_log::write("restoring registry entries (HKCU)"); + HKEY Registrykey = { 0 }; + if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam\\ActiveProcess", 0, KEY_ALL_ACCESS, &Registrykey) == ERROR_SUCCESS) { + // Restore the values. + RegSetValueExW(Registrykey, L"SteamClientDll", NULL, REG_SZ, (LPBYTE)OrgSteamCDir_hkcu, Size1_hkcu); + RegSetValueExW(Registrykey, L"SteamClientDll64", NULL, REG_SZ, (LPBYTE)OrgSteamCDir64_hkcu, Size2_hkcu); + + // Close the HKEY Handle. + RegCloseKey(Registrykey); + } else { + dbg_log::write("Unable to restore the original registry entry (HKCU), error = " + std::to_string(GetLastError())); + } +} + + +bool orig_steam_hklm = false; +WCHAR OrgInstallPath_hklm[8192] = { 0 }; +DWORD Size1_hklm = _countof(OrgInstallPath_hklm); +bool patch_registry_hklm() +{ + HKEY Registrykey = { 0 }; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, hklm_path.c_str(), 0, KEY_ALL_ACCESS, &Registrykey) == ERROR_SUCCESS) { + orig_steam_hklm = true; + // Get original values to restore later. + DWORD keyType = REG_SZ; + RegQueryValueExW(Registrykey, L"InstallPath", 0, &keyType, (LPBYTE)&OrgInstallPath_hklm, &Size1_hklm); + dbg_log::write("Found previous registry entry (HKLM) for Steam"); + } else if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, hklm_path.c_str(), 0, 0, REG_OPTION_NON_VOLATILE, + KEY_ALL_ACCESS, NULL, &Registrykey, NULL) == ERROR_SUCCESS) { + dbg_log::write("Created new registry entry (HKLM) for Steam"); + } else { + dbg_log::write("Unable to patch Registry (HKLM), error = " + std::to_string(GetLastError())); + return false; + } + + const auto my_path = pe_helpers::get_current_exe_path_w(); + RegSetValueExW(Registrykey, L"InstallPath", NULL, REG_SZ, (const BYTE *)my_path.c_str(), (my_path.size() + 1) * sizeof(my_path[0])); + RegSetValueExA(Registrykey, "SteamPID", NULL, REG_DWORD, (const BYTE *)&ProcessID, sizeof(DWORD)); + RegSetValueExA(Registrykey, "Universe", NULL, REG_SZ, (const BYTE *)STEAM_UNIVERSE, (DWORD)sizeof(STEAM_UNIVERSE)); + RegCloseKey(Registrykey); + return true; +} + +void cleanup_registry_hklm() +{ + if (!orig_steam_hklm) return; + + dbg_log::write("restoring registry entries (HKLM)"); + HKEY Registrykey = { 0 }; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, hklm_path.c_str(), 0, KEY_ALL_ACCESS, &Registrykey) == ERROR_SUCCESS) { + RegSetValueExW(Registrykey, L"InstallPath", NULL, REG_SZ, (const BYTE *)OrgInstallPath_hklm, Size1_hklm); + RegCloseKey(Registrykey); + } else { + dbg_log::write("Unable to restore the original registry entry (HKLM), error = " + std::to_string(GetLastError())); + } +} + + +bool orig_steam_hkcs_1 = false; +bool orig_steam_hkcs_2 = false; +WCHAR OrgCommand_hkcs[8192] = { 0 }; +DWORD Size1_hkcs = _countof(OrgCommand_hkcs); +bool patch_registry_hkcs() +{ + HKEY Registrykey = { 0 }; + if (RegOpenKeyExW(HKEY_CLASSES_ROOT, L"steam", 0, KEY_ALL_ACCESS, &Registrykey) == ERROR_SUCCESS) { + orig_steam_hkcs_1 = true; + dbg_log::write("Found previous registry entry (HKCS) #1 for Steam"); + } else if (RegCreateKeyExW(HKEY_CLASSES_ROOT, L"steam", 0, 0, REG_OPTION_NON_VOLATILE, + KEY_ALL_ACCESS, NULL, &Registrykey, NULL) == ERROR_SUCCESS) { + dbg_log::write("Created new registry entry (HKCS) #1 for Steam"); + } else { + dbg_log::write("Unable to patch Registry (HKCS) #1, error = " + std::to_string(GetLastError())); + return false; + } + + HKEY Registrykey_2 = { 0 }; + if (RegOpenKeyExW(HKEY_CLASSES_ROOT, L"steam\\Shell\\Open\\Command", 0, KEY_ALL_ACCESS, &Registrykey_2) == ERROR_SUCCESS) { + orig_steam_hkcs_2 = true; + // Get original values to restore later. + DWORD keyType = REG_SZ; + RegQueryValueExW(Registrykey_2, L"", 0, &keyType, (LPBYTE)&OrgCommand_hkcs, &Size1_hkcs); + dbg_log::write("Found previous registry entry (HKCS) #2 for Steam"); + } else if (RegCreateKeyExW(HKEY_CLASSES_ROOT, L"steam\\Shell\\Open\\Command", 0, 0, REG_OPTION_NON_VOLATILE, + KEY_ALL_ACCESS, NULL, &Registrykey_2, NULL) == ERROR_SUCCESS) { + dbg_log::write("Created new registry entry (HKCS) #2 for Steam"); + } else { + dbg_log::write("Unable to patch Registry (HKCS) #2, error = " + std::to_string(GetLastError())); + RegCloseKey(Registrykey); + return false; + } + + RegSetValueExA(Registrykey, "", NULL, REG_SZ, (const BYTE *)STEAM_URL_PROTOCOL, (DWORD)sizeof(STEAM_URL_PROTOCOL)); + RegSetValueExA(Registrykey, "URL Protocol", NULL, REG_SZ, (const BYTE *)"", (DWORD)sizeof("")); + RegCloseKey(Registrykey); + + const auto cmd = pe_helpers::get_current_exe_path_w() + L"steam.exe -- \"%1\""; + RegSetValueExW(Registrykey_2, L"", NULL, REG_SZ, (const BYTE *)cmd.c_str(), (cmd.size() + 1) * sizeof(cmd[0])); + RegCloseKey(Registrykey_2); + return true; +} + +void cleanup_registry_hkcs() +{ + if (orig_steam_hkcs_2) { + dbg_log::write("restoring registry entries (HKCU) #1"); + HKEY Registrykey = { 0 }; + if (RegOpenKeyExW(HKEY_CLASSES_ROOT, L"steam\\Shell\\Open\\Command", 0, KEY_ALL_ACCESS, &Registrykey) == ERROR_SUCCESS) { + RegSetValueExW(Registrykey, L"", NULL, REG_SZ, (const BYTE *)OrgCommand_hkcs, Size1_hkcs); + RegCloseKey(Registrykey); + } else { + dbg_log::write("Unable to restore the original registry entry (HKCS) #2, error = " + std::to_string(GetLastError())); + } + } + + if (!orig_steam_hkcs_1) { + dbg_log::write("removing registry entries (HKCU) #2 (added by loader)"); + HKEY Registrykey = { 0 }; + RegDeleteKeyA(HKEY_CLASSES_ROOT, "steam"); + } +} + + + +void set_steam_env_vars(const std::wstring &AppId) +{ + SetEnvironmentVariableW(L"SteamAppId", AppId.c_str()); + SetEnvironmentVariableW(L"SteamGameId", AppId.c_str()); + SetEnvironmentVariableW(L"SteamOverlayGameId", AppId.c_str()); +} + + int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { dbg_log::init(dbg_file.c_str()); @@ -155,13 +342,12 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance return 1; } - std::wstring Client64Path = common_helpers::to_absolute( - get_ini_value(L"SteamClient", L"SteamClient64Dll"), + ClientPath = common_helpers::to_absolute( + get_ini_value(L"SteamClient", L"SteamClientDll"), pe_helpers::get_current_exe_path_w() ); - - std::wstring ClientPath = common_helpers::to_absolute( - get_ini_value(L"SteamClient", L"SteamClientDll"), + Client64Path = common_helpers::to_absolute( + get_ini_value(L"SteamClient", L"SteamClient64Dll"), pe_helpers::get_current_exe_path_w() ); std::wstring ExeFile = common_helpers::to_absolute( @@ -174,22 +360,26 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance ); std::wstring ExeCommandLine = get_ini_value(L"SteamClient", L"ExeCommandLine"); std::wstring AppId = get_ini_value(L"SteamClient", L"AppId"); - std::wstring ForceInjectSteamClient = get_ini_value(L"SteamClient", L"ForceInjectSteamClient"); - std::wstring ResumeByDebugger = get_ini_value(L"Debug", L"ResumeByDebugger"); - // dlls to inject + std::wstring ForceInjectSteamClient = get_ini_value(L"Injection", L"ForceInjectSteamClient"); std::wstring DllsToInjectFolder = common_helpers::to_absolute( - get_ini_value(L"Extra", L"DllsToInjectFolder"), + get_ini_value(L"Injection", L"DllsToInjectFolder"), pe_helpers::get_current_exe_path_w() ); - std::wstring IgnoreInjectionError = get_ini_value(L"Extra", L"IgnoreInjectionError", L"1"); - std::wstring IgnoreLoaderArchDifference = get_ini_value(L"Extra", L"IgnoreLoaderArchDifference", L"0"); + std::wstring IgnoreInjectionError = get_ini_value(L"Injection", L"IgnoreInjectionError", L"1"); + std::wstring IgnoreLoaderArchDifference = get_ini_value(L"Injection", L"IgnoreLoaderArchDifference", L"0"); + + std::wstring PersistentMode = get_ini_value(L"Persistence", L"Mode", L"0"); + + // debug + std::wstring ResumeByDebugger = get_ini_value(L"Debug", L"ResumeByDebugger"); - to_bool_ini_val(ResumeByDebugger); to_bool_ini_val(ForceInjectSteamClient); to_bool_ini_val(IgnoreInjectionError); to_bool_ini_val(IgnoreLoaderArchDifference); + to_bool_ini_val(ResumeByDebugger); + if (PersistentMode != L"0" && PersistentMode != L"1" && PersistentMode != L"2") PersistentMode = L"0"; // log everything dbg_log::write(L"SteamClient::Exe: " + ExeFile); @@ -198,19 +388,24 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance dbg_log::write(L"SteamClient::AppId: " + AppId); dbg_log::write(L"SteamClient::SteamClient: " + ClientPath); dbg_log::write(L"SteamClient::SteamClient64Dll: " + Client64Path); + dbg_log::write(L"SteamClient::PersistentMode: " + PersistentMode); dbg_log::write(L"SteamClient::ForceInjectSteamClient: " + ForceInjectSteamClient); + dbg_log::write(L"Injection::DllsToInjectFolder: " + DllsToInjectFolder); + dbg_log::write(L"Injection::IgnoreInjectionError: " + IgnoreInjectionError); + dbg_log::write(L"Injection::IgnoreLoaderArchDifference: " + IgnoreLoaderArchDifference); dbg_log::write(L"Debug::ResumeByDebugger: " + ResumeByDebugger); - dbg_log::write(L"Extra::DllsToInjectFolder: " + DllsToInjectFolder); - dbg_log::write(L"Extra::IgnoreInjectionError: " + IgnoreInjectionError); - dbg_log::write(L"Extra::IgnoreLoaderArchDifference: " + IgnoreLoaderArchDifference); - if (AppId.size() && AppId[0]) { - SetEnvironmentVariableW(L"SteamAppId", AppId.c_str()); - SetEnvironmentVariableW(L"SteamGameId", AppId.c_str()); - SetEnvironmentVariableW(L"SteamOverlayGameId", AppId.c_str()); - } else { - dbg_log::write("You forgot to set the AppId"); - MessageBoxA(NULL, "You forgot to set the AppId.", "ColdClientLoader", MB_ICONERROR); + if (!common_helpers::file_exist(Client64Path)) { + dbg_log::write("Couldn't find the requested SteamClient64Dll"); + MessageBoxA(NULL, "Couldn't find the requested SteamClient64Dll.", "ColdClientLoader", MB_ICONERROR); + dbg_log::close(); + return 1; + } + + if (!common_helpers::file_exist(ClientPath)) { + dbg_log::write("Couldn't find the requested SteamClientDll"); + MessageBoxA(NULL, "Couldn't find the requested SteamClientDll.", "ColdClientLoader", MB_ICONERROR); + dbg_log::close(); return 1; } @@ -231,30 +426,19 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance dbg_log::close(); return 1; } - - if (!common_helpers::file_exist(Client64Path)) { - dbg_log::write("Couldn't find the requested SteamClient64Dll"); - MessageBoxA(NULL, "Couldn't find the requested SteamClient64Dll.", "ColdClientLoader", MB_ICONERROR); - dbg_log::close(); - return 1; - } - - if (!common_helpers::file_exist(ClientPath)) { - dbg_log::write("Couldn't find the requested SteamClientDll"); - MessageBoxA(NULL, "Couldn't find the requested SteamClientDll.", "ColdClientLoader", MB_ICONERROR); - dbg_log::close(); - return 1; - } - + + bool isDllsToInjectFolderFound = false; if (DllsToInjectFolder.size()) { - if (!common_helpers::dir_exist(DllsToInjectFolder)) { + isDllsToInjectFolderFound = common_helpers::dir_exist(DllsToInjectFolder); + if (!isDllsToInjectFolderFound) { dbg_log::write("Couldn't find the requested folder of dlls to inject"); MessageBoxA(NULL, "Couldn't find the requested folder of dlls to inject.", "ColdClientLoader", MB_ICONERROR); - dbg_log::close(); - return 1; + // it was requested to make this a non intrusive error, don't shutdown the whole thing because the folder is missing + // dbg_log::close(); + // return 1; } } - + auto exe_header = get_pe_header(ExeFile); if (exe_header.empty()) { dbg_log::write("Couldn't read the exe header"); @@ -277,8 +461,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance dbg_log::write("Detected exe arch: x64"); } - bool loader_is_32 = pe_helpers::is_module_32(GetModuleHandleW(NULL)); - if (pe_helpers::is_module_32(GetModuleHandleW(NULL))) { + if (loader_is_32) { dbg_log::write("Detected loader arch: x32"); } else { dbg_log::write("Detected loader arch: x64"); @@ -291,8 +474,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance } } - std::vector dlls_to_inject{}; - if (DllsToInjectFolder.size()) { + if (isDllsToInjectFolderFound) { std::wstring failed_dlls{}; dlls_to_inject = collect_dlls_to_inject(DllsToInjectFolder, is_exe_32, failed_dlls); if (failed_dlls.size() && IgnoreInjectionError.empty()) { @@ -307,63 +489,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance } } } - - HKEY Registrykey = { 0 }; - // Declare some variables to be used for Steam registry. - DWORD UserId = 0x03100004771F810D & 0xffffffff; - DWORD ProcessID = GetCurrentProcessId(); - - bool orig_steam = false; - DWORD keyType = REG_SZ; - WCHAR OrgSteamCDir[8192] = { 0 }; - WCHAR OrgSteamCDir64[8192] = { 0 }; - DWORD Size1 = _countof(OrgSteamCDir); - DWORD Size2 = _countof(OrgSteamCDir64); - if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam\\ActiveProcess", 0, KEY_ALL_ACCESS, &Registrykey) == ERROR_SUCCESS) - { - orig_steam = true; - // Get original values to restore later. - RegQueryValueExW(Registrykey, L"SteamClientDll", 0, &keyType, (LPBYTE)& OrgSteamCDir, &Size1); - RegQueryValueExW(Registrykey, L"SteamClientDll64", 0, &keyType, (LPBYTE)& OrgSteamCDir64, &Size2); - } else { - if (RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam\\ActiveProcess", 0, 0, REG_OPTION_NON_VOLATILE, - KEY_ALL_ACCESS, NULL, &Registrykey, NULL) != ERROR_SUCCESS) - { - dbg_log::write("Unable to patch Steam process informations on the Windows registry (ActiveProcess), error = " + std::to_string(GetLastError())); - MessageBoxA(NULL, "Unable to patch Steam process informations on the Windows registry.", "ColdClientLoader", MB_ICONERROR); - dbg_log::close(); - return 1; - } - } - - // Set values to Windows registry. - RegSetValueExA(Registrykey, "ActiveUser", NULL, REG_DWORD, (const BYTE *)&UserId, sizeof(DWORD)); - RegSetValueExA(Registrykey, "pid", NULL, REG_DWORD, (const BYTE *)&ProcessID, sizeof(DWORD)); - RegSetValueExW(Registrykey, L"SteamClientDll", NULL, REG_SZ, (const BYTE *)ClientPath.c_str(), (ClientPath.size() + 1) * sizeof(ClientPath[0])); - RegSetValueExW(Registrykey, L"SteamClientDll64", NULL, REG_SZ, (const BYTE *)Client64Path.c_str(), (Client64Path.size() + 1) * sizeof(Client64Path[0])); - RegSetValueExA(Registrykey, "Universe", NULL, REG_SZ, (const BYTE *)STEAM_UNIVERSE, (DWORD)sizeof(STEAM_UNIVERSE)); - - // Close the HKEY Handle. - RegCloseKey(Registrykey); - - // spawn the exe - STARTUPINFOW info = { 0 }; - SecureZeroMemory(&info, sizeof(info)); - info.cb = sizeof(info); - - PROCESS_INFORMATION processInfo = { 0 }; - SecureZeroMemory(&processInfo, sizeof(processInfo)); - - WCHAR CommandLine[16384] = { 0 }; - _snwprintf(CommandLine, _countof(CommandLine), L"\"%ls\" %ls %ls", ExeFile.c_str(), ExeCommandLine.c_str(), lpCmdLine); - if (!CreateProcessW(ExeFile.c_str(), CommandLine, NULL, NULL, TRUE, CREATE_SUSPENDED, NULL, ExeRunDir.c_str(), &info, &processInfo)) - { - dbg_log::write("Unable to load the requested EXE file"); - MessageBoxA(NULL, "Unable to load the requested EXE file.", "ColdClientLoader", MB_ICONERROR); - dbg_log::close(); - return 1; - } - if (ForceInjectSteamClient.size()) { if (is_exe_32) { dlls_to_inject.insert(dlls_to_inject.begin(), ClientPath); @@ -371,57 +496,143 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance dlls_to_inject.insert(dlls_to_inject.begin(), Client64Path); } } - for (const auto &dll : dlls_to_inject) { - dbg_log::write(L"Injecting dll: '" + dll + L"' ..."); - const char *err_inject = nullptr; - DWORD code = pe_helpers::loadlib_remote(processInfo.hProcess, dll, &err_inject); - if (code != ERROR_SUCCESS) { - std::wstring err_full = - L"Failed to inject the dll: " + dll + L"\n" + - common_helpers::str_to_w(err_inject) + L"\n" + - common_helpers::str_to_w(pe_helpers::get_err_string(code)) + L"\n" + - L"Error code = " + std::to_wstring(code) + L"\n"; - dbg_log::write(err_full); - if (IgnoreInjectionError.empty()) { - TerminateProcess(processInfo.hProcess, 1); - CloseHandle(processInfo.hProcess); - CloseHandle(processInfo.hThread); - MessageBoxW(NULL, err_full.c_str(), L"ColdClientLoader", MB_ICONERROR); - dbg_log::close(); - return 1; + + if (PersistentMode != L"2" ) { + if (AppId.size() && AppId[0]) { + set_steam_env_vars(AppId); + } else { + dbg_log::write("You forgot to set the AppId"); + MessageBoxA(NULL, "You forgot to set the AppId.", "ColdClientLoader", MB_ICONERROR); + return 1; + } + } else { // steam://run/ + constexpr const static wchar_t STEAM_LAUNCH_CMD_1[] = L"steam://run/"; + constexpr const static wchar_t STEAM_LAUNCH_CMD_2[] = L"run/"; + AppId.clear(); // we don't care about the app id in the ini + std::wstring my_cmd = lpCmdLine && lpCmdLine[0] ? std::wstring(lpCmdLine) : std::wstring(); + dbg_log::write(L"persistent mode 2 detecting steam launch cmd from: " + my_cmd); + if (my_cmd.find(STEAM_LAUNCH_CMD_1) == 0) { + AppId = my_cmd.substr(sizeof(STEAM_LAUNCH_CMD_1) / sizeof(wchar_t), my_cmd.find_first_of(L" \t")); + dbg_log::write("persistent mode 2 got steam launch cmd #1"); + } else if (my_cmd.find(STEAM_LAUNCH_CMD_2) == 0) { + AppId = my_cmd.substr(sizeof(STEAM_LAUNCH_CMD_2) / sizeof(wchar_t), my_cmd.find_first_of(L" \t")); + dbg_log::write("persistent mode 2 got steam launch cmd #2"); + } else { + dbg_log::write("persistent mode 2 didn't detect a valid steam launch cmd"); + } + + if (AppId.size()) { + set_steam_env_vars(AppId); + dbg_log::write(L"persistent mode 2 will use app id = " + AppId); + } else { + dbg_log::write("persistent mode 2 didn't find app id"); + } + } + + if (!patch_registry_hkcu()) { + dbg_log::close(); + MessageBoxA(NULL, "Unable to patch Registry (HKCU).", "ColdClientLoader", MB_ICONERROR); + return 1; + } + + if (!patch_registry_hklm()) { + cleanup_registry_hkcu(); + + dbg_log::close(); + MessageBoxA(NULL, "Unable to patch Registry (HKLM).", "ColdClientLoader", MB_ICONERROR); + return 1; + } + + // if (!patch_registry_hkcs()) { // this fails due to admin rights, not a big deal + // cleanup_registry_hkcu(); + // cleanup_registry_hklm(); + + // dbg_log::close(); + // MessageBoxA(NULL, "Unable to patch Registry (HKCS).", "ColdClientLoader", MB_ICONERROR); + // return 1; + // } + + if (PersistentMode != L"2" || AppId.size()) { // persistent mode 0 or 1, or mode 2 with defined app id + STARTUPINFOW info = { 0 }; + + SecureZeroMemory(&info, sizeof(info)); + info.cb = sizeof(info); + + PROCESS_INFORMATION processInfo = { 0 }; + SecureZeroMemory(&processInfo, sizeof(processInfo)); + + dbg_log::write("spawning the requested EXE file"); + WCHAR CommandLine[16384] = { 0 }; + _snwprintf(CommandLine, _countof(CommandLine), L"\"%ls\" %ls %ls", ExeFile.c_str(), ExeCommandLine.c_str(), lpCmdLine); + if (!CreateProcessW(ExeFile.c_str(), CommandLine, NULL, NULL, TRUE, CREATE_SUSPENDED, NULL, ExeRunDir.c_str(), &info, &processInfo)) { + dbg_log::write("Unable to load the requested EXE file"); + cleanup_registry_hkcu(); + cleanup_registry_hklm(); + cleanup_registry_hkcs(); + + dbg_log::close(); + MessageBoxA(NULL, "Unable to load the requested EXE file.", "ColdClientLoader", MB_ICONERROR); + return 1; + } + + dbg_log::write("injecting " + std::to_string(dlls_to_inject.size()) + " dlls"); + for (const auto &dll : dlls_to_inject) { + dbg_log::write(L"Injecting dll: '" + dll + L"' ..."); + const char *err_inject = nullptr; + DWORD code = pe_helpers::loadlib_remote(processInfo.hProcess, dll, &err_inject); + if (code != ERROR_SUCCESS) { + auto err_full = + L"Failed to inject the dll: " + dll + L"\n" + + common_helpers::str_to_w(err_inject) + L"\n" + + common_helpers::str_to_w(pe_helpers::get_err_string(code)) + L"\n" + + L"Error code = " + std::to_wstring(code) + L"\n"; + dbg_log::write(err_full); + if (IgnoreInjectionError.empty()) { + TerminateProcess(processInfo.hProcess, 1); + CloseHandle(processInfo.hProcess); + CloseHandle(processInfo.hThread); + + cleanup_registry_hkcu(); + cleanup_registry_hklm(); + cleanup_registry_hkcs(); + + dbg_log::close(); + MessageBoxW(NULL, err_full.c_str(), L"ColdClientLoader", MB_ICONERROR); + return 1; + } + } else { + dbg_log::write("Injected!"); } - } else { - dbg_log::write("Injected!"); } + + // run + if (ResumeByDebugger.empty()) { + dbg_log::write("resuming the main thread of the exe"); + // MessageBoxA(nullptr, "Wait then press ok", "Wait some seconds", MB_OK); + // games like house flipper 2 won't launch until the previous exe has exited, which takes some seconds + if (PersistentMode == L"2") std::this_thread::sleep_for(std::chrono::seconds(5)); + ResumeThread(processInfo.hThread); + } else { + std::string msg = "Attach a debugger now to PID " + std::to_string(processInfo.dwProcessId) + " and resume its main thread"; + dbg_log::write(msg); + MessageBoxA(NULL, msg.c_str(), "ColdClientLoader", MB_OK); + } + + // wait + WaitForSingleObject(processInfo.hThread, INFINITE); + + CloseHandle(processInfo.hProcess); + CloseHandle(processInfo.hThread); } - // run - if (ResumeByDebugger.empty()) { - ResumeThread(processInfo.hThread); - } else { - std::string msg = "Attach a debugger now to PID " + std::to_string(processInfo.dwProcessId) + " and resume its main thread"; - dbg_log::write(msg); - MessageBoxA(NULL, msg.c_str(), "ColdClientLoader", MB_OK); + if (PersistentMode == L"1" || (PersistentMode == L"2" && AppId.empty()) ) { + MessageBoxA(NULL, "Press OK when you have finished playing to close the loader", "Cold Client Loader (waiting)", MB_OK); } - // wait - WaitForSingleObject(processInfo.hThread, INFINITE); - - CloseHandle(processInfo.hProcess); - CloseHandle(processInfo.hThread); - - if (orig_steam) { - if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam\\ActiveProcess", 0, KEY_ALL_ACCESS, &Registrykey) == ERROR_SUCCESS) - { - // Restore the values. - RegSetValueExW(Registrykey, L"SteamClientDll", NULL, REG_SZ, (LPBYTE)OrgSteamCDir, Size1); - RegSetValueExW(Registrykey, L"SteamClientDll64", NULL, REG_SZ, (LPBYTE)OrgSteamCDir64, Size2); - - // Close the HKEY Handle. - RegCloseKey(Registrykey); - } else { - dbg_log::write("Unable to restore the original Steam process informations in the Windows registry, error = " + std::to_string(GetLastError())); - } + if (PersistentMode != L"2" || AppId.empty()) { // persistent mode 0 or 1, or mode 2 without app id + cleanup_registry_hkcu(); + cleanup_registry_hklm(); + cleanup_registry_hkcs(); } dbg_log::close(); diff --git a/tools/steamclient_loader/win/ColdClientLoader.ini b/tools/steamclient_loader/win/ColdClientLoader.ini index aa317b65..b4dde7c6 100644 --- a/tools/steamclient_loader/win/ColdClientLoader.ini +++ b/tools/steamclient_loader/win/ColdClientLoader.ini @@ -6,7 +6,7 @@ Exe=game.exe ExeRunDir= # any additional args to pass, ex: -dx11, also any args passed to the loader will be passed to the app ExeCommandLine= -# IMPORTANT +# IMPORTANT, unless [Persistence] Mode=2 AppId= # path to the steamclient dlls, both must be set, @@ -14,14 +14,10 @@ AppId= SteamClientDll=steamclient.dll SteamClient64Dll=steamclient64.dll +[Injection] # force inject steamclient dll instead of waiting for the app to load it ForceInjectSteamClient=0 -[Debug] -# don't call `ResumeThread()` on the main thread after spawning the .exe -ResumeByDebugger=0 - -[Extra] # path to a folder containing some dlls to inject into the app upon start # this folder will be traversed recursively # additionally, inside this folder you can create a file called `load_order.txt` and @@ -30,9 +26,22 @@ ResumeByDebugger=0 # example: #DllsToInjectFolder=extra_dlls DllsToInjectFolder= + # don't display an error message when a dll injection fails IgnoreInjectionError=1 # don't display an error message if the architecture of the loader is different from the app # this will result in a silent failure if a dll injection didn't succeed # both the loader and the app must have the same arch for the injection to work IgnoreLoaderArchDifference=0 + +[Persistence] +# 0 = turned off +# 1 = loader will spawn the exe and keep hanging in the background until you press "OK" +# 2 = loader will NOT spawn exe, it will just setup the required environemnt and keep hanging in the background +# you have to run the Exe manually, and finally press "OK" when you've finished playing +# it is advised to run the loader as admin in this mode +Mode=0 + +[Debug] +# don't call `ResumeThread()` on the main thread after spawning the .exe +ResumeByDebugger=0