Add steam proxy

This commit is contained in:
momo5502 2022-09-26 18:39:01 +02:00
parent 038621c438
commit bef4ec67b6
10 changed files with 571 additions and 0 deletions

View File

@ -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()

View File

@ -0,0 +1,233 @@
#include <std_include.hpp>
#include "loader/component_loader.hpp"
#include "steam_proxy.hpp"
#include "scheduler.hpp"
#include <utils/nt.hpp>
#include <utils/flags.hpp>
#include <utils/string.hpp>
#include <utils/binary_resource.hpp>
#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<void>("Steam_ReleaseUser", this->steam_pipe_,
this->global_user_);
}
this->steam_client_module_.invoke<bool>("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<void*>("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<void*>("Steam_CreateSteamPipe");
this->global_user_ = this->steam_client_module_.invoke<void*>(
"Steam_ConnectToGlobalUser", this->steam_pipe_);
this->client_user_ = this->client_engine_.invoke<void*>(8, this->steam_pipe_, this->global_user_);
// GetIClientUser
this->client_utils_ = this->client_engine_.invoke<void*>(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<bool>("BIsSubscribedApp", app_id))
{
//app_id = 480; // Spacewar
return ownership_state::unowned;
}
this->client_utils_.invoke<void>("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<const unsigned int*>(mod_id) | 0x80000000;
this->client_user_.invoke<bool>("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<bool>("Steam_BConnected", this->global_user_,
this->steam_pipe_)
&& this->steam_client_module_.invoke<bool>("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<component>()->get_overlay_module();
}
}
REGISTER_COMPONENT(steam_proxy::component)

View File

@ -0,0 +1,7 @@
#pragma once
#include <utils/nt.hpp>
namespace steam_proxy
{
const utils::nt::library& get_overlay_module();
}

View File

@ -11,3 +11,5 @@
#define DW_FASTFILE 305
#define DW_KEYS 306
#define DW_QOSCONFIG 307
#define RUNNER 308

View File

@ -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
/////////////////////////////////////////////////////////////////////////////

View File

@ -78,6 +78,7 @@
#include <variant>
#include <cassert>
#include <udis86.h>
#include <MinHook.h>
#include <asmjit/core/jitruntime.h>
#include <asmjit/x86/x86assembler.h>

View File

@ -0,0 +1,98 @@
#include <std_include.hpp>
#include "interface.hpp"
#include <utils/memory.hpp>
#include <utils/nt.hpp>
namespace steam
{
interface::interface() : interface(nullptr)
{
}
interface::interface(void* interface_ptr) : interface_ptr_(static_cast<void***>(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<const uint8_t*>(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<char*>(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<unsigned char*>(ud.pc) == 0xCC) break; // int 3
}
return {};
}
}

View File

@ -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 <typename T, typename... Args>
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<T(__thiscall*)(void*, Args ...)>(method)(this->interface_ptr_, args...);
}
template <typename T, typename... Args>
T invoke(const size_t table_entry, Args ... args)
{
if (!this->interface_ptr_)
{
throw std::runtime_error("Invalid interface pointer");
}
return static_cast<T(__thiscall*)(void*, Args ...)>((*this->interface_ptr_)[table_entry])(
this->interface_ptr_, args...);
}
private:
void*** interface_ptr_;
std::unordered_map<std::string, void*> methods_;
void* find_method(const std::string& name);
void* search_method(const std::string& name);
std::string analyze_method(const void* method_ptr);
};
}

100
src/runner/resource.rc Normal file
View File

@ -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

23
src/runner/runner.cpp Normal file
View File

@ -0,0 +1,23 @@
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <cstdlib>
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;
}