diff --git a/premake5.lua b/premake5.lua index ce950058..de3babc7 100644 --- a/premake5.lua +++ b/premake5.lua @@ -311,5 +311,21 @@ project "client" dependencies.imports() + +project "runner" + kind "WindowedApp" + language "C++" + + files {"./src/runner/**.rc", "./src/runner/**.hpp", "./src/runner/**.cpp", "./src/runner/resources/**.*"} + + includedirs {"./src/runner", "./src/common", "%{prj.location}/src"} + + links {"common"} + + resincludedirs {"$(ProjectDir)src"} + + dependencies.imports() + + group "Dependencies" dependencies.projects() diff --git a/src/client/component/steam_proxy.cpp b/src/client/component/steam_proxy.cpp new file mode 100644 index 00000000..9b358756 --- /dev/null +++ b/src/client/component/steam_proxy.cpp @@ -0,0 +1,233 @@ +#include +#include "loader/component_loader.hpp" +#include "steam_proxy.hpp" +#include "scheduler.hpp" + +#include +#include +#include +#include + +#include "resource.hpp" + +#include "steam/interface.hpp" +#include "steam/steam.hpp" + +namespace steam_proxy +{ + namespace + { + utils::binary_resource runner_file(RUNNER, "boiii-runner.exe"); + + enum class ownership_state + { + success, + unowned, + nosteam, + error, + }; + + bool is_disabled() + { + static const auto disabled = utils::flags::has_flag("nosteam"); + return disabled; + } + } + + class component final : public component_interface + { + public: + void pre_start() override + { + /*if (is_disabled()) + { + return; + }*/ + + this->load_client(); + this->clean_up_on_error(); + +#ifndef DEV_BUILD + try + { + const auto res = this->start_mod("\xE2\x98\x84\xEF\xB8\x8F" " BOIII"s, steam::SteamUtils()->GetAppID()); + + switch (res) + { + case ownership_state::nosteam: + throw std::runtime_error("Steam must be running to play this game!"); + case ownership_state::unowned: + throw std::runtime_error("You must own the game on steam to play this mod!"); + case ownership_state::error: + throw std::runtime_error("Failed to verify ownership of the game!"); + case ownership_state::success: + break; + } + } + catch (std::exception& e) + { + printf("Steam: %s\n", e.what()); + MessageBoxA(GetForegroundWindow(), e.what(), "Error", MB_ICONERROR); + TerminateProcess(GetCurrentProcess(), 1234); + } +#endif + } + + void pre_destroy() override + { + if (this->steam_client_module_) + { + if (this->steam_pipe_) + { + if (this->global_user_) + { + this->steam_client_module_.invoke("Steam_ReleaseUser", this->steam_pipe_, + this->global_user_); + } + + this->steam_client_module_.invoke("Steam_BReleaseSteamPipe", this->steam_pipe_); + } + } + } + + const utils::nt::library& get_overlay_module() const + { + return steam_overlay_module_; + } + + private: + utils::nt::library steam_client_module_{}; + utils::nt::library steam_overlay_module_{}; + + steam::interface client_engine_{}; + steam::interface client_user_{}; + steam::interface client_utils_{}; + + void* steam_pipe_ = nullptr; + void* global_user_ = nullptr; + + void* load_client_engine() const + { + if (!this->steam_client_module_) return nullptr; + + for (auto i = 1; i > 0; ++i) + { + std::string name = utils::string::va("CLIENTENGINE_INTERFACE_VERSION%03i", i); + auto* const client_engine = this->steam_client_module_ + .invoke("CreateInterface", name.data(), nullptr); + if (client_engine) return client_engine; + } + + return nullptr; + } + + void load_client() + { + const std::filesystem::path steam_path = steam::SteamAPI_GetSteamInstallPath(); + if (steam_path.empty()) return; + + utils::nt::library::load(steam_path / "tier0_s64.dll"); + utils::nt::library::load(steam_path / "vstdlib_s64.dll"); + this->steam_overlay_module_ = utils::nt::library::load(steam_path / "gameoverlayrenderer64.dll"); + this->steam_client_module_ = utils::nt::library::load(steam_path / "steamclient64.dll"); + if (!this->steam_client_module_) return; + + this->client_engine_ = load_client_engine(); + if (!this->client_engine_) return; + + this->steam_pipe_ = this->steam_client_module_.invoke("Steam_CreateSteamPipe"); + this->global_user_ = this->steam_client_module_.invoke( + "Steam_ConnectToGlobalUser", this->steam_pipe_); + this->client_user_ = this->client_engine_.invoke(8, this->steam_pipe_, this->global_user_); + // GetIClientUser + this->client_utils_ = this->client_engine_.invoke(14, this->steam_pipe_); // GetIClientUtils + } + + ownership_state start_mod(const std::string& title, const size_t app_id) + { + __try + { + return this->start_mod_unsafe(title, app_id); + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + this->do_cleanup(); + return ownership_state::error; + } + } + + ownership_state start_mod_unsafe(const std::string& title, size_t app_id) + { + if (!this->client_utils_ || !this->client_user_) + { + return ownership_state::nosteam; + } + + if (!this->client_user_.invoke("BIsSubscribedApp", app_id)) + { + //app_id = 480; // Spacewar + return ownership_state::unowned; + } + + this->client_utils_.invoke("SetAppIDForCurrentPipe", app_id, false); + + char our_directory[MAX_PATH] = {0}; + GetCurrentDirectoryA(sizeof(our_directory), our_directory); + + const auto path = runner_file.get_extracted_file(); + const std::string cmdline = utils::string::va("\"%s\" -proc %d", path.data(), GetCurrentProcessId()); + + steam::game_id game_id; + game_id.raw.type = 1; // k_EGameIDTypeGameMod + game_id.raw.app_id = app_id & 0xFFFFFF; + + const auto* mod_id = "bo3"; + game_id.raw.mod_id = *reinterpret_cast(mod_id) | 0x80000000; + + this->client_user_.invoke("SpawnProcess", path.data(), cmdline.data(), our_directory, + &game_id.bits, title.data(), 0, 0, 0); + + return ownership_state::success; + } + + void do_cleanup() + { + this->client_engine_ = nullptr; + this->client_user_ = nullptr; + this->client_utils_ = nullptr; + + this->steam_pipe_ = nullptr; + this->global_user_ = nullptr; + + this->steam_client_module_ = utils::nt::library{nullptr}; + } + + void clean_up_on_error() + { + scheduler::schedule([this]() + { + if (this->steam_client_module_ + && this->steam_pipe_ + && this->global_user_ + && this->steam_client_module_.invoke("Steam_BConnected", this->global_user_, + this->steam_pipe_) + && this->steam_client_module_.invoke("Steam_BLoggedOn", this->global_user_, this->steam_pipe_) + ) + { + return scheduler::cond_continue; + } + + this->do_cleanup(); + return scheduler::cond_end; + }); + } + }; + + const utils::nt::library& get_overlay_module() + { + // TODO: Find a better way to do this + return component_loader::get()->get_overlay_module(); + } +} + +REGISTER_COMPONENT(steam_proxy::component) diff --git a/src/client/component/steam_proxy.hpp b/src/client/component/steam_proxy.hpp new file mode 100644 index 00000000..02807691 --- /dev/null +++ b/src/client/component/steam_proxy.hpp @@ -0,0 +1,7 @@ +#pragma once +#include + +namespace steam_proxy +{ + const utils::nt::library& get_overlay_module(); +} diff --git a/src/client/resource.hpp b/src/client/resource.hpp index a9fc37da..3dc82d00 100644 --- a/src/client/resource.hpp +++ b/src/client/resource.hpp @@ -11,3 +11,5 @@ #define DW_FASTFILE 305 #define DW_KEYS 306 #define DW_QOSCONFIG 307 + +#define RUNNER 308 diff --git a/src/client/resource.rc b/src/client/resource.rc index a83d21ab..4007cfd4 100644 --- a/src/client/resource.rc +++ b/src/client/resource.rc @@ -102,6 +102,12 @@ DW_FASTFILE RCDATA "resources/dw/core_ffotd_tu32_593.ff" DW_KEYS RCDATA "resources/dw/keys.txt" DW_QOSCONFIG RCDATA "resources/dw/qosconfig4.csv" +#ifdef _DEBUG +RUNNER RCDATA "../../build/bin/x64/Debug/runner.exe" +#else +RUNNER RCDATA "../../build/bin/x64/Release/runner.exe" +#endif + #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// diff --git a/src/client/std_include.hpp b/src/client/std_include.hpp index 70994304..d606c2f2 100644 --- a/src/client/std_include.hpp +++ b/src/client/std_include.hpp @@ -78,6 +78,7 @@ #include #include +#include #include #include #include diff --git a/src/client/steam/interface.cpp b/src/client/steam/interface.cpp new file mode 100644 index 00000000..75ca922a --- /dev/null +++ b/src/client/steam/interface.cpp @@ -0,0 +1,98 @@ +#include +#include "interface.hpp" + +#include +#include + +namespace steam +{ + interface::interface() : interface(nullptr) + { + } + + interface::interface(void* interface_ptr) : interface_ptr_(static_cast(interface_ptr)) + { + } + + interface::operator bool() const + { + return this->interface_ptr_ != nullptr; + } + + void* interface::find_method(const std::string& name) + { + const auto method_entry = this->methods_.find(name); + if (method_entry != this->methods_.end()) + { + return method_entry->second; + } + + return this->search_method(name); + } + + void* interface::search_method(const std::string& name) + { + if (!utils::memory::is_bad_read_ptr(this->interface_ptr_)) + { + auto vftbl = *this->interface_ptr_; + + while (!utils::memory::is_bad_read_ptr(vftbl) && !utils::memory::is_bad_code_ptr(*vftbl)) + { + const auto ptr = *vftbl; + const auto result = this->analyze_method(ptr); + if (!result.empty()) + { + this->methods_[result] = ptr; + + if (result == name) + { + return ptr; + } + } + + ++vftbl; + } + } + + return {}; + } + + std::string interface::analyze_method(const void* method_ptr) + { + if (utils::memory::is_bad_code_ptr(method_ptr)) return {}; + + ud_t ud; + ud_init(&ud); + ud_set_mode(&ud, 64); + ud_set_pc(&ud, uint64_t(method_ptr)); + ud_set_input_buffer(&ud, static_cast(method_ptr), INT32_MAX); + + while (true) + { + ud_disassemble(&ud); + + if (ud_insn_mnemonic(&ud) == UD_Iret) + { + break; + } + + if (ud_insn_mnemonic(&ud) == UD_Ilea) + { + const auto* operand = ud_insn_opr(&ud, 1); + if (operand && operand->type == UD_OP_MEM && operand->base == UD_R_RIP) + { + auto* operand_ptr = reinterpret_cast(ud_insn_len(&ud) + ud_insn_off(&ud) + operand->lval. + sdword); + if (!utils::memory::is_bad_read_ptr(operand_ptr) && utils::memory::is_rdata_ptr(operand_ptr)) + { + return operand_ptr; + } + } + } + + if (*reinterpret_cast(ud.pc) == 0xCC) break; // int 3 + } + + return {}; + } +} diff --git a/src/client/steam/interface.hpp b/src/client/steam/interface.hpp new file mode 100644 index 00000000..c118c80e --- /dev/null +++ b/src/client/steam/interface.hpp @@ -0,0 +1,85 @@ +#pragma once + +#ifdef interface +#undef interface +#endif + +namespace steam +{ + struct raw_steam_id final + { + unsigned int account_id : 32; + unsigned int account_instance : 20; + unsigned int account_type : 4; + int universe : 8; + }; + + typedef union + { + raw_steam_id raw; + unsigned long long bits; + } steam_id; + +#pragma pack( push, 1 ) + struct raw_game_id final + { + unsigned int app_id : 24; + unsigned int type : 8; + unsigned int mod_id : 32; + }; + + typedef union + { + raw_game_id raw; + unsigned long long bits; + } game_id; +#pragma pack( pop ) + + class interface final + { + public: + + interface(); + interface(void* interface_ptr); + + operator bool() const; + + template + T invoke(const std::string& method_name, Args ... args) + { + if (!this->interface_ptr_) + { + throw std::runtime_error("Invalid interface pointer"); + } + + const auto method = this->find_method(method_name); + if (!method) + { + throw std::runtime_error("Unable to find method: " + method_name); + } + + return static_cast(method)(this->interface_ptr_, args...); + } + + template + T invoke(const size_t table_entry, Args ... args) + { + if (!this->interface_ptr_) + { + throw std::runtime_error("Invalid interface pointer"); + } + + return static_cast((*this->interface_ptr_)[table_entry])( + this->interface_ptr_, args...); + } + + private: + void*** interface_ptr_; + std::unordered_map methods_; + + void* find_method(const std::string& name); + void* search_method(const std::string& name); + + std::string analyze_method(const void* method_ptr); + }; +} diff --git a/src/runner/resource.rc b/src/runner/resource.rc new file mode 100644 index 00000000..ba86c6f4 --- /dev/null +++ b/src/runner/resource.rc @@ -0,0 +1,100 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "windows.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "#include ""windows.h""\r\n" + "\0" +END + +2 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 1,0,0,0 + PRODUCTVERSION 1,0,0,0 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE VFT_DLL + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "X Labs" + VALUE "FileDescription", "Steam mod runner" + VALUE "FileVersion", "1.0.0.0" + VALUE "InternalName", "Runner" + VALUE "LegalCopyright", "All rights reserved." + VALUE "OriginalFilename", "runner.exe" + VALUE "ProductName", "runner" + VALUE "ProductVersion", "1.0.0.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + +///////////////////////////////////////////////////////////////////////////// +// +// Binary Data +// + +102 ICON "../client/resources/icon.ico" + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/src/runner/runner.cpp b/src/runner/runner.cpp new file mode 100644 index 00000000..738a0b95 --- /dev/null +++ b/src/runner/runner.cpp @@ -0,0 +1,23 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include + +int __stdcall WinMain(HINSTANCE, HINSTANCE, PSTR, int) +{ + const auto* const command = "-proc "; + const char* parent_proc = strstr(GetCommandLineA(), command); + + if (parent_proc) + { + const auto pid = DWORD(atoi(parent_proc + strlen(command))); + auto* const process_handle = OpenProcess(SYNCHRONIZE, FALSE, pid); + if (process_handle) + { + WaitForSingleObject(process_handle, INFINITE); + CloseHandle(process_handle); + return 0; + } + } + + return 1; +}