Reworked late hooking

This commit is contained in:
acidicoala
2025-09-07 02:02:59 +05:00
parent 6b4b7610f4
commit 4c08816eb6
11 changed files with 196 additions and 50 deletions

View File

@@ -1,3 +1,6 @@
#include <regex>
#include <set>
#include <koalabox/config.hpp>
#include <koalabox/dll_monitor.hpp>
#include <koalabox/globals.hpp>
@@ -9,11 +12,13 @@
#include <koalabox/util.hpp>
#include <koalabox/win.hpp>
#include "build_config.h"
#include "smoke_api.hpp"
#include "smoke_api/config.hpp"
#include "smoke_api/steamclient/steamclient.hpp"
#include "steam_api/steam_interfaces.hpp"
#include "build_config.h"
// Hooking steam_api has shown itself to be less desirable than steamclient
// for the reasons outlined below:
@@ -38,11 +43,59 @@ namespace {
HMODULE original_steamapi_handle = nullptr;
std::set<std::string> find_steamclient_versions(const HMODULE steamapi_handle) noexcept {
try {
std::set<std::string> versions;
const auto rdata = kb::win::get_pe_section_or_throw(steamapi_handle, ".rdata").to_string();
const std::regex pattern(R"(SteamClient\d{3})");
auto matches_begin = std::sregex_iterator(rdata.begin(), rdata.end(), pattern);
auto matches_end = std::sregex_iterator();
for(std::sregex_iterator i = matches_begin; i != matches_end; ++i) {
versions.insert(i->str());
}
return versions;
} catch(const std::exception& e) {
LOG_ERROR("{} -> insert error: {}", __func__, e.what());
return {};
}
}
// ReSharper disable once CppDFAConstantFunctionResult
bool on_steamclient_loaded(const HMODULE steamclient_handle) noexcept {
auto* const steamapi_handle = original_steamapi_handle
? original_steamapi_handle
: GetModuleHandle(TEXT(STEAMAPI_DLL));
if(!steamapi_handle) {
LOG_ERROR("{} -> {} is not loaded", __func__, STEAMAPI_DLL);
return true;
}
static const auto CreateInterface$ = KB_WIN_GET_PROC(steamclient_handle, CreateInterface);
const auto steamclient_versions = find_steamclient_versions(steamapi_handle);
for(const auto& steamclient_version : steamclient_versions) {
if(CreateInterface$(steamclient_version.c_str(), nullptr)) {
LOG_WARN("'{}' was already initialized. SmokeAPI might not work as expected.", steamclient_version);
LOG_WARN("Probable cause: SmokeAPI was injected too late. If possible, try injecting it earlier.");
steam_interfaces::hook_steamclient_interface(steamclient_handle, steamclient_version);
} else {
LOG_INFO("'{}' is not initialized. Waiting for initialization.", steamclient_version);
}
}
KB_HOOK_DETOUR_MODULE(CreateInterface, steamclient_handle);
return true;
}
void start_dll_listener() {
kb::dll_monitor::init_listener(
{STEAMCLIENT_DLL},
[&](const HMODULE& module_handle, auto&) {
KB_HOOK_DETOUR_MODULE(CreateInterface, module_handle);
{
{STEAMCLIENT_DLL, on_steamclient_loaded}
}
);
}
@@ -82,6 +135,7 @@ namespace smoke_api {
self_path,
STEAMAPI_DLL
);
start_dll_listener();
}

View File

@@ -1,7 +1,7 @@
#include <koalabox/logger.hpp>
#include "steam_api/steam_interfaces.hpp"
#include "steam_client.hpp"
#include "steam_api/steam_interfaces.hpp"
namespace steam_client {
void* GetGenericInterface(

View File

@@ -9,6 +9,7 @@
#include "steam_api/steam_interfaces.hpp"
#include "smoke_api/smoke_api.hpp"
#include "smoke_api/steamclient/steamclient.hpp"
#include "virtuals/steam_api_virtuals.hpp"
namespace {
@@ -18,14 +19,15 @@ namespace {
void* function_address; // e.g. ISteamClient_GetISteamApps
};
// TODO: Split fallback into low and high versions
struct interface_data { // NOLINT(*-exception-escape)
struct interface_data_t { // NOLINT(*-exception-escape)
std::string fallback_version; // e.g. "SteamClient021"
// Key is function name without interface prefix
std::map<std::string, interface_entry> entry_map;
// e.g. {ENTRY(ISteamClient, GetISteamApps), ...}
};
std::map<std::string, interface_data> get_virtual_hook_map() {
// Key is interface name, e.g. "SteamClient"
std::map<std::string, interface_data_t> get_virtual_hook_map() {
#define ENTRY(INTERFACE, FUNC) \
{ \
#FUNC, { \
@@ -36,7 +38,7 @@ namespace {
return {
{
STEAM_APPS,
interface_data{
interface_data_t{
.fallback_version = "STEAMAPPS_INTERFACE_VERSION008",
.entry_map = {
ENTRY(ISteamApps, BIsSubscribedApp),
@@ -48,7 +50,7 @@ namespace {
},
{
STEAM_CLIENT,
interface_data{
interface_data_t{
.fallback_version = "SteamClient021",
.entry_map = {
ENTRY(ISteamClient, GetISteamApps),
@@ -60,7 +62,7 @@ namespace {
},
{
STEAM_GAME_SERVER,
interface_data{
interface_data_t{
.fallback_version = "SteamGameServer015",
.entry_map = {
ENTRY(ISteamGameServer, UserHasLicenseForApp),
@@ -69,7 +71,7 @@ namespace {
},
{
STEAM_HTTP,
interface_data{
interface_data_t{
.fallback_version = "STEAMHTTP_INTERFACE_VERSION003",
.entry_map = {
ENTRY(ISteamHTTP, GetHTTPResponseBodyData),
@@ -80,7 +82,7 @@ namespace {
},
{
STEAM_INVENTORY,
interface_data{
interface_data_t{
.fallback_version = "STEAMINVENTORY_INTERFACE_V003",
.entry_map = {
ENTRY(ISteamInventory, GetResultStatus),
@@ -95,7 +97,7 @@ namespace {
},
{
STEAM_USER,
interface_data{
interface_data_t{
.fallback_version = "SteamUser023",
.entry_map = {
ENTRY(ISteamUser, UserHasLicenseForApp),
@@ -105,8 +107,14 @@ namespace {
};
}
auto read_interface_lookup() {
std::map<std::string, std::map<std::string, uint16_t>> lookup_map;
// Key is function name, Value is ordinal
using ordinal_map_t = std::map<std::string, uint16_t>;
// Key is interface version string
using lookup_map_t = std::map<std::string, ordinal_map_t>;
lookup_map_t read_interface_lookup() {
lookup_map_t lookup_map;
const auto lookup_str = b::embed<"res/interface_lookup.json">().str();
const auto lookup_json = nlohmann::json::parse(lookup_str);
@@ -140,9 +148,9 @@ namespace steam_interfaces {
/**
* @param interface_ptr Pointer to the interface
* @param version_string Example: 'SteamClient020'
* @param version_string Example: 'SteamClient007'
*/
void hook_virtuals(void* interface_ptr, const std::string& version_string) {
void hook_virtuals(const void* interface_ptr, const std::string& version_string) {
if(interface_ptr == nullptr) {
// Game has tried to use an interface before initializing steam api
// This does happen in practice.
@@ -152,14 +160,10 @@ namespace steam_interfaces {
static std::mutex section;
const std::lock_guard guard(section);
static std::set<void*> processed_interfaces;
static std::set<const void*> processed_interfaces;
if(processed_interfaces.contains(interface_ptr)) {
LOG_DEBUG(
"Interface '{}' @ {} has already been processed.",
version_string,
interface_ptr
);
LOG_DEBUG("Interface '{}' @ {} has already been processed.", version_string, interface_ptr);
return;
}
processed_interfaces.insert(interface_ptr);
@@ -170,11 +174,7 @@ namespace steam_interfaces {
continue;
}
LOG_INFO(
"Processing '{}' @ {} found in virtual hook map",
version_string,
interface_ptr
);
LOG_INFO("Processing '{}' @ {} found in virtual hook map", version_string, interface_ptr);
const auto& lookup = find_lookup(version_string, data.fallback_version);
@@ -194,4 +194,63 @@ namespace steam_interfaces {
break;
}
}
void hook_steamclient_interface(
const HMODULE steamclient_handle,
const std::string& steam_client_interface_version
) noexcept {
try {
// Create a copy for modification
auto virtual_hook_map = get_virtual_hook_map();
// Remove steam client map since we don't want to hook its methods
virtual_hook_map.erase(STEAM_CLIENT);
// Map remaining virtual hook map to a set of keys
const auto prefixes = std::views::keys(virtual_hook_map) | std::ranges::to<std::set>();
// Prepare HSteamPipe and HSteamUser
const auto CreateInterface$ = KB_WIN_GET_PROC(steamclient_handle, CreateInterface);
const auto* const THIS = CreateInterface$(
steam_client_interface_version.c_str(), nullptr
);
hook_virtuals(THIS, steam_client_interface_version);
const auto interface_lookup = read_interface_lookup();
for(const auto& interface_version : interface_lookup | std::views::keys) {
// SteamUser and SteamPipe handles must match the ones previously used by the game,
// otherwise SteamAPI will just create new instances of interfaces, instead of returning
// existing instances that are used by the game. Usually these handles default to 1,
// but if a game creates several of them, then we need to somehow find them out dynamically.
constexpr auto steam_pipe = 1;
constexpr auto steam_user = 1;
const bool should_hook = std::ranges::any_of(
prefixes,
[&](const auto& prefix) {
return std::ranges::starts_with(interface_version, prefix);
}
);
if(not should_hook) {
continue;
}
const auto* const interface_ptr = ISteamClient_GetISteamGenericInterface(
ARGS(steam_user, steam_pipe, interface_version.c_str())
);
if(not interface_ptr) {
LOG_ERROR("Failed to get generic interface: '{}'", interface_version)
}
}
// ISteamClient_ReleaseUser(ARGS(steam_pipe, steam_user));
// ISteamClient_BReleaseSteamPipe(ARGS(steam_pipe));
kb::hook::unhook_vt_all(THIS);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Unhandled exception: {}", __func__, e.what());
}
}
}

View File

@@ -3,7 +3,16 @@
#include "smoke_api/types.hpp"
namespace steam_interfaces {
void hook_virtuals(const void* interface_ptr, const std::string& version_string);
void hook_virtuals(void* interface_ptr, const std::string& version_string);
/**
* A fallback mechanism used when SteamAPI has already been initialized.
* It will hook the SteamClient interface and hook its interface accessors.
* This allows us to hook interfaces that are no longer being created,
* such as in the case of late injection.
*/
void hook_steamclient_interface(
HMODULE steamclient_handle,
const std::string& steam_client_interface_version
) noexcept;
}

View File

@@ -7,13 +7,13 @@ VIRTUAL(void*) ISteamClient_GetISteamApps(
PARAMS(
const HSteamUser hSteamUser,
const HSteamPipe hSteamPipe,
const char* version
const char* pchVersion
)
) noexcept {
return steam_client::GetGenericInterface(
__func__,
version,
SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamApps, ARGS(hSteamUser, hSteamPipe, version))
pchVersion,
SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamApps, ARGS(hSteamUser, hSteamPipe, pchVersion))
);
}
@@ -21,13 +21,13 @@ VIRTUAL(void*) ISteamClient_GetISteamUser(
PARAMS(
const HSteamUser hSteamUser,
const HSteamPipe hSteamPipe,
const char* version
const char* pchVersion
)
) noexcept {
return steam_client::GetGenericInterface(
__func__,
version,
SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamUser, ARGS(hSteamUser, hSteamPipe, version))
pchVersion,
SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamUser, ARGS(hSteamUser, hSteamPipe, pchVersion))
);
}

View File

@@ -14,10 +14,12 @@ VIRTUAL(void*) ISteamClient_GetISteamUser(PARAMS(HSteamUser, HSteamPipe, const c
VIRTUAL(void*) ISteamClient_GetISteamGenericInterface(
PARAMS(HSteamUser, HSteamPipe, const char*)
) noexcept;
VIRTUAL(void*) ISteamClient_GetISteamInventory(
PARAMS(HSteamUser, HSteamPipe, const char*)
) noexcept;
// ISteamHTTP
@@ -47,6 +49,7 @@ VIRTUAL(bool) ISteamInventory_GetItemsByID(
VIRTUAL(bool) ISteamInventory_SerializeResult(
PARAMS(SteamInventoryResult_t, void*, uint32_t*)
) noexcept;
VIRTUAL(bool) ISteamInventory_GetItemDefinitionIDs(PARAMS(SteamItemDef_t*, uint32_t*)) noexcept;
VIRTUAL(bool) ISteamInventory_CheckResultSteamID(PARAMS(SteamInventoryResult_t, CSteamID)) noexcept;

View File

@@ -1,14 +1,21 @@
#include <mutex>
#include <koalabox/hook.hpp>
#include <koalabox/logger.hpp>
#include "smoke_api/steamclient/steamclient.hpp"
#include "smoke_api/types.hpp"
#include "steam_api/steam_client.hpp"
/**
* SmokeAPI implementation
*/
C_DECL(void*) CreateInterface(const char* interface_version, int* out_result) {
C_DECL(void*) CreateInterface(const char* interface_version, create_interface_result* out_result) {
// Mutex here helps us detect unintended recursion early on by throwing an exception.
static std::mutex section;
const std::lock_guard lock(section);
return steam_client::GetGenericInterface(
__func__,
interface_version,