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]
`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]: 第三方库声明
| ``` 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' ``` |
|
# 准确性与可靠性[^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, 主题工具, 内存修改, 动态链接库注入, 反混淆, 客户端加密, 开发辅助工具