跳到正文
Hacker News· michaelnovati·· 6 天前精选AI 评分83

开源 React 调试工具 Reactime 仓库被植入后门,22 个分支中 20 个受影响

Codesmith's flagship open source project compromised, thousands at risk

AI 导读

Deep Woods Security 披露,Codesmith 旗下 Open Source Labs 的开源 React 调试工具 Reactime 仓库被植入后门,22 个分支中有 20 个携带植入物,仅 master 和 dev 干净。

推荐理由

完整还原了 Reactime 仓库被植入后门的攻击链,并给出可自查的 IOC 与检测规则。

正文 · AI 翻译
PolinRider / Reactime: DPRK supply-chain compromise analysis

关键发现

  • Reactime,一个隶属于 Open Source Labs 的开源 React 调试工具,遭到了入侵。22 个分支中有 20 个携带了植入程序。只有 master 和 dev 是干净的。
  • 恶意提交的日期被回溯到 2020 年,并伪装成单文件更改。它重写了整棵目录树:387 个文件,超过 160,000 行。
  • 在 VS Code 中打开该仓库就足够了。一个 .vscode/tasks.json 任务会在 folderOpen 上运行,并执行一个伪装成 Font Awesome 字体的文件。该文件是 JavaScript。
  • 该植入程序没有硬编码命令服务器。它从以太坊交易的地址字节中读取 C2 IP(NullReceiver 技术)。
  • 在 Windows 上,它会将自身注入已安装的 Electron 应用,其中包括 VS Code 和 Discord,并在每次启动时重新解析其 C2。这种持久化方式并未出现在关于该攻击活动的公开报道中。
  • 该植入程序在行动前会对受害者进行画像分析。在一个 Ubuntu 虚拟机上,C2 回复了 Blocked (BOT) 后便停止了。完整的执行链是在一个为通过检测而构建的 Windows 诱饵上捕获的。
  • 提交给 VirusTotal 的五个样本:两个加载器变体、后门、被木马化的 Discord 核心、被修改的 VS Code 入口。五个样本的检测结果均为零。沙箱未记录到任何行为。
  • 投递方式和 C2 与 PolinRider 攻击活动及 NullReceiver 技术相匹配,这两者均被公开归因于朝鲜。我们将此归因视为与公开报道一致,而非我们自己的判定。

警告

2026 年 9 月 30 日晚上 11:31,Formation 联合创始人 Michael Novati 发布公开警告,称 Reactime 似乎遭到了入侵,人们应停止打开该仓库。他没有技术细节。但他有足够的信息说“停止”。

这是第一个公开信号,出现在任何技术分析存在之前。

随后出现了三个问题:这是真的吗,它是如何运作的,幕后黑手是谁。到 10 月 6 日,第一个答案已足够确凿,可以提交正式公告,Reactime issue #368:二十个分支包含恶意软件,请勿在 VS Code 中打开或构建。

目标

Reactime 是一个用于 React 应用时间旅行调试和性能监控的 Chrome 扩展。在 GitHub 上约有 2,200 颗星,210 个复刻,已上架 Chrome 网上应用店。

它隶属于 Open Source Labs,这是附属于软件工程驻留项目 Codesmith 的非营利开源加速器。Codesmith 的驻留学员将构建和迭代 OSLabs 工具作为项目的一部分,Reactime 多年来在学员批次间传承;其 README 中列有约九十位作者。这正是攻击者想要的画像:一个值得信赖的名字、悠久的历史、众多参与者,以及源源不断克隆它来开发的新开发者。

GitHub 上的 Open Source Labs。Reactime 是其置顶项目之一。

确认入侵

没有在真实机器上运行任何东西。我们将仓库的完整镜像拉取到一个隔离的虚拟机中,共 80,537 个对象,并在触碰任何文件之前先阅读了历史记录。

一个裸镜像克隆。staging 的顶端解析为 f04e99a。

接下来的一切都源自那一个提交。

伪造的提交

在 GitHub 上打开 f04e99a,它看起来平平无奇。提交信息是 Update StateRoute.tsx。作者是一个真实的 GitHub 账户,带有该账户的头像。日期是 2020 年 9 月 2 日。在一个拥有多年历史的分支上,五年前的一次单文件改动是页面上最不起眼的对象。

这些信号全都是自由文本。Git 不会验证是谁写的提交或何时写的;作者姓名、邮箱和时间戳都是提交者自己填写的字段,GitHub 只是原样渲染所给的内容。把已知贡献者的邮箱填进作者字段,GitHub 就会在旁边画出他的头像。把时钟设为 2020 年,GitHub 就把它归档到 2020 年。提交上没有签名,也没有验证徽章,而且没人会去留意它是否缺失。

暴露它的数字就在同一个屏幕上:387 个文件被更改,165,612 行新增,156,243 行删除。这不是一次单文件更新。新增和删除的数量几乎相等,因为整棵树是被整体替换的,而不是被编辑的;植入物是一个 24 行的任务文件和一个“字体”,被夹在 30 万行没人会读的改动里。GitHub 还显示该提交上有一个失败的检查,三个中的一个。没人对此采取行动。

单文件消息,借来的作者,日期标为 2020 年。387 个文件被更改。一个没人看的失败检查。

因为提交日期是 2020 年,它所在的分支看起来处于休眠状态。仓库的近期活动里没有任何东西指向它。开发者拉取 staging 并运行 git log 时,会在顶部看到一个五年前的提交,这对一个很久没人碰过的分支来说很正常。看起来陈旧而常规的被改写历史,正是这场行动有记录的签名;它就是这样先藏在软件包里,再藏在仓库里的。

二十个分支

这不是一个被投毒的分支。我们检查了每个分支是否有植入物的签名,即 .vscode/tasks.json 的存在。在 22 个分支中,有 20 个携带它。功能分支、个人补丁分支、staging、website。只有 dev 和 master 返回时是干净的。

每个分支,一个测试。只有 dev 和 master 是干净的。

加载器在所有二十个分支上字节完全相同:一个 git blob,被二十个分支顶端引用。一个植入物,被种了二十次。

这个选择是操作者对人们如何使用 GitHub 的解读。保持干净的两个分支,正是维护者会看的两个,也是仓库页面默认显示的两个。被命中的二十个,正是贡献者检出以接手某项工作时会用的,也是没人审查的。感染安静的分支,默认视图就保持诚实。

触发器

植入物不会等着被运行。.vscode/tasks.json 定义了一个标记为 eslint-check 的任务,这个名字属于任何 JavaScript 仓库,带有 runOn: folderOpen。在 VS Code 中打开该文件夹,任务就会运行。

标记为 eslint-check。在 folderOpen 上运行。从所有面板中隐藏。

这个任务被设计成在任何地方都能运行且不显示任何东西。命令先尝试 macOS 和 Linux 的 command -v node,再尝试 Windows 的 where node,然后落到 echo '',这样它永远不会大声失败。它被标记为 isBackground 和 hide;其呈现方式为 reveal: never、echo: false、close: true。终端面板永远不会打开。任务对九层目录深处的一个文件运行 node,位于一个测试夹具内,在一个名为 fonts 的文件夹里。

开发者与这个任务之间唯一的关卡是 VS Code 的工作区信任提示,它会询问你是否信任该文件夹的作者。任何克隆 Reactime 来开发它的人都会回答是。这正是该提示的用途。

伪装

该文件是 fa-solid-900.woff2,位于 tests/automated-tests/hooks-redux-router/Frontend/src/redux/actions/public/fonts/。文件名是 Font Awesome 的。目录是没人会打开的测试夹具。在文件树中它是一个图标字体,而字体二进制文件是审查者唯一不会读的那种文件。

加载器,混在合法的字体资源中。

它是 JavaScript。37,717 字节,没有换行,携带该行动的 9-0476-3 构建标记。同一加载器的第二个变体以 babel.config.js 形式发布。

不是字体。哈希和构建标记识别出这是加载器。
两个投递变体,一个加载器。两个哈希都在已公布的 IOC 集合中。

没人看得见它

我们把加载器提交到了 VirusTotal。60 个引擎中 0 个检出。

VirusTotal 自己把这个“字体”归类为 HTML。

原因在文件属性里:HTML 和 ASCII 文本,超长行,没有行终止符。没有明文恶意软件可供签名匹配,而一个披着字体扩展名的文本文件也不会触发启发式检测。

HTML,ASCII 文本,37,717 字节。

解包

加载器经过混淆。我们对其进行了美化,并用 webcrack 运行,以解开字符串数组编码、控制流平坦化和解码器包装。

解开了 3,290 个变换。字符串解码器被命名了。

第一阶段会获取一个更大的第二阶段。我们没有让它执行,而是挂钩了解码器和危险调用,对任何写入磁盘、生成进程或访问网络的操作打桩,然后让它解析自己的字符串。第二阶段以惰性文本形式输出。

载荷无需运行即可解析。

命令通道

代码中没有 C2 地址。植入程序从以太坊区块链上读取它。

配置中有一个硬编码的攻击者钱包、一个要查找的 nonce、一个公共区块链索引器,以及一组合法的以太坊 RPC 提供商。

死投钱包 0xa322...Ef1a 以及用于读取链的 RPC 提供商。

它向链查询攻击者的交易,并按发送者和 nonce 选择一笔。

遍历区块,从攻击者钱包中挑选具有目标 nonce 的那笔交易。

然后它取该交易的目标地址,并从地址字节中读出 C2 IP。前四个字节为主,后四个字节为备。命令服务器从不出现在代码或配置中,而且这种查询看起来就像普通的区块链流量。

“钱包地址”就是 C2 IP。这就是一个函数中的 NullReceiver。

独立研究人员已经记录了同一个 C2 地址和同一个钱包,并将其归因于朝鲜。

相同的 C2,相同的攻击者钱包,相同的归因。

执行与持久化

有了 C2 后,第二阶段会获取下一个载荷并以两种方式运行它:在进程内用 eval 运行,以及作为分离的子进程运行,窗口隐藏且输出被抑制。分离的进程在编辑器关闭后仍然存活。

eval(env + code) 和一个分离的 node -e,带有 windowsHide: true、stdio: ’ignore’。

诱饵

加载器在做任何事之前都会检查其宿主。我们第一次运行是在 10 月 6 日,在一个隔离的 Ubuntu 虚拟机中,只进行到 C2 就止步了:服务器将该机器识别为分析环境,返回 Blocked (BOT),并扣留了第一阶段之后的所有内容。于是我们搭建了一台看起来像开发者使用的 Windows 机器,默认的 PC 名称、一个可信的用户、Chrome、一张壁纸、没有任何虚拟机特征,并于 10 月 10 日在那里引爆了植入程序。它通过了分析器,运行了完整操作,我们从宿主侧捕获了完整的执行链。

Windows 运行显示了什么

完整链条,按其在诱饵上发生的顺序:

  1. VS Code 打开该文件夹。隐藏的 folderOpen 任务针对假字体(即第一阶段加载器)运行 node。
  2. 加载器从以太坊区块链解析出 C2,并通过 socket.io 连接到命令服务器,升级为 WebSocket。
  3. 它重写已安装的 Electron 应用程序。VS Code 得到一个被修改的 main.js 和一个投放的模块 main.inz.cjs。Discord 得到一个被木马化的 discord_desktop_core/index.js。对 npm 的同样尝试因权限问题失败。
  4. 它下载由 C2 提供的私有 Python 3.12.7 运行时并安装。受害者从未联系 python.org。
  5. Python 阶段收集机器信息,并将战利品以加密分片上传的形式外泄,并标记上该机器的身份。

步骤 3 和 4 是公开报道中没有的部分。PolinRider 被记录为一个通过包投递的窃取程序;在这台机器上,它还变成了一个持久化植入物,会从开发者每天打开的两个应用程序中重新感染自身。

在注入模块内部

main.inz.cjs 是后门。它根据被注入的宿主应用程序来标记自己。每次启动 VS Code 或 Discord 时,它都会独立地从区块链重新解析命令服务器并重新连接,因此即使原始克隆被删除且 C2 地址发生变化,植入物也能存活。它的字符串用自己特有的密码隐藏,一种由黄金比例步进的流密码,与加载器使用的种子字符混洗不同;两者被设计为分别解开。从其解码后的字符串表中,我们恢复了它轮换使用的完整以太坊 RPC 提供商列表,其中包括两个在当时的公开报道中尚未被记录过的。

我们提交了后门、被木马化的 Discord 核心以及修改过的 VS Code 入口。

main.inz.cjs。0/57。
哈希值,用于与你自己的遥测数据进行匹配。
没有检测,没有 MITRE 签名,没有 IDS 或 Sigma 命中,没有释放文件,没有网络。沙箱无法搭建一个真实的 VS Code 来注入,因此逻辑从未触发。
Discord index.js。0/61。
VS Code main.js。0/62。

五个样本。没有任何引擎检测到。签名防病毒软件对此次攻击活动视而不见。检测必须来自行为和规则,这就是我们同时发布两者的原因。

时间线

日期事件
2026 年 9 月下旬植入物被推送到二十个 Reactime 分支,提交日期被回溯到 2020 年
2026-09-30Michael Novati 的公开警告
2026-10-06静态分析和首次受控运行;加载器提交至 VirusTotal,0/60;建议作为问题 #368 提交
2026-10-10在 Windows 诱饵上完全引爆;捕获到窃取行为
2026-10-11第二阶段去混淆;对照公开报道检查归因;三个注入样本提交至 VirusTotal,全部 0 检测;发现仓库已发布

归因

与公开报道一致。并非独立判定。

PolinRider 是 Socket 在 2026 年 7 月给一个与朝鲜有关联的、针对开源开发者的供应链攻击活动起的名字:根据 Socket 的统计,从 npm 开始并蔓延到 Go 模块、Packagist 和一个 Chrome 扩展,共有 108 个包中的 162 个恶意发布产物。其记录在案的签名是维护者访问权限被入侵和历史被重写,因此更改看起来陈旧且例行公事。Reactime 符合该签名。不同之处在于投递方式:没有包被投毒,已发布的 npm 包保持干净。仓库本身是载体,而编辑器是触发器。

区块链死投是 NullReceiver 技术,已被命名和记录。从样本中恢复的 C2 地址和攻击者钱包与其他研究人员发布的内容相符。

在样本中观察到且在撰写本文时公开报道中没有的内容:

  1. 用于持久化的 Electron 应用程序注入。 VS Code 和 Discord 被重写,使植入物在每次启动时重新运行。Electron 注入总体上是一种已知技术;该攻击活动对其的使用似乎未被记录。
  2. 注入模块内部的每次启动 C2 重新解析,独立于加载器。
  3. 一种自定义流密码用于注入模块中的字符串混淆,与加载器的方案不同。
  4. 一个向受害者提供自身 Python 运行时的 C2,而不是让受害者自行获取。

以观察结果的形式报告,而非作为一种新技术。

公开参考资料:

你是否受到影响

如果你自 2026 年 9 月下旬以来从除 master 或 dev 以外的任何分支克隆了 Reactime,并在 VS Code 中打开过它,请将该机器视为已遭入侵。已发布的 npm 包 reactime 3.1.1 是干净的;暴露风险来自代码仓库,而非安装。

  • 轮换该机器能够访问的所有凭据。
  • 检查 VS Code 和 Discord 安装是否存在被修改的入口点。哈希值在 IOC 集合中。
  • 将 main.inz.cjs 和加载器哈希值与你的遥测数据进行比对。
  • 在克隆的仓库中审计 .vscode/tasks.json 是否存在 runOn: folderOpen。这是一个合法功能。它不应被指向字体。

方法

全程在隔离实验环境中进行。完整镜像克隆,先阅读再运行。在隔离的 Linux 虚拟机上对第一阶段进行受控运行并记录 TLS 会话密钥,随后在 Linux 运行被拒绝后,在专门构建的 Windows 诱饵上进行完整引爆。第二阶段在单独的中性化测试框架中恢复,解码器被挂钩,所有磁盘、进程和网络调用均被打桩。每个工件都已计算哈希;全部五个样本均已提交至 VirusTotal,此处按返回结果报告。归因留待公开报道,我们自己的观察结果已如此标注。

为何重要

克隆代码仓库如今等同于执行。一个 folderOpen 任务意味着信任决策发生在 git clone,而非 npm start。签名检测对全部五个样本均返回零结果;如果你的控制措施止步于杀毒软件,那么这场攻击活动对你而言就是隐形的。公共区块链是一条无法被关闭且看起来像正常流量的 C2 通道;封禁 IP 毫无用处,下一笔交易就会转移它。而且植入程序在采取任何行动之前会先判断你是否值得感染,因此通用沙箱看到的只是一个干净文件。行为报告必须来自恶意软件所信任的机器。

证据与披露

指标、YARA 和 Sigma 规则、去混淆后的后门以及所有哈希值:

https://github.com/deepwoodssec/polinrider-reactime-analysis

公开公告:Reactime 问题 #368,由 Deep Woods Security 于 2026 年 10 月 6 日提交。

已报告给 GitHub、CISA 和 FBI。

Michael Novati,Formation 联合创始人,发出了第一份公开警报。这份分析之所以存在,是因为他在任何人掌握细节之前就发声了。

来源:Hacker News · deepwoodssec.com