洛克王国:世界 — ACE 绕过与自定义 ReShade Addon 实现
接上篇解包分析,本文记录如何绕过 ACE 反作弊系统,编写自定义 ReShade addon 拦截绘制调用,实现隐藏角色鞋子的效果。
一、前情提要
在上一篇中,记录了 AEAD 加密 PAK 的解包过程,以及尝试了 8 种运行时注入方法,全部被 ACE 拦截。结论是:
ACE 是全内核级保护(ACE-BASE + ACE-ADVT + ACE-CORE),有自修复能力,常规手段无法绕过。
但是后来发现 B 站上有人成功实现了 ReShade 画质增强,这说明存在绕过 ACE 的方法。
二、突破点:d3d12.dll
2.1 发现
网友分享的 ReShade 画质增强包可以直接放在游戏 exe 目录下使用,核心文件是:
Win64/NRC/Binaries/Win64/
├── d3d12.dll ← ReShade 6.7.1 (伪装)
├── ReShade.ini
├── ReShade64.dll
├── reshade-shaders/
└── *.addon64 ← 各类 ReShade 插件
关键发现:ACE 的文件扫描不拦截 d3d12.dll。
✅ d3d12.dll → ACE 白名单外?不扫
❌ d3d11.dll → ACE 扫描拦截
❌ dxgi.dll → ACE 扫描拦截
游戏使用的是 DirectX 12 渲染管线,d3d12.dll 是系统合法 DLL。ReShade 利用这个特点,将自身编译为 d3d12.dll 放在游戏目录下,游戏启动时自动加载它,而 ACE 不会拦截。
2.2 ReShade 6.7.1 版
文件: d3d12.dll
实际: ReShade64.dll
版本: 6.7.1.2132
API: v18
这个版本的 ReShade 支持 addon 系统,可以编写 C++ 插件在游戏进程中运行。
三、编写自定义 Addon:ShoeHide
3.1 技术原理
ReShade API v18 提供了 draw_indexed 事件,可以在每个索引绘制调用前拦截:
// 事件签名
bool on_draw_indexed(api::command_list *cmd_list,uint32_t index_count, // 索引数量uint32_t instance_count, // 实例数量uint32_t first_index, // 起始索引int32_t vertex_offset, // 顶点偏移uint32_t first_instance // 起始实例
);
// 返回 true → 跳过该绘制调用
// 返回 false → 正常绘制
核心思路:每个 3D 模型在渲染时会产生固定的 (index_count, first_index) 组合,可以将其哈希化作为该模型的"指纹"。然后拦截匹配的 draw call。
3.2 哈希函数
static uint64_t make_hash(uint32_t index_count, uint32_t first_index) {return ((uint64_t)index_count << 20) | (first_index & 0xFFFFF);
}
这个简单的哈希将索引数和偏移打包为一个 64 位整数。对于不同的模型部件,只要它们的索引数或偏移不同,就会产生不同的哈希值。
3.3 完整代码 (src/main.cpp)
#include <Windows.h>
#include <cstdio>
#include <io.h>
#include <fcntl.h>#include "../reshade/reshade.hpp"#define MAX_SKIP 256
#define MAX_TRACKED 1024static uint64_t g_skip_hashes[MAX_SKIP];
static int g_skip_num = 0;static uint64_t g_tracked_hashes[MAX_TRACKED];
static uint32_t g_tracked_counts[MAX_TRACKED];
static int g_tracked_num = 0;static volatile int g_test_idx = -1;
static volatile bool g_tracking = false;
static WCHAR g_config_path[MAX_PATH] = {};static uint64_t make_hash(uint32_t index_count, uint32_t first_index) {return ((uint64_t)index_count << 20) | (first_index & 0xFFFFF);
}bool on_draw_indexed(reshade::api::command_list *, uint32_t index_count, uint32_t instance_count,uint32_t first_index, int32_t, uint32_t) {if (index_count == 0 || instance_count == 0)return false;uint64_t hash = make_hash(index_count, first_index);for (int i = 0; i < g_skip_num; i++) {if (g_skip_hashes[i] == hash)return true;}int ti = g_test_idx;if (ti >= 0 && ti < g_tracked_num && g_tracked_hashes[ti] == hash)return true;if (g_tracking) {for (int i = 0; i < g_tracked_num; i++) {if (g_tracked_hashes[i] == hash) {g_tracked_counts[i]++;return false;}}if (g_tracked_num < MAX_TRACKED) {int idx = g_tracked_num++;g_tracked_hashes[idx] = hash;g_tracked_counts[idx] = 1;}}return false;
}static void load_config() {int fd = _wopen(g_config_path, _O_RDONLY);if (fd < 0) return;FILE* f = _wfdopen(fd, L"r");if (!f) { _close(fd); return; }g_skip_num = 0;while (g_skip_num < MAX_SKIP && fscanf_s(f, "%llx", &g_skip_hashes[g_skip_num]) == 1)g_skip_num++;fclose(f);
}static void save_config() {int fd = _wopen(g_config_path, _O_WRONLY | _O_CREAT | _O_TRUNC, 0644);if (fd < 0) return;FILE* f = _wfdopen(fd, L"w");if (!f) { _close(fd); return; }for (int i = 0; i < g_skip_num; i++)fprintf(f, "%llx\n", g_skip_hashes[i]);fclose(f);
}DWORD WINAPI hotkey_thread(LPVOID) {bool f6p = false, f7p = false, f8p = false;int cyc = 0;Sleep(2000);while (true) {Sleep(80);bool f6 = (GetAsyncKeyState(VK_F6) & 0x8000) != 0;bool f7 = (GetAsyncKeyState(VK_F7) & 0x8000) != 0;bool f8 = (GetAsyncKeyState(VK_F8) & 0x8000) != 0;if (f6 && !f6p) {g_tracking = !g_tracking;g_tracked_num = 0;g_test_idx = -1;cyc = 0;char b[64];snprintf(b, sizeof(b), "[ShoeHide] Track: %s", g_tracking ? "ON" : "OFF");reshade::log::message(reshade::log::level::info, b);}if (f7 && !f7p && g_tracking && g_tracked_num > 0) {cyc = (cyc + 1) % g_tracked_num;g_test_idx = cyc;char b[128];snprintf(b, sizeof(b), "[ShoeHide] Test #%d/%d hash=0x%llX cnt=%u",cyc, g_tracked_num, g_tracked_hashes[cyc], g_tracked_counts[cyc]);reshade::log::message(reshade::log::level::info, b);}if (f8 && !f8p && g_test_idx >= 0 && g_test_idx < g_tracked_num) {uint64_t h = g_tracked_hashes[g_test_idx];bool dup = false;for (int i = 0; i < g_skip_num; i++) {if (g_skip_hashes[i] == h) dup = true;}if (!dup && g_skip_num < MAX_SKIP) {g_skip_hashes[g_skip_num++] = h;save_config();char b[128];snprintf(b, sizeof(b), "[ShoeHide] SAVED hash=0x%llX", h);reshade::log::message(reshade::log::level::info, b);}}f6p = f6; f7p = f7; f8p = f8;}
}extern "C" __declspec(dllexport) bool AddonInit(HMODULE addon_module, HMODULE reshade_module) {if (!reshade::register_addon(addon_module, reshade_module))return false;char base[MAX_PATH] = {}; size_t len = MAX_PATH;reshade::get_reshade_base_path(base, &len);MultiByteToWideChar(CP_UTF8, 0, base, -1, g_config_path, MAX_PATH);wcscat_s(g_config_path, L"\\ShoeHide.ini");load_config();char buf[64];snprintf(buf, sizeof(buf), "[ShoeHide] v6 loaded, skip=%d hashes", g_skip_num);reshade::log::message(reshade::log::level::info, buf);reshade::register_event<reshade::addon_event::draw_indexed>(on_draw_indexed);CreateThread(nullptr, 0, hotkey_thread, nullptr, 0, nullptr);return true;
}extern "C" __declspec(dllexport) void AddonUninit(HMODULE addon_module, HMODULE reshade_module) {reshade::unregister_event<reshade::addon_event::draw_indexed>(on_draw_indexed);reshade::unregister_addon(addon_module);
}BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD reason, LPVOID) {if (reason == DLL_PROCESS_ATTACH)DisableThreadLibraryCalls(hinstDLL);return TRUE;
}
3.4 热键操作
通过独立线程轮询 GetAsyncKeyState:
| 热键 | 功能 |
|---|---|
| F6 | 切换追踪模式(开始/停止收集 draw call 指纹) |
| F7 | 轮流隐藏已收集的 draw call(实时预览哪个是鞋子) |
| F8 | 将当前选中的 hash 永久保存到配置文件 |
四、编译与部署
4.1 项目结构
ShoeHide/
├── CMakeLists.txt
├── reshade/ ← ReShade SDK headers (v6.7.1)
│ ├── reshade.hpp
│ ├── reshade_events.hpp
│ ├── reshade_overlay.hpp
│ └── reshade_api*.hpp
└── src/└── main.cpp ← 约 170 行 C++
4.2 CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(ShoeHide VERSION 1.0.0 LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)
add_definitions(-DUNICODE -D_UNICODE)
include_directories(${CMAKE_SOURCE_DIR}/reshade)add_library(ShoeHide SHARED src/main.cpp)
set_target_properties(ShoeHide PROPERTIES OUTPUT_NAME "ShoeHide")
set_target_properties(ShoeHide PROPERTIES SUFFIX ".addon64")
4.3 构建
mkdir build && cd build
cmake .. -G "Visual Studio 17 2022" -A x64
cmake --build . --config Release
输出 ShoeHide.addon64,复制到游戏 exe 目录即可。
4.4 部署
将以下文件放在 Win64/NRC/Binaries/Win64/ 下:
d3d12.dll ← ReShade 主体 (rocowg 画质包)
ReShade.ini ← ReShade 配置
reshade-shaders/ ← 着色器文件
ShoeHide.addon64 ← 自定义鞋子隐藏插件
五、实际效果
5.1 衣柜/预览界面
在衣柜里,每个角色部件(头发、衣服、鞋子、挂件等)是独立的 draw call,可以精确隐藏单个部件。
F6 → 开启追踪 → 121 个唯一 draw call 被收集
F7 × 60 次 → 找到鞋子: hash=0x16EC00000
F8 → 保存
结果: 鞋子消失,其余部件完好 ✓
5.2 大世界
在大世界里,UE4 引擎为了性能优化,将角色所有部件合并为单个网格体进行渲染,因此所有部件共享同一个 draw call。
F6 → 开启追踪
F7 循环 → 找到 body hash=0x5B2900000
F8 → 保存
结果: 全身消失(衣服+鞋子+身体一起)
原因:这是 UE4 的场景加载优化——在进入大世界时引擎会将角色部件预合并,减少 draw call 数量。这不是运行时动态合并,所以无法在 draw call 层面分离。
5.3 效果对比
| 场景 | Draw Call 级别 | 隐藏精度 | 效果 |
|---|---|---|---|
| 衣柜/预览 | 独立 | 单部件 | 只遮鞋子 ✓ |
| 大世界 | 合并 | 全身 | 全身消失 |
六、开发过程中的踩坑记录
6.1 编译问题
- CMakeLists.txt 设置
LANGUAGES C但源文件是.cpp,需要改为LANGUAGES CXX UNICODE宏导致_stricmp类型不匹配,需使用 ANSI 版本 API- ReShade SDK 依赖链:
reshade.hpp → reshade_events.hpp → reshade_api.hpp → reshade_api_device.hpp → ...,需要下载全部头文件
6.2 运行时崩溃
- 第一次尝试:在 draw call 回调中使用
std::unordered_set+CRITICAL_SECTION→ 闪退(渲染线程中初始化临界区导致死锁) - 第二次尝试:在回调中进行文件 I/O (
save_config) → 闪退(渲染线程中写文件) - 第三次尝试:使用固定数组替代 STL 容器,去掉所有锁和文件 I/O → 成功
结论:draw call 回调在渲染线程中执行,必须极度轻量——不能分配内存、不能文件 I/O、不能用重型同步原语。
6.3 数据竞争
渲染回调(多线程)和热键线程共享数组,但没有使用互斥锁。这依赖于 ARM/x64 的对齐读写是原子的这一特性,且只在数组末尾追加数据(不修改已有元素)。
6.4 中文路径
Windows 的 ANSI fopen 无法处理中文路径。改用 _wopen + _wfdopen 的宽字符方案。同时 MultiByteToWideChar(CP_UTF8, ...) 将 ReShade 返回的 UTF-8 路径转换为宽字符。
七、技术总结
完整绕过路径
游戏启动→ 加载 d3d12.dll (ReShade 6.7.1)→ ACE 不拦截 d3d12.dll ✓→ ReShade 加载所有 .addon64 插件→ 加载 ShoeHide.addon64→ 注册 draw_indexed 事件钩子→ 按哈希值过滤 draw call→ 鞋子对应的 draw call 被跳过
关键突破点
- d3d12.dll 不被 ACE 扫描:这是整个方案的基础
- ReShade addon API:提供了合法的进程内代码执行渠道
- draw_indexed 事件:精确到每个 3D 模型的绘制拦截
- 固定数组 + 无锁设计:确保渲染线程安全
局限性
- 大世界中 UE4 将角色部件合并渲染,无法在 draw call 层面分离单个部件
- 该方法仅适用于衣柜/预览场景的精确隐藏
- 需要在 UI 界面手动操作 F6/F7/F8 标记目标
八、文件清单
| 文件 | 说明 |
|---|---|
ShoeHide.addon64 |
最终编译产物,放入游戏目录即可 |
src/main.cpp |
约 170 行核心代码 |
CMakeLists.txt |
CMake 构建配置 |
reshade/*.hpp |
ReShade API v18 头文件 |
ShoeHide.ini |
自动生成的配置文件,存储跳过的 hash 列表 |
九、参考资源
- ReShade: https://reshade.me/
- ReShade API 源码: https://github.com/crosire/reshade
- 洛克王国 画质包: B站视频 【洛克王国:世界】画质增强+帧生成 作者分享
- 3DMigoto: https://github.com/bo3b/3Dmigoto
- QuickBMS: https://aluigi.altervista.org/quickbms.htm
- UE4 AES 密钥收集: https://cs.rin.ru/forum/viewtopic.php?t=100672
Disclaimer: 本文为技术研究记录,所有操作均在本地单机环境完成。修改游戏客户端可能违反用户协议。本文内容仅用于学习和研究目的。
