TL博士
2026年7月下旬至8月初的两天内,发布了20个npm包,它们都声称是内部UI组件库和共享后端工具。然而,这些包都没有实现它们所宣称的功能。每个包都包含一个存根类、一个描述该包为内部库的README文件,以及一个名为`.npm`的文件。 _polyfill.js.
有趣的是,这个文件是从哪里调用的。 它不是一个安装钩子 — 清单文件中声明根本没有预安装、安装或后安装步骤。相反,最后一行是 index.js 内容如下: try { require("./_polyfill"); } catch (_) {}有效载荷在应用程序代码首次导入该软件包时运行。安装方式 --ignore-scripts, standard 关于降低安装时供应链风险的建议 并不能阻止它.
什么是 _polyfill.js 其操作是:从三个 Cloudflare Workers 主机之一检索特定于平台的可执行文件,将其写入一个随机名称的临时目录,将其标记为可执行文件,然后将其从父进程分离启动。该可执行文件本身是一个加载器:它按进程名称枚举端点安全产品,将当前用户名与已知的分析环境帐户进行比对,修补内存中的 AMSI 和 ETW 遥测接口,以 Windows 音频组件的名称安装持久化组件,并检索一个几兆字节大小的第三个二进制文件。第三阶段的文件以加密形式到达。 解密后在内存中执行,而不是写入磁盘。.
8月1日,在人工审核确认这些软件包恶意后大约四个小时,20个软件包名称中的19个在25秒内被其发布者从npm上移除。8月2日,其中一个软件包名称被另一个账户重新注册,但代码相同。 本文撰写之时,交付基础设施已能服务于所有三个阶段。
| 20 个 npm 名称,约 74 个已发布版本 | |
| 命名主题 | beaver-ui-* (13) accounts-* (3),以及 afisha-storybook-default, bcore-bravo-eslint-config, arbocrate-sla-prober-arbocrate-sla-prober-core, a.poltoradnev-package-c |
| 触发端口 | 模块导入(require),不是 npm 生命周期脚本 |
| 阶段 | _polyfill.js — 回收和发射 |
| 阶段 | 静态链接加载器,Linux 上的 UPX 打包;端点安全枚举,AMSI/ETW 补丁,持久化 |
| 阶段 | beacon_linux.bin / beacon_mac.bin / update_win.exe4–13 MB;加密交付,内存执行 |
| 交易平台 | Linux x64 和 arm64、macOS 通用版、Windows x64 |
| 发布商 | 两个账户,一套代码库 |
| 状态 | 20个名字中有19个未公开 由运营商发布;1 已重新发布并上线;交付基础设施已上线 |
攻击解剖
第一阶段——包裹
每个成员都发货相同的三文件形状。 index.js 导出一个类 在里面() 设置一个布尔值, 版本(),以及 配置() 它会合并一个选项对象。仅此而已。 README.md 提供安装行, 要求 行,以及句子“内部软件包。有关指南,请参阅 CONTRIBUTING.md”—— 贡献.md 不在压缩包里。
_polyfill.js 大小略小于 5 KB,内容与普通的底层代码类似。它首先进行三项环境检查:
if (process.env.DISABLE_TELEMETRY ||
process.env.ANALYTICS_OPT_OUT ||
process.env.DO_NOT_TRACK)
return; 这些变量是传统的隐私控制措施。它们控制着二进制文件是否被下载和执行。一台设置这些变量的机器。 请勿追踪 完全跳过。
接下来是一个只运行一次的戳记:一个位于 /tmp/.analytics_state 或 分析状态 下 %TEMP% 在 Windows 系统上,修改时间会与一个 20,357 秒的时间窗口进行比较。在这个时间窗口内,模块会立即返回结果,因此在构建过程中重复导入只会产生一次检索尝试,而不是数十次。
目标主机并非以字面值形式存在,而是在运行时由片段组装而成:
["oob-worker.cf102-","baf.workers.d","ev"].join("") 以这种方式构建了三台主机,并在使用前进行顺序打乱,因此每次执行的顺序都不同。检索是通过绑定到 IPv4 的 HTTPS GET 请求实现的。 用户代理 of node-fetch/2.6 — Node 进程可能发出的值。小于 1,000 字节的响应将被丢弃,并尝试连接下一个主机。
如果这三种方法都失败,模块将回退到 DNS 解析。它会发出一个 TXT 查询。 c. 获取数据块计数,然后请求 0. 通过 n. 每次处理十个响应,将它们连接起来,然后对结果进行 Base64 解码。这条路径通过 DNS TXT 记录承载相同的可执行文件,即使在出站 HTTP 请求被过滤但 DNS 解析未被过滤的环境中也能正常工作。
检索到的字节被写入 /var/tmp/.cache_<8-hex> 在 Unix 系统上,或者 %TEMP%\dotnet_diag_<8-hex>.exe 在 Windows 系统中,该文件的文件名显示为 .NET 诊断文件。在 Unix 系统中,该文件的权限设置为 0755。启动命令是: 卵 - 分离:是, 标准输入/输出:“忽略”和 取消引用()因此,子进程的生命周期比启动它的 Node 进程更长;在 Windows 系统上,调用会添加 windowsHide: true.
两个细节完善了整个图景。一个助手被定义为 const _l = (…a) => process.stderr.write(“”) 在检索过程的每个步骤中都会调用该函数——它会写入一个空字符串并丢弃其参数,占据日志记录所在的位置。每个软件包都会附带一个 81 KB 的文件。 lib/telemetry.js 其头部注释描述了一个“分析 SDK”,支持“HTTPS 传输、端点轮换和重试”以及“基于 DNS 的服务发现以实现故障转移”。当前版本不需要它。
第二阶段——装载机
Linux x64 二进制文件是一个静态链接的 ELF 文件,使用 UPX 3.96 压缩。macOS 版本是一个通用二进制文件,支持 x86_64 和 arm64 架构;Windows 版本是一个剥离符号表的 PE32+ 控制台可执行文件。这四个文件均已获取并进行了静态检查;但均未执行。
Windows 构建的导入和字符串表清楚地描述了其行为。它解析了 的IsDebuggerPresent, 检查远程调试器是否存在和 Nt查询信息过程并阅读 SYSTEM\CurrentControlSet\Services\Disk\Enum — 一个注册表路径,其值标识虚拟磁盘控制器。它调用 GetDiskFreeSpaceExW 和 全局内存状态Ex与检查主机资源是否与短期分析实例匹配一致。
它包含一个用户名列表: 当前用户, IT管理员, 李洪, 测试盒, hapubws, 米洛兹, 邓肯, 管理员, 恶意软件, 样品, 病毒, 环境其中一些是公开记录的分析环境的产物。
它还包含一份端点安全产品及其进程名称的列表,这些产品及其进程名称通过以下方式枚举: CreateToolhelp32Snapshot 和 Process32FirstW/Process32NextW:
- 众击猎鹰 (
csfalconservice,CSFalconContainer) - 炭黑 (
cb.exe) - Cylance (
cylancesvc.exe) - Bitdefender的 (
bdagent.exe) - 卡巴斯基 (
avp.exe,kavfs.exe) - 趋势科技和Deep Security (
coreServiceShell,ds_agent.exe) - FireEye/Trellix (
xagt.exe) - ium (
taniumclient.exe) - HitmanPro (
hmpalert.exe) - 松紧带 (
elastic-agent.exe,elastic-endpoint,filebeat.exe,winlogbeat.exe) - Wazuh 和 OSSEC (
wazuh-agent.exe,ossec-agent.exe) - 检查点
- Windows Defender的
还有两个符号: AmsiScanBuffer这是 Windows 公开的用于内存内容扫描的入口点, EtwEventWrite这是 Windows 事件跟踪记录事件的函数。两者都是进程内修改的常见目标,这种修改会抑制这些接口原本会生成的遥测数据。
在 Windows 系统中,持久化功能出现了两次。二进制文件被复制到 %APPDATA%\Microsoft\Windows\Themes\SystemSoundsService.exe并且有两种机制引用它:一个值 软件\Microsoft\Windows\CurrentVersion\运行以及注册为
schtasks /Create /F /SC ONLOGON
/TN "\Microsoft\Windows\Multimedia\SystemSoundsService"
/TR "\"%s\""
/RL HIGHEST 任务路径、任务名称和可执行文件名称都与真正的 Windows 音频组件一致。 /RL 最高 请求最高可用权限级别。
macOS 构建遵循相同的模式,使用适合平台的名称:它会将属性列表写入…… ~/Library/LaunchAgents/com.apple.windowserver.helper.plist 并将其可执行文件放置在 ~/.local/share/runtime/com.apple.runtime它会检查…… /Applications/VMware Fusion.app 和 /库/应用程序支持/VMware工具.
网络功能涵盖三种传输方式: WININET.dll 用于 HTTPS https://%s:%d%s 格式字符串, WS2_32.dll 插座,以及 DNSAPI.dll - 域名查询_A 和...一起 c.%s.%s 格式字符串——与第一阶段使用的块计数查询形状相同。
以上描述是检查二进制文件的导入、字符串和注册表路径后所显示的内容。对同一 Windows 样本进行的公开多引擎分析提供了仅凭静态读取导入表无法获得的细节,此处引用的是该分析结果,而非第一手观察结果。该分析报告称,AMSI 和 ETW 引用不仅已解析,而且已修补。 AmsiScanBuffer in amsi.dll 和 EtwEventWrite in ntdll.dll中 ——它会抑制对进程的内存内容扫描和事件跟踪。此外,它还会将反分析检查报告为基于时间的。 检查远程调试器是否存在并识别出 获取线程上下文 调用此函数作为硬件断点检查,读取调试寄存器。它还会报告一个单实例互斥锁。 Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}以及名称 系统声音服务 作为运行键值。该互斥值并未包含在此处恢复的字符串中——所使用的提取过滤器排除了用大写字母和十六进制字母括起来的标记——因此,它根据公开分析而非直接观察记录在指标表中。
阶段
加载器引用了同一主机上的另一组路径。分析期间所有这些路径均可访问: beacon_linux.bin 大小为 5.3 MB beacon_mac.bin 大小为 13.3 MB update_win.exe 大小为 10.3 MB, arm.bin 大小为 4.3 MB。这些文件未被检索,其行为未知。
对于 Windows 路径,对加载器的公开分析描述了第三阶段的处理方式:它通过 WinINet 获取,以加密形式到达,然后在内存中解密并以反射方式执行——分配内存、解析导入、处理重定位,并通过将控制权传递给新线程。 创建线程因此,第三阶段的数据永远不会写入文件系统,所以基于文件的检测和事后磁盘取证都无法从中恢复任何内容。这也意味着从传输主机检索字节将得到密文,而不是可分析的可执行文件。
时间线
所有时间戳均为 UTC,来自 npm 注册表元数据,该元数据独立于任何观察者记录软件包的创建和取消发布。
| 日期 | 什么是 |
|---|---|
| 2026-07-31 09:25:05 → 09:29:56 | 创建了十个名称,每个名称都从版本开始。 0.0.1大约间隔十三秒。 |
| 2026-08-01 09:38 → 11:29 | 现有名称通过不断膨胀的版本号向上攀升, 6.1.9 通过 6.1.12, 9.9.9 通过 9.9.12, 8.9.3 通过 8.9.6, 5.4.6 通过 5.4.9. |
| 2026-08-01 13:04:14 → 13:04:18 | 另有九个 beaver-ui-* 在四秒钟内创建的名称,每个名称都直接以 12.x 版本发布,没有任何先前的历史记录。 |
| 2026-08-01 ~13:00–14:30 | 经人工审核被判定为恶意版本的 |
| 2026-08-01 17:39:49 → 17:40:14 | 在 25 秒内,20 个未公布的名字中有 19 个依次出现,间隔约 1.2 秒。 |
| 2026-08-02 18:58:41 | a.poltoradnev-package-c 使用不同的维护者帐户重新创建,版本 0.0.2. |
| 2026-08-02 19:20:28 | 版本 33.9.5 以同名出版。 |
| 2026-08-02 20:14 | 所有三个交付主机均服务于第二阶段和第三阶段;DNS 回退响应以数据块计数为依据。 |
其中两条值得仔细探讨。
13:04 的突发操作在四秒钟内将九个软件包添加到注册表中,每个软件包都只有一个版本号在 12 左右的软件包,没有其他软件包。 0.0.1 下面这些。这种规模的版本号,基于一天前才发布的名称,根本无法描述发布历史。
取消发布流程同样机械:19 个名称,25 秒,均匀间隔。npm 将这些记录为普通的取消发布事件,而不是注册表发起的删除操作留下的占位符,后者会将操作归咎于发布帐户,而不是注册表。
妥协指标
主机和路径已被禁用。请勿在生产系统中解析或检索它们。
| 类型 | 价值 |
|---|---|
| 送货员 | oob-worker[.]cf99-9b3[.]workers[.]dev |
| 送货员 | oob-worker[.]cf102-baf[.]workers[.]dev |
| 送货员 | oob-worker[.]cf103-070[.]workers[.]dev |
| DNS传输 | tin[.]dl[.]well1[.]site (Linux x64) tina[.] (arm64) ldr[.] (苹果系统), win[.] (Windows)— 数据块计数 c.<domain>数据位于 <n>.<domain> |
| 第三阶段路径 | /pkg/package, /pkg/package-arm64, /pkg/loader_mac, /pkg/package.exe |
| 第三阶段路径 | /pkg/beacon_linux.bin, /pkg/beacon_mac.bin, /pkg/update_win.exe, /pkg/arm.bin |
| 删除路径(Unix) | /var/tmp/.cache_<8-hex> |
| 删除路径(Windows) | %TEMP%\dotnet_diag_<8-hex>.exe |
| 持久性(Windows) | %APPDATA%\Microsoft\Windows\Themes\SystemSoundsService.exe; 任务 \Microsoft\Windows\Multimedia\SystemSoundsService; 运行键值 SOFTWARE\Microsoft\Windows\CurrentVersion\Run\SystemSoundsService |
| 互斥锁(Windows) | Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9} |
| 持久性(macOS) | ~/Library/LaunchAgents/com.apple.windowserver.helper.plist;二进制 ~/.local/share/runtime/com.apple.runtime |
| 一次性印章 | /tmp/.analytics_state, %TEMP%\analytics_state (窗口期 20,357 秒) |
| 请求签名 | HTTPS GET,IPv4 固定,用户代理: node-fetch/2.6 |
| 第一阶段文件 | _polyfill.jsSHA-256 392dc9ec85042ce1796bfa9b8c7bf6e05c7710579aab8b7b1eb73788683883af |
| 未引用的文件 | lib/telemetry.jsSHA-256 2266c9b8e199daebe31a9afcb61d9b0d76706b63c92312fc8223d22de42e63c7 |
| 第二阶段(Linux x64) | SHA-256 7e486657f30594afda379b97030252a09a19fe8055e25c9e371544f59bd8e9e3 |
| 第二阶段(Linux arm64) | SHA-256 aafc37589c0d21fb729761a1579ca3b278b95bce152aacfa29243ffd27f5eeb7 |
| 第二阶段(macOS) | SHA-256 c214746c74cae8ece8bdaf69aa05da4db6ce013f9e77452d1eed1a002fd9ba00 |
| 第二阶段(Windows) | SHA-256 d632c2c0c7801c3da57cf849fe4b4ca8351d9a7fe73c31050bf5dbcc96a16791 |
包名
归因和观察到的行为
涉及两个 npm 账户。其中 19 个未公开的名称由一个账户发布; a.poltoradnev-package-c 该账户于8月2日使用不同的账户名和不同的电子邮件域名重新注册。两次注册的代码来源相同,第二次注册也使用了相同的投递主机、相同的DNS备用域名、相同的丢弃路径结构以及相同的运行一次标记。
重新出版的这套资料也展示了它的组装过程。版本 0.0.2 和 33.9.5 除了版本字符串之外,其他部分在字节上都是相同的。 的package.json,并 index.js 发货方式 0.0.2 声明 const VERSION = “33.9.5”这两件器物均取自同一棵树,之后才编号。 0.0.2 发展历史 0.0.2 发布以示支持。
整个软件包集的版本号遵循相同的逻辑。本次活动中的软件包名称更像是组织内部而非公共注册表中的事物:例如 UI 组件集、ESLint 配置、SLA 探测器、以人名命名的软件包。当构建过程通过名称解析依赖项并同时查询公共注册表和私有注册表时,版本号较高的版本将生效。 0.0.1 周一并携带 12.9.6 周二的比赛正好可以进行这样的比较。
这两次发布浪潮之间的差异也表明存在迭代。早期的代码将其有效载荷文件命名为 安装程序.js 并列出了五个配送主机;当前文件名为 _polyfill.js 并列出了三个。早期版本会在启动五秒后删除丢弃的文件,导致进程从已删除的 inode 运行;当前版本则不会。未引用的 lib/telemetry.js 仍然残留在焦油球中,仍在运输,但不再连接电线。
关于紧急停止开关,有一点值得事先说明。cis因为它与常见的解读相悖。 禁用遥测, 分析选择退出和 请勿追踪 这些设置会被优先考虑,并且在执行任何其他操作之前都会进行检查。如果研究人员的分析环境设置了其中任何一个设置,他们会发现该软件包实际上什么也没做。
影响以及防守者能做什么
导入时间触发的实际后果比较狭窄,值得单独说明。 npm install --ignore-scripts 阻止生命周期脚本运行,这是个合理的建议。但在这里不起作用,因为这段代码没有使用生命周期脚本。有效载荷会在应用程序代码、测试运行、打包工具或代码检查工具首次导入模块时运行。在安装后立即构建的 CI 运行器上,这两个操作仅相隔几秒钟。
对于网络狩猎而言,持久有效的信号是结构性的而非字面意义上的。主机、文件名和丢包路径都会改变;而这些结构性特征更难改变:
- 模块入口点,其最终语句为 try { require(“./x”); } catch (_) {} 围绕一个不属于导出 API 的文件。 一个真正包含 polyfill 的包会在顶部导入它并使用其结果。而忽略文件底部导入时出现的所有错误并非真正的错误处理。
- 由主机字符串组装而成 。加入(””) 从一系列片段中。 合法的代码没有理由将主机名拆分到数组元素中。
- 一个包,其声明的依赖项为空,但却试图访问 HTTPS, DNS和 子进程 导入时。 整个发现过程是在包含 UI 组件的软件包中完成检索和执行。
- 版本号与软件包的实际使用时间不符。 几天前,在两位数主版本号发布时首次公布的名称,期间没有任何版本更新,描述的并非其开发过程。
- 写信给 / var / tmp or %TEMP% 其次是 CHMOD 到 0755 和一个分离的产卵。 该序列短小、有序,且很少同时出现良性症状。
在 Windows 主机上,有两种成本低廉且置信度高的检查方法。持久化痕迹是固定的字符串,它们都不应该出现在干净的系统中:一个计划任务在 \Microsoft\Windows\Multimedia\SystemSoundsService一个名为 Run-key 的值 系统声音服务以及一个同名的可执行文件 %APPDATA%\Microsoft\Windows\Themes\真正的 Windows 音频服务并不位于这些位置。其次,加载器使用固定的单实例互斥锁。 Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}它在主机上的存在是一个指标,而且由于加载器将现有的互斥体视为“已经在运行”,因此预先创建互斥体是一种可行的短期预防措施,同时可以安排补救措施。
第三阶段的检测难度更大,值得明确。根据公开分析,该阶段的数据会被解密并映射到内存中,而不是写入磁盘,因此文件扫描和磁盘取证都无法检测到它。可观察到的只有加载器自身的痕迹——持久化条目、互斥锁以及向交付主机或 DNS-TXT 域发出的出站请求。
对于同时依赖私有注册表和公共注册表的环境,此处的缓解措施保持不变:限制内部名称的作用域,使其无法被公共名称满足,并将这些作用域的解析绑定到内部注册表。上述版本膨胀行为仅在更高的公共版本能够获得名称的情况下才有效。
最后,这份撤销声明值得仔细阅读,而非仅仅作为结尾。19 个域名在 25 秒内从注册局移除,其中 1 个域名在一天后以新账户重新出现,整个交付基础设施始终未受影响。移除软件包并不等同于终止操作;在撰写本文期间,主机仍在继续提供各个阶段的服务。任何在 7 月 31 日至 8 月 1 日期间解析过这些域名的依赖关系树,无论该域名是否仍然存在,都应被视为已有机会执行第一阶段。
案例
- Windows 加载器的多引擎分析 — 以上提及的 AMSI/ETW 补丁、反射式内存执行和互斥锁细节的来源。
- npm 脚本文档 — 此营销活动未使用的生命周期事件。
- npm 安装文档 — 封面 –忽略脚本 及其范围。




