From f299573f42b8478c51f1b2552d5ac1d85288efab Mon Sep 17 00:00:00 2001 From: Sean Du Date: Wed, 8 Jul 2026 12:22:04 +0800 Subject: [PATCH] Hook ObscuredPrefs startup values Signed-off-by: Sean Du --- README.md | 19 +- config.json.example | 3 +- docs/ObscuredPrefs 加解密分析文档.md | 364 +++++++++++++++++++++++++++ src/config.cpp | 7 +- src/config.hpp | 1 + src/hook.cpp | 193 ++++++++++++++ 6 files changed, 584 insertions(+), 3 deletions(-) create mode 100644 docs/ObscuredPrefs 加解密分析文档.md diff --git a/README.md b/README.md index 0b41f41..cf3811d 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - 等待 IL2CPP 运行时初始化稳定 - 可选地 hook `Stage.NetworkUtil.GetSchemeType()`,在需要时强制切到 `http` - 支持通过 `config.json` 覆盖 `api_url` 和 `asset_url` +- 支持 hook `CodeStage.AntiCheat.ObscuredTypes.ObscuredPrefs`,直接接管启动所需的 9 个整型参数 - 自动拉起同目录下的 `cgss-borderless-helper.exe`,用外部 helper 处理 `F11` 无边框全屏 **目录结构** @@ -53,7 +54,8 @@ make package "force_http": true, "api_url": "", "asset_url": "asset-starlight-stage.akamaized.net/", - "launch_borderless_helper": true + "launch_borderless_helper": true, + "viewer_id": 589089289 } ``` @@ -62,6 +64,20 @@ make package - `api_url`:必填。API 主机名,按原始字段格式填写,不带 scheme;留空时程序会弹窗提示 - `asset_url`:资源主机名,按原始字段格式填写,不带 scheme;留空时回落到 `asset-starlight-stage.akamaized.net/` - `launch_borderless_helper`:是否自动启动 `cgss-borderless-helper.exe`。默认 `true` +- `viewer_id`:启动参数接管里使用的 `VIEWER_ID`。默认 `589089289` + +当前版本会在运行时直接接管以下 `ObscuredPrefs` 整型键,不再依赖预先写注册表: +- `VIEWER_ID` +- `TUTORIAL_STEP` +- `TUTORIAL_LOADED_FROM_SERVER_FLAG` +- `TUTORIAL_IS_DIRTY_LOCAL_DATA` +- `POLICY_PRE_ANNOUNCE_DATE` +- `POLICY_ANNOUNCE_DATE` +- `BN_CONTENT_RUN` +- `BN_CONTENT_AGREE_ANALYSIS` +- `BN_CONTENT_AGREE_ADVERTISEMENT` + +其中只有 `VIEWER_ID` 通过 `config.json` 配置,其余值按当前已验证样本固定写入。 **使用方法** 1. 将 `build/version.dll`、`build/cgss-borderless-helper.exe` 和 `build/libwinpthread-1.dll` 一起复制到 `imascgstage.exe` 同目录 @@ -76,6 +92,7 @@ make package - `version.dll` 只在 `imascgstage.exe` 主进程中继续启动 hook 和 helper;被其他进程间接加载时会跳过。 - 不要只单独分发 `version.dll`。当前构建依赖 `libwinpthread-1.dll`,如果目标机器缺少它,Windows 会在进入代理代码前直接拒绝加载,此时通常表现为游戏无法启动,且不会生成任何日志。 - `make` 和 `make package` 都会自动把运行所需文件整理到 `build/package/cgss-dmm-hook/`,并生成带版本号的压缩包,例如 `build/package/cgss-dmm-hook-v2.1.zip`。 +- `ObscuredPrefs` 注册表键值对的加解密分析文档已随仓库提供,见 [ObscuredPrefs 加解密分析文档.md](docs/ObscuredPrefs 加解密分析文档.md)。 **致谢** - 本项目的 `version.dll` 代理注入思路与运行时 IL2CPP hook 方向,受 `gkms-localify-dmm` 项目启发。 diff --git a/config.json.example b/config.json.example index c803f9b..0763158 100644 --- a/config.json.example +++ b/config.json.example @@ -2,5 +2,6 @@ "force_http": true, "api_url": "", "asset_url": "asset-starlight-stage.akamaized.net/", - "launch_borderless_helper": true + "launch_borderless_helper": true, + "viewer_id": 589089289 } diff --git a/docs/ObscuredPrefs 加解密分析文档.md b/docs/ObscuredPrefs 加解密分析文档.md new file mode 100644 index 0000000..c69151c --- /dev/null +++ b/docs/ObscuredPrefs 加解密分析文档.md @@ -0,0 +1,364 @@ +# ObscuredPrefs 加解密分析文档 + +## 项目背景 + +**游戏名称:** 偶像大师灰姑娘女孩星光舞台 (`imascgstage`) +**平台:** Windows / Android +**目标:** 结合 Windows 注册表样本与 Android `dump.cs` / `libil2cpp.so`,还原可实际复现的注册表键值加解密实现。 + +--- + +## 一、最终结论 + +目前已经可以完整实现当前 Windows 注册表样本对应的 `ObscuredPrefs` 加解密。 + +已确认结论如下: + +1. 游戏使用 `CodeStage.AntiCheat.ObscuredTypes.ObscuredPrefs`。 +2. `ObscuredPrefs.encryptionKey = "e806f6"`。 +3. 注册表键名主干的算法是: + - `plainKey` UTF-8 + - 与 `e806f6` 循环 XOR + - Base64 编码 +4. 注册表键名的 `_h1234567890` 后缀**不属于 ACTk 本体**。 + - 它与 Unity Windows `PlayerPrefs` 的注册表 value name 后缀行为一致。 + - 对当前 9 个样本,后缀都能被 `djb2_xor(Base64主干)` 精确复现。 +5. 注册表值的实际布局是: + - `payload` + - `DataType` + - `VERSION(=2)` + - `deviceLockLevel` + - `checksum` + - 如果 `deviceLockLevel != 0`,则 `payload` 后、类型字节前还会多 4 字节 `deviceIDHash` +6. 当前样本的 `payload` 加密 key 不是单独的 `e806f6`,而是: + - `plainKey + "e806f6"` +7. 当前样本的 `checksum` 是: + - `xxHash32(明文 payload bytes, seed = 0)` + - 4 字节小端序存储 +8. 以上规则已经能**逐字节复现 9 个 Windows 注册表样本**。 + +--- + +## 二、证据来源 + +### 2.1 Android dump + +- `dump.cs`: `/home/aya/workspace/cgss/data/android/dump.cs` +- `libil2cpp.so`: `/home/aya/workspace/cgss/data/android/libil2cpp.so` +- `script.json`: `/home/aya/workspace/cgss/data/android/script.json` + +### 2.2 关键方法 + +`dump.cs` / `script.json` 可确认以下方法存在: + +- `ObscuredPrefs.EncryptKey` +- `ObscuredPrefs.GetEncryptedPrefsString` +- `ObscuredPrefs.EncryptData` +- `ObscuredPrefs.DecryptData` +- `ObscuredPrefs.CalculateChecksum` +- `ObscuredPrefs.EncryptDecryptBytes` +- `xxHash.CalculateHash` + +### 2.3 关键字段 + +`dump.cs` 中 `ObscuredPrefs` 静态字段布局如下: + +```csharp +private static string encryptionKey; // 0x0 +private static bool foreignSavesReported; // 0x8 +private static string deviceID; // 0x10 +private static uint deviceIDHash; // 0x18 +public static Action onAlterationDetected; // 0x20 +public static bool preservePlayerPrefs; // 0x28 +public static Action onPossibleForeignSavesDetected; // 0x30 +public static ObscuredPrefs.DeviceLockLevel lockToDevice; // 0x38 +public static bool readForeignSaves; // 0x39 +public static bool emergencyMode; // 0x3A +private static string deprecatedDeviceID; // 0x40 +``` + +这直接解释了 `DecryptData` 中对 `0x38 / 0x39 / 0x3A` 的读取含义。 + +--- + +## 三、键名算法 + +### 3.1 ACTk 部分 + +`ObscuredPrefs.EncryptKey` 在 Android `libil2cpp.so` 中的行为已经确认: + +1. 读取 `ObscuredPrefs.encryptionKey` +2. 调用 `ObscuredString.EncryptDecrypt(key, encryptionKey)` +3. 对结果调用 `Convert.ToBase64String` +4. 返回 Base64 字符串 + +也就是说,**ACTk 本体只负责生成 Base64 主干**,不负责 `_h...` 后缀。 + +### 3.2 Windows 注册表后缀 + +当前 Windows 样本的最终键名是: + +```text +_h +``` + +对 9 个样本全部验证后,后缀满足: + +```text +hash = djb2_xor(base64_stem) +``` + +其中: + +```text +init = 5381 +for each byte: + hash = ((hash << 5) + hash) ^ byte +``` + +32 位无符号溢出。 + +例如 `VIEWER_ID`: + +```text +plainKey = VIEWER_ID +XOR key = e806f6 +XOR 后 = 33 71 75 61 23 64 3a 71 74 +Base64 = M3F1YSNkOnF0 +djb2_xor(Base64) = 4073495316 +最终键名 = M3F1YSNkOnF0_h4073495316 +``` + +### 3.3 关于 `_h...` 来源的判断 + +这部分并非从 Android ACTk 代码直接读出,而是结合以下事实作出的结论: + +1. `EncryptKey()` 本体不拼接 `_h...` +2. 当前 9 个样本都与 `djb2_xor(Base64主干)` 完全一致 +3. `ObscuredPrefs.GetEncryptedPrefsString()` / `SetInt()` 最终调用的是 Unity `PlayerPrefs` + +因此可判断: + +```text +_h...` 后缀是 Windows PlayerPrefs 注册表 value name 规则,而不是 ACTk 自己定义的附加校验段。 +``` + +--- + +## 四、值算法 + +### 4.1 值结构 + +`EncryptData` / `DecryptData` 结合反汇编可以还原出当前版本结构: + +当 `deviceLockLevel == 0` 时: + +```text +payload | dataType | version | deviceLock | checksum +``` + +当 `deviceLockLevel != 0` 时: + +```text +payload | deviceIDHash | dataType | version | deviceLock | checksum +``` + +然后整体做 Base64,最终以 ASCII 写入注册表,再补一个结尾 `00` 字节。 + +### 4.2 payload 的 XOR key + +这是值加密实现里的关键点。 + +`EncryptData` 在进入 `EncryptDecryptBytes` 之前,会先做: + +```text +String.Concat(plainKey, encryptionKey) +``` + +再把这个结果作为 XOR key。 + +因此真实规则不是: + +```text +payload ^= "e806f6" +``` + +而是: + +```text +payload ^= (plainKey + "e806f6") +``` + +### 4.3 checksum + +`DecryptData` 的校验流程可确认: + +1. 从尾部取 4 字节 checksum +2. 取出 payload +3. 先把 payload XOR 解密成明文 bytes +4. 对**明文 bytes**做 `xxHash.CalculateHash(payload, len, 0)` +5. 与尾部 checksum 比较 + +因此当前版本的 checksum 为: + +```text +xxHash32(plainPayloadBytes, seed = 0) +``` + +并按小端序写入尾部。 + +### 4.4 `CalculateChecksum(string)` 的实际作用 + +Android dump 中确实存在: + +```csharp +private static uint CalculateChecksum(string input); +``` + +但它不是当前 value trailer 的生成函数。 + +从调用关系看,它用于: + +- `DeviceIDHash` +- `SetNewCryptoKey` +- 若干设备锁相关逻辑 + +而不是 `EncryptData` / `DecryptData` 里这 4 字节 payload checksum。 + +因此,当前 value 尾部 checksum 应直接归为 `xxHash32(plainPayloadBytes)`,而不是 `CalculateChecksum(string)` 的结果。 + +--- + +## 五、样本验证结果 + +### 5.1 当前 9 个样本可完全复现 + +按以下规则: + +1. key stem = `Base64(XOR(plainKey, "e806f6"))` +2. key suffix = `"_h" + djb2_xor(key stem)` +3. int payload = `struct.pack(" 2025-07-25T14:11:26Z +``` + +这些结果与字段语义能够直接对应,可作为当前样本的 payload 明文解释。 + +--- + +## 六、关于 ObscuredInt 的关系 + +已确认: + +- `ObscuredInt.cryptoKey = 444444` +- `ObscuredInt.Encrypt/Decrypt(value, key) = value ^ key` + +但就**当前 Windows 注册表格式实现**来说,不需要再额外引入 `ObscuredInt`。 + +原因是: + +1. 当前样本的 payload 明文已经能直接解释为合理业务值 +2. 现有样本的写回复现不需要额外 `^ 444444` +3. 我们已经能逐字节复现游戏样本 + +因此,对“如何完整实现当前注册表加解密”这个目标来说,答案已经足够闭合。 + +`ObscuredInt` 更像是业务对象在内存中的字段包装,而不是当前注册表 payload 必须再做的一层存储编码。 + +--- + +## 七、仓库中的实现 + +当前实现文件: + +- `obscured_prefs.py` + +相关辅助脚本: + +- `decrypt_all.py` +- `decrypt_tool.py` +- `encrypt_tool.py` +- `verify_roundtrip.py` +- `analyze_registry.py` +- `analyze_values.py` + +这些脚本都基于本文确认的同一套规则实现。 + +--- + +## 八、可直接使用的实现摘要 + +### 8.1 键名 + +```text +stem = Base64(XOR(plainKey, "e806f6")) +registryKey = stem + "_h" + djb2_xor(stem) +``` + +### 8.2 Int 值 + +```text +plainBytes = little_endian_int32(value) +xorKey = plainKey + "e806f6" +payload = XOR(plainBytes, xorKey) +checksum = xxHash32(plainBytes) +raw = payload + [0x05, 0x02, 0x00] + little_endian_uint32(checksum) +registryValue = hex(ASCII(Base64(raw))) + "00" +``` + +### 8.3 解密 + +```text +raw = Base64Decode(ASCII(hexBytesWithoutTrailing00)) +payload = raw[:-7] +type = raw[-7] +version = raw[-6] +deviceLock = raw[-5] +checksum = little_endian_uint32(raw[-4:]) +plainBytes = XOR(payload, plainKey + "e806f6") +verify xxHash32(plainBytes) == checksum +``` + +--- + +## 九、结论 + +这次分析后,以下问题都已经闭合: + +1. 键名主干如何生成 +2. `_h...` 后缀如何生成 +3. 值的真实布局 +4. payload 的真实 XOR key +5. checksum 的真实算法 +6. 现有 9 个 Windows 样本如何逐字节复现 + +因此,针对当前游戏 Windows 端注册表样本,已经可以认为: + +```text +ObscuredPrefs 加解密实现已完整还原。 +``` diff --git a/src/config.cpp b/src/config.cpp index 67c4775..6f80b2e 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -64,7 +64,8 @@ namespace config { " \"force_http\": true,\r\n" " \"api_url\": \"\",\r\n" " \"asset_url\": \"asset-starlight-stage.akamaized.net/\",\r\n" - " \"launch_borderless_helper\": true\r\n" + " \"launch_borderless_helper\": true,\r\n" + " \"viewer_id\": 589089289\r\n" "}\r\n"; } @@ -120,12 +121,16 @@ namespace config { read_string(document, "api_url", g_urls.api_url); read_string(document, "asset_url", g_urls.asset_url); read_bool(document, "launch_borderless_helper", g_urls.launch_borderless_helper); + if (document.HasMember("viewer_id") && document["viewer_id"].IsInt()) { + g_urls.viewer_id = document["viewer_id"].GetInt(); + } hook_logf("[cgss-dmm-hook] force_http=%s", g_urls.force_http ? "true" : "false"); hook_logf( "[cgss-dmm-hook] launch_borderless_helper=%s", g_urls.launch_borderless_helper ? "true" : "false" ); + hook_logf("[cgss-dmm-hook] viewer_id=%d", g_urls.viewer_id); if (!g_urls.api_url.empty()) { hook_logf("[cgss-dmm-hook] normalized api_url=%s", g_urls.api_url.c_str()); diff --git a/src/config.hpp b/src/config.hpp index 4921435..8a482c2 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -6,6 +6,7 @@ namespace config { std::string api_url; std::string asset_url; bool launch_borderless_helper = true; + int viewer_id = 589089289; }; bool load(); diff --git a/src/hook.cpp b/src/hook.cpp index 0e87f42..40760bd 100644 --- a/src/hook.cpp +++ b/src/hook.cpp @@ -8,10 +8,14 @@ namespace { using GetStringFn = void* (*)(const MethodInfo*); using SetStringFn = void (*)(void*, const MethodInfo*); using SetSchemeModeFn = void (*)(int, const MethodInfo*); + using ObscuredPrefsGetIntFn = int (*)(void*, int, const MethodInfo*); + using ObscuredPrefsHasKeyFn = bool (*)(void*, const MethodInfo*); + using ObscuredPrefsSetIntFn = void (*)(void*, int, const MethodInfo*); constexpr int kHttpSchemeType = 0; constexpr DWORD kHookPollIntervalMs = 500; constexpr DWORD kGameAssemblySettleDelayMs = 100; constexpr int kHookPollMaxAttempts = 600; + constexpr LONG kPrefsHookLogLimit = 32; volatile LONG g_hook_installed = 0; GetSchemeTypeFn g_orig_get_scheme_type = nullptr; @@ -28,6 +32,9 @@ namespace { SetStringFn g_orig_custom_preference_set_tele_scope_server_url = nullptr; SetStringFn g_orig_custom_preference_set_concert_server_url = nullptr; SetStringFn g_orig_custom_preference_set_resource_server_url = nullptr; + ObscuredPrefsGetIntFn g_orig_obscured_prefs_get_int = nullptr; + ObscuredPrefsHasKeyFn g_orig_obscured_prefs_has_key = nullptr; + ObscuredPrefsSetIntFn g_orig_obscured_prefs_set_int = nullptr; volatile LONG g_logged_gameassembly = 0; volatile LONG g_exports_state = 0; ULONGLONG g_gameassembly_seen_tick = 0; @@ -45,6 +52,92 @@ namespace { volatile LONG g_cp_set_tele_scope_hook_called = 0; volatile LONG g_cp_set_concert_hook_called = 0; volatile LONG g_cp_set_resource_hook_called = 0; + volatile LONG g_obscured_get_int_hook_calls = 0; + volatile LONG g_obscured_has_key_hook_calls = 0; + volatile LONG g_obscured_set_int_hook_calls = 0; + + struct ObscuredIntOverride { + const char* key; + int value; + }; + + constexpr ObscuredIntOverride kObscuredIntOverrides[] = { + {"TUTORIAL_STEP", 1000}, + {"TUTORIAL_LOADED_FROM_SERVER_FLAG", 0}, + {"TUTORIAL_IS_DIRTY_LOCAL_DATA", 0}, + {"POLICY_PRE_ANNOUNCE_DATE", 1753452686}, + {"POLICY_ANNOUNCE_DATE", 1753452686}, + {"BN_CONTENT_RUN", 2}, + {"BN_CONTENT_AGREE_ANALYSIS", 1}, + {"BN_CONTENT_AGREE_ADVERTISEMENT", 1}, + }; + + const char* il2cpp_string_chars(void* string_object) { + if (!string_object) { + return nullptr; + } + auto* raw = reinterpret_cast(string_object); + return reinterpret_cast(raw + 16); + } + + std::string il2cpp_string_to_utf8(void* string_object) { + if (!string_object) { + return {}; + } + + struct Il2CppStringLayout { + void* klass; + void* monitor; + int32_t length; + wchar_t chars[1]; + }; + + auto* string_value = reinterpret_cast(string_object); + if (string_value->length <= 0) { + return {}; + } + + int size = WideCharToMultiByte( + CP_UTF8, + 0, + string_value->chars, + string_value->length, + nullptr, + 0, + nullptr, + nullptr + ); + if (size <= 0) { + return {}; + } + + std::string result(static_cast(size), '\0'); + WideCharToMultiByte( + CP_UTF8, + 0, + string_value->chars, + string_value->length, + result.data(), + size, + nullptr, + nullptr + ); + return result; + } + + bool lookup_obscured_int_override(const std::string& key, int& out_value) { + if (key == "VIEWER_ID") { + out_value = config::get().viewer_id; + return true; + } + for (const auto& entry : kObscuredIntOverrides) { + if (key == entry.key) { + out_value = entry.value; + return true; + } + } + return false; + } void* make_api_url_string() { const auto& urls = config::get(); @@ -216,6 +309,67 @@ namespace { } } + int obscured_prefs_get_int_hook(void* key, int default_value, const MethodInfo* method) { + auto key_string = il2cpp_string_to_utf8(key); + int override_value = 0; + bool matched = lookup_obscured_int_override(key_string, override_value); + LONG count = InterlockedIncrement(&g_obscured_get_int_hook_calls); + if (count <= kPrefsHookLogLimit) { + hook_logf( + "[cgss-dmm-hook] ObscuredPrefs.GetInt hook invoked key=%s matched=%s value=%d default=%d count=%ld", + key_string.empty() ? "(null)" : key_string.c_str(), + matched ? "true" : "false", + matched ? override_value : default_value, + default_value, + count + ); + } + if (matched) { + return override_value; + } + return g_orig_obscured_prefs_get_int ? g_orig_obscured_prefs_get_int(key, default_value, method) + : default_value; + } + + bool obscured_prefs_has_key_hook(void* key, const MethodInfo* method) { + auto key_string = il2cpp_string_to_utf8(key); + int override_value = 0; + bool matched = lookup_obscured_int_override(key_string, override_value); + LONG count = InterlockedIncrement(&g_obscured_has_key_hook_calls); + if (count <= kPrefsHookLogLimit) { + hook_logf( + "[cgss-dmm-hook] ObscuredPrefs.HasKey hook invoked key=%s matched=%s count=%ld", + key_string.empty() ? "(null)" : key_string.c_str(), + matched ? "true" : "false", + count + ); + } + if (matched) { + return true; + } + return g_orig_obscured_prefs_has_key ? g_orig_obscured_prefs_has_key(key, method) : false; + } + + void obscured_prefs_set_int_hook(void* key, int value, const MethodInfo* method) { + auto key_string = il2cpp_string_to_utf8(key); + int override_value = 0; + bool matched = lookup_obscured_int_override(key_string, override_value); + LONG count = InterlockedIncrement(&g_obscured_set_int_hook_calls); + if (count <= kPrefsHookLogLimit) { + hook_logf( + "[cgss-dmm-hook] ObscuredPrefs.SetInt hook invoked key=%s matched=%s incoming=%d stored=%d count=%ld", + key_string.empty() ? "(null)" : key_string.c_str(), + matched ? "true" : "false", + value, + matched ? override_value : value, + count + ); + } + if (g_orig_obscured_prefs_set_int) { + g_orig_obscured_prefs_set_int(key, matched ? override_value : value, method); + } + } + void patch_custom_preference_scheme(bool log_success = true) { if (!config::get().force_http) { return; @@ -403,6 +557,27 @@ namespace { auto cp_set_resource_server_url_addr = il2cpp_symbols::get_method_pointer( "Assembly-CSharp.dll", "Cute", "CustomPreference", "SetResourceServerURL", 1 ); + auto obscured_prefs_get_int_addr = il2cpp_symbols::get_method_pointer( + "Assembly-CSharp.dll", + "CodeStage.AntiCheat.ObscuredTypes", + "ObscuredPrefs", + "GetInt", + 2 + ); + auto obscured_prefs_has_key_addr = il2cpp_symbols::get_method_pointer( + "Assembly-CSharp.dll", + "CodeStage.AntiCheat.ObscuredTypes", + "ObscuredPrefs", + "HasKey", + 1 + ); + auto obscured_prefs_set_int_addr = il2cpp_symbols::get_method_pointer( + "Assembly-CSharp.dll", + "CodeStage.AntiCheat.ObscuredTypes", + "ObscuredPrefs", + "SetInt", + 2 + ); auto mh_status = MH_Initialize(); if (mh_status != MH_OK && mh_status != MH_ERROR_ALREADY_INITIALIZED) { @@ -530,6 +705,24 @@ namespace { "Cute.CustomPreference.SetResourceServerURL" ); } + hook_method( + obscured_prefs_get_int_addr, + reinterpret_cast(&obscured_prefs_get_int_hook), + reinterpret_cast(&g_orig_obscured_prefs_get_int), + "CodeStage.AntiCheat.ObscuredTypes.ObscuredPrefs.GetInt" + ); + hook_method( + obscured_prefs_has_key_addr, + reinterpret_cast(&obscured_prefs_has_key_hook), + reinterpret_cast(&g_orig_obscured_prefs_has_key), + "CodeStage.AntiCheat.ObscuredTypes.ObscuredPrefs.HasKey" + ); + hook_method( + obscured_prefs_set_int_addr, + reinterpret_cast(&obscured_prefs_set_int_hook), + reinterpret_cast(&g_orig_obscured_prefs_set_int), + "CodeStage.AntiCheat.ObscuredTypes.ObscuredPrefs.SetInt" + ); InterlockedExchange(&g_hook_installed, 1); if (urls.force_http) {