Files
MusicServer/tools/mssdk-sign/REVERSE.md
T
snowy-yang 14f7295fd4 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
排除,不随仓库分发。
2026-10-03 10:50:34 +08:00

5.8 KiB

metasec 签名组件逆向记录

面向维护者:汽水客户端升级后签名失效时,先看这里。 相关代码:mssdk-sign.cpp(签名服务);构建与接口用法见同目录 README.md。 记录时间:2026-10-02,样本:soda-mssdk/{metasecml.dll, bdms.node}。

1. 速览

项 结论
组件身份 字节 metasec 官方组件,内部名 metasecbaseml 4.6.1.0(2024-09-04)
数字签名 Authenticode 主体 Douyin Vision Co., Ltd.(DigiCert G4 Code Signing)
保护 自解密壳 + 运行期解密字符串表;LoadLibrary 后内存里即为明文镜像
真实签名入口 metasecml.dll + 0x52af0(本版本),char* sign(const char* url, const char* headers)
需要 init 吗 不需要,冷进程直接调即可出有效签名
有效性 已端到端验证:签名被 api.qishui.com 接受;不签名 / 设备不匹配 → HTTP 200 + 0 字节
产出能力 X-Helios + X-Medusa(部分场景附 X-Neptune);不含 Argus/Ladon/Khronos/Gorgon/a_bogus

2. 两个文件的关系

  • metasecml.dll(8.8MB):算法本体。导出表只有 3 个符号,且都是幌子:
    • o0o0(0x191c0):线程局部状态初始化(读 TEB→TLS 槽,条件成立时 memset 一个 0x148 字节结构)。
    • MSBridgeML / MSBridgeOV(同为 0x1d010):同一个桩 —— 调一次内部函数后无条件返回 true,签名结果不经它返回。
  • bdms.node(164KB):官方客户端的 N-API 胶水。对 JS 暴露 init / generateHttpSignatureHeaders / report; 通过延迟导入 metasecml.dll 的 o0o0、MSBridgeML 来用 DLL。

3. 真实调用 ABI(核心)

// headers: 官方展平形式 —— 每行 "名字\r\n值",行间也用 "\r\n" 连接
char* sign(const char* url, const char* headers_lines);
  • 地址:image base + 0x52af0(该处是 jmp 跳板,实体在 0x52b00)。
  • 返回:"名字\r\n值\r\n…" 的签名头串,堆分配;释放函数 = image base + 0x1d8c0。
  • 线程:内部有全局/TLS 状态,串行调用(服务端已加互斥锁)。
  • addon 是怎么拿到这个地址的:运行时 DLL 改写了它的 IAT 槽(bdms.node 的 .data 偏移 0x25e38), 槽里躺着的就是 base+0x52af0 —— 这解释了「导出表说是 0x1d010,实际却在 0x52af0」的矛盾。
  • 壳的形态:磁盘上只有 PE 头 + 单个 8.8MB 加密节 .xs2(熵≈8.0),.text/.rdata/.data/.xs0 均无 raw 数据; 加载时解密进内存 → 内存 dump 即得完整镜像(重定位已修好)。

4. 行为规则(实测)

场景 服务端反应
不带签名 HTTP 200 + 0 字节
带正确签名 HTTP 200 + 正常 JSON(曲名/时长/档位齐全)
签名后改 URL 里的 device_id HTTP 200 + 0 字节 → 签名与 URL 设备参数绑死
headers 少一个(尤其 cookie、x-ss-stub) HTTP 200 + 0 字节 → 签名覆盖全部请求头
  • x-ss-stub = body 的 MD5(大写),既是签名输入也必须随请求发出。
  • 场景决定头集合:传完整真实头 → X-Helios + X-Medusa(有时附 X-Neptune); 只传一个 user-agent 这类极简头 → 只给 X-Neptune(网页兜底场景)。 所以调用方必须把「真实要发出的那一份头」整套传进来。
  • 输出含时间戳/nonce,每次不同属正常。
  • 签名有效 ≠ 能拿整曲:匿名仍只有 30 秒试听(实测 236.6s 曲目 stream_duration=30), 瓶颈是登录 sessionid,不是签名。

5. 内部结构(RTTI 符号泄漏)

类 / 符号 作用
IMSHttpReqSignModule / MSHttpReqSignModule HTTP 请求签名(本服务的调用目标)
IMSWebsocketReqSignModule WebSocket 请求签名
MSBlackBox / IMSBlackBoxItem 黑盒数据采集
IMSSecDeviceIDModule 设备指纹(device id)
IMSRiskInspectModule / IMSRiskInspector 风控检测
IMSReportModule 数据上报
MSPBSInt32/64、MSPBString(List)(__ms__) protobuf 序列化
MSNetInterceptor::FuncInterceptor + Cronet_UrlRequest_Create、Cronet_Engine_SetMD5Header 注入 Chromium Cronet 网络栈(x-ss-stub 的 MD5 由它算)
MSConfig / MSJSON / MSHttpClient / MSTaskManager 基础设施;日志用 spdlog,源路径 utils/matrix/os/windows/ms_log.cc

6. 保护强度评估

  • 壳是「自解密 + 字符串按需解密」,没有代码虚拟化;脱壳后是正常的 MSVC x64 代码,可读可跟。
  • 敏感字符串(如 X-Helios)静态搜不到,运行期才解密;镜像里没找到明文标准 AES S-box (只有 CRC32 表、Base64 表、TEA/Blowfish 类常量)。
  • 结论:算法级复刻不划算——黑盒调用的集成成本≈0,而且能随官方升级同步。

7. 客户端升级后怎么重新定位偏移

--selftest 会大声失败,不会静默签出无效头。定位步骤:

  1. 从新版客户端取 metasecml.dll(解包方式见 sdk/qs/mssdk.ts 头部注释)。
  2. 跑 mssdk-sign.exe --selftest --dll <新dll>:会打印它读到的 DLL 版本;失败即偏移变了。
  3. 找新偏移(仓库 tmp/ 下现成脚本):
    • tmp/metasec-rebuild.py —— 内存脱壳 + 重建可分析 PE;
    • tmp/bdms-abi.py —— 读 bdms.node 运行时 IAT 槽,直接拿到真实签名函数地址(最省事);
    • tmp/metasec-call-test.py —— 用候选偏移直接调用,看返回是否形如 X-…\r\n…。
  4. 用 --sign-rva 0xXXXXX 验证,通过后写进 mssdk-sign.cpp 的 KNOWN_VERSIONS。
  5. 复验:node client.mjs --send 应得到 HTTP 200 + 非空 body。

8. 边界

  • metasecml.dll / bdms.node 是官方专有二进制,不入库、不随代码分发;各自从自己的官方客户端取,放 exe 同目录。
  • 用途限于个人自用/互操作性研究;不要对外提供签名服务,也不要用于批量刷接口。
  • 签名服务本身不联网、不落 cookie;部署仍建议只监听 127.0.0.1 并设置 --token。