allow specifying a load order via a file called load_order.txt inside the root of the dlls to inject folder + updated readmes

This commit is contained in:
otavepto 2024-01-19 03:47:58 +02:00
parent d881806644
commit e2cb2e0f38
12 changed files with 225 additions and 76 deletions

View File

@ -1,8 +1,11 @@
* a new experimental library to patch Stub drm v3.1 in memory, check the readme of the cold client loader
* cold client loader can now inject dlls, also it can force inject the `steamclient(64).dll` library
* cold client loader can inject the dlls according to a user sepcified order, check the readme and the provided example
* cold client loader will now treat relative paths as relative to its own path
* cold client loader now doesn't need an explicit setting for the `ExeRunDir`, by default it would be the folder of the exe
* in cold client loader, the option `ResumeByDebugger` is now available in the release build
* cold client loader will display a nag about architecture difference if for example the app was 32-bit and the loader was 64-bit,
this could be disabled via the setting `IgnoreLoaderArchDifference=1`
* the cold client loader will output useful debug info when the debug build is used
* fixed a problem in the overlay which would cause a crash for the guest player when an invitation was sent
* imitate how the DOS Stub is manipulated during/after the build

View File

@ -478,7 +478,8 @@ echo: & echo:
:: copy configs + examples
if %last_code% equ 0 (
echo // copying readmes + files examples
xcopy /y /s "post_build\steam_settings.EXAMPLE\" "%build_root_dir%\steam_settings.EXAMPLE\"
xcopy /y /s /e /r "post_build\steam_settings.EXAMPLE\" "%build_root_dir%\steam_settings.EXAMPLE\"
xcopy /y /s /e /r "post_build\win\ColdClientLoader.EXAMPLE\" "%steamclient_dir%\EXAMPLE\"
copy /y "%tools_src_dir%\steamclient_loader\win\ColdClientLoader.ini" "%steamclient_dir%\"
copy /y "post_build\README.release.md" "%build_root_dir%\"
copy /y "CHANGELOG.md" "%build_root_dir%\"

View File

@ -106,6 +106,34 @@ bool common_helpers::ends_with_i(const std::wstring &target, const std::wstring
}
std::string common_helpers::to_lower(std::string str)
{
std::string _str(str.size(), '\0');
std::transform(str.begin(), str.end(), _str.begin(), [](char c) { return std::tolower(c); });
return _str;
}
std::wstring common_helpers::to_lower(std::wstring wstr)
{
std::wstring _wstr(wstr.size(), '\0');
std::transform(wstr.begin(), wstr.end(), _wstr.begin(), [](wchar_t c) { return std::tolower(c); });
return _wstr;
}
std::string common_helpers::to_upper(std::string str)
{
std::string _str(str.size(), '\0');
std::transform(str.begin(), str.end(), _str.begin(), [](char c) { return std::toupper(c); });
return _str;
}
std::wstring common_helpers::to_upper(std::wstring wstr)
{
std::wstring _wstr(wstr.size(), '\0');
std::transform(wstr.begin(), wstr.end(), _wstr.begin(), [](wchar_t c) { return std::toupper(c); });
return _wstr;
}
std::filesystem::path to_absolute_impl(const std::filesystem::path &path, const std::filesystem::path &base)
{
if (path.is_absolute()) {

View File

@ -24,6 +24,14 @@ bool ends_with_i(const std::string &target, const std::string &query);
bool ends_with_i(const std::wstring &target, const std::wstring &query);
std::string to_lower(std::string str);
std::wstring to_lower(std::wstring wstr);
std::string to_upper(std::string str);
std::wstring to_upper(std::wstring wstr);
std::string to_absolute(const std::string &path, const std::string &base = std::string());
std::wstring to_absolute(const std::wstring &path, const std::wstring &base = std::wstring());

View File

@ -31,6 +31,9 @@ You do not need to create a `steam_interfaces.txt` file for the `steamclient` ve
* `DllsToInjectFolder`: path to a folder containing dlls to force inject into the app upon start,
the loader will attempt to detect the dll architecture (32 or 64 bit), if it didn't match the architecture of the exe, then it will ignored
* `IgnoreInjectionError`: setting this to `1` or `y` or `true` will prevent the loader from displaying an error message when a dll injection fails
* `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
**Note** that any arguments passed to `steamclient_loader.exe` via command line will be passed to the target `.exe`.
@ -38,6 +41,14 @@ Example: `steamclient_loader.exe` `-dx11`
If the additional exe arguments were both: passed via command line and set in the `.ini` file, then both will be cocatenated and passed to the exe.
This allows the loader to be used/called from other external apps which set additional args.
### `DllsToInjectFolder`
The folder specified by this identifier should contain the .dll files you'd like to inject in the app earlier during its creation.
All the subfolders inside this folder will be traversed recursively, and the .dll files inside these subfolders will be loaded/injected.
Additionaly, you can create a file called `load_order.txt` inside your folder (root level, not inside any subdir), mentioning on each line the .dll files to inject, the order of the lines will instruct the loader which .dll to inject first, the .dll mentioned on the first line will be injected first and so on.
Each line inside this file has to be the relative path to the target .dll, and it should be relative to your folder. Check the example.
Any .dll file not mentioned in this file will be loaded later, but in random order.
---
## `extra_dlls`

View File

@ -0,0 +1,36 @@
# modified version of ColdClientLoader originally by Rat431
[SteamClient]
# path to game exe, absolute or relative to the loader
Exe=my_app.exe
# empty means the folder of the exe
ExeRunDir=
# any additional args to pass, ex: -dx11, also any args passed to the loader will be passed to the app
ExeCommandLine=
# IMPORTANT
AppId=123
# path to the steamclient dlls, both must be set,
# absolute paths or relative to the loader
SteamClientDll=steamclient.dll
SteamClient64Dll=steamclient64.dll
# 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
# inside it, specify line by line the order of the dlls that have to be injected
# each line should be a relative path of the dll, relative to the injection folder
DllsToInjectFolder=extra_dlls.EXAMPLE
# don't display an error message when a dll injection fails
IgnoreInjectionError=0
# 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

View File

@ -0,0 +1,3 @@
dlls_subdir\3_my_third_dll.dll
2_my_second_dll.dll

View File

@ -12,6 +12,7 @@
#include <tchar.h>
#include <stdio.h>
#include <string>
#include <set>
static const std::wstring IniFile = pe_helpers::get_current_exe_path_w() + L"ColdClientLoader.ini";
@ -59,6 +60,90 @@ static std::vector<uint8_t> get_pe_header(const std::wstring &filepath)
}
}
static std::vector<std::wstring> collect_dlls_to_inject(
const std::wstring &extra_dlls_folder,
bool is_exe_32,
std::wstring &failed_dlls = std::wstring{})
{
const auto load_order_file = std::filesystem::path(extra_dlls_folder) / "load_order.txt";
std::vector<std::wstring> dlls_to_inject{};
for (auto const& dir_entry :
std::filesystem::recursive_directory_iterator(extra_dlls_folder, std::filesystem::directory_options::follow_directory_symlink)) {
if (std::filesystem::is_directory(dir_entry.path())) continue;
auto dll_path = dir_entry.path().wstring();
// ignore this file if it is the load order file
if (common_helpers::to_upper(dll_path) == common_helpers::to_upper(load_order_file.wstring())) continue;
auto dll_header = get_pe_header(dll_path);
if (dll_header.empty()) {
dbg_log::write(L"Failed to get PE header of dll: " + dll_path);
failed_dlls += dll_path + L"\n";
continue;
}
bool is_dll_32 = pe_helpers::is_module_32((HMODULE)&dll_header[0]);
bool is_dll_64 = pe_helpers::is_module_64((HMODULE)&dll_header[0]);
if ((!is_dll_32 && !is_dll_64) || (is_dll_32 && is_dll_64)) { // ARM, or just a regular file
dbg_log::write(L"Dll " + dll_path + L" is neither 32 nor 64 bit and will be ignored");
failed_dlls += dll_path + L"\n";
continue;
}
if (is_dll_32 == is_exe_32) { // same arch
dlls_to_inject.push_back(dll_path);
dbg_log::write(L"Dll " + dll_path + L" will be injected");
} else {
dbg_log::write(L"Dll " + dll_path + L" has a different arch than the exe and will be ignored");
failed_dlls += dll_path + L"\n";
}
}
std::vector<std::wstring> ordered_dlls_to_inject{};
{
dbg_log::write(L"Searching for load order file: " + load_order_file.wstring());
auto f_order = std::wifstream(load_order_file, std::ios::in);
if (f_order.is_open()) {
dbg_log::write(L"Reading load order file: " + load_order_file.wstring());
std::wstring line{};
while (std::getline(f_order, line)) {
auto abs = common_helpers::to_absolute(line, extra_dlls_folder);
auto abs_upper = common_helpers::to_upper(abs);
dbg_log::write(L"Load order line: " + abs_upper);
auto it = std::find_if(dlls_to_inject.begin(), dlls_to_inject.end(), [&abs_upper](const std::wstring &dll_to_inject) {
return common_helpers::to_upper(dll_to_inject) == abs_upper;
});
if (it != dlls_to_inject.end()) {
dbg_log::write("Found the dll specified by the load order line");
ordered_dlls_to_inject.push_back(*it);
// mark for deletion
it->clear();
}
}
f_order.close();
}
}
// add the remaining dlls
for (auto &dll : dlls_to_inject) {
if (dll.size()) {
ordered_dlls_to_inject.push_back(dll);
}
}
return ordered_dlls_to_inject;
}
static void to_bool_ini_val(std::wstring &val)
{
for (auto &c : val) {
c = (wchar_t)std::tolower((int)c);
}
if (val != L"1" && val != L"y" && val != L"yes" && val != L"true") {
val.clear();
}
}
int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
{
dbg_log::init(dbg_file.c_str());
@ -89,16 +174,22 @@ 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 InjectClient = get_ini_value(L"SteamClient", L"ForceInjectSteamClient");
std::wstring ForceInjectSteamClient = get_ini_value(L"SteamClient", L"ForceInjectSteamClient");
std::wstring resume_by_dbg = get_ini_value(L"Debug", L"ResumeByDebugger");
std::wstring ResumeByDebugger = get_ini_value(L"Debug", L"ResumeByDebugger");
// dlls to inject
std::wstring extra_dlls_folder = common_helpers::to_absolute(
std::wstring DllsToInjectFolder = common_helpers::to_absolute(
get_ini_value(L"Extra", 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");
to_bool_ini_val(ResumeByDebugger);
to_bool_ini_val(ForceInjectSteamClient);
to_bool_ini_val(IgnoreInjectionError);
to_bool_ini_val(IgnoreLoaderArchDifference);
// log everything
dbg_log::write(L"SteamClient::Exe: " + ExeFile);
@ -107,10 +198,11 @@ 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::ForceInjectSteamClient: " + InjectClient);
dbg_log::write(L"Debug::ResumeByDebugger: " + resume_by_dbg);
dbg_log::write(L"Extra::DllsToInjectFolder: " + extra_dlls_folder);
dbg_log::write(L"SteamClient::ForceInjectSteamClient: " + ForceInjectSteamClient);
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());
@ -131,7 +223,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
if (ExeRunDir.empty()) {
ExeRunDir = std::filesystem::path(ExeFile).parent_path().wstring();
dbg_log::write(L"Setting exe run dir to: " + ExeRunDir);
dbg_log::write(L"Setting ExeRunDir to: " + ExeRunDir);
}
if (!common_helpers::dir_exist(ExeRunDir)) {
dbg_log::write("Couldn't find the requested Exe run dir");
@ -154,29 +246,8 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
return 1;
}
for (auto &c : resume_by_dbg) {
c = (wchar_t)std::tolower((int)c);
}
if (resume_by_dbg != L"1" && resume_by_dbg != L"y" && resume_by_dbg != L"yes" && resume_by_dbg != L"true") {
resume_by_dbg.clear();
}
for (auto &c : IgnoreInjectionError) {
c = (wchar_t)std::tolower((int)c);
}
if (IgnoreInjectionError != L"1" && IgnoreInjectionError != L"y" && IgnoreInjectionError != L"yes" && IgnoreInjectionError != L"true") {
IgnoreInjectionError.clear();
}
for (auto &c : InjectClient) {
c = (wchar_t)std::tolower((int)c);
}
if (InjectClient != L"1" && InjectClient != L"y" && InjectClient != L"yes" && InjectClient != L"true") {
InjectClient.clear();
}
if (extra_dlls_folder.size()) {
if (!common_helpers::dir_exist(extra_dlls_folder)) {
if (DllsToInjectFolder.size()) {
if (!common_helpers::dir_exist(DllsToInjectFolder)) {
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();
@ -215,42 +286,15 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
if (loader_is_32 != is_exe_32) {
dbg_log::write("Arch of loader and requested exe are different, it is advised to use the appropriate one");
MessageBoxA(NULL, "Arch of loader and requested exe are different,\nit is advised to use the appropriate one.", "ColdClientLoader", MB_OK);
if (IgnoreLoaderArchDifference.empty()) {
MessageBoxA(NULL, "Arch of loader and requested exe are different,\nit is advised to use the appropriate one.", "ColdClientLoader", MB_OK);
}
}
std::vector<std::wstring> dlls_to_inject{};
if (extra_dlls_folder.size()) {
std::wstring failed_dlls = std::wstring{};
for (auto const& dir_entry :
std::filesystem::recursive_directory_iterator(extra_dlls_folder, std::filesystem::directory_options::follow_directory_symlink)) {
if (std::filesystem::is_directory(dir_entry.path())) continue;
auto dll_path = dir_entry.path().wstring();
auto dll_header = get_pe_header(dll_path);
if (dll_header.empty()) {
dbg_log::write(L"Failed to get PE header of dll: " + dll_path);
failed_dlls += dll_path + L"\n";
continue;
}
bool is_dll_32 = pe_helpers::is_module_32((HMODULE)&dll_header[0]);
bool is_dll_64 = pe_helpers::is_module_64((HMODULE)&dll_header[0]);
if ((!is_dll_32 && !is_dll_64) || (is_dll_32 && is_dll_64)) { // ARM, or just a regular file
dbg_log::write(L"Dll " + dll_path + L" is neither 32 nor 64 bit and will be ignored");
failed_dlls += dll_path + L"\n";
continue;
}
if ((is_dll_32 && is_exe_32) || (is_dll_64 && is_exe_64)) {
dlls_to_inject.push_back(dll_path);
dbg_log::write(L"Dll " + dll_path + L" will be injected");
} else {
dbg_log::write(L"Dll " + dll_path + L" has a different arch than the exe and will be ignored");
failed_dlls += dll_path + L"\n";
}
}
if (DllsToInjectFolder.size()) {
std::wstring failed_dlls{};
dlls_to_inject = collect_dlls_to_inject(DllsToInjectFolder, is_exe_32, failed_dlls);
if (failed_dlls.size() && IgnoreInjectionError.empty()) {
int choice = MessageBoxW(
NULL,
@ -320,19 +364,20 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
return 1;
}
if (InjectClient.size()) {
if (ForceInjectSteamClient.size()) {
if (is_exe_32) {
dlls_to_inject.insert(dlls_to_inject.begin(), ClientPath);
} else {
dlls_to_inject.insert(dlls_to_inject.begin(), Client64Path);
}
}
for (const auto &dll_path : dlls_to_inject) {
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_path, &err_inject);
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_path + L"\n" +
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";
@ -346,12 +391,12 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
return 1;
}
} else {
dbg_log::write("Injected!");
}
}
// run
if (resume_by_dbg.empty()) {
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";

View File

@ -1,15 +1,20 @@
# modified version of ColdClientLoader originally by Rat431
[SteamClient]
# path to game exe, absolute or relative to the loader
Exe=game.exe
# empty means the folder of the 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
AppId=
# path to the steamclient dlls, both must be set,
# absolute paths or relative to the loader
SteamClientDll=steamclient.dll
SteamClient64Dll=steamclient64.dll
# inject `steamclient(64).dll`
# force inject steamclient dll instead of waiting for the app to load it
ForceInjectSteamClient=0
[Debug]
@ -17,8 +22,17 @@ ForceInjectSteamClient=0
ResumeByDebugger=0
[Extra]
; path to a folder containing dlls to force inject into the app upon start
; extra_dlls
# 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
# inside it, specify line by line the order of the dlls that have to be injected
# each line should be a relative path of the dll, relative to the injection folder
# example:
#DllsToInjectFolder=extra_dlls
DllsToInjectFolder=
; don't display an error message when a dll injection fails
# 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