From feef2db6ac8ad10e96cd588bb533811113f5ce9d Mon Sep 17 00:00:00 2001 From: MoeGrid Date: Fri, 31 Jul 2026 15:39:45 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E6=AC=A1=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 60 +++ CMakeLists.txt | 33 ++ README.md | 156 ++++++++ config.ini | 13 + res/gen_keys.py | 72 ++++ res/keys.json | 686 +++++++++++++++++++++++++++++++++++ src/config.h | 18 + src/driver_service.cpp | 101 ++++++ src/driver_service.h | 16 + src/fscrypt.cpp | 412 +++++++++++++++++++++ src/fscrypt.h | 82 +++++ src/icf_mount.cpp | 400 ++++++++++++++++++++ src/icf_mount.h | 34 ++ src/icf_parser.cpp | 227 ++++++++++++ src/icf_parser.h | 98 +++++ src/main.cpp | 158 ++++++++ src/utils/aes.cpp | 80 ++++ src/utils/aes.h | 18 + src/utils/bcrypt_util.cpp | 54 +++ src/utils/bcrypt_util.h | 27 ++ src/utils/crc32.cpp | 26 ++ src/utils/crc32.h | 5 + src/utils/crypto_constants.h | 16 + src/utils/files.cpp | 86 +++++ src/utils/files.h | 24 ++ src/utils/game_keys.h | 97 +++++ src/utils/log.cpp | 62 ++++ src/utils/log.h | 37 ++ src/utils/strings.cpp | 68 ++++ src/utils/strings.h | 28 ++ src/vhd_mount.cpp | 217 +++++++++++ src/vhd_mount.h | 36 ++ 32 files changed, 3447 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 config.ini create mode 100644 res/gen_keys.py create mode 100644 res/keys.json create mode 100644 src/config.h create mode 100644 src/driver_service.cpp create mode 100644 src/driver_service.h create mode 100644 src/fscrypt.cpp create mode 100644 src/fscrypt.h create mode 100644 src/icf_mount.cpp create mode 100644 src/icf_mount.h create mode 100644 src/icf_parser.cpp create mode 100644 src/icf_parser.h create mode 100644 src/main.cpp create mode 100644 src/utils/aes.cpp create mode 100644 src/utils/aes.h create mode 100644 src/utils/bcrypt_util.cpp create mode 100644 src/utils/bcrypt_util.h create mode 100644 src/utils/crc32.cpp create mode 100644 src/utils/crc32.h create mode 100644 src/utils/crypto_constants.h create mode 100644 src/utils/files.cpp create mode 100644 src/utils/files.h create mode 100644 src/utils/game_keys.h create mode 100644 src/utils/log.cpp create mode 100644 src/utils/log.h create mode 100644 src/utils/strings.cpp create mode 100644 src/utils/strings.h create mode 100644 src/vhd_mount.cpp create mode 100644 src/vhd_mount.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d53906 --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# ===== IDE / 编辑器 ===== +.idea/ +.vscode/ +.vs/ +*.user +*.suo +*.sln.docstates + +# ===== CMake 构建 ===== +cmake-build-*/ +build/ +out/ + +# CMake 生成的文件(防止在源码根目录直接运行cmake时污染) +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +CMakeUserPresets.json +compile_commands.json + +# ===== MSVC 编译产物 ===== +*.obj +*.pdb +*.ilk +*.idb +*.exp +*.lib +*.dll +*.exe +*.manifest +*.res +*.i +*.ddi +*.modmap +*.dd + +# ===== Ninja ===== +.ninja_deps +.ninja_log +build.ninja + +# ===== 项目运行时产物 ===== +# overlay差分盘(程序运行时在exe目录生成) +overlay.vhd +# sgfscrypt驱动文件(构建时拷贝到输出目录) +sgfscrypt.sys +sgfscrypt.inf + +# ===== 项目私有目录/文件 ===== +driver/ +refer/ +overlay/ +check/ +note.txt + +# ===== Windows 系统文件 ===== +Thumbs.db +ehthumbs.db +desktop.ini +$RECYCLE.BIN/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7c23400 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.18) +project(sega_mount) + +set(CMAKE_CXX_STANDARD 20) + +add_executable(sega_mount + src/main.cpp + src/driver_service.cpp + src/fscrypt.cpp + src/icf_parser.cpp + src/icf_mount.cpp + src/vhd_mount.cpp + src/utils/aes.cpp + src/utils/bcrypt_util.cpp + src/utils/crc32.cpp + src/utils/files.cpp + src/utils/log.cpp + src/utils/strings.cpp +) + +target_include_directories(sega_mount PRIVATE src) + +if (WIN32) + target_link_libraries(sega_mount PRIVATE bcrypt virtdisk) + # 源文件含中文注释,指定UTF-8编码避免MSVC解析错误 + target_compile_options(sega_mount PRIVATE /utf-8) + # 指定Windows SDK最低版本为Win10,启用virtdisk.h等结构体定义 + target_compile_definitions(sega_mount PRIVATE + WINVER=0x0A00 + _WIN32_WINNT=0x0A00 + NTDDI_VERSION=0x0A000000 + ) +endif () diff --git a/README.md b/README.md new file mode 100644 index 0000000..05e9fc4 --- /dev/null +++ b/README.md @@ -0,0 +1,156 @@ +# sega-mount + +SEGA游戏容器挂载工具。解析 ICF 配置文件,通过 fscrypt 驱动解密挂载 APP/OPT 容器,并创建 overlay 差分盘实现可写层,最后通过重解析点将卷设备链接到指定目录。 + +## 功能 + +- 解析 ICF 文件,筛选 Active 状态的 APP/OPT 条目(同 part 自动取最新版本) +- 通过 `sgfscrypt` 驱动解密挂载 APP/OPT 容器(AES-128-CBC) +- 创建 overlay.vhd 差分盘作为可写层,挂载到指定盘符 +- 递归拷贝 overlay 目录文件到差分盘(首次创建时) +- 通过 NTFS 重解析点(Mount Point)将卷设备链接到目录 +- 支持 config.ini 覆盖密钥,空则用内置密钥表 + +## 构建 + +### 环境要求 + +- C++20(MSVC 编译器) +- CMake 3.18+ +- Windows SDK 10.0 +- CLion(推荐)或命令行构建 + +### 步骤 + +```bash +mkdir build && cd build +cmake .. -G "Visual Studio 17 2022" +cmake --build . --config Release +``` + +CLion 用户直接打开项目即可,CMakeLists.txt 已配置好 `/utf-8` 和 SDK 版本宏。 + +### 生成密钥头文件 + +内置密钥表由 `res/gen_keys.py` 从 `res/keys.json` 生成: + +```bash +python res/gen_keys.py +``` + +输出 `src/utils/game_keys.h`,包含 76 条游戏的 AES 密钥(编译期 `constexpr` 数组,无需运行时解析)。`keys.json` 更新后重新运行此脚本即可。 + +## 使用 + +### 命令 + +``` +sega_mount -m [config.ini] 挂载 +sega_mount -u [config.ini] 卸载 +``` + +不指定 config.ini 路径时,默认读取 exe 所在目录的 `config.ini`。 + +> 需要管理员权限运行(驱动服务安装需要)。 + +### config.ini 格式 + +```ini +[CONFIG] +ICF_PATH = D:\SDGB_1.55.01\amfs\ICF1 +IMAGE_DIR = D:\SDGB_1.55.01 +OVERLAY_DIR = D:\SDGB_1.55.01\overlay +MOUNT_LETTER = X +APP_LINK = C:\Mount\App +OPT_LINK = C:\Mount\Option + +[KEYS] +APP_KEY = 7CA4E6B6F3D6E8B26472973887D7FA3A +APP_IV = 53FE7135762DE3F97E7FE76B0FEF3F27 +OPT_KEY = 5C84A9E726EAA5DD351F2B0750C23697 +OPT_IV = C063BF6F562D084D7963C987F5281761 +``` + +| 字段 | 说明 | +|------|------| +| `ICF_PATH` | ICF 文件路径(必填) | +| `IMAGE_DIR` | APP/OPT 镜像文件所在目录,空则用 exe 所在目录 | +| `OVERLAY_DIR` | overlay 源目录,空或不存在则跳过拷贝 | +| `MOUNT_LETTER` | overlay 卷盘符,默认 X | +| `APP_LINK` | APP 链接基础路径,空则不创建链接 | +| `OPT_LINK` | OPT 链接基础路径,空则不创建链接 | +| `APP_KEY` / `APP_IV` | APP 容器密钥覆盖(hex),空则用内置密钥表 | +| `OPT_KEY` / `OPT_IV` | OPT 容器密钥覆盖(hex),空则用内置密钥表 | + +### LINK 输出 + +挂载成功后,链接目录结构: + +``` +C:\Mount\App\0\ → FscryptDisk_APP_0 (part 0) +C:\Mount\App\1\ → FscryptDisk_APP_1 (part 1) +C:\Mount\Option\A005\ → FscryptDisk_OPT_A005 +C:\Mount\Option\A011\ → FscryptDisk_OPT_A011 +``` + +## 挂载/卸载流程 + +### 挂载 (-m) + +1. 读取并解析 ICF 文件(AES 解密 → 结构体解析) +2. 筛选 Active APP 条目,按 part_index 排序,验证 base version 依赖链 +3. 依次挂载 APP 容器(向 fscrypt 驱动发送 MOUNT IOCTL) +4. 创建/复用 overlay.vhd 差分盘,挂载到指定盘符 + - overlay.vhd 不存在 → 创建差分盘 + 拷贝 overlay 目录文件 + - overlay.vhd 已存在 → 直接挂载(沿用数据) +5. 挂载 OPT 容器 +6. 创建 APP/OPT 链接(NTFS 重解析点) + +### 卸载 (-u) + +1. 移除 APP/OPT 链接(RemoveDirectory 移除 junction) +2. 移除 overlay 盘符 + detach overlay.vhd(保留文件) +3. 倒序卸载 APP 容器(向驱动发送 UNMOUNT IOCTL) +4. 卸载 OPT 容器 + +## 项目结构 + +``` +sega-mount/ +├── CMakeLists.txt +├── config.ini # 配置文件示例 +├── res/ +│ ├── keys.json # 游戏密钥表(源数据) +│ └── gen_keys.py # 密钥表头文件生成脚本 +├── src/ +│ ├── main.cpp # 入口:INI 解析 + 命令分发 +│ ├── config.h # Config 结构体定义 +│ ├── driver_service.cpp # sgfscrypt 驱动服务管理 +│ ├── fscrypt.cpp # 容器挂载/卸载/链接(fscrypt IOCTL) +│ ├── icf_parser.cpp # ICF 文件解析(解密 + 结构体) +│ ├── icf_mount.cpp # ICF 挂载编排(APP + overlay + OPT + LINK) +│ ├── vhd_mount.cpp # VHD 创建/挂载/卸载(virtdisk API) +│ └── utils/ +│ ├── aes.cpp # AES-128-CBC 加解密(BCrypt) +│ ├── bcrypt_util.cpp # BCrypt 辅助函数 +│ ├── crc32.cpp # CRC32 校验 +│ ├── files.cpp # 文件读取/目录拷贝/exe路径 +│ ├── log.cpp # 日志库(彩色输出 + 等级) +│ ├── strings.cpp # 字符串/路径转换工具 +│ ├── crypto_constants.h # BootId/ICF 通用密钥常量 +│ └── game_keys.h # 自动生成的游戏密钥表 +``` + +## 技术细节 + +- **ICF 解析**:ICF 文件经 AES-128-CBC 解密后,按 0x40 字节 section 依次解析 Header 和 Entry,支持 Pack/App/Opt 三种类型 +- **容器挂载**:读取容器 BootId(0x60 字节)→ AES 解密 → 解析容器元数据 → 构造 0x280 字节 IOCTL 输入缓冲 → 发送 MOUNT IOCTL +- **overlay 差分盘**:使用 `CreateVirtualDisk` 创建差分 VHD,父盘指向最后一个 APP 容器内的 `internal_N.vhd`;差分盘保留不删除,仅首次创建时拷贝 overlay 目录文件 +- **密钥查找优先级**:config.ini `[KEYS]` section 覆盖 → 内置 `game_keys::ENTRIES` 密钥表按 GameID 查找 +- **日志格式**:`[LEVEL][MODULE] message`,彩色输出,不打印敏感信息(密钥/IV) + +## 依赖 + +- `sgfscrypt.sys` 驱动(需放在 exe 同目录) +- Windows API:`virtdisk.h`(VHD)、`winioctl.h`(重解析点)、`bcrypt.h`(AES) +- 无第三方 C++ 库依赖 \ No newline at end of file diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..2fa47f2 --- /dev/null +++ b/config.ini @@ -0,0 +1,13 @@ +[CONFIG] +ICF_PATH = D:\SDGB_1.55.01\amfs\ICF1 +IMAGE_DIR = D:\SDGB_1.55.01 +OVERLAY_DIR = D:\SDGB_1.55.01\overlay +MOUNT_LETTER = X +APP_LINK = C:\Mount\App +OPT_LINK = C:\Mount\Option + +[KEYS] +APP_KEY = 7CA4E6B6F3D6E8B26472973887D7FA3A +APP_IV = 53FE7135762DE3F97E7FE76B0FEF3F27 +OPT_KEY = 5C84A9E726EAA5DD351F2B0750C23697 +OPT_IV = C063BF6F562D084D7963C987F5281761 diff --git a/res/gen_keys.py b/res/gen_keys.py new file mode 100644 index 0000000..1a2df2c --- /dev/null +++ b/res/gen_keys.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +""" +将 res/keys.json 生成为 src/utils/game_keys.h 头文件 +KEY/IV 直接生成为字节数组,C++ 端无需 hex_to_bytes 转换 + +用法(在项目根目录运行): python res/gen_keys.py +""" + +import json +import sys +from pathlib import Path + +JSON_PATH = Path("./res/keys.json") +OUTPUT_PATH = Path("./src/utils/game_keys.h") + + +def hex_to_c_array(hex_str): + """将hex字符串转为C字节数组初始化列表,如 {0xA5, 0x37, ...}""" + return "{" + ", ".join( + "0x" + hex_str[i:i + 2].upper() for i in range(0, len(hex_str), 2) + ) + "}" + + +def main(): + if not JSON_PATH.exists(): + print("Error: " + str(JSON_PATH) + " not found", file=sys.stderr) + sys.exit(1) + + with open(JSON_PATH, "r", encoding="utf-8") as f: + entries = json.load(f) + + out = [] + out.append("#pragma once") + out.append("") + out.append("#include ") + out.append("") + out.append("// 此文件由 gen_keys.py 从 res/keys.json 自动生成,请勿手动编辑") + out.append("// 源文件: res/keys.json (" + str(len(entries)) + " 条记录)") + out.append("") + out.append("namespace game_keys {") + out.append(" // 单个游戏的密钥条目(KEY/IV为16字节原始数组,无需运行时hex转换)") + out.append(" struct Entry {") + out.append(" const char *game_id;") + out.append(" const uint8_t app_key[16];") + out.append(" const uint8_t app_iv[16];") + out.append(" const uint8_t opt_key[16];") + out.append(" const uint8_t opt_iv[16];") + out.append(" };") + out.append("") + out.append(" // 所有游戏的密钥表(编译期常量,无需运行时解析)") + out.append(" inline constexpr Entry ENTRIES[] = {") + for e in entries: + line = ' {"' + e["GameID"] + '", ' + \ + hex_to_c_array(e["AppKey"]) + ", " + \ + hex_to_c_array(e["AppIV"]) + ", " + \ + hex_to_c_array(e["OptKey"]) + ", " + \ + hex_to_c_array(e["OptIV"]) + "}," + out.append(line) + out.append(" };") + out.append("}") + out.append("") + + content = "\n".join(out) + OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(OUTPUT_PATH, "w", encoding="utf-8") as f: + f.write(content) + + print("Generated " + str(OUTPUT_PATH) + " (" + str(len(entries)) + " entries)") + + +if __name__ == "__main__": + main() diff --git a/res/keys.json b/res/keys.json new file mode 100644 index 0000000..8a151da --- /dev/null +++ b/res/keys.json @@ -0,0 +1,686 @@ +[ + { + "GameID": "SBZS", + "GameKey": "A53780999F0DF49CBD8075C86D150566", + "GameIV": "E2048E0E673CD0C8F3A79E72FEAF4A62", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "2ECBCFF65CE0ABECC10547F8AC8351D8", + "AppIV": "F2AC6C2817D0574BBA113D497E319F3E" + }, + { + "GameID": "SBZT", + "GameKey": "C613529289708843B858AE80396DE7AB", + "GameIV": "76C111FD52F654CA61A3FB0758FA8309", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "9AB9CE55ED9C194A715A73A7699F795B", + "AppIV": "8552DE88FEDDA6E859369FB000F44D5B" + }, + { + "GameID": "SBZU", + "GameKey": "E6EF248B72D31DEAB42FE73906C4C9F0", + "GameIV": "0A7D94EC3DAFD9CCCF9F599CB245BDB1", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "EB1228254CDD3077EB3E441C0227BF40", + "AppIV": "3F9B4676118CEE129FE2F1CB2747BCA5" + }, + { + "GameID": "SBZV", + "GameKey": "1EB26F806696CD872DEA14262F8C7B4C", + "GameIV": "EB0A261B4DB7ED9D89FE5EB24AF7054D", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "3274A399594D84779625940B69C02D3F", + "AppIV": "675BA66D29C87923F5F154C406AFEE42" + }, + { + "GameID": "SDAP", + "GameKey": "A8FEEA8AF2950EA933392A7E7A515AC2", + "GameIV": "6C7E0E3B88CE678FB033E456E5343D72", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "41B5027C5E99D94AA9335D6D71838ECF", + "AppIV": "D195D7AD2814207115DB3531200AE3BA" + }, + { + "GameID": "SDAQ", + "GameKey": "C5DEF48491A17ABFAF14F70ACC068C4D", + "GameIV": "3E416A45DC0E442A13FC058F36DEBE89", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "C28F22BC1B339AE64180739886DC83D6", + "AppIV": "0A29FD145D72BF8DEDD436025DF0A9FC" + }, + { + "GameID": "SDAV", + "GameKey": "6F239C60B73FB2259A40ECFDC1FF541A", + "GameIV": "A5E047B8A29F27014E4D512E08954FF0", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "EED95513266A499A55E265B049169C44", + "AppIV": "84C0E5931D91A6A477D62C271546056E" + }, + { + "GameID": "SDBE", + "GameKey": "F88B7A295516361C4720B11B39C0BB89", + "GameIV": "9303368A79D66F0AB626AE02F3450BF6", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "7053FB944572E5B631A665CEF4B5BCDD", + "AppIV": "AE4D7E884002C79EB35711554D613057" + }, + { + "GameID": "SDBN", + "GameKey": "2F35C2E8024539D1A19ECE74D2FD1A97", + "GameIV": "917F9466B2B7571DC9FE2608470AADAD", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "C1F14AE2E85B095E313C8BAEC125805E", + "AppIV": "3C538EEA66251ACD5404B93F8976A7F7" + }, + { + "GameID": "SDBT", + "GameKey": "F65A75BEC7F0DD4D08A690F219AB14EF", + "GameIV": "CAE2CEE2CC88188F661893E0696BBE2A", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "A6A870671FD432EC637ADF7A822F97DA", + "AppIV": "2C277F31CD550CFA2C993B4DD56B85AE" + }, + { + "GameID": "SDBX", + "GameKey": "7CC784A2B8D4577B77FFE000C7AA4BBD", + "GameIV": "DCCC018210E7D0FE283A468FD9375D59", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "3DC19C2D0C20AC199D5FA46E7F6335A6", + "AppIV": "D8F029EC90FE55BE67584F742C55EF8B" + }, + { + "GameID": "SDBZ", + "GameKey": "A09F1E9AED8E14B3F2D285E50FA4DCBC", + "GameIV": "3282AB559319FB67A168E0803B235091", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "521BDE4460F4184EDD879136ADEEA5EE", + "AppIV": "1B8324032DB69D7B0954794AA229FE68" + }, + { + "GameID": "SDCA", + "GameKey": "48FF08937104B2A83CCB282468A5C702", + "GameIV": "D7782A8594B74BC91491EA9DBC693395", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "1649490A03D6C2AEC1C496982CB0405C", + "AppIV": "4680711C7E67A26F9230D5AF74B5DCFB" + }, + { + "GameID": "SDCD", + "GameKey": "9DA25C8104D3CD5A3161229DE221ACE9", + "GameIV": "8BC818BEF7FF3C3431BA11EDA5C5FB48", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "43B38502D8F6D3C7B02B95FC28DB5308", + "AppIV": "6DFCB94BF74F152B55F3E0C7F35B44B5" + }, + { + "GameID": "SDCF", + "GameKey": "E45390726D4747CA28086C667214CFE7", + "GameIV": "36336C65FC649205231745CE6A9CE0B8", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "DF986883DA837538E37B959A3E4117CD", + "AppIV": "DABF539738852F17714811AF70435A83" + }, + { + "GameID": "SDCH", + "GameKey": "260B3364400E701920B6DED70BC39271", + "GameIV": "5EAB7342D2D79B09FE0E00F81E325307", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "E2DA769E94F1D3ACA1930CDBE0708C9F", + "AppIV": "C7DCCE203C84AB0477236D697570DADC" + }, + { + "GameID": "SDCR", + "GameKey": "70A02D1FAC466632771C843886CE13DC", + "GameIV": "E8FFB97A988D6C845317E324A7BD4A22", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "4961A51FD36F14E72664F52373052160", + "AppIV": "25D7D1341A282C5E0A34C64562C023EC" + }, + { + "GameID": "SDCT", + "GameKey": "B751601015BAE0A26EC2CE0115C136DA", + "GameIV": "946A0D219DF2C355447517056C952F93", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "D6AE51F10EC76DA93C981800FC3AD3CB", + "AppIV": "FB8E43E280D330D06581732F2E11A6DC" + }, + { + "GameID": "SDCX", + "GameKey": "20DE66FA6791EEB961405183710B3C1D", + "GameIV": "D391E4B68D109D28A3CDA97B8BB74DAA", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "79504CCC509B67D1F7A3F593E6F9D9D6", + "AppIV": "1551EA8926F2AEE233EEC309DE3E5F3C" + }, + { + "GameID": "SDDB", + "GameKey": "1AE3DCE135807FD020D3820FCAE65B90", + "GameIV": "6ADE2B3A5A7E388F29EC9D8CECB840D0", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "875679B2CD1637962B0DB25C51FB21A6", + "AppIV": "8EF44722A0566E8F572356245687FBE5" + }, + { + "GameID": "SDDD", + "GameKey": "619410D29DF4F940177ACCD859D97E8E", + "GameIV": "16497FE160E28E601A49D16DB18F2541", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "564E967873DE6CBCD22EFECA6952E9DC", + "AppIV": "4E3DD465CF09CD82B259F7BED5FC2D6D" + }, + { + "GameID": "SDDD", + "GameKey": "6591D8D2E84B21D197753804D47C2E48", + "GameIV": "D842A5C6F75B35C7257C0EC9BA30DED1", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "75713E3A6340416598CFA9E38FBA68E4", + "AppIV": "59A14E3E13A6F8B867561EF734E1A5FF" + }, + { + "GameID": "SDDF", + "GameKey": "A6497BC4BB114A1F8F24AA756D2AF2D2", + "GameIV": "00BBACA3CDCE3DCA0174C9F26DC65120", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "65058573A0CB81749E694AE164C61B04", + "AppIV": "981C4F45E3C6958F054E5D00916BDF2B" + }, + { + "GameID": "SDDJ", + "GameKey": "32AE1BA642A3166FFD75D2DB116E8814", + "GameIV": "95972D0C401E4306D9FBF459EED36170", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "630FE52276537BD7FB267ADF175F4E99", + "AppIV": "DC5755BE57DED2CDB34433BBBA2204FF" + }, + { + "GameID": "SDDL", + "GameKey": "5A837D9EC1B3FB37F844E3ECD40ACACD", + "GameIV": "AD46FDC45AC915D55C5DCBA55A600D39", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "992458295FD06D6A8AF0DFB3F6854C19", + "AppIV": "8484906D4CD5FD225E032843ED37495D" + }, + { + "GameID": "SDDM", + "GameKey": "7A5F4E97AB1790DFF41C1CA4A061AC12", + "GameIV": "410280B2458399D7CA59283AB4AB46E1", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "0127958210F6AE9BDEB8975018B5AF24", + "AppIV": "181716BADCCFF4BC2B1E29AE02A1BBBB" + }, + { + "GameID": "SDDN", + "GameKey": "9B3B2090947A2486F0F3555C6DB98E56", + "GameIV": "D5BE04A1303C1DD9385486CF0DF68088", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "41DD8E66290117AC67D311A2F0A6416E", + "AppIV": "73E18E8418F6CEEFB11E2767FDEA190C" + }, + { + "GameID": "SDDP", + "GameKey": "E6E91C814845C68667950B52774E610F", + "GameIV": "43227E2DCD1A1A1134E4F60BDB6E1D89", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "CF6D64427EECA47674E17BCD46D1EA8C", + "AppIV": "CE5174093D26CA2A31B58541E85AC276" + }, + { + "GameID": "SDDS", + "GameKey": "5272EA6AE573FD2ED90FFBFF4E3B170C", + "GameIV": "45437CA754B19C4B9F70C4DD0331F432", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "161BEC6D90989D0E26D791170607A440", + "AppIV": "81DC26A27028E2092332038AA1BFFC47" + }, + { + "GameID": "SDDT", + "GameKey": "764B8462192DBA6655E2A0E49635A90B", + "GameIV": "9BF8267AD7E3C7B3179F5ECD661DE76A", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "3F7658728B9517D3314E684FA2E2A045", + "AppIV": "41578833C547AAFF04DB597A6E9EB784" + }, + { + "GameID": "SDDT", + "GameKey": "665ED0587A16A9AFDD46D59596663ACA", + "GameIV": "90A6F613EF5F7A3C9C932D6B7BB8BC5B", + "OptKey": "813A1158C52B82089D710AEC3B5FCC91", + "OptIV": "A12F44F9F1BE52E6994AC6E052C15965", + "AppKey": "2169343646A3248619755B68E6EF2C2C", + "AppIV": "6982406E69C9EC2574EDC5253FC6A324" + }, + { + "GameID": "SDDU", + "GameKey": "8D31FD5E238CD55BD2C69418F2A57A21", + "GameIV": "E9C935BBFB3257816402064FA484F55F", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "649AE9982625F90C55AF86713C55D3FD", + "AppIV": "187116FC4647A7D3B6F2303A34F0A2FE" + }, + { + "GameID": "SDDW", + "GameKey": "C1F88053CCF85D67CB8556D9A0CA7CC3", + "GameIV": "085CA00674C1C4EB142FCF0A3495856B", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "118565D344F3E14CA69299EEAC049BB9", + "AppIV": "9D6D392EC35ED94EF9FE0A5BE0573981" + }, + { + "GameID": "SDDX", + "GameKey": "E2D4514CB65BF10EC75C8F916D215E08", + "GameIV": "9C1923F45F7A48ED812A2C9F8DE0BE13", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "428BFF0F9E7AAFC169A7A75751FFDA98", + "AppIV": "F8250594F425332C6D349D7EA0E86669" + }, + { + "GameID": "SDEA", + "GameKey": "BBFEF63A99E6ED7E8B188765F9A59B36", + "GameIV": "9FA9E78A402E5E52994DC31B95967791", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "9F9CF148AC3C50AAF925AF1DFB27F58B", + "AppIV": "4D8EBBD971896B8A4A3DD84A23B329FC" + }, + { + "GameID": "SDEB", + "GameKey": "DFD69032CEA0AAB606EB2C4A409F2D35", + "GameIV": "F55F915DC3608ABB127C5D0CF7826AC9", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "D511ED690415F6359843A134FD47836A", + "AppIV": "AC139B382ACDD112E31564EA7F38186C" + }, + { + "GameID": "SDEC", + "GameKey": "02AF292B025A67EE82BED12E8898BF34", + "GameIV": "4B143B304693B5238BAAF8FC5A6E5C01", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "F272E5016863AF2BA0337F50DE686F6E", + "AppIV": "5327E132631E7F71B61BE7CC0DF382CE" + }, + { + "GameID": "SDED", + "GameKey": "2687C32336142426FD927613D0925133", + "GameIV": "A1CAE504C8C5E08C04D992EDBC5A4E3A", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "21FCEC779A16769F5277A36FB542992C", + "AppIV": "22B50239F1B40CCC3E55A2D69C69B160" + }, + { + "GameID": "SDED", + "GameKey": "D92F3AEE4C1B486D8A9F321C56B8FA10", + "GameIV": "5B7E9C20A1F6432EB3C6FF9EA1143D98", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "DE78B3F3D3358F40D552CBF58921A499", + "AppIV": "20F333CC757462842656B430C7823434" + }, + { + "GameID": "SDEE", + "GameKey": "3A72741FF61D178AFB79FF1BB160728F", + "GameIV": "2DA1CD60559BC9F3450AFD13F220A49E", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "191EB7440672DAB08DDBB7195EFB356F", + "AppIV": "C278B5386DC38BD76D71DBCD826954CF" + }, + { + "GameID": "SDEG", + "GameKey": "8F14C90D89EC333DF010F9942CDC5676", + "GameIV": "E1F1BC99B8E3BA5F63322363DB7C6D51", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "721853DBE2D30BAFE24F0EDBD210DEEB", + "AppIV": "4DFB0BCEC86159AAB297166BCD509E6F" + }, + { + "GameID": "SDEJ", + "GameKey": "F4A506F8906C19C4639110EA0D84ABFE", + "GameIV": "5E1F6C4A1188EECBB758787DF1FDD2D9", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "9DE1EA6AE38D9011F55D8EE864395D24", + "AppIV": "F60CDE21982876D12D17662A48D90836" + }, + { + "GameID": "SDEM", + "GameKey": "464B92E7D9E50CE6D92C9E370D5FDF2B", + "GameIV": "5076349FDD5839D0C94E6271D1B7E2FB", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "700617F293696C07FB9F356D3B99240D", + "AppIV": "667D026D6CDF329FF351DBAF7098E81D" + }, + { + "GameID": "SDEP", + "GameKey": "ABDCD0D2E165F26C4CAEB58CEE0734B4", + "GameIV": "CEA3E44F35FD6C3D1D74B78BE7384782", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "FA2B7CA53A823C152D940972CBF532F5", + "AppIV": "F4AF35120C48617704BB5B8471797A62" + }, + { + "GameID": "SDER", + "GameKey": "F0903BC3FF83434CC45893290257A7F7", + "GameIV": "B8151112A3E81CA7049EAE11A36F7362", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "7D73367EBB218EC82930D58DC6D7950B", + "AppIV": "9788C3ECA2DB6BA92BAC4F6F7B706308" + }, + { + "GameID": "SDET", + "GameKey": "37416FB467F7BDBCBBFFDDF2914BCBF5", + "GameIV": "648065B8A84D7278F5FCE2F2684657D2", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "4643E7B2C3006E0264163EDC8545FB72", + "AppIV": "612BCA81EA2958FFBAC36F780F1ED688" + }, + { + "GameID": "SDEU", + "GameKey": "5B1909AD9CB17AF336D282D7D9445DF4", + "GameIV": "BA360F8C2B7F9DE16E2A7CE2CB324A0A", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "23B3E9BB47E3AC9998F6E6C1ADC4AE33", + "AppIV": "A964714CEA60688407BF554BD1C27EC2" + }, + { + "GameID": "SDEV", + "GameKey": "7FF2A3A5D06B372BB2A527BB213EEFF3", + "GameIV": "10EBB95FADB2C84AE75916D32D1E3C43", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "3C1F018D88926D98163B07A1563A4818", + "AppIV": "CA7373C9C7DFEBAC0FC24254C030E4AD" + }, + { + "GameID": "SDEZ", + "GameKey": "EB7B708E6D996ED3241F1769F82BA4F0", + "GameIV": "110BB7D93549498351E4E4A455E113EB", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "D136EBA05D40E82682E6AAD8D9E8688C", + "AppIV": "C484DEEAA0249EF46695F63694B7372F" + }, + { + "GameID": "SDFA", + "GameKey": "79F8198C9D5AC8437138594879190E66", + "GameIV": "A638FFE37A1F63B16C770BE48E1F2F69", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "8E816B4362DB24A230877885864D206D", + "AppIV": "8E5A0BA6A0A1150D47D12BDB64DEBBA7" + }, + { + "GameID": "SDFE", + "GameKey": "0261F06FD9946B01608D1582A2BAF4EE", + "GameIV": "7A1B5A6755F6C18639CCFAEF078B8728", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "F61719C371E5BCA6788C139A53091617", + "AppIV": "67D43173E343813FA2097FD32992A8E2" + }, + { + "GameID": "SDFE", + "GameKey": "FA8BFA200F2D472E3A700C2BF8548DED", + "GameIV": "F4C72A1B9BEAFECCB479FDF99420020B", + "OptKey": "813A1158C52B82089D710AEC3B5FCC91", + "OptIV": "A12F44F9F1BE52E6994AC6E052C15965", + "AppKey": "24F963D0A7CF3D65F3670977FAA5DABA", + "AppIV": "37A175838F1E5C6D105F4FB0599F607B" + }, + { + "GameID": "SDFG", + "GameKey": "5703455E6C6387B456240FFB1D37D9D5", + "GameIV": "2E6B48A1B83FB3F157F4203EF0E74FDB", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "3398FB86BFE630A14979411879861AC7", + "AppIV": "A794C49C2C7639CD80571807C17246FF" + }, + { + "GameID": "SDFL", + "GameKey": "E06C1C41A89E29724479CB3546D9BF5C", + "GameIV": "014FA325941612C624490F496854A79A", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "2449B48067B9176A6E0F9563481E97F4", + "AppIV": "616F8710454632EB4FB1D89D8C19C19A" + }, + { + "GameID": "SDFN", + "GameKey": "281D50321112A3E23B1F15FED5CCE35A", + "GameIV": "ADBAF6CC997B689715A6432A2D2B8C0B", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "29F62E22C6A9FD8BE327631C68546405", + "AppIV": "2A860976E6D98513825F291E56CFB5EE" + }, + { + "GameID": "SDFP", + "GameKey": "7AC4DC21598A9704B1BAA44BD5A61787", + "GameIV": "9F10BE20644AB29C279C2D1E0DE69B2D", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "570B87263A7CA0AA4C1388E204EE6D4B", + "AppIV": "7640886011A2300A91FAD9F36A8C4775" + }, + { + "GameID": "SDFT", + "GameKey": "F23FCA0721136FEF222343A9971C8D6C", + "GameIV": "A915572D4AC5CF70BDF3ED5F592D5619", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "92A25F388C50737E39C3C2F006645F31", + "AppIV": "A97E72F990417488CB4C67F8F0C3FB25" + }, + { + "GameID": "SDFV", + "GameKey": "41E88EF71E353A8118C265CA1C5411DE", + "GameIV": "D973F89D7E1B730FC5B69AF73047ADAB", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "FE82DB9A60295D829B95F03C2276018B", + "AppIV": "34D82772AE18174F0A181DC53399EA9C" + }, + { + "GameID": "SDGA", + "GameKey": "48E025DB167E6DDBD644470761B8F13A", + "GameIV": "77A5DAB4A96DA910769F8177B5CC8314", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "0A6610A62EF670C65B7E7B1750FFB7A1", + "AppIV": "17A2A22915F81C5896EDBBA4C412585E" + }, + { + "GameID": "SDGB", + "GameKey": "65BF2FD5B58BD9F152201493B26D22C4", + "GameIV": "496836BEFDAD86ACD968A2B00576042A", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "7CA4E6B6F3D6E8B26472973887D7FA3A", + "AppIV": "53FE7135762DE3F97E7FE76B0FEF3F27" + }, + { + "GameID": "SDGH", + "GameKey": "36D93AA95A3AF61F381B1A966B022D4A", + "GameIV": "C9B6E4E9DDE93B52981AC59C43BA405A", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "B3E30E7EABAC3767ADE13C69C9B2F22B", + "AppIV": "03DEAEA3742D69675B36CDDC8B15AC91" + }, + { + "GameID": "SDGK", + "GameKey": "9B6A779462BADCA6AB9D31EB4CAA82D3", + "GameIV": "47E4949A368E6FBEEC411AB6593BA6E1", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "9DC4A17FC39FCA5A8A358984801CAAA7", + "AppIV": "E0445B11DCFA0DAE56C85E8787E11D9B" + }, + { + "GameID": "SDGP", + "GameKey": "28CF1676E84CA7F51AEE5951EFEE1915", + "GameIV": "DCC11502A8DF74FAC4C8451DDA49B631", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "C87AB31247E7B6FF95FDD79FB91F9F37", + "AppIV": "2467AB3C031E3DC0568B7077EFD27C36" + }, + { + "GameID": "SDGQ", + "GameKey": "4BA8B06F1D06642D95C2FE3637E8AA14", + "GameIV": "3276BFD62B11A0623CF7E00D3D35A969", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "C5356DAE7B066BCE88984AEC36DEB62D", + "AppIV": "4E9F2982460E2FD907BDE15709EDFBA7" + }, + { + "GameID": "SDGS", + "GameKey": "9359E460857ADE9D8C6848FFC6DBCE12", + "GameIV": "DEE1137D3075F6342E5414EE020C8DDA", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "A5150CC5065D2C59EE2F8F332CBD29D5", + "AppIV": "84014D26696F290AD7EAD70C7549BD81" + }, + { + "GameID": "SDGT", + "GameKey": "B7317E58BA349BD5083BEDE40ED46011", + "GameIV": "3496BD50B3A8219CA683AEDF65F88012", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "9D0BBA20D1E84F2459399F5383BEEE72", + "AppIV": "5D340013FDFB2464D253093602FE4B64" + }, + { + "GameID": "SDGV", + "GameKey": "FFE2B24922A81545FFE237AD9DC8830F", + "GameIV": "E00111F7B80C776D98E0E2C02ACF6582", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "573F8CC44F10F31EC749B695EBE886A8", + "AppIV": "6BFCA86F9D208A7944CDFC25EA3CD220" + }, + { + "GameID": "SDGY", + "GameKey": "4A90AE39D573B7467584EDA2A75D57C7", + "GameIV": "4E658B8254EA74A5947052FDF8480283", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "C04B663A59055ACBDFEBC6D3DF0E6A04", + "AppIV": "76FC5F1D88605107947D0C1FF347022D" + }, + { + "GameID": "SDGZ", + "GameKey": "7B5A682F348716C1EF474336DAE0A9AE", + "GameIV": "AC00D0E835003AA83969DE5C7EB8D9FE", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "9AD74EFB208D6EE4FE5EE770331712CF", + "AppIV": "45F6E53F0EB8FAE6665B45444A61E266" + }, + { + "GameID": "SDHD", + "GameKey": "5B7C9D1CADC06352B2FB13625CA84751", + "GameIV": "3283E14746A69FDA67F2EF8F97B0049E", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "3ABD00D7A820CE862EAF474BF6C8F33E", + "AppIV": "0F1E7EEA78DA7E037E0552C2843E1B6A" + }, + { + "GameID": "SDHD", + "GameKey": "93D37F8554C2710EA50466C8A45DF3FB", + "GameIV": "3C58D00419186C3B435B767A8602437B", + "OptKey": "813A1158C52B82089D710AEC3B5FCC91", + "OptIV": "A12F44F9F1BE52E6994AC6E052C15965", + "AppKey": "81B158CBB88370A1264C99800C5DDE4D", + "AppIV": "6FD501ED31EBE11826DBAC3946E24D57" + }, + { + "GameID": "SDHH", + "GameKey": "F4D35DFC5EADD0E41F3CEC79EB769F7B", + "GameIV": "D045FD4217DA3FB06B450C653D42F831", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "FC6F887F3717C5D6713113B92FA3FB27", + "AppIV": "AD76606460DBE1E91E41BEF7AB0C1535" + }, + { + "GameID": "SDHJ", + "GameKey": "3988C8ED7CCA22C396E6CA1600C712BF", + "GameIV": "BAB62A0484C6EF1B516F04EAF9792411", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "985EA66ECB5B1F208C90E2B898F0B073", + "AppIV": "164A65422E7F01B7F1B0849FC7737CDB" + }, + { + "GameID": "SDHK", + "GameKey": "5D6062E6B084DFFB12BA6FFA47C0A4BE", + "GameIV": "106CD4D807F81A83CA9E9EDB5B641649", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "BC92D63C2A099CA2315A483C3041FDD7", + "AppIV": "B14D8449B6D4325D83A2774B13DD21FF" + }, + { + "GameID": "SDHN", + "GameKey": "A51196D719F8596B0960B9C3D7B3C8BC", + "GameIV": "BCD7287F0C5D7055BBFBD2BC213CFBBA", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "892123A26D7C03D49EDD12A80EE0C58F", + "AppIV": "76AA15A6868B8DBDF7207906354D5169" + }, + { + "GameID": "SDHR", + "GameKey": "2E7A6DBA5533FB29F8B575FDFF55AE44", + "GameIV": "8FBA8203E734CF2A884FC1C699A95379", + "OptKey": "5C84A9E726EAA5DD351F2B0750C23697", + "OptIV": "C063BF6F562D084D7963C987F5281761", + "AppKey": "1FB897CAB97C8170A6AC0A21685C58D9", + "AppIV": "F9B60F65B01E8E836A4BC20F7D39FAF5" + } +] \ No newline at end of file diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..0ed439e --- /dev/null +++ b/src/config.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +// 配置信息(从config.ini读取) +struct Config { + std::wstring icf_path; // ICF文件路径 + std::wstring image_dir; // APP/OPT镜像文件所在目录 + std::wstring overlay_dir; // overlay源目录(空或不存在则跳过拷贝) + wchar_t mount_letter = L'X'; // overlay卷盘符 + std::wstring app_link; // APP链接基础路径(空则不LINK) + std::wstring opt_link; // OPT链接基础路径(空则不LINK) + // [KEYS] section:hex字符串,空表示用内置密钥表 + std::string app_key; // APP密钥hex + std::string app_iv; // APP IV hex + std::string opt_key; // OPT密钥hex + std::string opt_iv; // OPT IV hex +}; diff --git a/src/driver_service.cpp b/src/driver_service.cpp new file mode 100644 index 0000000..cab3654 --- /dev/null +++ b/src/driver_service.cpp @@ -0,0 +1,101 @@ +#define LOG_MODULE "DRIVER" +#include "driver_service.h" +#include "utils/files.h" +#include "utils/strings.h" +#include "utils/log.h" + +#include +#include + +namespace driver { + bool ensure_running(std::string *error) { + SC_HANDLE hScm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); + if (!hScm) { + if (error) *error = "OpenSCManager failed: " + std::to_string(GetLastError()); + return false; + } + + // Try to open existing service + SC_HANDLE hSvc = OpenServiceW(hScm, SERVICE_NAME, + SERVICE_ALL_ACCESS); + + if (!hSvc) { + DWORD last = GetLastError(); + if (last != ERROR_SERVICE_DOES_NOT_EXIST) { + if (error) *error = "OpenService failed: " + std::to_string(last); + CloseServiceHandle(hScm); + return false; + } + + // Service does not exist - create it + std::wstring driver_path = utils::get_exe_dir() + L"\\" + DRIVER_FILENAME; + hSvc = CreateServiceW( + hScm, SERVICE_NAME, SERVICE_NAME, + SERVICE_ALL_ACCESS, + SERVICE_KERNEL_DRIVER, + SERVICE_DEMAND_START, + SERVICE_ERROR_NORMAL, + driver_path.c_str(), + nullptr, nullptr, nullptr, nullptr, nullptr); + + if (!hSvc) { + if (error) *error = "CreateService failed: " + std::to_string(GetLastError()); + CloseServiceHandle(hScm); + return false; + } + LOG_INFO("Service installed: {}", utils::wstr_to_str(driver_path)); + } + + // Query current state + SERVICE_STATUS status{}; + if (!QueryServiceStatus(hSvc, &status)) { + if (error) *error = "QueryServiceStatus failed: " + std::to_string(GetLastError()); + CloseServiceHandle(hSvc); + CloseServiceHandle(hScm); + return false; + } + + if (status.dwCurrentState == SERVICE_RUNNING) { + CloseServiceHandle(hSvc); + CloseServiceHandle(hScm); + return true; + } + + // Start the service + if (!StartServiceW(hSvc, 0, nullptr)) { + DWORD last = GetLastError(); + if (last != ERROR_SERVICE_ALREADY_RUNNING) { + if (error) *error = "StartService failed: " + std::to_string(last); + CloseServiceHandle(hSvc); + CloseServiceHandle(hScm); + return false; + } + } + + // Wait for it to actually enter RUNNING state + int tries = 30; + while (tries-- > 0) { + if (!QueryServiceStatus(hSvc, &status)) break; + if (status.dwCurrentState == SERVICE_RUNNING) { + LOG_INFO("Service started"); + CloseServiceHandle(hSvc); + CloseServiceHandle(hScm); + return true; + } + if (status.dwCurrentState == SERVICE_STOPPED) break; + Sleep(100); + } + + if (status.dwCurrentState != SERVICE_RUNNING) { + if (error) *error = "Service failed to reach RUNNING state"; + CloseServiceHandle(hSvc); + CloseServiceHandle(hScm); + return false; + } + + LOG_INFO("Service started"); + CloseServiceHandle(hSvc); + CloseServiceHandle(hScm); + return true; + } +} diff --git a/src/driver_service.h b/src/driver_service.h new file mode 100644 index 0000000..454827c --- /dev/null +++ b/src/driver_service.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace driver { + constexpr auto SERVICE_NAME = L"sgfscrypt"; + constexpr auto DRIVER_FILENAME = L"sgfscrypt.sys"; + + // Ensure the sgfscrypt service is installed and running. + // - If service does not exist: create it (driver path = same dir as exe), + // then start it. + // - If service exists but stopped: start it. + // - If already running: no-op. + // Returns true on success. On failure, fills `error`. + bool ensure_running(std::string *error = nullptr); +} diff --git a/src/fscrypt.cpp b/src/fscrypt.cpp new file mode 100644 index 0000000..78c2dbb --- /dev/null +++ b/src/fscrypt.cpp @@ -0,0 +1,412 @@ +#define LOG_MODULE "FSCRYPT" +#include "fscrypt.h" +#include "utils/strings.h" +#include "utils/aes.h" +#include "utils/crypto_constants.h" +#include "utils/game_keys.h" +#include "utils/log.h" +#include +#include +#include + +namespace fscrypt { + // 解析解密后的BootId,提取容器类型、数据偏移、扇区大小等信息 + bool parse_boot_id(const uint8_t *decrypted_bootid, size_t size, BootIdInfo *info, std::string *error) { + if (size < 0x38) { + if (error) *error = "Decrypted BootId is too small"; + return false; + } + info->type = static_cast(decrypted_bootid[13]); + + auto read_u64_le = [](const uint8_t *p) -> uint64_t { + uint64_t v = 0; + for (int i = 0; i < 8; i++) v |= static_cast(p[i]) << (8 * i); + return v; + }; + + info->data_offset = read_u64_le(decrypted_bootid + 32); + info->data_size = read_u64_le(decrypted_bootid + 40); + info->sector_size = read_u64_le(decrypted_bootid + 48); + info->total_sectors = info->sector_size * info->data_size; + info->encrypted_offset = (info->data_size * (info->data_offset - info->sector_size)) << 8; + return true; + } + + // 根据BootId容器类型识别容器名称和镜像类型(OS/APP/OPTION) + bool recognize_container(ContainerType type, const char **container_name, const char **image_type, + std::string *error) { + switch (static_cast(type)) { + case 0: + *container_name = "NTFS Container"; + *image_type = "OS"; + return true; + case 1: + *container_name = "NTFS Container"; + *image_type = "APP"; + return true; + case 2: + *container_name = "EXFAT Container"; + *image_type = "OPTION"; + return true; + default: + if (error) *error = std::format("Unsupported BootId container type: {}", static_cast(type)); + return false; + } + } + + // 查找AES密钥和IV:优先用config.ini override,为空则查内置密钥表 + // 规则:取文件名前4字符作为GameID,.app用AppKey/AppIV,.opt用OptKey/OptIV + bool lookup_key_iv(const std::wstring &container_path, uint8_t out_key[16], uint8_t out_iv[16], + std::string *error, const KeyOverride *override_keys) { + // 提取文件名 + std::wstring filename = container_path; + size_t slash = filename.find_last_of(L"\\/"); + if (slash != std::wstring::npos) filename = filename.substr(slash + 1); + + // GameID = 文件名前4个字符 + if (filename.size() < 4) { + if (error) *error = "Filename too short for GameID"; + return false; + } + std::string game_id; + game_id.reserve(4); + for (int i = 0; i < 4; i++) { + game_id.push_back(static_cast(filename[i])); + } + + // 判断扩展名:.app 或 .opt + bool is_app = filename.size() >= 4 && + _wcsicmp(filename.substr(filename.size() - 4).c_str(), L".app") == 0; + bool is_opt = filename.size() >= 4 && + _wcsicmp(filename.substr(filename.size() - 4).c_str(), L".opt") == 0; + if (!is_app && !is_opt) { + if (error) *error = "Filename must end with .app or .opt"; + return false; + } + + // 优先使用config.ini指定的密钥(override) + if (override_keys) { + const std::string &key_hex = is_app ? override_keys->app_key_hex : override_keys->opt_key_hex; + const std::string &iv_hex = is_app ? override_keys->app_iv_hex : override_keys->opt_iv_hex; + if (!key_hex.empty() && !iv_hex.empty()) { + if (!utils::hex_to_bytes(key_hex, out_key, 16) || + !utils::hex_to_bytes(iv_hex, out_iv, 16)) { + if (error) *error = std::format("Invalid hex in config override: {}/{}", key_hex, iv_hex); + return false; + } + return true; + } + } + + // 从编译期密钥表查找GameID,直接拷贝字节数组 + for (const auto &entry: game_keys::ENTRIES) { + if (_stricmp(entry.game_id, game_id.c_str()) != 0) continue; + + const uint8_t *key = is_app ? entry.app_key : entry.opt_key; + const uint8_t *iv = is_app ? entry.app_iv : entry.opt_iv; + memcpy(out_key, key, 16); + memcpy(out_iv, iv, 16); + return true; + } + + if (error) *error = std::format("GameID {} not found in keys table", game_id); + return false; + } + + + // 挂载容器:读取BootId、解密、识别类型,向驱动发送MOUNT IOCTL + MountResult mount_container(const std::wstring &container_path, const std::wstring &tag, + const KeyOverride *override_keys) { + MountResult result = {}; + + // Open Fscrypt driver + HANDLE hDriver = CreateFileW(L"\\\\.\\Fscrypt", GENERIC_READ | GENERIC_WRITE, + 0, nullptr, OPEN_EXISTING, 0x80, nullptr); + if (hDriver == INVALID_HANDLE_VALUE) { + result.error = std::format("Open Fscrypt Driver Failed: {}", GetLastError()); + return result; + } + + // Look up AES key and IV from embedded keys.json based on filename + uint8_t key[16]; + uint8_t app_iv[16]; + std::string err; + if (!lookup_key_iv(container_path, key, app_iv, &err, override_keys)) { + result.error = std::format("Lookup key/IV failed: {}", err); + CloseHandle(hDriver); + return result; + } + + // Read container BootId (0x60 bytes) + FILE *container_file = nullptr; + if (_wfopen_s(&container_file, container_path.c_str(), L"rb") != 0 || !container_file) { + result.error = "Open container file failed"; + CloseHandle(hDriver); + return result; + } + uint8_t bootid_encrypted[BOOTID_SIZE] = {}; + size_t bootid_read = fread(bootid_encrypted, 1, BOOTID_SIZE, container_file); + if (bootid_read != BOOTID_SIZE) { + result.error = "Read BootId failed"; + fclose(container_file); + CloseHandle(hDriver); + return result; + } + + // Decrypt BootId with hardcoded key (CBC) + err.clear(); + auto bootid_decrypted = aes::cbc_decrypt(crypto::BOOT_KEY, crypto::BOOT_IV, + bootid_encrypted, BOOTID_SIZE, &err); + if (bootid_decrypted.empty()) { + result.error = std::format("Decrypt BootId failed: {}", err); + fclose(container_file); + CloseHandle(hDriver); + return result; + } + + // Parse BootId + BootIdInfo info{}; + if (!parse_boot_id(bootid_decrypted.data(), bootid_decrypted.size(), &info, &err)) { + result.error = std::format("Parse BootId failed: {}", err); + fclose(container_file); + CloseHandle(hDriver); + return result; + } + + // Recognize container type + const char *container_name = nullptr; + const char *image_type = nullptr; + if (!recognize_container(info.type, &container_name, &image_type, &err)) { + result.error = std::format("Recognize BootId container failed: {}", err); + fclose(container_file); + CloseHandle(hDriver); + return result; + } + + LOG_INFO("{} ({})", container_name, image_type); + fclose(container_file); + + // Prepare mount IOCTL input buffer (total 0x280 = 640 bytes) + // Layout from fscrypt_link.exe sub_140005620 (Buffer is wchar_t[], offsets in bytes): + // 0x000: int32 code = 640 + // 0x004: wchar_t path[258] (NT path: \??\...) + // 0x208: uint64_t unk_208 = 0x200000 + // 0x210: uint64_t unk_210 = 0 + // 0x218: uint64_t unk_218 = 1 + // 0x220: uint8_t key[16] (AES key from key.bin / JSON) + // 0x230: uint8_t iv[16] (AES IV, calculated or from JSON) + // 0x240: wchar_t tag[32] +#pragma pack(push, 1) + struct MountInBuffer { + int code; + wchar_t path[258]; + uint64_t unk_208; + uint64_t unk_210; + uint64_t unk_218; + uint8_t key[16]; + uint8_t iv[16]; + wchar_t tag[32]; + }; +#pragma pack(pop) + static_assert(sizeof(MountInBuffer) == 0x280, "MountInBuffer must be 0x280"); + + MountInBuffer in_buf{}; + memset(&in_buf, 0, sizeof(in_buf)); + in_buf.code = 640; + + // Build NT path: \??\ + std::wstring nt_path = std::format(L"\\??\\{}", utils::normalize_path(container_path)); + wcsncpy_s(in_buf.path, nt_path.c_str(), 257); + + in_buf.unk_208 = 0x200000; + in_buf.unk_210 = 0; + in_buf.unk_218 = 1; + + // Copy AES key and IV from JSON lookup (key at 0x220, iv at 0x230) + memcpy(in_buf.key, key, 16); + memcpy(in_buf.iv, app_iv, 16); + + // Copy tag + wcsncpy_s(in_buf.tag, tag.c_str(), 31); + + // Send mount IOCTL + uint8_t out_buf[1024] = {}; + DWORD bytes_returned = 0; + if (!DeviceIoControl(hDriver, IOCTL_FSCRYPT_MOUNT, + &in_buf, sizeof(in_buf), + out_buf, sizeof(out_buf), + &bytes_returned, nullptr)) { + result.error = std::format("DeviceIoControl failed, Error = {}", GetLastError()); + CloseHandle(hDriver); + return result; + } + + // Query symlink to verify mount + std::wstring device_path = std::format(L"\\\\.\\FscryptDisk_{}", tag); + HANDLE hCheck = CreateFileW(device_path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, 0, nullptr); + if (hCheck != INVALID_HANDLE_VALUE) { + CloseHandle(hCheck); + result.success = true; + } else { + LOG_ERROR("Query Symbol Link Failed"); + result.success = false; + } + + CloseHandle(hDriver); + return result; + } + + // 卸载容器:向驱动发送UNMOUNT IOCTL + bool unmount_container(const std::wstring &tag) { + HANDLE hDriver = CreateFileW(L"\\\\.\\Fscrypt", GENERIC_READ | GENERIC_WRITE, + 0, nullptr, OPEN_EXISTING, 0x80, nullptr); + if (hDriver == INVALID_HANDLE_VALUE) { + LOG_ERROR("Open Fscrypt Driver Failed: {}", GetLastError()); + return false; + } + + struct UnmountInBuffer { + int code; + wchar_t path[258]; + }; + + UnmountInBuffer in_buf{}; + memset(&in_buf, 0, sizeof(in_buf)); + in_buf.code = 516; // 0x204 + + // Build path: \??\FscryptDisk_ (no trailing backslash in comment) + std::wstring nt_path = std::format(L"\\??\\FscryptDisk_{}\\", tag); + wcsncpy_s(in_buf.path, nt_path.c_str(), 257); + + uint8_t out_buf[1024] = {}; + DWORD bytes_returned = 0; + if (DeviceIoControl(hDriver, IOCTL_FSCRYPT_UNMOUNT, + &in_buf, sizeof(in_buf), + out_buf, sizeof(out_buf), + &bytes_returned, nullptr)) { + CloseHandle(hDriver); + return true; + } + + LOG_ERROR("UNMOUNT failed: Error = {}", GetLastError()); + CloseHandle(hDriver); + return false; + } + + // 链接容器:为已挂载的卷设备创建重解析点(目录链接) + bool link_container(const std::wstring &tag, const std::wstring &link_path) { + // Verify device exists: \\.\FscryptDisk_ + std::wstring device_path = std::format(L"\\\\.\\FscryptDisk_{}", tag); + HANDLE hCheck = CreateFileW(device_path.c_str(), 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, 0, nullptr); + if (hCheck == INVALID_HANDLE_VALUE) { + LOG_ERROR("LINK failed: Non-existent device"); + return false; + } + CloseHandle(hCheck); + + // Build source: NT path format \??\FscryptDisk_ (with trailing backslash) + std::wstring source = std::format(L"\\??\\FscryptDisk_{}\\", tag); + + // Normalize link_path: replace / with \, strip trailing backslashes + std::wstring target = link_path; + for (auto &c: target) if (c == L'/') c = L'\\'; + while (target.size() > 3 && target.back() == L'\\') target.pop_back(); + + // 创建重解析点 + // REPARSE_DATA_BUFFER结构(共16字节头部): + // 0x00: ReparseTag (4字节) + // 0x04: ReparseDataLength (2字节) + // 0x06: Reserved (2字节) + // 0x08: SubstituteNameOffset (2字节) + // 0x0A: SubstituteNameLength (2字节) + // 0x0C: PrintNameOffset (2字节) + // 0x0E: PrintNameLength (2字节) + // 0x10: PathBuffer (变长) + size_t sub_len = source.size(); + size_t print_len = target.size(); + size_t buf_size = 16 + (sub_len + 1) * 2 + (print_len + 1) * 2; + + std::vector reparse_buf(buf_size, 0); + // ReparseTag + *reinterpret_cast(reparse_buf.data()) = IO_REPARSE_TAG_MOUNT_POINT; + // ReparseDataLength + *reinterpret_cast(reparse_buf.data() + 4) = static_cast(buf_size - 8); + // Reserved + *reinterpret_cast(reparse_buf.data() + 6) = 0; + // SubstituteNameOffset + *reinterpret_cast(reparse_buf.data() + 8) = 0; + // SubstituteNameLength + *reinterpret_cast(reparse_buf.data() + 10) = static_cast(sub_len * 2); + // PrintNameOffset + *reinterpret_cast(reparse_buf.data() + 12) = static_cast((sub_len + 1) * 2); + // PrintNameLength + *reinterpret_cast(reparse_buf.data() + 14) = static_cast(print_len * 2); + + // Copy substitute name (source) + memcpy(reparse_buf.data() + 16, source.c_str(), sub_len * 2); + // null terminator + *reinterpret_cast(reparse_buf.data() + 16 + sub_len * 2) = 0; + // Copy print name (target) + memcpy(reparse_buf.data() + 16 + (sub_len + 1) * 2, target.c_str(), print_len * 2); + // null terminator + *reinterpret_cast(reparse_buf.data() + 16 + (sub_len + 1) * 2 + print_len * 2) = 0; + + // 递归创建目录及其父目录(CreateDirectoryW无法递归创建父目录) + namespace fs = std::filesystem; + std::error_code mk_ec; + fs::create_directories(link_path, mk_ec); + if (mk_ec && !fs::exists(link_path)) { + LOG_ERROR("CreateDirectory failed for {}: {} {}", utils::wstr_to_str(link_path), mk_ec.value(), + mk_ec.message()); + return false; + } + + // Open the directory + HANDLE hDir = CreateFileW(link_path.c_str(), GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (hDir == INVALID_HANDLE_VALUE) { + LOG_ERROR("CreateFileW failed for {}: {}", utils::wstr_to_str(link_path), GetLastError()); + return false; + } + + DWORD bytes_returned = 0; + bool ok = DeviceIoControl(hDir, FSCTL_SET_REPARSE_POINT, + reparse_buf.data(), static_cast(buf_size), + nullptr, 0, &bytes_returned, nullptr); + if (!ok) { + LOG_ERROR("FSCTL_SET_REPARSE_POINT failed: {}", GetLastError()); + } + CloseHandle(hDir); + + if (ok) { + LOG_INFO("Linked {} -> {}", utils::wstr_to_str(target), utils::wstr_to_str(source)); + } + return ok; + } + + // 取消链接:移除重解析点目录(junction),不影响目标卷设备 + bool unlink_container(const std::wstring &link_path) { + // 规范化路径 + std::wstring path = link_path; + for (auto &c: path) if (c == L'/') c = L'\\'; + while (path.size() > 3 && path.back() == L'\\') path.pop_back(); + + // 不存在则视为已移除 + if (GetFileAttributesW(path.c_str()) == INVALID_FILE_ATTRIBUTES) { + return true; + } + + // mount point重解析点可直接用RemoveDirectoryW移除(会删除junction而非目标内容) + if (!RemoveDirectoryW(path.c_str())) { + LOG_ERROR("RemoveDirectory failed for {}: {}", utils::wstr_to_str(path), GetLastError()); + return false; + } + LOG_INFO("Removed link {}", utils::wstr_to_str(path)); + return true; + } +} diff --git a/src/fscrypt.h b/src/fscrypt.h new file mode 100644 index 0000000..7367d52 --- /dev/null +++ b/src/fscrypt.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include + +namespace fscrypt { + constexpr size_t BOOTID_SIZE = 0x60; + + // AES密钥和IV已统一至 utils/crypto_constants.h 的 crypto::BOOT_KEY / BOOT_IV + + // IOCTL码(FSCTL_SET_REPARSE_POINT和IO_REPARSE_TAG_MOUNT_POINT + // 已在winioctl.h/winnt.h中定义) + constexpr DWORD IOCTL_FSCRYPT_MOUNT = 0x22E008; + constexpr DWORD IOCTL_FSCRYPT_UNMOUNT = 0x22E00C; + + // 容器类型 + enum class ContainerType : uint8_t { + Ntfs = 0, // NTFS容器 + NtfsApp = 1, // NTFS APP容器 + Exfat = 2 // EXFAT容器 + }; + + // BootId解析结果 + struct BootIdInfo { + ContainerType type; // 容器类型 + uint64_t data_offset; // 数据偏移 + uint64_t data_size; // 数据大小 + uint64_t sector_size; // 扇区大小 + uint64_t total_sectors; // 总扇区数 + uint64_t encrypted_offset; // 加密偏移 + const char *container_name; // 容器名称 + const char *image_type; // 镜像类型 + }; + + // 挂载结果 + struct MountResult { + bool success; + std::wstring nt_target; + std::string error; + }; + + // 解析解密后的BootId + bool parse_boot_id(const uint8_t *decrypted_bootid, size_t size, + BootIdInfo *info, std::string *error = nullptr); + + // 识别容器类型,返回容器名称和镜像类型 + bool recognize_container(ContainerType type, + const BootIdInfo &info, + const char **container_name, + const char **image_type, + std::string *error = nullptr); + + // 密钥覆盖配置(从config.ini读取,hex字符串,空表示用内置密钥表) + struct KeyOverride { + std::string app_key_hex; + std::string app_iv_hex; + std::string opt_key_hex; + std::string opt_iv_hex; + }; + + // 查找AES密钥和IV:优先用override,为空则查内置密钥表 + // 规则:取文件名前4字符作为GameID,.app用AppKey/AppIV,.opt用OptKey/OptIV + bool lookup_key_iv(const std::wstring &container_path, + uint8_t out_key[16], uint8_t out_iv[16], + std::string *error = nullptr, + const KeyOverride *override_keys = nullptr); + + // 挂载容器:读取BootId、解密、识别类型,向驱动发送MOUNT IOCTL + MountResult mount_container(const std::wstring &container_path, + const std::wstring &tag, + const KeyOverride *override_keys = nullptr); + + // 卸载容器:向驱动发送UNMOUNT IOCTL + bool unmount_container(const std::wstring &tag); + + // 链接容器:为已挂载的卷设备创建重解析点(目录链接) + bool link_container(const std::wstring &tag, const std::wstring &link_path); + + // 取消链接:移除重解析点(目录链接),不影响已挂载的卷设备 + bool unlink_container(const std::wstring &link_path); +} diff --git a/src/icf_mount.cpp b/src/icf_mount.cpp new file mode 100644 index 0000000..6af092d --- /dev/null +++ b/src/icf_mount.cpp @@ -0,0 +1,400 @@ +#define LOG_MODULE "ICF" +#include "icf_mount.h" +#include "icf_parser.h" +#include "fscrypt.h" +#include "vhd_mount.h" +#include "utils/files.h" +#include "utils/strings.h" +#include "utils/log.h" +#include +#include +#include +#include + +namespace icf_mount { + // 前向声明 + static std::vector get_active_apps(const icf::IcfFile &icf_file); + + static std::vector get_active_opts(const icf::IcfFile &icf_file); + + // 解析ICF文件并挂载所有Active状态的APP和OPT容器 + // 流程:挂载APP → 创建/挂载overlay → 挂载OPT → 创建LINK + IcfMountResult mount_icf_apps(const Config &cfg) { + IcfMountResult result{}; + result.success = false; + + // 读取ICF文件 + auto icf_data = utils::read_file(cfg.icf_path); + if (icf_data.empty()) { + result.error = "Failed to read ICF file"; + return result; + } + + // 解密并解析ICF(parse_file内部会处理解密) + std::string parse_err; + auto icf_file = icf::parse_file(icf_data, &parse_err); + if (!icf_file) { + result.error = std::format("Parse ICF failed: {}", parse_err); + return result; + } + + // 筛选Active状态的APP条目,按part_index排序 + auto app_entries = get_active_apps(*icf_file); + if (app_entries.empty()) { + result.error = "No active APP entries found in ICF"; + return result; + } + + // 验证base version依赖关系 + // part 0 没有base version + // part n (n>0) 的 base_version 应该等于 part n-1 的 version + for (size_t i = 1; i < app_entries.size(); i++) { + auto *cur = app_entries[i]; + auto *prev = app_entries[i - 1]; + if (cur->part_index != prev->part_index + 1) { + result.error = std::format("Part index not consecutive: {} -> {}", prev->part_index, cur->part_index); + return result; + } + if (cur->base_version != prev->version) { + result.error = std::format("Base version mismatch for part {}: {} != {}", cur->part_index, + cur->base_version, prev->version); + return result; + } + } + + // APP/OPT文件所在目录:优先使用image_dir,否则使用exe所在目录 + std::wstring dir = cfg.image_dir.empty() ? utils::get_exe_dir() : cfg.image_dir; + + // 构造密钥override(config.ini [KEYS] section,空则用内置密钥表) + fscrypt::KeyOverride key_override; + key_override.app_key_hex = cfg.app_key; + key_override.app_iv_hex = cfg.app_iv; + key_override.opt_key_hex = cfg.opt_key; + key_override.opt_iv_hex = cfg.opt_iv; + + // 依次挂载每个APP容器 + bool all_ok = true; + for (auto entry: app_entries) { + MountEntry mount_entry{}; + mount_entry.filename = entry->filename; + mount_entry.version = entry->version; + mount_entry.part_index = entry->part_index; + mount_entry.tag = std::format("APP_{}", entry->part_index); + mount_entry.vhd_success = false; + + std::wstring app_path = std::format(L"{}\\{}", dir, utils::to_wstring(entry->filename)); + std::wstring tag = std::format(L"APP_{}", entry->part_index); + + LOG_INFO("Mount part {}: {}", entry->part_index, entry->filename); + + auto mount_res = fscrypt::mount_container(app_path, tag, &key_override); + mount_entry.success = mount_res.success; + mount_entry.error = mount_res.error; + + if (!mount_res.success) { + LOG_ERROR("Mount part {} failed: {}", entry->part_index, mount_res.error); + all_ok = false; + } + + result.apps.push_back(mount_entry); + } + + // 所有APP容器挂载完成后,处理overlay写层: + // overlay.vhd不存在 → 创建差分盘 + 拷贝overlay目录文件 + // overlay.vhd已存在 → 直接挂载(数据已在里面,无需重新拷贝) + if (all_ok && !app_entries.empty()) { + auto *last = app_entries.back(); + std::wstring exe_dir = utils::get_exe_dir(); + std::wstring overlay_vhd = std::format(L"{}\\overlay.vhd", exe_dir); + std::wstring last_tag = std::format(L"APP_{}", last->part_index); + std::wstring parent_for_create = std::format(L"\\\\?\\FscryptDisk_{}\\internal_{}.vhd", last_tag, + last->part_index); + + bool vhd_exists = GetFileAttributesW(overlay_vhd.c_str()) != INVALID_FILE_ATTRIBUTES; + + if (!vhd_exists) { + // 创建overlay.vhd差分盘 + LOG_INFO("Creating overlay.vhd (parent: internal_{}.vhd)", last->part_index); + std::string ov_err; + if (!vhd_mount::create_diff_vhd(overlay_vhd, parent_for_create, &ov_err)) { + LOG_ERROR("Overlay create failed: {}", ov_err); + all_ok = false; + } + } else { + LOG_INFO("Reusing existing overlay.vhd"); + // 先尝试detach,处理上次未正常卸载的情况 + vhd_mount::detach_vhd(overlay_vhd, nullptr); + } + + if (all_ok) { + // 挂载overlay.vhd + vhd_mount::MountedVhdInfo vhd_info{}; + std::string attach_err; + if (!vhd_mount::attach_vhd(overlay_vhd, vhd_info, &attach_err)) { + LOG_ERROR("Overlay attach failed: {}", attach_err); + all_ok = false; + } else { + result.apps.back().vhd_success = true; + + // 分配盘符到overlay卷 + char drive = static_cast(cfg.mount_letter); + std::wstring mount_point = std::format(L"{}:\\", cfg.mount_letter); + DeleteVolumeMountPointW(mount_point.c_str()); // 清理可能残留的旧挂载点 + if (!SetVolumeMountPointW(mount_point.c_str(), vhd_info.volume_guid.c_str())) { + LOG_ERROR("Assign drive {} failed: Error = {}", drive, GetLastError()); + all_ok = false; + } else { + LOG_INFO("Overlay mounted to {}:", drive); + + // 仅新建overlay.vhd时才拷贝overlay目录文件 + // OVERLAY_DIR为空或不存在时跳过拷贝 + if (!vhd_exists) { + if (!cfg.overlay_dir.empty()) { + DWORD attr = GetFileAttributesW(cfg.overlay_dir.c_str()); + bool dir_exists = attr != INVALID_FILE_ATTRIBUTES && + attr & FILE_ATTRIBUTE_DIRECTORY; + if (dir_exists) { + std::string cp_err; + size_t copied = 0; + if (!utils::copy_dir_overwrite(cfg.overlay_dir, mount_point, + &cp_err, &copied)) { + LOG_ERROR("Overlay copy failed: {}", cp_err); + all_ok = false; + } else { + LOG_INFO("Copied {} files", copied); + } + } else { + LOG_INFO("OVERLAY_DIR not found, skip copy"); + } + } else { + LOG_INFO("OVERLAY_DIR empty, skip copy"); + } + } + } + } + } + } + + // 挂载OPT容器(独立于APP/overlay,不受其结果影响) + auto opt_entries = get_active_opts(*icf_file); + for (auto entry: opt_entries) { + MountEntry mount_entry{}; + mount_entry.filename = entry->filename; + mount_entry.version = entry->version; + mount_entry.part_index = entry->part_index; + mount_entry.tag = std::format("OPT_{}", entry->version); + mount_entry.vhd_success = false; + + std::wstring opt_path = std::format(L"{}\\{}", dir, utils::to_wstring(entry->filename)); + std::wstring opt_tag = std::format(L"OPT_{}", utils::to_wstring(entry->version)); + + LOG_INFO("Mount OPT {}: {}", entry->version, entry->filename); + + auto mount_res = fscrypt::mount_container(opt_path, opt_tag, &key_override); + mount_entry.success = mount_res.success; + mount_entry.error = mount_res.error; + + if (!mount_res.success) { + LOG_ERROR("Mount OPT {} failed: {}", entry->version, mount_res.error); + } + + result.opts.push_back(mount_entry); + } + + // 创建APP LINK(仅对挂载成功的APP) + if (!cfg.app_link.empty()) { + for (const auto &app: result.apps) { + if (!app.success) continue; + std::wstring tag = std::format(L"APP_{}", app.part_index); + std::wstring link_path = std::format(L"{}\\{}", cfg.app_link, app.part_index); + if (!fscrypt::link_container(tag, link_path)) { + LOG_ERROR("Link APP {} failed", app.part_index); + } + } + } + + // 创建OPT LINK(仅对挂载成功的OPT) + if (!cfg.opt_link.empty()) { + for (const auto &opt: result.opts) { + if (!opt.success) continue; + std::wstring tag = std::format(L"OPT_{}", utils::to_wstring(opt.version)); + std::wstring link_path = std::format(L"{}\\{}", cfg.opt_link, utils::to_wstring(opt.version)); + if (!fscrypt::link_container(tag, link_path)) { + LOG_ERROR("Link OPT {} failed", opt.version); + } + } + } + + result.success = all_ok; + if (!all_ok) { + result.error = "Some APPs failed to mount"; + } + return result; + } + + // 从ICF中获取所有Active状态的APP条目,同一part_index只保留timestamp最新的,按part_index升序 + static std::vector get_active_apps(const icf::IcfFile &icf_file) { + // 按 part_index 分组,每组保留 timestamp 最大的条目(timestamp格式为 YYYYMMDDhhmmss,定长数字串可直接比较) + std::map latest_by_part; + for (const auto &entry: icf_file.entries) { + if (entry.type == icf::EntryType::App && + static_cast(entry.status) == 0x0102) { + auto it = latest_by_part.find(entry.part_index); + if (it == latest_by_part.end() || entry.timestamp > it->second->timestamp) { + latest_by_part[entry.part_index] = &entry; + } + } + } + // map 按 key 升序,直接收集 values 即可 + std::vector app_entries; + app_entries.reserve(latest_by_part.size()); + for (const auto *entry: latest_by_part | std::views::values) { + app_entries.push_back(entry); + } + return app_entries; + } + + // 从ICF中获取所有Active状态的OPT条目,同一version只保留timestamp最新的 + static std::vector get_active_opts(const icf::IcfFile &icf_file) { + // OPT的version字段为4字符名称(如A001),按此分组去重 + std::map latest_by_name; + for (const auto &entry: icf_file.entries) { + if (entry.type == icf::EntryType::Opt && + static_cast(entry.status) == 0x0102) { + auto it = latest_by_name.find(entry.version); + if (it == latest_by_name.end() || entry.timestamp > it->second->timestamp) { + latest_by_name[entry.version] = &entry; + } + } + } + std::vector opt_entries; + opt_entries.reserve(latest_by_name.size()); + for (const auto *entry: latest_by_name | std::views::values) { + opt_entries.push_back(entry); + } + return opt_entries; + } + + // 解析ICF文件并卸载所有容器 + // 流程:移除LINK → 卸载overlay → 倒序卸载APP → 卸载OPT + IcfMountResult unmount_icf_apps(const Config &cfg) { + IcfMountResult result{}; + result.success = false; + + // 读取并解析ICF + auto icf_data = utils::read_file(cfg.icf_path); + if (icf_data.empty()) { + result.error = "Failed to read ICF file"; + return result; + } + + std::string parse_err; + auto icf_file = icf::parse_file(icf_data, &parse_err); + if (!icf_file) { + result.error = std::format("Parse ICF failed: {}", parse_err); + return result; + } + + auto app_entries = get_active_apps(*icf_file); + auto opt_entries = get_active_opts(*icf_file); + + bool all_ok = true; + + // 1) 移除APP LINK + if (!cfg.app_link.empty()) { + for (auto *entry: app_entries) { + std::wstring link_path = std::format(L"{}\\{}", cfg.app_link, entry->part_index); + fscrypt::unlink_container(link_path); + } + } + + // 2) 移除OPT LINK + if (!cfg.opt_link.empty()) { + for (auto *entry: opt_entries) { + std::wstring link_path = std::format(L"{}\\{}", cfg.opt_link, + utils::to_wstring(entry->version)); + fscrypt::unlink_container(link_path); + } + } + + // 3) overlay.vhd:移除盘符 + detach(保留文件供下次沿用) + std::wstring exe_dir = utils::get_exe_dir(); + std::wstring overlay_vhd = std::format(L"{}\\overlay.vhd", exe_dir); + { + char drive = static_cast(cfg.mount_letter); + std::wstring mount_point = std::format(L"{}:\\", cfg.mount_letter); + if (DeleteVolumeMountPointW(mount_point.c_str())) { + LOG_INFO("Removed drive letter {}", drive); + } + + WIN32_FILE_ATTRIBUTE_DATA fa; + if (GetFileAttributesExW(overlay_vhd.c_str(), GetFileExInfoStandard, &fa)) { + LOG_INFO("Detaching overlay.vhd..."); + std::string vhd_err; + if (!vhd_mount::detach_vhd(overlay_vhd, &vhd_err)) { + LOG_ERROR("Overlay detach failed: {}", vhd_err); + all_ok = false; + } else { + LOG_INFO("Detached overlay.vhd"); + } + } else { + LOG_INFO("overlay.vhd not found, skip detach"); + } + } + + // 4) 倒序卸载APP容器 + for (auto it = app_entries.rbegin(); it != app_entries.rend(); ++it) { + auto *entry = *it; + MountEntry mount_entry{}; + mount_entry.filename = entry->filename; + mount_entry.version = entry->version; + mount_entry.part_index = entry->part_index; + mount_entry.tag = std::format("APP_{}", entry->part_index); + mount_entry.vhd_success = false; + + std::wstring tag = std::format(L"APP_{}", entry->part_index); + + LOG_INFO("Unmount part {}: {}", entry->part_index, entry->filename); + + bool ok = fscrypt::unmount_container(tag); + mount_entry.success = ok; + if (!ok) { + mount_entry.error = "Unmount failed"; + LOG_ERROR("Unmount part {} failed", entry->part_index); + all_ok = false; + } + + result.apps.push_back(mount_entry); + } + + // 5) 卸载OPT容器 + for (auto *entry: opt_entries) { + MountEntry mount_entry{}; + mount_entry.filename = entry->filename; + mount_entry.version = entry->version; + mount_entry.part_index = entry->part_index; + mount_entry.tag = std::format("OPT_{}", entry->version); + mount_entry.vhd_success = false; + + std::wstring opt_tag = std::format(L"OPT_{}", utils::to_wstring(entry->version)); + + LOG_INFO("Unmount OPT {}: {}", entry->version, entry->filename); + + bool ok = fscrypt::unmount_container(opt_tag); + mount_entry.success = ok; + if (!ok) { + mount_entry.error = "Unmount failed"; + LOG_ERROR("Unmount OPT {} failed", entry->version); + all_ok = false; + } + + result.opts.push_back(mount_entry); + } + + result.success = all_ok; + if (!all_ok) { + result.error = "Some containers failed to unmount"; + } + return result; + } +} diff --git a/src/icf_mount.h b/src/icf_mount.h new file mode 100644 index 0000000..e1b0734 --- /dev/null +++ b/src/icf_mount.h @@ -0,0 +1,34 @@ +#pragma once + +#include "config.h" +#include +#include + +namespace icf_mount { + // 单个容器挂载结果(APP和OPT通用) + struct MountEntry { + std::string filename; // 容器文件名 + std::string version; // 版本号(OPT为4字符名称) + uint8_t part_index; // part编号 + std::string tag; // 挂载标签 (APP_0, OPT_A001, ...) + bool success; // 容器是否挂载成功 + std::string error; // 错误信息 + bool vhd_success; // VHD是否挂载成功(仅APP有overlay) + }; + + // ICF挂载结果 + struct IcfMountResult { + bool success; + std::string error; + std::vector apps; + std::vector opts; + }; + + // 解析ICF文件并挂载其中所有Active状态的APP和OPT容器 + // 流程:挂载APP → 创建/挂载overlay → 挂载OPT → 创建LINK + IcfMountResult mount_icf_apps(const Config &cfg); + + // 解析ICF文件并卸载所有容器 + // 流程:移除LINK → 卸载overlay → 倒序卸载APP → 卸载OPT + IcfMountResult unmount_icf_apps(const Config &cfg); +} diff --git a/src/icf_parser.cpp b/src/icf_parser.cpp new file mode 100644 index 0000000..a3b9414 --- /dev/null +++ b/src/icf_parser.cpp @@ -0,0 +1,227 @@ +#include "icf_parser.h" +#include "utils/aes.h" +#include "utils/crc32.h" +#include "utils/crypto_constants.h" +#include +#include + +namespace icf { + static std::string dec2(uint8_t val) { + return std::format("{:02}", val); + } + + static std::string dec4(uint16_t val) { + return std::format("{:04}", val); + } + + std::string decode_version(const uint8_t *ver, bool zeropad) { + uint16_t major = static_cast(ver[3]) << 8 | ver[2]; + std::string major_str = zeropad ? dec4(major) : std::to_string(major); + return std::format("{}.{}.{}", major_str, dec2(ver[1]), dec2(ver[0])); + } + + std::string decode_time(const uint8_t *vals) { + uint16_t year = static_cast(vals[1]) << 8 | vals[0]; + return std::format("{:04}{:02}{:02}{:02}{:02}{:02}", year, vals[2], vals[3], vals[4], vals[5], vals[6]); + } + + std::string encode_status(uint16_t magic) { + magic &= 0xFFFF; + switch (magic) { + case 0x0102: return ""; + case 0x0201: return "#downloading"; + case 0x0202: return "#staged"; + default: { + uint16_t be = (magic >> 8 | magic << 8) & 0xFFFF; + return std::format("#{:04x}", be); + } + } + } + + std::string sanity_check(const uint8_t *data, size_t length) { + if (!data) return "Invalid input"; + if (length < 0x40) return "Bad length"; + if (length & 0xF) return "Bad alignment"; + + uint32_t len_field = *reinterpret_cast(data + 4); + if (len_field != length) return "Bad length"; + + uint32_t crc_field = *reinterpret_cast(data + 0); + uint32_t crc_calc = crc32(data + 4, length - 4); + if (crc_calc != crc_field) return "Bad CRC"; + + uint16_t enc_sections = *reinterpret_cast(data + 0x10); + uint32_t num_sections = length / 0x40; + if (enc_sections != num_sections - 1) return "Bad section count"; + + uint32_t crc2 = 0; + for (uint32_t i = 1; i < num_sections; i++) { + const uint8_t *secdata = data + i * 0x40; + if (secdata[0] != 2 || secdata[1] != 1) continue; + crc2 ^= crc32(secdata, 0x40); + } + uint32_t crcsum2 = *reinterpret_cast(data + 0x20); + if (crcsum2 != crc2) return "Bad section CRC"; + + return ""; + } + + std::optional > decrypt(const std::vector &encrypted, + std::string *error) { + if (encrypted.size() & 0xF) { + if (error) *error = "Data length not aligned to 16 bytes"; + return std::nullopt; + } + std::string err; + auto result = aes::cbc_decrypt(crypto::BOOT_KEY, crypto::BOOT_IV, encrypted.data(), encrypted.size(), &err); + if (result.empty()) { + if (error) *error = std::format("Decryption failed: {}", err); + return std::nullopt; + } + return result; + } + + std::optional parse_file(const std::vector &encrypted_data, + std::string *error) { + IcfFile file; + + std::string dec_err; + auto decrypted = decrypt(encrypted_data, &dec_err); + if (!decrypted) { + if (error) *error = dec_err; + return std::nullopt; + } + + file.raw_data = *decrypted; + + std::string sanity_err = sanity_check(file.raw_data.data(), file.raw_data.size()); + file.sanity_error = sanity_err; + + std::memcpy(&file.header, file.raw_data.data(), sizeof(IcfHeader)); + + char game_id[5] = {}; + char plat_id[4] = {}; + std::memcpy(game_id, file.header.game_id, 4); + std::memcpy(plat_id, file.header.platform_id, 3); + std::string hdr_name = std::format("{}_{}", game_id, plat_id); + if (file.header.platform_gen) { + hdr_name += static_cast('0' + file.header.platform_gen); + } + hdr_name += ".icf"; + file.header_filename = hdr_name; + + auto num_sections = static_cast(file.raw_data.size() / 0x40); + uint32_t sysver = 0; + std::vector app_datanames; + std::vector app_timestamps; + + for (uint32_t i = 1; i < num_sections; i++) { + DecodedEntry entry; + entry.section_index = i; + + const auto *sec = reinterpret_cast(file.raw_data.data() + i * 0x40); + + entry.type = static_cast(sec->type); + entry.status = static_cast(sec->magic); + entry.part_index = sec->part_index; + entry.timestamp = decode_time(sec->timestamp); + + std::string pfx, sfx, dataname; + + switch (sec->type) { + case 0: { + sfx = "0.pack"; + pfx = std::string(file.header.platform_id, 3); + dataname = decode_version(sec->version, true); + entry.version = dataname; + if (sysver != 0) { + entry.error = "Redundant SYSTEM image"; + } + sysver = *reinterpret_cast(sec->platform_version); + break; + } + case 1: { + dataname = decode_version(sec->version); + entry.version = dataname; + app_datanames.push_back(dataname); + app_timestamps.push_back(entry.timestamp); + + if (sec->part_index) { + std::string basename = decode_version(sec->base_version); + entry.base_version = basename; + entry.base_timestamp = decode_time(sec->base_timestamp); + + bool found_base = false; + for (size_t k = 0; k < app_datanames.size(); k++) { + if (app_datanames[k] == basename) { + if (app_timestamps[k] != entry.base_timestamp) { + entry.error = "Base APP timestamp mismatch"; + } + found_base = true; + break; + } + } + if (!found_base) { + entry.error = std::format("Unable to locate base APP {}", basename); + } + sfx = std::format("{}_{}.app", sec->part_index, basename); + } else { + sfx = "0.app"; + } + + if (*reinterpret_cast(sec->platform_version) != sysver) { + if (entry.error.empty()) entry.error = "SYSTEM version mismatch"; + } + + pfx = std::string(file.header.game_id, 4); + break; + } + case 2: { + sfx = "0.opt"; + pfx = std::string(file.header.game_id, 4); + dataname = std::string(reinterpret_cast(sec->version), 4); + entry.version = dataname; + break; + } + default: { + entry.error = std::format("Unknown Type: {}", sec->type); + file.entries.push_back(entry); + continue; + } + } + + entry.filename = std::format("{}_{}_{}_{}{}", pfx, dataname, entry.timestamp, sfx, + encode_status(sec->magic)); + + file.entries.push_back(entry); + } + + return file; + } + + std::string infer_filename(const IcfFile &file) { + if (!file.sanity_error.empty()) return ""; + + std::string game_id(file.header.game_id, 4); + std::string plat_id(file.header.platform_id, 3); + std::string rom_ver; + std::string data_ver; + + for (const auto &entry: file.entries) { + if (static_cast(entry.status) != 0x0102) continue; + switch (entry.type) { + case EntryType::App: + rom_ver = entry.version; + break; + case EntryType::Opt: + if (entry.version > data_ver) data_ver = entry.version; + break; + default: + break; + } + } + + if (!data_ver.empty()) data_ver = "_" + data_ver; + return std::format("{}_{}_{}{}.icf", game_id, plat_id, rom_ver, data_ver); + } +} diff --git a/src/icf_parser.h b/src/icf_parser.h new file mode 100644 index 0000000..d701997 --- /dev/null +++ b/src/icf_parser.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include + +namespace icf { + constexpr size_t SECTION_SIZE = 0x40; + constexpr size_t HEADER_SIZE = 0x40; + +#pragma pack(push, 1) + struct IcfHeader { + uint32_t overall_crc; + uint32_t total_length; + uint8_t reserved_08[8]; + uint16_t num_sections; + uint8_t reserved_12[6]; + char game_id[4]; + char platform_id[3]; + uint8_t platform_gen; + uint32_t section_crc; + uint8_t reserved_24[28]; + }; +#pragma pack(pop) + + static_assert(sizeof(IcfHeader) == 0x40, "IcfHeader size must be 0x40"); + + enum class EntryType : uint8_t { + Pack = 0, + App = 1, + Opt = 2 + }; + + enum class EntryStatus : uint16_t { + Active = 0x0102, + Downloading = 0x0201, + Staged = 0x0202 + }; + +#pragma pack(push, 1) + struct IcfSection { + uint16_t magic; + uint8_t reserved_02[2]; + uint8_t type; + uint8_t part_index; + uint8_t type_reserved[2]; + uint8_t reserved_08[24]; + uint8_t version[4]; + uint8_t timestamp[8]; + uint8_t platform_version[4]; + uint8_t base_version[4]; + uint8_t base_timestamp[8]; + uint8_t reserved_3c[4]; + }; +#pragma pack(pop) + + static_assert(sizeof(IcfSection) == 0x40, "IcfSection size must be 0x40"); + + struct DecodedEntry { + std::string filename; + size_t section_index = 0; + EntryType type = EntryType::Pack; + EntryStatus status = EntryStatus::Active; + std::string version; + std::string timestamp; + uint8_t part_index = 0; + std::string base_version; + std::string base_timestamp; + std::string error; + }; + + struct IcfFile { + IcfHeader header{}; + std::vector raw_data; + std::vector entries; + std::string header_filename; + std::string sanity_error; + }; + + // AES密钥和IV已统一至 utils/crypto_constants.h 的 crypto::BOOT_KEY / BOOT_IV + + std::string decode_version(const uint8_t *ver, bool zeropad = false); + + std::string decode_time(const uint8_t *vals); + + std::string encode_status(uint16_t magic); + + std::optional parse_file(const std::vector &encrypted_data, + std::string *error = nullptr); + + std::optional > decrypt(const std::vector &encrypted, + std::string *error = nullptr); + + std::string sanity_check(const uint8_t *data, size_t length); + + std::string infer_filename(const IcfFile &file); +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..d95e061 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,158 @@ +#define LOG_MODULE "MAIN" +#include "config.h" +#include "driver_service.h" +#include "icf_mount.h" +#include "utils/strings.h" +#include "utils/files.h" +#include "utils/log.h" +#include +#include + +// 去除字符串首尾空白 +static std::string trim(const std::string &s) { + auto start = s.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) return ""; + auto end = s.find_last_not_of(" \t\r\n"); + return s.substr(start, end - start + 1); +} + +// 解析config.ini,将键值对填入Config +static bool parse_config(const std::wstring &path, Config &cfg, std::string *error) { + std::ifstream file(path); + if (!file.is_open()) { + if (error) *error = "Cannot open config file: " + utils::wstr_to_str(path); + return false; + } + + std::string line; + while (std::getline(file, line)) { + line = trim(line); + if (line.empty() || line[0] == ';' || line[0] == '#') continue; + if (line[0] == '[') continue; // 跳过section头 + + size_t eq = line.find('='); + if (eq == std::string::npos) continue; + + std::string key = trim(line.substr(0, eq)); + std::string val = trim(line.substr(eq + 1)); + + if (key == "ICF_PATH") { + cfg.icf_path = utils::to_wstring(val); + } else if (key == "IMAGE_DIR") { + cfg.image_dir = utils::to_wstring(val); + } else if (key == "OVERLAY_DIR") { + cfg.overlay_dir = utils::to_wstring(val); + } else if (key == "MOUNT_LETTER") { + if (!val.empty()) { + char ch = val[0]; + if (ch >= 'a' && ch <= 'z') ch -= 'a' - 'A'; + cfg.mount_letter = static_cast(ch); + } + } else if (key == "APP_LINK") { + cfg.app_link = utils::to_wstring(val); + } else if (key == "OPT_LINK") { + cfg.opt_link = utils::to_wstring(val); + } else if (key == "APP_KEY") { + cfg.app_key = val; + } else if (key == "APP_IV") { + cfg.app_iv = val; + } else if (key == "OPT_KEY") { + cfg.opt_key = val; + } else if (key == "OPT_IV") { + cfg.opt_iv = val; + } + } + return true; +} + +static void print_usage() { + LOG_INFO( + "Usage:\n" + " sega_mount -m [config.ini] 挂载\n" + " sega_mount -u [config.ini] 卸载\n" + "\n" + " config.ini默认读取exe所在目录,也可在命令行指定路径\n" + " Warn: UNMOUNT maybe unstable" + ); +} + +int main(int argc, char *argv[]) { + // 设置控制台为UTF-8代码页,配合/utf-8编译选项避免中文乱码 + SetConsoleOutputCP(CP_UTF8); + SetConsoleCP(CP_UTF8); + + if (argc < 2) { + print_usage(); + return 1; + } + + // 解析命令:-m 挂载,-u 卸载 + std::string cmd = argv[1]; + bool is_mount = cmd == "-m"; + bool is_unmount = cmd == "-u"; + if (!is_mount && !is_unmount) { + LOG_ERROR("Unknown command: {}", cmd); + print_usage(); + return 1; + } + + // 确定config.ini路径:优先命令行参数,否则使用exe所在目录 + std::wstring config_path; + if (argc >= 3) { + config_path = utils::to_wstring(argv[2]); + } else { + config_path = std::format(L"{}\\config.ini", utils::get_exe_dir()); + } + + // 解析config.ini + Config cfg; + std::string cfg_err; + if (!parse_config(config_path, cfg, &cfg_err)) { + LOG_ERROR("{}", cfg_err); + return 1; + } + + if (cfg.icf_path.empty()) { + LOG_ERROR("ICF_PATH is empty in config"); + return 1; + } + + LOG_INFO("Config loaded from {}", utils::wstr_to_str(config_path)); + LOG_INFO(" ICF_PATH: {}", utils::wstr_to_str(cfg.icf_path)); + LOG_INFO(" IMAGE_DIR: {}", utils::wstr_to_str(cfg.image_dir)); + LOG_INFO(" OVERLAY_DIR: {}", utils::wstr_to_str(cfg.overlay_dir)); + LOG_INFO(" MOUNT_LETTER: {}", static_cast(cfg.mount_letter)); + LOG_INFO(" APP_LINK: {}", utils::wstr_to_str(cfg.app_link)); + LOG_INFO(" OPT_LINK: {}", utils::wstr_to_str(cfg.opt_link)); + LOG_INFO(" APP_KEY: {}", cfg.app_key.empty() ? "(use built-in table)" : "(override)"); + LOG_INFO(" OPT_KEY: {}", cfg.opt_key.empty() ? "(use built-in table)" : "(override)"); + + // 确保sgfscrypt服务已安装并运行 + std::string svc_err; + if (!driver::ensure_running(&svc_err)) { + LOG_ERROR("Failed to start driver service: {}", svc_err); + LOG_ERROR("Hint: run this program as Administrator"); + return 1; + } + + if (is_mount) { + auto result = icf_mount::mount_icf_apps(cfg); + if (!result.success) { + LOG_ERROR("Mount failed: {}", result.error); + return 1; + } + LOG_INFO("Mount Success: {} APPs, {} OPTs mounted", + result.apps.size(), result.opts.size()); + return 0; + } + + // is_unmount + auto result = icf_mount::unmount_icf_apps(cfg); + if (!result.success) { + LOG_ERROR("Unmount failed: {}", result.error); + return 1; + } + LOG_INFO("Unmount Success: {} APPs, {} OPTs unmounted", + result.apps.size(), result.opts.size()); + return 0; +} diff --git a/src/utils/aes.cpp b/src/utils/aes.cpp new file mode 100644 index 0000000..841b5f1 --- /dev/null +++ b/src/utils/aes.cpp @@ -0,0 +1,80 @@ +#include "aes.h" +#include "bcrypt_util.h" +#include "strings.h" +#include +#include +#include +#include +#include + +namespace aes { + // AES-128-CBC加密 + std::vector cbc_encrypt(const uint8_t *key, const uint8_t *iv, + const uint8_t *input, size_t length, + std::string *error) { + std::vector output; + BCRYPT_ALG_HANDLE hAlg = nullptr; + if (!bcrypt_util::open(hAlg, BCRYPT_CHAIN_MODE_CBC, error)) return output; + + BCRYPT_KEY_HANDLE hKey = nullptr; + std::vector key_obj; + if (!bcrypt_util::generate_symmetric_key(hAlg, hKey, key_obj, + key, KEY_SIZE_128, error)) { + BCryptCloseAlgorithmProvider(hAlg, 0); + return output; + } + + output.resize(length); + std::vector iv_copy(iv, iv + BLOCK_SIZE); + ULONG cb_output = 0; + NTSTATUS st = BCryptEncrypt(hKey, bcrypt_util::to_puchar(input), + static_cast(length), nullptr, + iv_copy.data(), BLOCK_SIZE, + output.data(), static_cast(length), + &cb_output, 0); + BCryptDestroyKey(hKey); + BCryptCloseAlgorithmProvider(hAlg, 0); + if (!BCRYPT_SUCCESS(st)) { + if (error) *error = std::format("BCryptEncrypt failed: {}", utils::ntstatus_str(st)); + output.clear(); + return output; + } + output.resize(cb_output); + return output; + } + + // AES-128-CBC解密 + std::vector cbc_decrypt(const uint8_t *key, const uint8_t *iv, + const uint8_t *input, size_t length, + std::string *error) { + std::vector output; + BCRYPT_ALG_HANDLE hAlg = nullptr; + if (!bcrypt_util::open(hAlg, BCRYPT_CHAIN_MODE_CBC, error)) return output; + + BCRYPT_KEY_HANDLE hKey = nullptr; + std::vector key_obj; + if (!bcrypt_util::generate_symmetric_key(hAlg, hKey, key_obj, + key, KEY_SIZE_128, error)) { + BCryptCloseAlgorithmProvider(hAlg, 0); + return output; + } + + output.resize(length); + std::vector iv_copy(iv, iv + BLOCK_SIZE); + ULONG cb_output = 0; + NTSTATUS st = BCryptDecrypt(hKey, bcrypt_util::to_puchar(input), + static_cast(length), nullptr, + iv_copy.data(), BLOCK_SIZE, + output.data(), static_cast(length), + &cb_output, 0); + BCryptDestroyKey(hKey); + BCryptCloseAlgorithmProvider(hAlg, 0); + if (!BCRYPT_SUCCESS(st)) { + if (error) *error = std::format("BCryptDecrypt failed: {}", utils::ntstatus_str(st)); + output.clear(); + return output; + } + output.resize(cb_output); + return output; + } +} diff --git a/src/utils/aes.h b/src/utils/aes.h new file mode 100644 index 0000000..5f9fa26 --- /dev/null +++ b/src/utils/aes.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +namespace aes { + constexpr size_t BLOCK_SIZE = 16; + constexpr size_t KEY_SIZE_128 = 16; + + std::vector cbc_encrypt(const uint8_t *key, const uint8_t *iv, + const uint8_t *input, size_t length, + std::string *error = nullptr); + + std::vector cbc_decrypt(const uint8_t *key, const uint8_t *iv, + const uint8_t *input, size_t length, + std::string *error = nullptr); +} diff --git a/src/utils/bcrypt_util.cpp b/src/utils/bcrypt_util.cpp new file mode 100644 index 0000000..794c88c --- /dev/null +++ b/src/utils/bcrypt_util.cpp @@ -0,0 +1,54 @@ +#include "bcrypt_util.h" +#include "strings.h" +#include +#include + +namespace bcrypt_util { + // 打开BCrypt算法提供者并设置链模式 + bool open(BCRYPT_ALG_HANDLE &hAlg, const wchar_t *chaining, std::string *err) { + NTSTATUS st = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, nullptr, 0); + if (!BCRYPT_SUCCESS(st)) { + if (err) *err = std::format("BCryptOpenAlgorithmProvider failed: {}", utils::ntstatus_str(st)); + return false; + } + st = BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE, to_puchar(chaining), + sizeof(wchar_t) * (wcslen(chaining) + 1), 0); + if (!BCRYPT_SUCCESS(st)) { + if (err) *err = std::format("BCryptSetProperty failed: {}", utils::ntstatus_str(st)); + BCryptCloseAlgorithmProvider(hAlg, 0); + return false; + } + return true; + } + + // 获取对称密钥对象所需缓冲区大小 + bool get_object_length(BCRYPT_ALG_HANDLE hAlg, ULONG &cb_obj, std::string *err) { + ULONG cb_result = 0; + NTSTATUS st = BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, + reinterpret_cast(&cb_obj), + sizeof(cb_obj), &cb_result, 0); + if (!BCRYPT_SUCCESS(st)) { + if (err) *err = std::format("BCryptGetProperty(OBJECT_LENGTH) failed: {}", utils::ntstatus_str(st)); + return false; + } + return true; + } + + // 生成对称密钥,key_obj由调用方持有 + bool generate_symmetric_key(BCRYPT_ALG_HANDLE hAlg, + BCRYPT_KEY_HANDLE &hKey, + std::vector &key_obj, + const uint8_t *key, ULONG key_len, + std::string *err) { + ULONG cb_obj = 0; + if (!get_object_length(hAlg, cb_obj, err)) return false; + key_obj.resize(cb_obj); + NTSTATUS st = BCryptGenerateSymmetricKey(hAlg, &hKey, key_obj.data(), cb_obj, + to_puchar(key), key_len, 0); + if (!BCRYPT_SUCCESS(st)) { + if (err) *err = std::format("BCryptGenerateSymmetricKey failed: {}", utils::ntstatus_str(st)); + return false; + } + return true; + } +} diff --git a/src/utils/bcrypt_util.h b/src/utils/bcrypt_util.h new file mode 100644 index 0000000..ccf2885 --- /dev/null +++ b/src/utils/bcrypt_util.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +namespace bcrypt_util { + // 将const T*转换为PUCHAR,供BCrypt API使用(BCrypt参数声明为非const但实际不修改输入) + template + PUCHAR to_puchar(const T *p) { + return reinterpret_cast(const_cast(p)); + } + + // 打开BCrypt算法提供者并设置链模式 + bool open(BCRYPT_ALG_HANDLE &hAlg, const wchar_t *chaining, std::string *err = nullptr); + + // 获取对称密钥对象所需缓冲区大小 + bool get_object_length(BCRYPT_ALG_HANDLE hAlg, ULONG &cb_obj, std::string *err = nullptr); + + // 生成对称密钥,key_obj由调用方持有 + bool generate_symmetric_key(BCRYPT_ALG_HANDLE hAlg, + BCRYPT_KEY_HANDLE &hKey, + std::vector &key_obj, + const uint8_t *key, ULONG key_len, + std::string *err = nullptr); +} diff --git a/src/utils/crc32.cpp b/src/utils/crc32.cpp new file mode 100644 index 0000000..a31a40b --- /dev/null +++ b/src/utils/crc32.cpp @@ -0,0 +1,26 @@ +#include "crc32.h" + +static uint32_t crc32_table[256]; +static bool table_initialized = false; + +static void init_crc32_table() { + if (table_initialized) return; + for (int i = 0; i < 256; i++) { + uint32_t c = i; + for (int z = 8; z; z--) { + uint32_t mask = 0xEDB88320; + c = c & 1 ? c >> 1 ^ mask : c >> 1; + } + crc32_table[i] = c; + } + table_initialized = true; +} + +uint32_t crc32(const uint8_t *data, size_t length, uint32_t crc) { + init_crc32_table(); + crc = ~crc; + for (size_t i = 0; i < length; i++) { + crc = crc32_table[(crc ^ data[i]) & 0xff] ^ crc >> 8; + } + return ~crc; +} diff --git a/src/utils/crc32.h b/src/utils/crc32.h new file mode 100644 index 0000000..b284eef --- /dev/null +++ b/src/utils/crc32.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +uint32_t crc32(const uint8_t *data, size_t length, uint32_t crc = 0); diff --git a/src/utils/crypto_constants.h b/src/utils/crypto_constants.h new file mode 100644 index 0000000..d9cdee1 --- /dev/null +++ b/src/utils/crypto_constants.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace crypto { + // BootId / ICF 文件共用的 AES-128 密钥和 IV + constexpr uint8_t BOOT_KEY[16] = { + 0x09, 0xca, 0x5e, 0xfd, 0x30, 0xc9, 0xaa, 0xef, + 0x38, 0x04, 0xd0, 0xa7, 0xe3, 0xfa, 0x71, 0x20 + }; + + constexpr uint8_t BOOT_IV[16] = { + 0xb1, 0x55, 0xc2, 0x2c, 0x2e, 0x7f, 0x04, 0x91, + 0xfa, 0x7f, 0x0f, 0xdc, 0x21, 0x7a, 0xff, 0x90 + }; +} diff --git a/src/utils/files.cpp b/src/utils/files.cpp new file mode 100644 index 0000000..f250501 --- /dev/null +++ b/src/utils/files.cpp @@ -0,0 +1,86 @@ +#include "utils/files.h" +#include +#include +#include +#include + +namespace utils { + // 读取文件全部内容 + std::vector read_file(const std::wstring &path) { + std::vector data; + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file.is_open()) return data; + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + data.resize(static_cast(size)); + if (!file.read(reinterpret_cast(data.data()), size)) { + data.clear(); + } + return data; + } + + // 获取文件所在目录路径 + std::wstring get_directory(const std::wstring &path) { + size_t pos = path.find_last_of(L"\\/"); + if (pos == std::wstring::npos) return L"."; + return path.substr(0, pos); + } + + // 获取当前exe所在目录 + std::wstring get_exe_dir() { + wchar_t buf[32768]; + DWORD len = GetModuleFileNameW(nullptr, buf, _countof(buf)); + if (len == 0 || len == _countof(buf)) return L"."; + return get_directory(std::wstring(buf, len)); + } + + // 递归拷贝目录 + bool copy_dir_overwrite(const std::wstring &src_dir, + const std::wstring &dst_dir, + std::string *error, + size_t *out_files_copied) { + namespace fs = std::filesystem; + std::error_code ec; + size_t count = 0; + + if (!fs::exists(src_dir, ec)) { + if (out_files_copied) *out_files_copied = count; + return true; + } + + for (auto it = fs::recursive_directory_iterator( + src_dir, fs::directory_options::skip_permission_denied, ec); + it != fs::recursive_directory_iterator(); it.increment(ec)) { + if (ec) { + if (error) *error = std::format("iter error: {}", ec.value()); + return false; + } + auto &entry = *it; + fs::path rel = entry.path().lexically_relative(src_dir); + fs::path dst = fs::path(dst_dir) / rel; + + if (entry.is_directory()) { + fs::create_directories(dst, ec); + if (ec) { + if (error) + *error = std::format("mkdir: {} {}", ec.value(), dst.string()); + return false; + } + continue; + } + if (entry.is_regular_file()) { + fs::create_directories(dst.parent_path(), ec); + fs::copy_file(entry.path(), dst, + fs::copy_options::overwrite_existing, ec); + if (ec) { + if (error) + *error = std::format("copy_file: {} {}", ec.value(), dst.string()); + return false; + } + count++; + } + } + if (out_files_copied) *out_files_copied = count; + return true; + } +} diff --git a/src/utils/files.h b/src/utils/files.h new file mode 100644 index 0000000..78e2c35 --- /dev/null +++ b/src/utils/files.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include + +namespace utils { + // 读取文件全部内容到字节数组,失败返回空vector + std::vector read_file(const std::wstring &path); + + // 获取文件所在目录路径(末尾不带反斜杠),无分隔符时返回 "." + std::wstring get_directory(const std::wstring &path); + + // 获取当前exe所在目录(末尾不带反斜杠) + std::wstring get_exe_dir(); + + // 递归拷贝 src_dir -> dst_dir,文件已存在时强制覆盖 + // 返回:成功返回true,out_files_copied 输出已拷贝文件数 + bool copy_dir_overwrite(const std::wstring &src_dir, + const std::wstring &dst_dir, + std::string *error, + size_t *out_files_copied); +} diff --git a/src/utils/game_keys.h b/src/utils/game_keys.h new file mode 100644 index 0000000..0b9443d --- /dev/null +++ b/src/utils/game_keys.h @@ -0,0 +1,97 @@ +#pragma once + +#include + +// 此文件由 gen_keys.py 从 res/keys.json 自动生成,请勿手动编辑 +// 源文件: res/keys.json (76 条记录) + +namespace game_keys { + // 单个游戏的密钥条目(KEY/IV为16字节原始数组,无需运行时hex转换) + struct Entry { + const char *game_id; + const uint8_t app_key[16]; + const uint8_t app_iv[16]; + const uint8_t opt_key[16]; + const uint8_t opt_iv[16]; + }; + + // 所有游戏的密钥表(编译期常量,无需运行时解析) + inline constexpr Entry ENTRIES[] = { + {"SBZS", {0x2E, 0xCB, 0xCF, 0xF6, 0x5C, 0xE0, 0xAB, 0xEC, 0xC1, 0x05, 0x47, 0xF8, 0xAC, 0x83, 0x51, 0xD8}, {0xF2, 0xAC, 0x6C, 0x28, 0x17, 0xD0, 0x57, 0x4B, 0xBA, 0x11, 0x3D, 0x49, 0x7E, 0x31, 0x9F, 0x3E}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SBZT", {0x9A, 0xB9, 0xCE, 0x55, 0xED, 0x9C, 0x19, 0x4A, 0x71, 0x5A, 0x73, 0xA7, 0x69, 0x9F, 0x79, 0x5B}, {0x85, 0x52, 0xDE, 0x88, 0xFE, 0xDD, 0xA6, 0xE8, 0x59, 0x36, 0x9F, 0xB0, 0x00, 0xF4, 0x4D, 0x5B}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SBZU", {0xEB, 0x12, 0x28, 0x25, 0x4C, 0xDD, 0x30, 0x77, 0xEB, 0x3E, 0x44, 0x1C, 0x02, 0x27, 0xBF, 0x40}, {0x3F, 0x9B, 0x46, 0x76, 0x11, 0x8C, 0xEE, 0x12, 0x9F, 0xE2, 0xF1, 0xCB, 0x27, 0x47, 0xBC, 0xA5}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SBZV", {0x32, 0x74, 0xA3, 0x99, 0x59, 0x4D, 0x84, 0x77, 0x96, 0x25, 0x94, 0x0B, 0x69, 0xC0, 0x2D, 0x3F}, {0x67, 0x5B, 0xA6, 0x6D, 0x29, 0xC8, 0x79, 0x23, 0xF5, 0xF1, 0x54, 0xC4, 0x06, 0xAF, 0xEE, 0x42}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDAP", {0x41, 0xB5, 0x02, 0x7C, 0x5E, 0x99, 0xD9, 0x4A, 0xA9, 0x33, 0x5D, 0x6D, 0x71, 0x83, 0x8E, 0xCF}, {0xD1, 0x95, 0xD7, 0xAD, 0x28, 0x14, 0x20, 0x71, 0x15, 0xDB, 0x35, 0x31, 0x20, 0x0A, 0xE3, 0xBA}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDAQ", {0xC2, 0x8F, 0x22, 0xBC, 0x1B, 0x33, 0x9A, 0xE6, 0x41, 0x80, 0x73, 0x98, 0x86, 0xDC, 0x83, 0xD6}, {0x0A, 0x29, 0xFD, 0x14, 0x5D, 0x72, 0xBF, 0x8D, 0xED, 0xD4, 0x36, 0x02, 0x5D, 0xF0, 0xA9, 0xFC}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDAV", {0xEE, 0xD9, 0x55, 0x13, 0x26, 0x6A, 0x49, 0x9A, 0x55, 0xE2, 0x65, 0xB0, 0x49, 0x16, 0x9C, 0x44}, {0x84, 0xC0, 0xE5, 0x93, 0x1D, 0x91, 0xA6, 0xA4, 0x77, 0xD6, 0x2C, 0x27, 0x15, 0x46, 0x05, 0x6E}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDBE", {0x70, 0x53, 0xFB, 0x94, 0x45, 0x72, 0xE5, 0xB6, 0x31, 0xA6, 0x65, 0xCE, 0xF4, 0xB5, 0xBC, 0xDD}, {0xAE, 0x4D, 0x7E, 0x88, 0x40, 0x02, 0xC7, 0x9E, 0xB3, 0x57, 0x11, 0x55, 0x4D, 0x61, 0x30, 0x57}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDBN", {0xC1, 0xF1, 0x4A, 0xE2, 0xE8, 0x5B, 0x09, 0x5E, 0x31, 0x3C, 0x8B, 0xAE, 0xC1, 0x25, 0x80, 0x5E}, {0x3C, 0x53, 0x8E, 0xEA, 0x66, 0x25, 0x1A, 0xCD, 0x54, 0x04, 0xB9, 0x3F, 0x89, 0x76, 0xA7, 0xF7}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDBT", {0xA6, 0xA8, 0x70, 0x67, 0x1F, 0xD4, 0x32, 0xEC, 0x63, 0x7A, 0xDF, 0x7A, 0x82, 0x2F, 0x97, 0xDA}, {0x2C, 0x27, 0x7F, 0x31, 0xCD, 0x55, 0x0C, 0xFA, 0x2C, 0x99, 0x3B, 0x4D, 0xD5, 0x6B, 0x85, 0xAE}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDBX", {0x3D, 0xC1, 0x9C, 0x2D, 0x0C, 0x20, 0xAC, 0x19, 0x9D, 0x5F, 0xA4, 0x6E, 0x7F, 0x63, 0x35, 0xA6}, {0xD8, 0xF0, 0x29, 0xEC, 0x90, 0xFE, 0x55, 0xBE, 0x67, 0x58, 0x4F, 0x74, 0x2C, 0x55, 0xEF, 0x8B}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDBZ", {0x52, 0x1B, 0xDE, 0x44, 0x60, 0xF4, 0x18, 0x4E, 0xDD, 0x87, 0x91, 0x36, 0xAD, 0xEE, 0xA5, 0xEE}, {0x1B, 0x83, 0x24, 0x03, 0x2D, 0xB6, 0x9D, 0x7B, 0x09, 0x54, 0x79, 0x4A, 0xA2, 0x29, 0xFE, 0x68}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDCA", {0x16, 0x49, 0x49, 0x0A, 0x03, 0xD6, 0xC2, 0xAE, 0xC1, 0xC4, 0x96, 0x98, 0x2C, 0xB0, 0x40, 0x5C}, {0x46, 0x80, 0x71, 0x1C, 0x7E, 0x67, 0xA2, 0x6F, 0x92, 0x30, 0xD5, 0xAF, 0x74, 0xB5, 0xDC, 0xFB}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDCD", {0x43, 0xB3, 0x85, 0x02, 0xD8, 0xF6, 0xD3, 0xC7, 0xB0, 0x2B, 0x95, 0xFC, 0x28, 0xDB, 0x53, 0x08}, {0x6D, 0xFC, 0xB9, 0x4B, 0xF7, 0x4F, 0x15, 0x2B, 0x55, 0xF3, 0xE0, 0xC7, 0xF3, 0x5B, 0x44, 0xB5}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDCF", {0xDF, 0x98, 0x68, 0x83, 0xDA, 0x83, 0x75, 0x38, 0xE3, 0x7B, 0x95, 0x9A, 0x3E, 0x41, 0x17, 0xCD}, {0xDA, 0xBF, 0x53, 0x97, 0x38, 0x85, 0x2F, 0x17, 0x71, 0x48, 0x11, 0xAF, 0x70, 0x43, 0x5A, 0x83}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDCH", {0xE2, 0xDA, 0x76, 0x9E, 0x94, 0xF1, 0xD3, 0xAC, 0xA1, 0x93, 0x0C, 0xDB, 0xE0, 0x70, 0x8C, 0x9F}, {0xC7, 0xDC, 0xCE, 0x20, 0x3C, 0x84, 0xAB, 0x04, 0x77, 0x23, 0x6D, 0x69, 0x75, 0x70, 0xDA, 0xDC}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDCR", {0x49, 0x61, 0xA5, 0x1F, 0xD3, 0x6F, 0x14, 0xE7, 0x26, 0x64, 0xF5, 0x23, 0x73, 0x05, 0x21, 0x60}, {0x25, 0xD7, 0xD1, 0x34, 0x1A, 0x28, 0x2C, 0x5E, 0x0A, 0x34, 0xC6, 0x45, 0x62, 0xC0, 0x23, 0xEC}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDCT", {0xD6, 0xAE, 0x51, 0xF1, 0x0E, 0xC7, 0x6D, 0xA9, 0x3C, 0x98, 0x18, 0x00, 0xFC, 0x3A, 0xD3, 0xCB}, {0xFB, 0x8E, 0x43, 0xE2, 0x80, 0xD3, 0x30, 0xD0, 0x65, 0x81, 0x73, 0x2F, 0x2E, 0x11, 0xA6, 0xDC}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDCX", {0x79, 0x50, 0x4C, 0xCC, 0x50, 0x9B, 0x67, 0xD1, 0xF7, 0xA3, 0xF5, 0x93, 0xE6, 0xF9, 0xD9, 0xD6}, {0x15, 0x51, 0xEA, 0x89, 0x26, 0xF2, 0xAE, 0xE2, 0x33, 0xEE, 0xC3, 0x09, 0xDE, 0x3E, 0x5F, 0x3C}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDDB", {0x87, 0x56, 0x79, 0xB2, 0xCD, 0x16, 0x37, 0x96, 0x2B, 0x0D, 0xB2, 0x5C, 0x51, 0xFB, 0x21, 0xA6}, {0x8E, 0xF4, 0x47, 0x22, 0xA0, 0x56, 0x6E, 0x8F, 0x57, 0x23, 0x56, 0x24, 0x56, 0x87, 0xFB, 0xE5}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDDD", {0x56, 0x4E, 0x96, 0x78, 0x73, 0xDE, 0x6C, 0xBC, 0xD2, 0x2E, 0xFE, 0xCA, 0x69, 0x52, 0xE9, 0xDC}, {0x4E, 0x3D, 0xD4, 0x65, 0xCF, 0x09, 0xCD, 0x82, 0xB2, 0x59, 0xF7, 0xBE, 0xD5, 0xFC, 0x2D, 0x6D}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDDD", {0x75, 0x71, 0x3E, 0x3A, 0x63, 0x40, 0x41, 0x65, 0x98, 0xCF, 0xA9, 0xE3, 0x8F, 0xBA, 0x68, 0xE4}, {0x59, 0xA1, 0x4E, 0x3E, 0x13, 0xA6, 0xF8, 0xB8, 0x67, 0x56, 0x1E, 0xF7, 0x34, 0xE1, 0xA5, 0xFF}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDDF", {0x65, 0x05, 0x85, 0x73, 0xA0, 0xCB, 0x81, 0x74, 0x9E, 0x69, 0x4A, 0xE1, 0x64, 0xC6, 0x1B, 0x04}, {0x98, 0x1C, 0x4F, 0x45, 0xE3, 0xC6, 0x95, 0x8F, 0x05, 0x4E, 0x5D, 0x00, 0x91, 0x6B, 0xDF, 0x2B}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDDJ", {0x63, 0x0F, 0xE5, 0x22, 0x76, 0x53, 0x7B, 0xD7, 0xFB, 0x26, 0x7A, 0xDF, 0x17, 0x5F, 0x4E, 0x99}, {0xDC, 0x57, 0x55, 0xBE, 0x57, 0xDE, 0xD2, 0xCD, 0xB3, 0x44, 0x33, 0xBB, 0xBA, 0x22, 0x04, 0xFF}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDDL", {0x99, 0x24, 0x58, 0x29, 0x5F, 0xD0, 0x6D, 0x6A, 0x8A, 0xF0, 0xDF, 0xB3, 0xF6, 0x85, 0x4C, 0x19}, {0x84, 0x84, 0x90, 0x6D, 0x4C, 0xD5, 0xFD, 0x22, 0x5E, 0x03, 0x28, 0x43, 0xED, 0x37, 0x49, 0x5D}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDDM", {0x01, 0x27, 0x95, 0x82, 0x10, 0xF6, 0xAE, 0x9B, 0xDE, 0xB8, 0x97, 0x50, 0x18, 0xB5, 0xAF, 0x24}, {0x18, 0x17, 0x16, 0xBA, 0xDC, 0xCF, 0xF4, 0xBC, 0x2B, 0x1E, 0x29, 0xAE, 0x02, 0xA1, 0xBB, 0xBB}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDDN", {0x41, 0xDD, 0x8E, 0x66, 0x29, 0x01, 0x17, 0xAC, 0x67, 0xD3, 0x11, 0xA2, 0xF0, 0xA6, 0x41, 0x6E}, {0x73, 0xE1, 0x8E, 0x84, 0x18, 0xF6, 0xCE, 0xEF, 0xB1, 0x1E, 0x27, 0x67, 0xFD, 0xEA, 0x19, 0x0C}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDDP", {0xCF, 0x6D, 0x64, 0x42, 0x7E, 0xEC, 0xA4, 0x76, 0x74, 0xE1, 0x7B, 0xCD, 0x46, 0xD1, 0xEA, 0x8C}, {0xCE, 0x51, 0x74, 0x09, 0x3D, 0x26, 0xCA, 0x2A, 0x31, 0xB5, 0x85, 0x41, 0xE8, 0x5A, 0xC2, 0x76}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDDS", {0x16, 0x1B, 0xEC, 0x6D, 0x90, 0x98, 0x9D, 0x0E, 0x26, 0xD7, 0x91, 0x17, 0x06, 0x07, 0xA4, 0x40}, {0x81, 0xDC, 0x26, 0xA2, 0x70, 0x28, 0xE2, 0x09, 0x23, 0x32, 0x03, 0x8A, 0xA1, 0xBF, 0xFC, 0x47}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDDT", {0x3F, 0x76, 0x58, 0x72, 0x8B, 0x95, 0x17, 0xD3, 0x31, 0x4E, 0x68, 0x4F, 0xA2, 0xE2, 0xA0, 0x45}, {0x41, 0x57, 0x88, 0x33, 0xC5, 0x47, 0xAA, 0xFF, 0x04, 0xDB, 0x59, 0x7A, 0x6E, 0x9E, 0xB7, 0x84}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDDT", {0x21, 0x69, 0x34, 0x36, 0x46, 0xA3, 0x24, 0x86, 0x19, 0x75, 0x5B, 0x68, 0xE6, 0xEF, 0x2C, 0x2C}, {0x69, 0x82, 0x40, 0x6E, 0x69, 0xC9, 0xEC, 0x25, 0x74, 0xED, 0xC5, 0x25, 0x3F, 0xC6, 0xA3, 0x24}, {0x81, 0x3A, 0x11, 0x58, 0xC5, 0x2B, 0x82, 0x08, 0x9D, 0x71, 0x0A, 0xEC, 0x3B, 0x5F, 0xCC, 0x91}, {0xA1, 0x2F, 0x44, 0xF9, 0xF1, 0xBE, 0x52, 0xE6, 0x99, 0x4A, 0xC6, 0xE0, 0x52, 0xC1, 0x59, 0x65}}, + {"SDDU", {0x64, 0x9A, 0xE9, 0x98, 0x26, 0x25, 0xF9, 0x0C, 0x55, 0xAF, 0x86, 0x71, 0x3C, 0x55, 0xD3, 0xFD}, {0x18, 0x71, 0x16, 0xFC, 0x46, 0x47, 0xA7, 0xD3, 0xB6, 0xF2, 0x30, 0x3A, 0x34, 0xF0, 0xA2, 0xFE}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDDW", {0x11, 0x85, 0x65, 0xD3, 0x44, 0xF3, 0xE1, 0x4C, 0xA6, 0x92, 0x99, 0xEE, 0xAC, 0x04, 0x9B, 0xB9}, {0x9D, 0x6D, 0x39, 0x2E, 0xC3, 0x5E, 0xD9, 0x4E, 0xF9, 0xFE, 0x0A, 0x5B, 0xE0, 0x57, 0x39, 0x81}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDDX", {0x42, 0x8B, 0xFF, 0x0F, 0x9E, 0x7A, 0xAF, 0xC1, 0x69, 0xA7, 0xA7, 0x57, 0x51, 0xFF, 0xDA, 0x98}, {0xF8, 0x25, 0x05, 0x94, 0xF4, 0x25, 0x33, 0x2C, 0x6D, 0x34, 0x9D, 0x7E, 0xA0, 0xE8, 0x66, 0x69}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDEA", {0x9F, 0x9C, 0xF1, 0x48, 0xAC, 0x3C, 0x50, 0xAA, 0xF9, 0x25, 0xAF, 0x1D, 0xFB, 0x27, 0xF5, 0x8B}, {0x4D, 0x8E, 0xBB, 0xD9, 0x71, 0x89, 0x6B, 0x8A, 0x4A, 0x3D, 0xD8, 0x4A, 0x23, 0xB3, 0x29, 0xFC}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDEB", {0xD5, 0x11, 0xED, 0x69, 0x04, 0x15, 0xF6, 0x35, 0x98, 0x43, 0xA1, 0x34, 0xFD, 0x47, 0x83, 0x6A}, {0xAC, 0x13, 0x9B, 0x38, 0x2A, 0xCD, 0xD1, 0x12, 0xE3, 0x15, 0x64, 0xEA, 0x7F, 0x38, 0x18, 0x6C}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDEC", {0xF2, 0x72, 0xE5, 0x01, 0x68, 0x63, 0xAF, 0x2B, 0xA0, 0x33, 0x7F, 0x50, 0xDE, 0x68, 0x6F, 0x6E}, {0x53, 0x27, 0xE1, 0x32, 0x63, 0x1E, 0x7F, 0x71, 0xB6, 0x1B, 0xE7, 0xCC, 0x0D, 0xF3, 0x82, 0xCE}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDED", {0x21, 0xFC, 0xEC, 0x77, 0x9A, 0x16, 0x76, 0x9F, 0x52, 0x77, 0xA3, 0x6F, 0xB5, 0x42, 0x99, 0x2C}, {0x22, 0xB5, 0x02, 0x39, 0xF1, 0xB4, 0x0C, 0xCC, 0x3E, 0x55, 0xA2, 0xD6, 0x9C, 0x69, 0xB1, 0x60}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDED", {0xDE, 0x78, 0xB3, 0xF3, 0xD3, 0x35, 0x8F, 0x40, 0xD5, 0x52, 0xCB, 0xF5, 0x89, 0x21, 0xA4, 0x99}, {0x20, 0xF3, 0x33, 0xCC, 0x75, 0x74, 0x62, 0x84, 0x26, 0x56, 0xB4, 0x30, 0xC7, 0x82, 0x34, 0x34}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDEE", {0x19, 0x1E, 0xB7, 0x44, 0x06, 0x72, 0xDA, 0xB0, 0x8D, 0xDB, 0xB7, 0x19, 0x5E, 0xFB, 0x35, 0x6F}, {0xC2, 0x78, 0xB5, 0x38, 0x6D, 0xC3, 0x8B, 0xD7, 0x6D, 0x71, 0xDB, 0xCD, 0x82, 0x69, 0x54, 0xCF}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDEG", {0x72, 0x18, 0x53, 0xDB, 0xE2, 0xD3, 0x0B, 0xAF, 0xE2, 0x4F, 0x0E, 0xDB, 0xD2, 0x10, 0xDE, 0xEB}, {0x4D, 0xFB, 0x0B, 0xCE, 0xC8, 0x61, 0x59, 0xAA, 0xB2, 0x97, 0x16, 0x6B, 0xCD, 0x50, 0x9E, 0x6F}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDEJ", {0x9D, 0xE1, 0xEA, 0x6A, 0xE3, 0x8D, 0x90, 0x11, 0xF5, 0x5D, 0x8E, 0xE8, 0x64, 0x39, 0x5D, 0x24}, {0xF6, 0x0C, 0xDE, 0x21, 0x98, 0x28, 0x76, 0xD1, 0x2D, 0x17, 0x66, 0x2A, 0x48, 0xD9, 0x08, 0x36}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDEM", {0x70, 0x06, 0x17, 0xF2, 0x93, 0x69, 0x6C, 0x07, 0xFB, 0x9F, 0x35, 0x6D, 0x3B, 0x99, 0x24, 0x0D}, {0x66, 0x7D, 0x02, 0x6D, 0x6C, 0xDF, 0x32, 0x9F, 0xF3, 0x51, 0xDB, 0xAF, 0x70, 0x98, 0xE8, 0x1D}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDEP", {0xFA, 0x2B, 0x7C, 0xA5, 0x3A, 0x82, 0x3C, 0x15, 0x2D, 0x94, 0x09, 0x72, 0xCB, 0xF5, 0x32, 0xF5}, {0xF4, 0xAF, 0x35, 0x12, 0x0C, 0x48, 0x61, 0x77, 0x04, 0xBB, 0x5B, 0x84, 0x71, 0x79, 0x7A, 0x62}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDER", {0x7D, 0x73, 0x36, 0x7E, 0xBB, 0x21, 0x8E, 0xC8, 0x29, 0x30, 0xD5, 0x8D, 0xC6, 0xD7, 0x95, 0x0B}, {0x97, 0x88, 0xC3, 0xEC, 0xA2, 0xDB, 0x6B, 0xA9, 0x2B, 0xAC, 0x4F, 0x6F, 0x7B, 0x70, 0x63, 0x08}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDET", {0x46, 0x43, 0xE7, 0xB2, 0xC3, 0x00, 0x6E, 0x02, 0x64, 0x16, 0x3E, 0xDC, 0x85, 0x45, 0xFB, 0x72}, {0x61, 0x2B, 0xCA, 0x81, 0xEA, 0x29, 0x58, 0xFF, 0xBA, 0xC3, 0x6F, 0x78, 0x0F, 0x1E, 0xD6, 0x88}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDEU", {0x23, 0xB3, 0xE9, 0xBB, 0x47, 0xE3, 0xAC, 0x99, 0x98, 0xF6, 0xE6, 0xC1, 0xAD, 0xC4, 0xAE, 0x33}, {0xA9, 0x64, 0x71, 0x4C, 0xEA, 0x60, 0x68, 0x84, 0x07, 0xBF, 0x55, 0x4B, 0xD1, 0xC2, 0x7E, 0xC2}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDEV", {0x3C, 0x1F, 0x01, 0x8D, 0x88, 0x92, 0x6D, 0x98, 0x16, 0x3B, 0x07, 0xA1, 0x56, 0x3A, 0x48, 0x18}, {0xCA, 0x73, 0x73, 0xC9, 0xC7, 0xDF, 0xEB, 0xAC, 0x0F, 0xC2, 0x42, 0x54, 0xC0, 0x30, 0xE4, 0xAD}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDEZ", {0xD1, 0x36, 0xEB, 0xA0, 0x5D, 0x40, 0xE8, 0x26, 0x82, 0xE6, 0xAA, 0xD8, 0xD9, 0xE8, 0x68, 0x8C}, {0xC4, 0x84, 0xDE, 0xEA, 0xA0, 0x24, 0x9E, 0xF4, 0x66, 0x95, 0xF6, 0x36, 0x94, 0xB7, 0x37, 0x2F}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDFA", {0x8E, 0x81, 0x6B, 0x43, 0x62, 0xDB, 0x24, 0xA2, 0x30, 0x87, 0x78, 0x85, 0x86, 0x4D, 0x20, 0x6D}, {0x8E, 0x5A, 0x0B, 0xA6, 0xA0, 0xA1, 0x15, 0x0D, 0x47, 0xD1, 0x2B, 0xDB, 0x64, 0xDE, 0xBB, 0xA7}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDFE", {0xF6, 0x17, 0x19, 0xC3, 0x71, 0xE5, 0xBC, 0xA6, 0x78, 0x8C, 0x13, 0x9A, 0x53, 0x09, 0x16, 0x17}, {0x67, 0xD4, 0x31, 0x73, 0xE3, 0x43, 0x81, 0x3F, 0xA2, 0x09, 0x7F, 0xD3, 0x29, 0x92, 0xA8, 0xE2}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDFE", {0x24, 0xF9, 0x63, 0xD0, 0xA7, 0xCF, 0x3D, 0x65, 0xF3, 0x67, 0x09, 0x77, 0xFA, 0xA5, 0xDA, 0xBA}, {0x37, 0xA1, 0x75, 0x83, 0x8F, 0x1E, 0x5C, 0x6D, 0x10, 0x5F, 0x4F, 0xB0, 0x59, 0x9F, 0x60, 0x7B}, {0x81, 0x3A, 0x11, 0x58, 0xC5, 0x2B, 0x82, 0x08, 0x9D, 0x71, 0x0A, 0xEC, 0x3B, 0x5F, 0xCC, 0x91}, {0xA1, 0x2F, 0x44, 0xF9, 0xF1, 0xBE, 0x52, 0xE6, 0x99, 0x4A, 0xC6, 0xE0, 0x52, 0xC1, 0x59, 0x65}}, + {"SDFG", {0x33, 0x98, 0xFB, 0x86, 0xBF, 0xE6, 0x30, 0xA1, 0x49, 0x79, 0x41, 0x18, 0x79, 0x86, 0x1A, 0xC7}, {0xA7, 0x94, 0xC4, 0x9C, 0x2C, 0x76, 0x39, 0xCD, 0x80, 0x57, 0x18, 0x07, 0xC1, 0x72, 0x46, 0xFF}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDFL", {0x24, 0x49, 0xB4, 0x80, 0x67, 0xB9, 0x17, 0x6A, 0x6E, 0x0F, 0x95, 0x63, 0x48, 0x1E, 0x97, 0xF4}, {0x61, 0x6F, 0x87, 0x10, 0x45, 0x46, 0x32, 0xEB, 0x4F, 0xB1, 0xD8, 0x9D, 0x8C, 0x19, 0xC1, 0x9A}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDFN", {0x29, 0xF6, 0x2E, 0x22, 0xC6, 0xA9, 0xFD, 0x8B, 0xE3, 0x27, 0x63, 0x1C, 0x68, 0x54, 0x64, 0x05}, {0x2A, 0x86, 0x09, 0x76, 0xE6, 0xD9, 0x85, 0x13, 0x82, 0x5F, 0x29, 0x1E, 0x56, 0xCF, 0xB5, 0xEE}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDFP", {0x57, 0x0B, 0x87, 0x26, 0x3A, 0x7C, 0xA0, 0xAA, 0x4C, 0x13, 0x88, 0xE2, 0x04, 0xEE, 0x6D, 0x4B}, {0x76, 0x40, 0x88, 0x60, 0x11, 0xA2, 0x30, 0x0A, 0x91, 0xFA, 0xD9, 0xF3, 0x6A, 0x8C, 0x47, 0x75}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDFT", {0x92, 0xA2, 0x5F, 0x38, 0x8C, 0x50, 0x73, 0x7E, 0x39, 0xC3, 0xC2, 0xF0, 0x06, 0x64, 0x5F, 0x31}, {0xA9, 0x7E, 0x72, 0xF9, 0x90, 0x41, 0x74, 0x88, 0xCB, 0x4C, 0x67, 0xF8, 0xF0, 0xC3, 0xFB, 0x25}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDFV", {0xFE, 0x82, 0xDB, 0x9A, 0x60, 0x29, 0x5D, 0x82, 0x9B, 0x95, 0xF0, 0x3C, 0x22, 0x76, 0x01, 0x8B}, {0x34, 0xD8, 0x27, 0x72, 0xAE, 0x18, 0x17, 0x4F, 0x0A, 0x18, 0x1D, 0xC5, 0x33, 0x99, 0xEA, 0x9C}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDGA", {0x0A, 0x66, 0x10, 0xA6, 0x2E, 0xF6, 0x70, 0xC6, 0x5B, 0x7E, 0x7B, 0x17, 0x50, 0xFF, 0xB7, 0xA1}, {0x17, 0xA2, 0xA2, 0x29, 0x15, 0xF8, 0x1C, 0x58, 0x96, 0xED, 0xBB, 0xA4, 0xC4, 0x12, 0x58, 0x5E}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDGB", {0x7C, 0xA4, 0xE6, 0xB6, 0xF3, 0xD6, 0xE8, 0xB2, 0x64, 0x72, 0x97, 0x38, 0x87, 0xD7, 0xFA, 0x3A}, {0x53, 0xFE, 0x71, 0x35, 0x76, 0x2D, 0xE3, 0xF9, 0x7E, 0x7F, 0xE7, 0x6B, 0x0F, 0xEF, 0x3F, 0x27}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDGH", {0xB3, 0xE3, 0x0E, 0x7E, 0xAB, 0xAC, 0x37, 0x67, 0xAD, 0xE1, 0x3C, 0x69, 0xC9, 0xB2, 0xF2, 0x2B}, {0x03, 0xDE, 0xAE, 0xA3, 0x74, 0x2D, 0x69, 0x67, 0x5B, 0x36, 0xCD, 0xDC, 0x8B, 0x15, 0xAC, 0x91}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDGK", {0x9D, 0xC4, 0xA1, 0x7F, 0xC3, 0x9F, 0xCA, 0x5A, 0x8A, 0x35, 0x89, 0x84, 0x80, 0x1C, 0xAA, 0xA7}, {0xE0, 0x44, 0x5B, 0x11, 0xDC, 0xFA, 0x0D, 0xAE, 0x56, 0xC8, 0x5E, 0x87, 0x87, 0xE1, 0x1D, 0x9B}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDGP", {0xC8, 0x7A, 0xB3, 0x12, 0x47, 0xE7, 0xB6, 0xFF, 0x95, 0xFD, 0xD7, 0x9F, 0xB9, 0x1F, 0x9F, 0x37}, {0x24, 0x67, 0xAB, 0x3C, 0x03, 0x1E, 0x3D, 0xC0, 0x56, 0x8B, 0x70, 0x77, 0xEF, 0xD2, 0x7C, 0x36}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDGQ", {0xC5, 0x35, 0x6D, 0xAE, 0x7B, 0x06, 0x6B, 0xCE, 0x88, 0x98, 0x4A, 0xEC, 0x36, 0xDE, 0xB6, 0x2D}, {0x4E, 0x9F, 0x29, 0x82, 0x46, 0x0E, 0x2F, 0xD9, 0x07, 0xBD, 0xE1, 0x57, 0x09, 0xED, 0xFB, 0xA7}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDGS", {0xA5, 0x15, 0x0C, 0xC5, 0x06, 0x5D, 0x2C, 0x59, 0xEE, 0x2F, 0x8F, 0x33, 0x2C, 0xBD, 0x29, 0xD5}, {0x84, 0x01, 0x4D, 0x26, 0x69, 0x6F, 0x29, 0x0A, 0xD7, 0xEA, 0xD7, 0x0C, 0x75, 0x49, 0xBD, 0x81}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDGT", {0x9D, 0x0B, 0xBA, 0x20, 0xD1, 0xE8, 0x4F, 0x24, 0x59, 0x39, 0x9F, 0x53, 0x83, 0xBE, 0xEE, 0x72}, {0x5D, 0x34, 0x00, 0x13, 0xFD, 0xFB, 0x24, 0x64, 0xD2, 0x53, 0x09, 0x36, 0x02, 0xFE, 0x4B, 0x64}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDGV", {0x57, 0x3F, 0x8C, 0xC4, 0x4F, 0x10, 0xF3, 0x1E, 0xC7, 0x49, 0xB6, 0x95, 0xEB, 0xE8, 0x86, 0xA8}, {0x6B, 0xFC, 0xA8, 0x6F, 0x9D, 0x20, 0x8A, 0x79, 0x44, 0xCD, 0xFC, 0x25, 0xEA, 0x3C, 0xD2, 0x20}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDGY", {0xC0, 0x4B, 0x66, 0x3A, 0x59, 0x05, 0x5A, 0xCB, 0xDF, 0xEB, 0xC6, 0xD3, 0xDF, 0x0E, 0x6A, 0x04}, {0x76, 0xFC, 0x5F, 0x1D, 0x88, 0x60, 0x51, 0x07, 0x94, 0x7D, 0x0C, 0x1F, 0xF3, 0x47, 0x02, 0x2D}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDGZ", {0x9A, 0xD7, 0x4E, 0xFB, 0x20, 0x8D, 0x6E, 0xE4, 0xFE, 0x5E, 0xE7, 0x70, 0x33, 0x17, 0x12, 0xCF}, {0x45, 0xF6, 0xE5, 0x3F, 0x0E, 0xB8, 0xFA, 0xE6, 0x66, 0x5B, 0x45, 0x44, 0x4A, 0x61, 0xE2, 0x66}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDHD", {0x3A, 0xBD, 0x00, 0xD7, 0xA8, 0x20, 0xCE, 0x86, 0x2E, 0xAF, 0x47, 0x4B, 0xF6, 0xC8, 0xF3, 0x3E}, {0x0F, 0x1E, 0x7E, 0xEA, 0x78, 0xDA, 0x7E, 0x03, 0x7E, 0x05, 0x52, 0xC2, 0x84, 0x3E, 0x1B, 0x6A}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDHD", {0x81, 0xB1, 0x58, 0xCB, 0xB8, 0x83, 0x70, 0xA1, 0x26, 0x4C, 0x99, 0x80, 0x0C, 0x5D, 0xDE, 0x4D}, {0x6F, 0xD5, 0x01, 0xED, 0x31, 0xEB, 0xE1, 0x18, 0x26, 0xDB, 0xAC, 0x39, 0x46, 0xE2, 0x4D, 0x57}, {0x81, 0x3A, 0x11, 0x58, 0xC5, 0x2B, 0x82, 0x08, 0x9D, 0x71, 0x0A, 0xEC, 0x3B, 0x5F, 0xCC, 0x91}, {0xA1, 0x2F, 0x44, 0xF9, 0xF1, 0xBE, 0x52, 0xE6, 0x99, 0x4A, 0xC6, 0xE0, 0x52, 0xC1, 0x59, 0x65}}, + {"SDHH", {0xFC, 0x6F, 0x88, 0x7F, 0x37, 0x17, 0xC5, 0xD6, 0x71, 0x31, 0x13, 0xB9, 0x2F, 0xA3, 0xFB, 0x27}, {0xAD, 0x76, 0x60, 0x64, 0x60, 0xDB, 0xE1, 0xE9, 0x1E, 0x41, 0xBE, 0xF7, 0xAB, 0x0C, 0x15, 0x35}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDHJ", {0x98, 0x5E, 0xA6, 0x6E, 0xCB, 0x5B, 0x1F, 0x20, 0x8C, 0x90, 0xE2, 0xB8, 0x98, 0xF0, 0xB0, 0x73}, {0x16, 0x4A, 0x65, 0x42, 0x2E, 0x7F, 0x01, 0xB7, 0xF1, 0xB0, 0x84, 0x9F, 0xC7, 0x73, 0x7C, 0xDB}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDHK", {0xBC, 0x92, 0xD6, 0x3C, 0x2A, 0x09, 0x9C, 0xA2, 0x31, 0x5A, 0x48, 0x3C, 0x30, 0x41, 0xFD, 0xD7}, {0xB1, 0x4D, 0x84, 0x49, 0xB6, 0xD4, 0x32, 0x5D, 0x83, 0xA2, 0x77, 0x4B, 0x13, 0xDD, 0x21, 0xFF}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDHN", {0x89, 0x21, 0x23, 0xA2, 0x6D, 0x7C, 0x03, 0xD4, 0x9E, 0xDD, 0x12, 0xA8, 0x0E, 0xE0, 0xC5, 0x8F}, {0x76, 0xAA, 0x15, 0xA6, 0x86, 0x8B, 0x8D, 0xBD, 0xF7, 0x20, 0x79, 0x06, 0x35, 0x4D, 0x51, 0x69}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + {"SDHR", {0x1F, 0xB8, 0x97, 0xCA, 0xB9, 0x7C, 0x81, 0x70, 0xA6, 0xAC, 0x0A, 0x21, 0x68, 0x5C, 0x58, 0xD9}, {0xF9, 0xB6, 0x0F, 0x65, 0xB0, 0x1E, 0x8E, 0x83, 0x6A, 0x4B, 0xC2, 0x0F, 0x7D, 0x39, 0xFA, 0xF5}, {0x5C, 0x84, 0xA9, 0xE7, 0x26, 0xEA, 0xA5, 0xDD, 0x35, 0x1F, 0x2B, 0x07, 0x50, 0xC2, 0x36, 0x97}, {0xC0, 0x63, 0xBF, 0x6F, 0x56, 0x2D, 0x08, 0x4D, 0x79, 0x63, 0xC9, 0x87, 0xF5, 0x28, 0x17, 0x61}}, + }; +} diff --git a/src/utils/log.cpp b/src/utils/log.cpp new file mode 100644 index 0000000..e40f9a3 --- /dev/null +++ b/src/utils/log.cpp @@ -0,0 +1,62 @@ +#include "log.h" +#include +#include + +namespace logger { + // 全局最低输出等级 + static auto g_min_level = Level::Debug; + + // 等级对应的标签文字(固定5字符宽度对齐) + static const char *level_tag(Level level) { + switch (level) { + case Level::Debug: return "DEBUG"; + case Level::Info: return "INFO "; + case Level::Warn: return "WARN "; + case Level::Error: return "ERROR"; + default: return "?????"; + } + } + + // 等级对应的 Windows 控制台颜色属性 + static WORD level_color(Level level) { + switch (level) { + case Level::Debug: return FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY; // 灰白 + case Level::Info: return FOREGROUND_GREEN | FOREGROUND_INTENSITY; // 亮绿 + case Level::Warn: return FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY; // 亮黄 + case Level::Error: return FOREGROUND_RED | FOREGROUND_INTENSITY; // 亮红 + default: return FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE; + } + } + + void set_min_level(Level level) { + g_min_level = level; + } + + void output_raw(Level level, const char *module, const std::string &message) { + if (static_cast(level) < static_cast(g_min_level)) return; + + HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE); + WORD orig_attr = 7; // 默认灰白 + if (hStdout != INVALID_HANDLE_VALUE) { + CONSOLE_SCREEN_BUFFER_INFO csbi{}; + if (GetConsoleScreenBufferInfo(hStdout, &csbi)) { + orig_attr = csbi.wAttributes; + } + SetConsoleTextAttribute(hStdout, level_color(level)); + } + + // 输出: [LEVEL][MODULE] message\n + std::fputs("[", stdout); + std::fputs(level_tag(level), stdout); + std::fputs("][", stdout); + std::fputs(module, stdout); + std::fputs("] ", stdout); + std::fputs(message.c_str(), stdout); + std::fputc('\n', stdout); + std::fflush(stdout); + + if (hStdout != INVALID_HANDLE_VALUE) { + SetConsoleTextAttribute(hStdout, orig_attr); + } + } +} diff --git a/src/utils/log.h b/src/utils/log.h new file mode 100644 index 0000000..2a858dd --- /dev/null +++ b/src/utils/log.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace logger { + // 日志等级 + enum class Level { + Debug, + Info, + Warn, + Error + }; + + // 设置全局最低输出等级(低于此等级的日志不输出),默认 Debug(全部输出) + void set_min_level(Level level); + + // 原始字符串输出(已格式化完成),带模块名 + void output_raw(Level level, const char *module, const std::string &message); + + // 格式化输出(内部使用,外部建议用 LOG_XXX 宏) + template + void output(Level level, const char *module, std::format_string fmt_str, Args &&... args) { + output_raw(level, module, std::format(fmt_str, std::forward(args)...)); + } +} + +// 每个cpp文件在include本头文件前定义 LOG_MODULE 可自定义模块名 +#ifndef LOG_MODULE +#define LOG_MODULE "????" +#endif + +// 日志宏,自动附带 LOG_MODULE 作为模块名,格式: [LEVEL][MODULE] message +#define LOG_DEBUG(...) logger::output(logger::Level::Debug, LOG_MODULE, __VA_ARGS__) +#define LOG_INFO(...) logger::output(logger::Level::Info, LOG_MODULE, __VA_ARGS__) +#define LOG_WARN(...) logger::output(logger::Level::Warn, LOG_MODULE, __VA_ARGS__) +#define LOG_ERROR(...) logger::output(logger::Level::Error, LOG_MODULE, __VA_ARGS__) diff --git a/src/utils/strings.cpp b/src/utils/strings.cpp new file mode 100644 index 0000000..4c98b42 --- /dev/null +++ b/src/utils/strings.cpp @@ -0,0 +1,68 @@ +#include "strings.h" +#include +#include + +namespace utils { + // 将NTSTATUS转换为0x%08X格式的字符串 + std::string ntstatus_str(NTSTATUS status) { + return std::format("0x{:08X}", static_cast(status)); + } + + // 将十六进制字符串转换为字节数组 + bool hex_to_bytes(const std::string &hex, uint8_t *out, size_t len) { + if (hex.size() < len * 2) return false; + for (size_t i = 0; i < len; i++) { + unsigned val = 0; + if (sscanf_s(hex.c_str() + i * 2, "%02x", &val) != 1) return false; + out[i] = static_cast(val); + } + return true; + } + + // 规范化路径:将/转换为\,不翻倍反斜杠 + std::wstring normalize_path(const std::wstring &path) { + std::wstring result; + result.reserve(path.size() + 8); + for (wchar_t c: path) { + if (c == L'/') c = L'\\'; + result += c; + } + return result; + } + + // 将wstring转为string + std::string wstr_to_str(const std::wstring &ws) { + std::string s; + for (wchar_t c: ws) { + if (c < 0x80) s.push_back(static_cast(c)); + else { + s += std::format("\\x{:04X}", static_cast(c)); + } + } + return s; + } + + // 将UTF-8 char*字符串转为wstring + std::wstring to_wstring(const char *s) { + if (!s) return L""; + int len = MultiByteToWideChar(CP_UTF8, 0, s, -1, nullptr, 0); + if (len <= 0) return L""; + std::wstring ws(static_cast(len - 1), 0); + MultiByteToWideChar(CP_UTF8, 0, s, -1, ws.data(), len); + return ws; + } + + // 将string转为wstring(UTF-8) + std::wstring to_wstring(const std::string &s) { + return to_wstring(s.c_str()); + } + + // 将字节数组格式化为十六进制小写字符串 + std::string bytes_to_hex(const uint8_t *data, size_t len) { + std::string s; + for (size_t i = 0; i < len; i++) { + s += std::format("{:02x}", data[i]); + } + return s; + } +} diff --git a/src/utils/strings.h b/src/utils/strings.h new file mode 100644 index 0000000..cbfbc25 --- /dev/null +++ b/src/utils/strings.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +namespace utils { + // 将NTSTATUS转换为0x%08X格式的字符串 + std::string ntstatus_str(NTSTATUS status); + + // 将十六进制字符串转换为字节数组 + bool hex_to_bytes(const std::string &hex, uint8_t *out, size_t len); + + // 规范化路径:将/转换为\,不翻倍反斜杠 + std::wstring normalize_path(const std::wstring &path); + + // 将wstring转为string + std::string wstr_to_str(const std::wstring &ws); + + // 将UTF-8 char*字符串转为wstring + std::wstring to_wstring(const char *s); + + // 将string转为wstring(UTF-8) + std::wstring to_wstring(const std::string &s); + + // 将字节数组格式化为十六进制小写字符串 + std::string bytes_to_hex(const uint8_t *data, size_t len); +} diff --git a/src/vhd_mount.cpp b/src/vhd_mount.cpp new file mode 100644 index 0000000..f2e6437 --- /dev/null +++ b/src/vhd_mount.cpp @@ -0,0 +1,217 @@ +#include "vhd_mount.h" +#include "utils/strings.h" +#include +#include +#include +#include +#include + +namespace vhd_mount { + // ===== 内部 helper ===== + + // 打开VHD句柄,失败返回INVALID_HANDLE_VALUE并填充error + static HANDLE open_vhd(const std::wstring &vhd_path, VIRTUAL_DISK_ACCESS_MASK access_mask, DWORD rw_depth, + std::string *error) { + VIRTUAL_STORAGE_TYPE vst = {}; + vst.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHD; + vst.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT; + + OPEN_VIRTUAL_DISK_PARAMETERS open_params = {}; + open_params.Version = OPEN_VIRTUAL_DISK_VERSION_1; + open_params.Version1.RWDepth = rw_depth; + + HANDLE hVhd = nullptr; + DWORD err = OpenVirtualDisk(&vst, vhd_path.c_str(), + access_mask, + OPEN_VIRTUAL_DISK_FLAG_NONE, + &open_params, &hVhd); + if (err != ERROR_SUCCESS) { + if (error) + *error = std::format("OpenVirtualDisk failed: {} path={}", err, utils::wstr_to_str(vhd_path)); + return INVALID_HANDLE_VALUE; + } + return hVhd; + } + + // 通过任意卷设备路径(\\.\X: 或 \\?\Volume{GUID})获取卷所在磁盘号 + static bool device_get_disk_number(const std::wstring &device_path, DWORD &disk_number) { + HANDLE hVol = CreateFileW(device_path.c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + nullptr); + if (hVol == INVALID_HANDLE_VALUE) return false; + + struct VolExtentsBuf { + DWORD NumberOfDiskExtents; + DISK_EXTENT Extents[256]; + } buf{}; + std::memset(&buf, 0, sizeof(buf)); + DWORD bytes_returned = 0; + BOOL ok = DeviceIoControl(hVol, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, + nullptr, 0, + &buf, sizeof(buf), + &bytes_returned, nullptr); + CloseHandle(hVol); + if (!ok || buf.NumberOfDiskExtents == 0) return false; + disk_number = buf.Extents[0].DiskNumber; + return true; + } + + // 获取指定物理磁盘号上的所有卷GUID路径(封装FindFirstVolumeW,不重复扫描) + // 返回的路径形如 \\?\Volume{GUID}\(带尾部反斜杠) + static bool get_volumes_on_disk(DWORD disk_number, + std::vector &out_volumes) { + out_volumes.clear(); + wchar_t vol_name[MAX_PATH]; + HANDLE hFind = FindFirstVolumeW(vol_name, _countof(vol_name)); + if (hFind == INVALID_HANDLE_VALUE) { + return false; + } + do { + std::wstring guid_path(vol_name); + // FindFirstVolumeW返回\\?\前缀,CreateFile需要\\.\前缀才能打开卷设备 + std::wstring dev_path = guid_path; + if (dev_path.size() >= 4 && dev_path.substr(0, 4) == L"\\\\?\\") { + dev_path = std::format(L"\\\\.\\{}", dev_path.substr(4)); + } + if (!dev_path.empty() && dev_path.back() == L'\\') dev_path.pop_back(); + + DWORD disk_num = 0; + if (!device_get_disk_number(dev_path, disk_num)) continue; + if (disk_num != disk_number) continue; + out_volumes.push_back(guid_path); + } while (FindNextVolumeW(hFind, vol_name, _countof(vol_name))); + FindVolumeClose(hFind); + return true; + } + + // ===== 公开接口 ===== + + // 可写挂载VHD,返回磁盘号、物理路径、卷GUID(不分配盘符) + // 流程:Open(ATTACH_RW|GET_INFO) → Attach(NO_DRIVE_LETTER) → GetVirtualDiskPhysicalPath → 查找卷GUID + bool attach_vhd(const std::wstring &vhd_path, MountedVhdInfo &info, std::string *error) { + // 可写方式打开overlay层(父盘仍是只读的,overlay本身本地可写) + HANDLE hVhd = open_vhd(vhd_path, + VIRTUAL_DISK_ACCESS_ATTACH_RW | VIRTUAL_DISK_ACCESS_GET_INFO, + 1, // 顶层1层可写 + error); + if (hVhd == INVALID_HANDLE_VALUE) return false; + + // 挂载VHD(可写,不自动分配盘符) + ATTACH_VIRTUAL_DISK_PARAMETERS attach_params = {}; + attach_params.Version = ATTACH_VIRTUAL_DISK_VERSION_1; + attach_params.Version1.Reserved = 0; + DWORD err = AttachVirtualDisk(hVhd, nullptr, + ATTACH_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME | + ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER, + 0, &attach_params, nullptr); + if (err != ERROR_SUCCESS) { + if (error) *error = std::format("AttachVirtualDisk (RW) failed: {}", err); + CloseHandle(hVhd); + return false; + } + + // 通过VHD句柄直接获取物理磁盘路径(\\.\PhysicalDriveX),重试等待系统定位 + // 等待:20次 × 100ms = 最长2秒 + wchar_t phys_path[MAX_PATH] = {}; + ULONG path_size = sizeof(phys_path); + err = ERROR_NOT_READY; + for (int retry = 0; retry < 20; retry++) { + path_size = sizeof(phys_path); + err = GetVirtualDiskPhysicalPath(hVhd, &path_size, phys_path); + if (err == ERROR_SUCCESS) break; + Sleep(100); + } + CloseHandle(hVhd); + + if (err != ERROR_SUCCESS) { + if (error) *error = std::format("GetVirtualDiskPhysicalPath failed: {}", err); + return false; + } + + info.physical_path = phys_path; + + // 解析 "\\.\PhysicalDriveX" 中的磁盘号 + DWORD disk_num = 0; + if (swscanf_s(phys_path, L"\\\\.\\PhysicalDrive%u", &disk_num) != 1) { + if (error) + *error = std::format("Failed to parse physical disk path: {}", utils::wstr_to_str(phys_path)); + return false; + } + info.disk_number = disk_num; + + // 在该磁盘上查找卷GUID路径(重试等待卷枚举就绪) + // 等待:20次 × 100ms = 最长2秒 + for (int retry = 0; retry < 20; retry++) { + std::vector vols; + get_volumes_on_disk(disk_num, vols); + if (!vols.empty()) { + info.volume_guid = vols.front(); + return true; + } + Sleep(100); + } + + if (error) *error = std::format("no volume found on disk {}", disk_num); + return false; + } + + bool detach_vhd(const std::wstring &vhd_path, std::string *error) { + HANDLE hVhd = open_vhd(vhd_path, VIRTUAL_DISK_ACCESS_DETACH | VIRTUAL_DISK_ACCESS_GET_INFO, 0, error); + if (hVhd == INVALID_HANDLE_VALUE) return false; + + DWORD err = DetachVirtualDisk(hVhd, DETACH_VIRTUAL_DISK_FLAG_NONE, 0); + CloseHandle(hVhd); + + if (err != ERROR_SUCCESS) { + if (error) *error = std::format("DetachVirtualDisk failed: {}", err); + return false; + } + return true; + } + + bool create_diff_vhd(const std::wstring &vhd_path, const std::wstring &parent_vhd_path, std::string *error) { + // 如果上次运行遗留了已挂载的overlay.vhd,先尝试卸载再删除 + std::string detach_err; + (void) detach_vhd(vhd_path, &detach_err); + DeleteFileW(vhd_path.c_str()); + + VIRTUAL_STORAGE_TYPE vst = {}; + vst.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHD; + vst.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT; + + CREATE_VIRTUAL_DISK_PARAMETERS params = {}; + params.Version = CREATE_VIRTUAL_DISK_VERSION_2; + params.Version2.UniqueId = GUID_NULL; + params.Version2.MaximumSize = 0; // 差分盘跟随父盘大小 + params.Version2.BlockSizeInBytes = 0; + params.Version2.SectorSizeInBytes = 512; // VHD1必须为512 + params.Version2.ParentPath = parent_vhd_path.c_str(); + params.Version2.SourcePath = nullptr; + params.Version2.OpenFlags = OPEN_VIRTUAL_DISK_FLAG_NONE; + params.Version2.ResiliencyGuid = GUID_NULL; + + // Version2强制要求VirtualDiskAccessMask必须为VIRTUAL_DISK_ACCESS_NONE + HANDLE hVhd = nullptr; + DWORD err = CreateVirtualDisk(&vst, + vhd_path.c_str(), + VIRTUAL_DISK_ACCESS_NONE, + nullptr, + CREATE_VIRTUAL_DISK_FLAG_NONE, + 0, + ¶ms, + nullptr, + &hVhd); + if (err != ERROR_SUCCESS) { + if (error) + *error = std::format("CreateVirtualDisk failed: {} path={} parent={}", err, + utils::wstr_to_str(vhd_path), utils::wstr_to_str(parent_vhd_path)); + return false; + } + CloseHandle(hVhd); + return true; + } +} // namespace vhd_mount diff --git a/src/vhd_mount.h b/src/vhd_mount.h new file mode 100644 index 0000000..c9cc132 --- /dev/null +++ b/src/vhd_mount.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +namespace vhd_mount { + // VHD挂载后返回的信息 + struct MountedVhdInfo { + DWORD disk_number; // 物理磁盘号 + std::wstring physical_path; // 形如 \\.\PhysicalDriveX + std::wstring volume_guid; // 形如 \\?\Volume{GUID} (带尾部反斜杠) + }; + + // 可写挂载VHD,返回磁盘号、物理路径、卷GUID(不分配盘符) + // 内部依次:OpenVirtualDisk → AttachVirtualDisk(NO_DRIVE_LETTER) + // → GetVirtualDiskPhysicalPath → 查找卷GUID + bool attach_vhd(const std::wstring &vhd_path, + MountedVhdInfo &info, + std::string *error); + + // 卸载VHD文件 + // vhd_path: 与挂载时相同的路径 + // error: 输出错误信息 + // 返回:成功返回true + bool detach_vhd(const std::wstring &vhd_path, + std::string *error); + + // 创建差分VHD(overlay层),父盘指向parent_vhd_path + // vhd_path: 输出VHD文件路径(将被创建,如exe目录\overlay.vhd) + // parent_vhd_path: 父盘NT路径,如 \\?\FscryptDisk_APP_N\internal_N.vhd + // error: 输出错误信息 + // 返回:成功返回true + bool create_diff_vhd(const std::wstring &vhd_path, + const std::wstring &parent_vhd_path, + std::string *error); +} // namespace vhd_mount