shdwmtr/resident

GitHub: shdwmtr/resident

resident 通过内存级 hook 实时还原 Steam CEF 中被混淆的 CSS 类名,帮助主题开发者构建稳定且面向未来的自定义主题。

Stars: 3 | Forks: 0

# 常驻 🪑[^1] **恢复**(RES**tore**)Steam 内部的类**标识符**(**IDENT**ifiers)。 这是一款极其小巧、高性能且独立的实用工具,通过修改堆内存,实时还原 Steam CEF 中被混淆的压缩类名。它**没有运行时开销**。该工具对于创建稳定且面向未来的主题特别有用。 ``` diff --git a/tmp/before.html b/tmp/after.html index ec676a2..1ca47d7 100644 --- a/tmp/before.html +++ b/tmp/after.html @@ -1,17 +1,17 @@ -
+
``` `resident` 是完全原生的、无依赖的、自包含的且可移植的。它不需要 CEF 远程调试,几乎没有内存占用,并且**不会**写入磁盘。 它完全实现了自举,**绝对不需要**用户进行任何干预。 该工具完全独立于插件加载器,并同时支持 Steam Deck 和客户端。 # 安装[^2] 打包注意事项:resident 是完全无状态的,且没有配置文件。如果你要打包 resident,建议直接将其保留在你的安装文件夹中,并 简单地将 soname/dll 硬链接/软链接(在 Windows 上,默认情况下软链接需要管理员权限,而硬链接不需要)到 webhelpers 目录下。结束 Steam 的 webhelper 进程会导致 Steam 重启它,这会根据情况加载或卸载 resident。 ## Linux ``` # 安装 resident $ mv /path/to/resident.so ~/.steam/steam/ubuntu12_64/libXtst.so.6 # 卸载 resident $ rm ~/.steam/steam/ubuntu12_64/libXtst.so.6 ``` ## Windows 将已安装的 `resident.dll` 二进制文件复制到 `steamwebhelper.exe` 所在的目录中,并将其重命名为 `version.dll`。 如果你是硬链接到 webhelpers 所在目录,同样的规则也适用;它必须被命名为 `version.dll`。 # 解析器[^3]
``` stateDiagram-v2 [*] --> Scanning Scanning --> ReadKey : identifier or '"' Scanning --> Scanning : other ReadKey --> AwaitHash : key + ':"' ReadKey --> Scanning : no match / rewind AwaitHash --> Validate : 18–30 chars + '"' AwaitHash --> Scanning : out of range Validate --> Rewrite : is_css_hash ✓ Validate --> Scanning : plain word / false positive Rewrite --> Scanning : flush + emit 'hash key' ```
`resident` 在字节流上实现了一个线性时间的词法转换器(即[有限状态转换器](https://en.wikipedia.org/wiki/Finite-state_transducer))。 它采用[贪婪](https://en.wikipedia.org/wiki/Greedy_algorithm)、左锚定的 PEG 识别机制,带有单次有界回溯重写,扫描下方 EBNF 语法定义的两种 token 模式。 ## [EBNF](https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form)(扩展的巴科斯范式)语法 ``` pattern ::= quoted_key | bare_key quoted_key ::= '"' key_q '"' ':"' hash '"' bare_key ::= key_b ':"' hash '"' key_q ::= [a-zA-Z_][a-zA-Z0-9_-]* key_b ::= [a-zA-Z_][a-zA-Z0-9_]* hash ::= [a-zA-Z0-9_-]{18,30} ``` ## 高级概述 类模块将按下图所示被重写。这发生在 CEF 层面,Steam 和 CEF 都会以为内容 `B` 就是原始内容,它们双方对此毫无察觉。 在原生 C 绑定、CDP(Inspector Network 标签页)和 JS 层面,这*就是* Steam 原始的 JS —— 尽管事实并非如此。 ``` e.exports = { "duration-app-launch": "800ms", VCenter: "_1T7c8767I5SNmJ3DC5uSr8", BackgroundAnimation: "_1noPMemGV6O50ZVadg1Cxf", "ItemFocusAnim-darkGrey": "_1ulNzAcgjuBmpgvc5wqLMD", "ItemFocusAnim-translucent-white-10": "_3pmHfms_Y73-eEZ6-BSy3j", "ItemFocusAnimBorder-darkGrey": "_335Bbo4P8V_X7iOeO9gc2e", focusAnimation: "_2v_k6SupaG0hlrpoCWsND4" } ``` ``` e.exports = { "duration-app-launch": "800ms", // skipped, not a class defined by our lang. VCenter:"_1T7c8767I5SNmJ3DC5uSr8 VCenter", BackgroundAnimation:"_1noPMemGV6O50ZVadg1Cxf BackgroundAnimation", "ItemFocusAnim-darkGrey":"_1ulNzAcgjuBmpgvc5wqLMD ItemFocusAnim-darkGrey", "ItemFocusAnim-translucent-white-10":"_3pmHfms_Y73-eEZ6-BSy3j ItemFocusAnim-translucent-white-10", "ItemFocusAnimBorder-darkGrey":"_335Bbo4P8V_X7iOeO9gc2e ItemFocusAnimBorder-darkGrey", focusAnimation:"_2v_k6SupaG0hlrpoCWsND4 focusAnimation" } ``` ## 其他重写 `*.classList.add/remove`:这两个方法原本都只接收单个类,在我们的补丁之后不再如此。 * 检测:`classList.add(IDENT[()].IDENT) and classList.remove(IDENT[()].IDENT)` * 重写:`classList.add(...IDENT[()].IDENT.split(" "))` 如果没有此补丁,add/remove 将直接被传入一个被 hook 的类。 ### 修改前: ``` // focusAnimation: "_2v_k6SupaG0hlrpoCWsND4 focusAnimation" ...classList.add(A().focusAnimation) // err: add/remove only accept 1 class per ``` ### 修改后: ``` // focusAnimation: "_2v_k6SupaG0hlrpoCWsND4 focusAnimation" ...classList.add(...A().focusAnimation.split(" ")) // adds both, hell yeah ``` 根据我搜索的结果,这是我们唯一可能遇到问题的情况。 所有其他 API 在运行时都是安全的。(不包括 `document.querySelector` 等。Valve 的开发者很聪明,逻辑上不会在 React 中使用原生的 DOM API) # 基准测试[^4] 这些基准测试是在 Intel i9-14900k 上运行 100 次后的平均修补时间。 事实上;本库的服务器比 Steam 内置的环回速度快了一个数量级。 在启动时,Steam 会线性地读取 steamui/ 中的所有文件,(似乎)没有分块, 并将它们缓存在页面缓存中。使用此库时,Steam 在初次缓存时会*强制*错过 `chunk~*.js`。 这带来了更快、更省资源的启动过程(在我的机器上测量约为 3 秒)。 这就是为什么 Millennium 将主题移到了 steamui/ 之外,因为任何包含大量单独读取操作(`.git`、`node_modules/` 等)的超深目录,都会导致 CPU 在 L3 缓存压力下出现恐慌,根据你的配置,这有时会使启动过程(线性地)变慢 15、20 甚至 n 秒。 # 准确性与可靠性[^5] 修补程序无法生成无效的语法,这完全被该语言排除了。从技术上讲,它*确实可能*通过修改不在类模块中的字符串而产生运行时错误,但它的参数限制非常严格——这极不可能发生。 # Hooking[^6] ## Windows Hooking 依赖于 DLL 查找路径劫持。**steamwebhelper.exe** *链接了* **version.dll**(意味着 wloader 会在 `main()/__constructor__()` 之前将 version.dll 加载到进程中)以用于版本实用工具。 **version.dll** 是 Windows 官方作为内置系统组件发布的,位于 System32 中。历史上,出于安全原因(存在争议),**version.dll** 从未成为 [**KnownDLL**](https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order),这意味着 当 wloader 对 **version.dll** 调用 LoadLibrary 时,它实际上会尝试直接在当前工作目录(cwd)而不是 System32 中查找它。这意味着我们可以在 **steamwebhelper.exe** 所在的任何位置创建一个*伪造的* **version.dll**,如果我们从 System32 导入并重新导出真实 **version.dll** 的符号表,我们就获得了对 Steam webhelper 的内部内存访问权限。接下来,我们就可以设置下面记录的 hook 了。 ## Unix 与 Windows 原理相同,但使用 X11 依赖项作为劫持目标。`libXtst.so.6` 总是由 Steam 的 pressure vessel 启动的 webhelper 加载。pressure vessel 将 `LD_LIBRARY_PATH` 指向位于 `~/.steam/steam/ubuntu12_32/` 的容器。 然而,它们的库路径规范不够严格。我们可以将一个 shim 偷偷放入 `~/.steam/steam/ubuntu12_64/libXtst.so.6`,在搜索路径解析中优先于 `~/.steam/steam/ubuntu12_32/steam-runtime/amd64/usr/lib/x86_64-linux-gnu/libXtst.so.6`,通过 `HOOK_FUNC` 将调用管道引回原函数,并使用 `__attribute__((constructor))` 来设置下面记录的 hook。 ``` flowchart LR S[steam.exe] --> W[Starts steamwebhelper.exe] W --> K[Requests version.dll\nchecks KnownDLLs registry key] K --> N[Not listed in KnownDLLs\nfalls back to standard search order] N --> A[Loader loads our ./version.dll] A --> R[Re-exports real version.dll exports] R --> SW[steamwebhelper.exe runs] A --> CT[constructor runs\ninstall_hook] CT --> DL[dl_iterate_phdr\nfinds cef_browser_host_create_browser\nin libcef.so] DL --> IN[snare_inline_new\npatches bytes at cef fn entry\nto redirect to tramp_] SW --> CEF[CEF calls\ncef_browser_host_create_browser\nclient, url, settings...] IN --> CEF CEF --> TR[tramp_ intercepts\ncef_browser_host_create_browser] TR --> PH[Patches client vtable\nclient->get_request_handler\n-> hooked_get_request_handler] PH --> OC[Calls original\ncef_browser_host_create_browser\nvia snare trampoline] OC --> SW SW --> GRQ[CEF calls\nclient->get_request_handler] GRQ --> HRQ[hooked_get_request_handler\ncalls original get_request_handler\ngets Steam's cef_request_handler_t] HRQ --> PRQ[Patches returned handler\nhandler->get_resource_request_handler\n-> hooked_get_resource] PRQ --> RH[Returns modified\ncef_request_handler_t to CEF] RH --> GRES[CEF calls\nget_resource_request_handler\nwith cef_request_t] GRES --> HRS[hooked_get_resource\nreads request->get_url] HRS --> CHK{chunk~*.js\n+ has local file?} CHK -->|No| PASS[Calls orig_get_resource\nreturns Steam's handler] CHK -->|Yes| DROP[Disposes Steam's handler\nif it returned one] DROP --> CUST[Returns custom\nsteamloopback_request_handler_t\nwith resident read callbacks] ``` # 构建[^7] 所有构建说明均假设宿主机为 Linux。它在 Windows 上也能正常编译,我只是没有相关的文档。 ``` # 构建 resident $ make # 将 resident 永久安装到 steam $ make install # 从 Linux 交叉编译 Windows 版本(需要 `mingw-w64-gcc`) $ make cross ``` 在 Arch Linux 上,使用以下命令安装交叉编译器: ``` $ sudo pacman -S mingw-w64-gcc ``` # 第三方库[^8] [libsnare.h](https://github.com/shdwmtr/libsnare.h) 我编写的高度稳定的、兼容 c/cxx/asm 的单头文件 hooking 库,适用于 x86/x64/arm64。支持 inline hook 和 PLT/IAT hook。跨平台支持 linux/windows/macos。 [^1]: 关于 resident [^2]: 安装 resident [^3]: 了解 resident 解析器的语义 [^4]: Resident 基准测试结果 [^5]: 准确性与可靠性 [^6]: Hooking 算法 [^7]: 构建 libresident [^8]: 第三方库声明
标签:CEF, Steam, UML, 主题工具, 内存修改, 动态链接库注入, 反混淆, 客户端加密, 开发辅助工具