Hook ObscuredPrefs startup values
Signed-off-by: Sean Du <do4suki@gmail.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
- 等待 IL2CPP 运行时初始化稳定
|
- 等待 IL2CPP 运行时初始化稳定
|
||||||
- 可选地 hook `Stage.NetworkUtil.GetSchemeType()`,在需要时强制切到 `http`
|
- 可选地 hook `Stage.NetworkUtil.GetSchemeType()`,在需要时强制切到 `http`
|
||||||
- 支持通过 `config.json` 覆盖 `api_url` 和 `asset_url`
|
- 支持通过 `config.json` 覆盖 `api_url` 和 `asset_url`
|
||||||
|
- 支持 hook `CodeStage.AntiCheat.ObscuredTypes.ObscuredPrefs`,直接接管启动所需的 9 个整型参数
|
||||||
- 自动拉起同目录下的 `cgss-borderless-helper.exe`,用外部 helper 处理 `F11` 无边框全屏
|
- 自动拉起同目录下的 `cgss-borderless-helper.exe`,用外部 helper 处理 `F11` 无边框全屏
|
||||||
|
|
||||||
**目录结构**
|
**目录结构**
|
||||||
@@ -53,7 +54,8 @@ make package
|
|||||||
"force_http": true,
|
"force_http": true,
|
||||||
"api_url": "",
|
"api_url": "",
|
||||||
"asset_url": "asset-starlight-stage.akamaized.net/",
|
"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;留空时程序会弹窗提示
|
- `api_url`:必填。API 主机名,按原始字段格式填写,不带 scheme;留空时程序会弹窗提示
|
||||||
- `asset_url`:资源主机名,按原始字段格式填写,不带 scheme;留空时回落到 `asset-starlight-stage.akamaized.net/`
|
- `asset_url`:资源主机名,按原始字段格式填写,不带 scheme;留空时回落到 `asset-starlight-stage.akamaized.net/`
|
||||||
- `launch_borderless_helper`:是否自动启动 `cgss-borderless-helper.exe`。默认 `true`
|
- `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` 同目录
|
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` 只在 `imascgstage.exe` 主进程中继续启动 hook 和 helper;被其他进程间接加载时会跳过。
|
||||||
- 不要只单独分发 `version.dll`。当前构建依赖 `libwinpthread-1.dll`,如果目标机器缺少它,Windows 会在进入代理代码前直接拒绝加载,此时通常表现为游戏无法启动,且不会生成任何日志。
|
- 不要只单独分发 `version.dll`。当前构建依赖 `libwinpthread-1.dll`,如果目标机器缺少它,Windows 会在进入代理代码前直接拒绝加载,此时通常表现为游戏无法启动,且不会生成任何日志。
|
||||||
- `make` 和 `make package` 都会自动把运行所需文件整理到 `build/package/cgss-dmm-hook/`,并生成带版本号的压缩包,例如 `build/package/cgss-dmm-hook-v2.1.zip`。
|
- `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` 项目启发。
|
- 本项目的 `version.dll` 代理注入思路与运行时 IL2CPP hook 方向,受 `gkms-localify-dmm` 项目启发。
|
||||||
|
|||||||
+2
-1
@@ -2,5 +2,6 @@
|
|||||||
"force_http": true,
|
"force_http": true,
|
||||||
"api_url": "",
|
"api_url": "",
|
||||||
"asset_url": "asset-starlight-stage.akamaized.net/",
|
"asset_url": "asset-starlight-stage.akamaized.net/",
|
||||||
"launch_borderless_helper": true
|
"launch_borderless_helper": true,
|
||||||
|
"viewer_id": 589089289
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
<base64_stem>_h<decimal_hash>
|
||||||
|
```
|
||||||
|
|
||||||
|
对 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("<i", value)`
|
||||||
|
4. encrypted payload = `XOR(payload, plainKey + "e806f6")`
|
||||||
|
5. checksum = `xxHash32(payload)`
|
||||||
|
6. raw = `encryptedPayload + 05 + 02 + 00 + checksum_le`
|
||||||
|
7. value = `hex(ASCII(Base64(raw))) + 00`
|
||||||
|
|
||||||
|
可以逐字节复现全部 9 组样本。
|
||||||
|
|
||||||
|
### 5.2 样本对应的明文值
|
||||||
|
|
||||||
|
当前样本的真实 payload 明文如下:
|
||||||
|
|
||||||
|
| 键名 | 明文值 |
|
||||||
|
|------|--------|
|
||||||
|
| `VIEWER_ID` | `589089289` |
|
||||||
|
| `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` |
|
||||||
|
|
||||||
|
其中两个时间值转为 UTC 时间是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1753452686 -> 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 加解密实现已完整还原。
|
||||||
|
```
|
||||||
+6
-1
@@ -64,7 +64,8 @@ namespace config {
|
|||||||
" \"force_http\": true,\r\n"
|
" \"force_http\": true,\r\n"
|
||||||
" \"api_url\": \"\",\r\n"
|
" \"api_url\": \"\",\r\n"
|
||||||
" \"asset_url\": \"asset-starlight-stage.akamaized.net/\",\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";
|
"}\r\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,12 +121,16 @@ namespace config {
|
|||||||
read_string(document, "api_url", g_urls.api_url);
|
read_string(document, "api_url", g_urls.api_url);
|
||||||
read_string(document, "asset_url", g_urls.asset_url);
|
read_string(document, "asset_url", g_urls.asset_url);
|
||||||
read_bool(document, "launch_borderless_helper", g_urls.launch_borderless_helper);
|
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] force_http=%s", g_urls.force_http ? "true" : "false");
|
||||||
hook_logf(
|
hook_logf(
|
||||||
"[cgss-dmm-hook] launch_borderless_helper=%s",
|
"[cgss-dmm-hook] launch_borderless_helper=%s",
|
||||||
g_urls.launch_borderless_helper ? "true" : "false"
|
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()) {
|
if (!g_urls.api_url.empty()) {
|
||||||
hook_logf("[cgss-dmm-hook] normalized api_url=%s", g_urls.api_url.c_str());
|
hook_logf("[cgss-dmm-hook] normalized api_url=%s", g_urls.api_url.c_str());
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace config {
|
|||||||
std::string api_url;
|
std::string api_url;
|
||||||
std::string asset_url;
|
std::string asset_url;
|
||||||
bool launch_borderless_helper = true;
|
bool launch_borderless_helper = true;
|
||||||
|
int viewer_id = 589089289;
|
||||||
};
|
};
|
||||||
|
|
||||||
bool load();
|
bool load();
|
||||||
|
|||||||
+193
@@ -8,10 +8,14 @@ namespace {
|
|||||||
using GetStringFn = void* (*)(const MethodInfo*);
|
using GetStringFn = void* (*)(const MethodInfo*);
|
||||||
using SetStringFn = void (*)(void*, const MethodInfo*);
|
using SetStringFn = void (*)(void*, const MethodInfo*);
|
||||||
using SetSchemeModeFn = void (*)(int, 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 int kHttpSchemeType = 0;
|
||||||
constexpr DWORD kHookPollIntervalMs = 500;
|
constexpr DWORD kHookPollIntervalMs = 500;
|
||||||
constexpr DWORD kGameAssemblySettleDelayMs = 100;
|
constexpr DWORD kGameAssemblySettleDelayMs = 100;
|
||||||
constexpr int kHookPollMaxAttempts = 600;
|
constexpr int kHookPollMaxAttempts = 600;
|
||||||
|
constexpr LONG kPrefsHookLogLimit = 32;
|
||||||
|
|
||||||
volatile LONG g_hook_installed = 0;
|
volatile LONG g_hook_installed = 0;
|
||||||
GetSchemeTypeFn g_orig_get_scheme_type = nullptr;
|
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_tele_scope_server_url = nullptr;
|
||||||
SetStringFn g_orig_custom_preference_set_concert_server_url = nullptr;
|
SetStringFn g_orig_custom_preference_set_concert_server_url = nullptr;
|
||||||
SetStringFn g_orig_custom_preference_set_resource_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_logged_gameassembly = 0;
|
||||||
volatile LONG g_exports_state = 0;
|
volatile LONG g_exports_state = 0;
|
||||||
ULONGLONG g_gameassembly_seen_tick = 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_tele_scope_hook_called = 0;
|
||||||
volatile LONG g_cp_set_concert_hook_called = 0;
|
volatile LONG g_cp_set_concert_hook_called = 0;
|
||||||
volatile LONG g_cp_set_resource_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<const unsigned char*>(string_object);
|
||||||
|
return reinterpret_cast<const char*>(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<const Il2CppStringLayout*>(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_t>(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() {
|
void* make_api_url_string() {
|
||||||
const auto& urls = config::get();
|
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) {
|
void patch_custom_preference_scheme(bool log_success = true) {
|
||||||
if (!config::get().force_http) {
|
if (!config::get().force_http) {
|
||||||
return;
|
return;
|
||||||
@@ -403,6 +557,27 @@ namespace {
|
|||||||
auto cp_set_resource_server_url_addr = il2cpp_symbols::get_method_pointer(
|
auto cp_set_resource_server_url_addr = il2cpp_symbols::get_method_pointer(
|
||||||
"Assembly-CSharp.dll", "Cute", "CustomPreference", "SetResourceServerURL", 1
|
"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();
|
auto mh_status = MH_Initialize();
|
||||||
if (mh_status != MH_OK && mh_status != MH_ERROR_ALREADY_INITIALIZED) {
|
if (mh_status != MH_OK && mh_status != MH_ERROR_ALREADY_INITIALIZED) {
|
||||||
@@ -530,6 +705,24 @@ namespace {
|
|||||||
"Cute.CustomPreference.SetResourceServerURL"
|
"Cute.CustomPreference.SetResourceServerURL"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
hook_method(
|
||||||
|
obscured_prefs_get_int_addr,
|
||||||
|
reinterpret_cast<void*>(&obscured_prefs_get_int_hook),
|
||||||
|
reinterpret_cast<void**>(&g_orig_obscured_prefs_get_int),
|
||||||
|
"CodeStage.AntiCheat.ObscuredTypes.ObscuredPrefs.GetInt"
|
||||||
|
);
|
||||||
|
hook_method(
|
||||||
|
obscured_prefs_has_key_addr,
|
||||||
|
reinterpret_cast<void*>(&obscured_prefs_has_key_hook),
|
||||||
|
reinterpret_cast<void**>(&g_orig_obscured_prefs_has_key),
|
||||||
|
"CodeStage.AntiCheat.ObscuredTypes.ObscuredPrefs.HasKey"
|
||||||
|
);
|
||||||
|
hook_method(
|
||||||
|
obscured_prefs_set_int_addr,
|
||||||
|
reinterpret_cast<void*>(&obscured_prefs_set_int_hook),
|
||||||
|
reinterpret_cast<void**>(&g_orig_obscured_prefs_set_int),
|
||||||
|
"CodeStage.AntiCheat.ObscuredTypes.ObscuredPrefs.SetInt"
|
||||||
|
);
|
||||||
|
|
||||||
InterlockedExchange(&g_hook_installed, 1);
|
InterlockedExchange(&g_hook_installed, 1);
|
||||||
if (urls.force_http) {
|
if (urls.force_http) {
|
||||||
|
|||||||
Reference in New Issue
Block a user