chore(tools): 入库汽水签名工具链(独立签名服务与逆向记录)

- tools/mssdk-sign/:C++ 实现的签名服务,附 mssdk 内部结构记录、MinGW 与
  MSVC 两套构建脚本、调用示例 client.mjs 与自检入口
- signer/server.mjs:Node 实现的等价签名服务,面向「主服务不在 Windows 上」
  的场景——bdms.node 是 Windows PE 原生模块,只能由 Windows 版 Node 加载,
  故把「加载官方模块」关进独立进程,主服务经 HTTP 取 X-Helios / X-Medusa

两者都不被 src/sdk 引用:运行期走 sdk/qs/mssdk.ts 直接加载官方 bdms.node,
它们是组件升级、签名失效时重新定位入口并验证的手段,放在仓库里才能跟着
代码一起维护。

专有二进制(mssdk-sign.exe、metasecml.dll)由 tools/mssdk-sign/.gitignore
排除,不随仓库分发。
This commit is contained in:
snowy-yang committed 2026-10-03 10:50:34 +08:00
1 parent dd28ba157c
commit 14f7295fd4
8 files changed
+1392

No files matched your search

+186
View File
@@ -0,0 +1,186 @@
# mssdk-sign —— 汽水音乐 metasec 签名 HTTP 服务
把汽水 PC 客户端自带的 `metasecml.dll`(字节 metasec,官方签名主体 *Douyin Vision Co., Ltd.*)
关在一个进程里,对外只暴露一个 HTTP `/sign`。于是:
- **Windows**:直接跑。
- **Linux**:丢进 **Wine** 跑,宿主机的服务(MusicServer 等)用 HTTP 调它 —— 专有 DLL 不出 Wine 前缀,宿主机不需要 Windows 运行时。
对应地,MusicServer 侧 `sdk/qs/mssdk.ts` 里那套「进程内 require bdms.node」可以换成「HTTP 调本服务」,
跨平台部署就不用装官方客户端了(二进制仍需自己从官方客户端里取,不入库、不再分发)。
## 文件
| 文件 | 说明 |
|---|---|
| `mssdk-sign.cpp` | 全部实现(单文件,无第三方依赖:Win32 + Winsock + 自带 MD5/JSON) |
| `build.bat` | Windows/MSVC 构建 |
| `build-mingw.sh` | Linux 交叉编译成 exe(给 Wine 跑) |
| `client.mjs` | Node 客户端示例(可直接 `node client.mjs --send` 验证) |
| `REVERSE.md` | **逆向记录**:DLL 身份/壳/真实调用 ABI/行为规则/升级后如何重新定位偏移 |
## 构建
Windows(MSVC,需要 VS BuildTools):
```bat
build.bat
:: 生成 mssdk-sign.exe
mssdk-sign.exe --selftest --dll .\metasecml.dll
```
Linux → Wine(MinGW-w64 交叉编译,静态链接,不依赖 libstdc++/winpthread DLL):
```bash
sudo apt install mingw-w64
./build-mingw.sh # 生成 mssdk-sign.exe
```
> 注意:源码含中文注释/字符串,**MSVC 必须带 `/utf-8`**(`build.bat` 已带),否则会报一堆莫名其妙的语法错误。
## 运行
Windows:
```bat
mssdk-sign.exe --port 16889 --dll .\metasecml.dll --token 你的口令 -v
```
Wine(建议放在自己的 prefix 里,和 DLL 同目录):
```bash
mkdir -p /opt/mssdk-sign && cd /opt/mssdk-sign
cp mssdk-sign.exe metasecml.dll .
WINEPREFIX=/opt/wine-mssdk wine mssdk-sign.exe --selftest --dll ./metasecml.dll # 先自检
WINEPREFIX=/opt/wine-mssdk wine mssdk-sign.exe --port 16889 --bind 0.0.0.0 --token 你的口令 -v
```
systemd 单元示例:
```ini
[Unit]
Description=mssdk-sign (metasec signer in Wine)
After=network.target
[Service]
Environment=WINEPREFIX=/opt/wine-mssdk
Environment=WINEDEBUG=-all
WorkingDirectory=/opt/mssdk-sign
ExecStart=/usr/bin/wine /opt/mssdk-sign/mssdk-sign.exe --port 16889 --bind 127.0.0.1 --token CHANGE_ME
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
```
参数:
| 参数 | 说明 |
|---|---|
| `--port <n>` | 监听端口,默认 16889 |
| `--bind <addr>` | 默认 `127.0.0.1`;容器/跨机调用才用 `0.0.0.0`(务必配 token) |
| `--dll <path>` | `metasecml.dll` 路径,默认 exe 同目录 |
| `--token <t>` | 启用鉴权(`Authorization: Bearer <t>` 或 `X-Auth-Token: <t>`) |
| `--sign-rva <hex>` | 手动指定签名函数偏移(默认按 DLL 版本表,见下) |
| `--selftest` | 只做「加载 DLL + 签名自检」后退出(部署前必跑) |
| `-v` | 打印每个请求的签名耗时 |
## 接口
### `GET /healthz`(免鉴权)
```json
{"ok":true,"service":"mssdk-sign","version":"0.1.0",
"signer":"D:/MusicServer/soda-mssdk/metasecml.dll",
"dllVersion":"4.6.1.0","signRva":"0x52af0","auth":true}
```
### `POST /sign`
```jsonc
{
"url": "https://api.qishui.com/luna/pc/track_v2?aid=386088&…", // 必填,带完整 query
"body": "{\"track_id\":\"…\"}", // 可选:给了且没带 x-ss-stub 时,服务端自动补
"headers": { // 必填:真实要发出去的那一份
"content-type": "application/json; charset=utf-8",
"user-agent": "LunaPC/3.7.0(452316191)",
"x-luna-background-type": "foreground",
"x-tt-trace-id": "00-…-…-01"
}
}
```
```json
{
"ok": true,
"headers": {"X-Helios":"…","X-Medusa":"…","X-Neptune":"…"},
"added": {"x-ss-stub":"DBA20AB94302CB03AC25A216DD953258"},
"elapsed_ms": 43
}
```
失败时:`{"ok":false,"error":"…"}` + 400(入参错)/ 401(token)/ 500(签名失败)。
### 三条硬性约定(踩过坑)
1. **`headers` 必须是真实要发出的那一份**,一个都不能少(尤其 `cookie`、`x-ss-stub`)。签名覆盖 URL + 全部请求头,
少一个服务端就回 `HTTP 200 + 空 body`。
2. **`x-ss-stub` = body 的 MD5(大写)**,既是签名输入也要随请求发出。本服务在你传了 `body` 时会自动算并回传
(`added.x-ss-stub`),记得把它一起发出去。
3. **设备要成套**:URL 里的 `device_id` / `iid` / `fp` 必须是你自己的设备;签名和 URL 里的 `device_id` 绑死,
改了 URL 设备号 → 服务端 `200 + 空 body`(实测)。
### 客户端(Node)
```js
import { signRequest } from './client.mjs'
const { headers, added } = await signRequest(url, headersToSend, body)
await fetch(url, { method: 'POST', headers: { ...headersToSend, ...headers, ...added }, body })
```
集成到 MusicServer 时,把 `sdk/qs/mssdk.ts` 的 `signQishuiRequest()` 换成上面这次 HTTP 调用即可
(签名覆盖的 headers 与请求实际发出的 headers 保持同一份、同一顺序)。
自检:
```bash
node client.mjs # healthz + 签一次(不发真实请求)
node client.mjs --send # 顺带发给 api.qishui.com 验证签名被接受
```
## 它是怎么调 DLL 的(逆向结论,2026-10-02)
- 导出表里的 `MSBridgeML` / `MSBridgeOV` / `o0o0` 都是幌子(桩 / 线程状态初始化),
**真正的签名函数在 `image base + 0x52af0`**,原型:
```c
char* sign(const char* url, const char* headers_lines); // headers: "名\r\n值\r\n…"
```
- 返回 `"名\r\n值\r\n…"` 的签名头串,由 DLL 自己的释放函数(`base + 0x1d8c0`)回收。
- **不需要 init**;DLL 版本(本机样本)= `metasecbaseml 4.6.1.0`。
- 调用是**串行**的(内部有全局/TLS 状态),服务端用互斥锁包住,实测 20 并发全部成功。
## 客户端升级了怎么办(DLL 换了,偏移会变)
`--selftest` 会**大声失败**,不会静默出错。定位新偏移:
1. 从新版客户端里取 `metasecml.dll`(解包方式见 MusicServer 的 `sdk/qs/mssdk.ts` 注释)。
2. 拿真实请求自检,确认签名是否被接受(`node client.mjs --send`)。
3. 找新偏移:
- 先在 `KNOWN_VERSIONS` 里加版本,用旧偏移试;不行就:
- 用 MusicServer 仓库里的脚本脱壳 + 定位:`tmp/metasec-rebuild.py`(内存脱壳 + 重建 PE)、
`tmp/metasec-call-test.py`(直接调用试偏移)、`tmp/bdms-abi.py`(读 `bdms.node` 运行时 IAT 槽,
那里躺着真实地址)。找到后用 `--sign-rva 0xXXXXX` 验证,再写进 `KNOWN_VERSIONS`。
## 已知限制 / 未验证项
- **Wine 下未实测**(本机没有 Wine):DLL 依赖的都是 Wine 常规实现的 Win32 API(kernel32/advapi32/ole32/
winhttp/ws2_32/imm32/…),但 metasec 有设备指纹采集,行为可能和原生不同。部署前请先在目标机跑
`wine mssdk-sign.exe --selftest` 和 `node client.mjs --send`。若加载失败,先看 `WINEDEBUG=+loaddll`
的缺失依赖,必要时 `winetricks corefonts vcrun2019`、`winecfg` 里把 Windows 版本设为 Win10。
- `metasecml.dll` 是**官方专有二进制**(签名主体抖音视界),不入库、不再分发;本目录只放源码与构建脚本,
DLL 自己从官方客户端取,放 exe 同目录即可。
- 签名服务本身不联网、不存 cookie;但仍建议只监听 `127.0.0.1` 并设置 `--token`,不要暴露公网。