From baa0f6e7e7627d50f97dca669fc46d5fb3231ab0 Mon Sep 17 00:00:00 2001 From: Sean Du Date: Sat, 20 Jun 2026 06:01:36 +0800 Subject: [PATCH] Add configurable URL hooks Signed-off-by: Sean Du --- CMakeLists.txt | 2 + Makefile | 2 + README.md | 19 +++++++- config.json.example | 4 ++ src/config.cpp | 105 +++++++++++++++++++++++++++++++++++++++++ src/config.hpp | 11 +++++ src/hook.cpp | 57 ++++++++++++++++++++++ src/il2cpp_symbols.cpp | 12 ++++- src/il2cpp_symbols.hpp | 3 ++ src/log.cpp | 16 +++++++ src/stdinclude.hpp | 5 ++ 11 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 config.json.example create mode 100644 src/config.cpp create mode 100644 src/config.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b9b6a49..632751d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(version SHARED src/proxy.cpp src/hook.cpp src/log.cpp + src/config.cpp src/il2cpp_symbols.cpp src/version.rc src/version.def @@ -25,6 +26,7 @@ target_include_directories(version PRIVATE deps/minhook/include deps/minhook/src deps/minhook/src/hde + deps/rapidjson/include ) target_compile_definitions(version PRIVATE diff --git a/Makefile b/Makefile index 0f1c505..8ab94a1 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ CXX := x86_64-w64-mingw32-g++ WINDRES := x86_64-w64-mingw32-windres INCLUDES := -Isrc -Ideps/minhook/include -Ideps/minhook/src -Ideps/minhook/src/hde +INCLUDES += -Ideps/rapidjson/include DEFINES := -DNOMINMAX -DWIN32_LEAN_AND_MEAN CFLAGS := -std=gnu11 $(DEFINES) $(INCLUDES) CXXFLAGS := -std=gnu++20 -fno-exceptions -fno-rtti $(DEFINES) $(INCLUDES) @@ -34,6 +35,7 @@ CXX_SRCS := \ src/proxy.cpp \ src/hook.cpp \ src/log.cpp \ + src/config.cpp \ src/il2cpp_symbols.cpp RC_SRCS := src/version.rc diff --git a/README.md b/README.md index b7ccf72..5719ef0 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ - 转发 `UnityPlayer.dll` 实际导入的少量 `version.dll` 导出函数 - 等待 IL2CPP 运行时初始化稳定 - 运行时 hook `Stage.NetworkUtil.GetSchemeType()`,强制返回 `0` +- 支持通过 `config.json` 覆盖 `api_url` 和 `asset_url` **目录结构** - `src/`:代理、日志、IL2CPP 符号解析、hook 逻辑 @@ -16,6 +17,7 @@ - `GameAssembly.dll` 出现时,IL2CPP runtime 可能还不能安全访问,因此 hook 线程会额外等待一小段时间 - 运行时日志会写到游戏目录下的 `cgss-http-hook.log` - 生成的 DLL 包含 Windows 版本资源,定义在 `src/version.rc` +- `config.json` 中的 URL 会自动规范化:若写了 `http://` 或 `https://` 会自动去掉,若末尾缺少 `/` 会自动补上 **构建** ```sh @@ -28,7 +30,20 @@ cmake --build build **输出** - `build/version.dll` +**config.json** +```json +{ + "api_url": "apis.game.starlight-stage.jp/", + "asset_url": "asset-starlight-stage.akamaized.net/" +} +``` + +字段说明: +- `api_url`:API 主机名,按原始字段格式填写,不带 scheme +- `asset_url`:资源主机名,按原始字段格式填写,不带 scheme + **使用方法** 1. 将 `build/version.dll` 复制到 `imascgstage.exe` 同目录 -2. 启动游戏 -3. 如果 hook 未生效,查看 `cgss-http-hook.log` +2. 如需覆盖地址,在同目录放置 `config.json` +3. 启动游戏 +4. 如果 hook 未生效,查看 `cgss-http-hook.log` diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..825dd6f --- /dev/null +++ b/config.json.example @@ -0,0 +1,4 @@ +{ + "api_url": "apis.game.starlight-stage.jp/", + "asset_url": "asset-starlight-stage.akamaized.net/" +} diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..4943b59 --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,105 @@ +#include "stdinclude.hpp" + +#include "config.hpp" + +namespace config { + namespace { + OverrideUrls g_urls; + bool g_loaded = false; + + std::string get_config_path() { + char path[MAX_PATH] = {}; + DWORD len = GetModuleFileNameA(nullptr, path, MAX_PATH); + if (len == 0 || len >= MAX_PATH) { + return {}; + } + + char* last_sep = path; + for (char* p = path; *p != '\0'; ++p) { + if (*p == '\\' || *p == '/') { + last_sep = p; + } + } + *last_sep = '\0'; + lstrcatA(path, "\\config.json"); + return path; + } + + std::string normalize_base_url(std::string value) { + if (value.empty()) { + return value; + } + + constexpr const char* kHttp = "http://"; + constexpr const char* kHttps = "https://"; + + if (value.rfind(kHttp, 0) == 0) { + value.erase(0, lstrlenA(kHttp)); + } else if (value.rfind(kHttps, 0) == 0) { + value.erase(0, lstrlenA(kHttps)); + } + + while (!value.empty() && value.front() == '/') { + value.erase(value.begin()); + } + + if (value.empty()) { + return value; + } + + if (value.back() != '/') { + value.push_back('/'); + } + + return value; + } + + void read_string(const rapidjson::Document& document, const char* key, std::string& out) { + if (!document.HasMember(key) || !document[key].IsString()) { + return; + } + out = normalize_base_url(document[key].GetString()); + } + } + + void load() { + if (g_loaded) { + return; + } + g_loaded = true; + + const auto config_path = get_config_path(); + if (config_path.empty()) { + hook_log("[cgss-http-hook] failed to resolve config.json path"); + return; + } + + std::ifstream config_stream(config_path); + if (!config_stream.is_open()) { + hook_log("[cgss-http-hook] config.json not found, using defaults"); + return; + } + + rapidjson::IStreamWrapper wrapper(config_stream); + rapidjson::Document document; + document.ParseStream(wrapper); + if (document.HasParseError() || !document.IsObject()) { + hook_log("[cgss-http-hook] failed to parse config.json"); + return; + } + + read_string(document, "api_url", g_urls.api_url); + read_string(document, "asset_url", g_urls.asset_url); + + if (!g_urls.api_url.empty()) { + hook_logf("[cgss-http-hook] normalized api_url=%s", g_urls.api_url.c_str()); + } + if (!g_urls.asset_url.empty()) { + hook_logf("[cgss-http-hook] normalized asset_url=%s", g_urls.asset_url.c_str()); + } + } + + const OverrideUrls& get() { + return g_urls; + } +} diff --git a/src/config.hpp b/src/config.hpp new file mode 100644 index 0000000..f910812 --- /dev/null +++ b/src/config.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace config { + struct OverrideUrls { + std::string api_url; + std::string asset_url; + }; + + void load(); + const OverrideUrls& get(); +} diff --git a/src/hook.cpp b/src/hook.cpp index 955f354..66eb583 100644 --- a/src/hook.cpp +++ b/src/hook.cpp @@ -1,7 +1,10 @@ #include "stdinclude.hpp" +#include "config.hpp" + namespace { using GetSchemeTypeFn = int (*)(); + using GetStringFn = void* (*)(); constexpr int kHttpSchemeType = 0; constexpr DWORD kHookPollIntervalMs = 500; constexpr DWORD kGameAssemblySettleDelayMs = 5000; @@ -9,6 +12,8 @@ namespace { volatile LONG g_hook_installed = 0; GetSchemeTypeFn g_orig_get_scheme_type = nullptr; + GetStringFn g_orig_get_application_server_url = nullptr; + GetStringFn g_orig_get_resource_server_url = nullptr; volatile LONG g_logged_gameassembly = 0; volatile LONG g_exports_state = 0; ULONGLONG g_gameassembly_seen_tick = 0; @@ -21,6 +26,22 @@ namespace { return kHttpSchemeType; } + void* get_application_server_url_hook() { + const auto& urls = config::get(); + if (!urls.api_url.empty()) { + return il2cpp_symbols::new_string(urls.api_url.c_str()); + } + return g_orig_get_application_server_url ? g_orig_get_application_server_url() : nullptr; + } + + void* get_resource_server_url_hook() { + const auto& urls = config::get(); + if (!urls.asset_url.empty()) { + return il2cpp_symbols::new_string(urls.asset_url.c_str()); + } + return g_orig_get_resource_server_url ? g_orig_get_resource_server_url() : nullptr; + } + void patch_custom_preference_scheme() { auto custom_preference = il2cpp_symbols::get_class( "Assembly-CSharp.dll", "Cute", "CustomPreference" @@ -77,6 +98,13 @@ namespace { return; } + auto get_application_server_url_addr = il2cpp_symbols::get_method_pointer( + "Assembly-CSharp.dll", "Stage", "NetworkUtil", "GetApplicationServerUrl", 0 + ); + auto get_resource_server_url_addr = il2cpp_symbols::get_method_pointer( + "Assembly-CSharp.dll", "Stage", "NetworkUtil", "GetResourceServerUrl", 0 + ); + auto mh_status = MH_Initialize(); if (mh_status != MH_OK && mh_status != MH_ERROR_ALREADY_INITIALIZED) { hook_log("[cgss-http-hook] MH_Initialize failed"); @@ -99,6 +127,34 @@ namespace { return; } + if (get_application_server_url_addr) { + auto create_status = MH_CreateHook( + reinterpret_cast(get_application_server_url_addr), + reinterpret_cast(&get_application_server_url_hook), + reinterpret_cast(&g_orig_get_application_server_url) + ); + if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { + auto enable_status = MH_EnableHook(reinterpret_cast(get_application_server_url_addr)); + if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { + hook_log("[cgss-http-hook] hooked Stage.NetworkUtil.GetApplicationServerUrl"); + } + } + } + + if (get_resource_server_url_addr) { + auto create_status = MH_CreateHook( + reinterpret_cast(get_resource_server_url_addr), + reinterpret_cast(&get_resource_server_url_hook), + reinterpret_cast(&g_orig_get_resource_server_url) + ); + if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { + auto enable_status = MH_EnableHook(reinterpret_cast(get_resource_server_url_addr)); + if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { + hook_log("[cgss-http-hook] hooked Stage.NetworkUtil.GetResourceServerUrl"); + } + } + } + InterlockedExchange(&g_hook_installed, 1); hook_log("[cgss-http-hook] hooked Stage.NetworkUtil.GetSchemeType"); } @@ -117,5 +173,6 @@ namespace { } void start_hook_thread() { + config::load(); CreateThread(nullptr, 0, init_thread, nullptr, 0, nullptr); } diff --git a/src/il2cpp_symbols.cpp b/src/il2cpp_symbols.cpp index 16f7556..1f252e9 100644 --- a/src/il2cpp_symbols.cpp +++ b/src/il2cpp_symbols.cpp @@ -10,6 +10,7 @@ il2cpp_class_get_field_from_name_t il2cpp_symbols::il2cpp_class_get_field_from_n il2cpp_method_get_name_t il2cpp_symbols::il2cpp_method_get_name = nullptr; il2cpp_thread_attach_t il2cpp_symbols::il2cpp_thread_attach = nullptr; il2cpp_thread_current_t il2cpp_symbols::il2cpp_thread_current = nullptr; +il2cpp_string_new_t il2cpp_symbols::il2cpp_string_new = nullptr; il2cpp_runtime_class_init_t il2cpp_symbols::il2cpp_runtime_class_init = nullptr; il2cpp_field_get_flags_t il2cpp_symbols::il2cpp_field_get_flags = nullptr; il2cpp_field_static_set_value_t il2cpp_symbols::il2cpp_field_static_set_value = nullptr; @@ -33,6 +34,7 @@ void il2cpp_symbols::init(HMODULE game_module) { resolve(game_module, "il2cpp_method_get_name", il2cpp_method_get_name); resolve(game_module, "il2cpp_thread_attach", il2cpp_thread_attach); resolve(game_module, "il2cpp_thread_current", il2cpp_thread_current); + resolve(game_module, "il2cpp_string_new", il2cpp_string_new); resolve(game_module, "il2cpp_runtime_class_init", il2cpp_runtime_class_init); resolve(game_module, "il2cpp_field_get_flags", il2cpp_field_get_flags); resolve(game_module, "il2cpp_field_static_set_value", il2cpp_field_static_set_value); @@ -42,7 +44,8 @@ bool il2cpp_symbols::ready() { return il2cpp_domain_get && il2cpp_domain_assembly_open && il2cpp_assembly_get_image && il2cpp_class_from_name && il2cpp_class_get_method_from_name && il2cpp_class_get_field_from_name && il2cpp_thread_attach && - il2cpp_runtime_class_init && il2cpp_field_get_flags && il2cpp_field_static_set_value; + il2cpp_string_new && il2cpp_runtime_class_init && + il2cpp_field_get_flags && il2cpp_field_static_set_value; } bool il2cpp_symbols::domain_ready() { @@ -69,6 +72,13 @@ bool il2cpp_symbols::attach_thread() { return true; } +void* il2cpp_symbols::new_string(const char* value) { + if (!value || !attach_thread() || !il2cpp_string_new) { + return nullptr; + } + return il2cpp_string_new(value); +} + void* il2cpp_symbols::get_class(const char* assembly_name, const char* namespaze, const char* klass_name) { if (!attach_thread()) { hook_log("[cgss-http-hook] il2cpp domain not ready"); diff --git a/src/il2cpp_symbols.hpp b/src/il2cpp_symbols.hpp index 62dbefc..3ad6129 100644 --- a/src/il2cpp_symbols.hpp +++ b/src/il2cpp_symbols.hpp @@ -15,6 +15,7 @@ typedef FieldInfo* (*il2cpp_class_get_field_from_name_t)(void* klass, const char typedef const char* (*il2cpp_method_get_name_t)(MethodInfo* method); typedef void (*il2cpp_thread_attach_t)(void* domain); typedef void* (*il2cpp_thread_current_t)(); +typedef void* (*il2cpp_string_new_t)(const char* str); typedef void (*il2cpp_runtime_class_init_t)(void* klass); typedef uint32_t (*il2cpp_field_get_flags_t)(FieldInfo* field); typedef void (*il2cpp_field_static_set_value_t)(FieldInfo* field, void* value); @@ -51,6 +52,7 @@ namespace il2cpp_symbols { extern il2cpp_method_get_name_t il2cpp_method_get_name; extern il2cpp_thread_attach_t il2cpp_thread_attach; extern il2cpp_thread_current_t il2cpp_thread_current; + extern il2cpp_string_new_t il2cpp_string_new; extern il2cpp_runtime_class_init_t il2cpp_runtime_class_init; extern il2cpp_field_get_flags_t il2cpp_field_get_flags; extern il2cpp_field_static_set_value_t il2cpp_field_static_set_value; @@ -60,6 +62,7 @@ namespace il2cpp_symbols { bool ready(); bool domain_ready(); bool attach_thread(); + void* new_string(const char* value); void* get_class(const char* assembly_name, const char* namespaze, const char* klass_name); bool set_static_int_field(void* klass, const char* field_name, int value); uintptr_t get_method_pointer(const char* assembly_name, const char* namespaze, diff --git a/src/log.cpp b/src/log.cpp index fa06662..1f840e0 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -1,5 +1,7 @@ #include "stdinclude.hpp" +#include + namespace { HANDLE g_log_file = INVALID_HANDLE_VALUE; @@ -53,3 +55,17 @@ void hook_log(const char* message) { WriteFile(log_file, message, lstrlenA(message), &ignored, nullptr); WriteFile(log_file, "\r\n", 2, &ignored, nullptr); } + +void hook_logf(const char* format, ...) { + if (!format) { + return; + } + + char buffer[1024] = {}; + va_list args; + va_start(args, format); + wvsprintfA(buffer, format, args); + va_end(args); + + hook_log(buffer); +} diff --git a/src/stdinclude.hpp b/src/stdinclude.hpp index 9dcf882..5ddd60e 100644 --- a/src/stdinclude.hpp +++ b/src/stdinclude.hpp @@ -3,9 +3,14 @@ #include #include +#include +#include #include +#include +#include #include "il2cpp_symbols.hpp" void hook_log(const char* message); +void hook_logf(const char* format, ...);