DevAdeelAhmad/polinrider-removal-guide

GitHub: DevAdeelAhmad/polinrider-removal-guide

一份面向 macOS 开发者的 PolinRider npm 供应链恶意软件检测、清除与凭据恢复的实战应急响应指南。

Stars: 0 | Forks: 0

# PolinRider npm 供应链恶意软件 — 检测与清除指南 (macOS) 一份实用且经过实践检验的指南,用于识别、遏制和清除 PolinRider 家族的加载器。该恶意软件会将自身注入到 JavaScript/TypeScript 项目 配置文件中,并通过 Node 工具链重新运行。专为 macOS 上使用 Node、npm/pnpm 以及 VS Code / Cursor 等编辑器的开发者编写。 ## TL;DR 1. 一个隐藏且经过高度混淆的 `node -e` 进程不断生成,并尝试从 keychain 中读取 您的 `github.com` 凭据(您会收到不断重复的 keychain 密码提示)。 2. 其源头是一个 **附加到您的项目配置文件**(`postcss.config.*`、`tailwind.config.*` 等)中的混淆代码块,通过大量空格将其隐藏在屏幕可视区域之外。 3. 当您的编辑器/工具链加载这些配置时,它会重新运行,并且可以通过编辑器中自动 运行 `npm install`/`npm exec` 的功能(例如 TypeScript Automatic Type Acquisition、MCP servers)潜入。 4. 修复方法 = 查找并从源文件中剥离注入的代码块,清理 build/exec 缓存, 重新安装 Node,验证没有任何进程重新生成,然后 **从另一台不同的设备更改所有凭据**。 ## 1. 如何识别它 ### 症状清单 - macOS keychain 提示不断重新出现: *“security 想要使用您的储存在 'github.com' 中的机密信息”* — 而且点击 **拒绝 (Deny)** 只会让它再次弹出。 - 您的 IDE、language server 或 `localhost` 开发服务器停止正常工作。 - 活动监视器中有许多几乎完全相同的 `node` 进程(具有相同的线程/端口计数)。 - 没有明显原因的高后台 CPU 或风扇活动。 ### 使用终端进行确认 查找混淆的加载器进程。特征模式以一个全局 赋值和一个自定义的字符串重组解码器开始: ``` ps -Awwo pid,ppid,command | grep "global\['_V'\]" | grep -v grep ``` 该家族中常见的特征(在进程或文件中出现以下任何一项都是危险信号): - `global['_V']='A10'` 或 `global['!']='10'` - 一个通过 `String.fromCharCode(127)` 和 `.split().join()` 链构建字符串的自解码函数 - 诸如 `nWk(9608)` 之类的调用,以及诸如 `_$_1e42`、`_$_16d1` 之类的混淆标识符 - 内联的 `node -e ""` 调用 - `global["_t_t"]`、`global["_t_c"]`、`clientCode`、`global._R` 脚手架代码 ### 计数检查其是否正在重新生成 ``` ps -Awwo command | grep -c "global\['_V'\]" ``` `0` 表示 *此时* 没有任何东西在运行。由于加载器仅在触发时才会启动, 因此需要随时间观察它,而不是仅信任一次检查结果: ``` for i in $(seq 1 36); do echo "$(date +%H:%M:%S) count=$(ps -Awwo command | grep -c "global\['_V'\]")" sleep 5 done ``` 在几分钟内运行此命令,**同时打开您的编辑器和项目**。如果您在打开文件夹或编辑文件时, 看到计数从 0 跳到 1+,说明您已经重现了触发条件。 ## 2. 在磁盘上查找源头 加载器通常附加在合法的项目配置文件中,通过一大段空格与 真正的配置分隔开,因此在您滚动查看时它是隐藏的。搜索您的代码目录: ``` grep -rIl \ -e "global\['_V'\]='A10'" \ -e "global\['!'\]='10'" \ -e "_\$_1e42" \ -e "nWk(9608)" \ ~/Code \ --include="*.js" --include="*.mjs" --include="*.cjs" --include="*.ts" --include="*.tsx" \ 2>/dev/null | grep -vE "node_modules|/\.next/" ``` (将 `~/Code` 替换为您的代码仓库所在的位置。) ### 区分真实命中与误报 一些合法文件包含大型 base64/编码的代码块,可能会匹配到宽泛的 grep 搜索结果。 **不要删除这些文件:** - Prisma engine 文件:`query_compiler_*.wasm-base64.js` / `.mjs` - WASM bundles:`HermesParserWASM.js` 及类似文件 - 您自己在 `.next/`、`dist/`、`build/` 下的构建输出(可重新生成 — 清理源码后重新构建即可) - 您或他人添加的任何扫描脚本(例如,专为此恶意软件命名的文件)— 它们为了检测恶意软件,是 *故意* 包含该特征的 **真实的命中** 是那些本应简短且易于阅读的手写配置文件, 例如: - `postcss.config.mjs` / `postcss.config.js` - `tailwind.config.js` / `tailwind.config.ts` - `next.config.*`、`vite.config.*` 等 检查可疑文件以确认 — 查看第一行和最后一行: ``` f=~/Code/path/to/postcss.config.mjs wc -l "$f"; echo "--- head ---"; head -5 "$f"; echo "--- tail ---"; tail -5 "$f" ``` 确认感染的文件看起来像是一个正常的配置(例如以 `export default config;` 结尾),然后是一大堆空格,接着在同一行末尾出现 `global['!']='10';var _$_1e42=...`。 ## 3. 移除注入的 payload 备份每个文件,然后从注入标记处开始删除直到文件末尾的所有内容 (这保留了您的真实配置,仅移除了恶意的尾部代码): ``` for f in \ ~/Code/path/to/postcss.config.mjs \ ~/Code/path/to/tailwind.config.js; do cp "$f" "$f.infected.bak" # delete from the injected marker through the end of the file perl -0pi -e "s/\s*global\['!'\]='10';.*\$//s" "$f" echo "=== $f ===" tail -3 "$f" echo "signature remaining: $(grep -c "_\$_1e42\|global\['!'\]" "$f")" done ``` 每个文件上显示 `signature remaining: 0` 表示它已清理干净。验证文件是否仍然 以您的真实配置结尾。 验证清理过的文件后,删除 `.infected.bak` 隔离副本: ``` rm ~/Code/path/to/*.infected.bak ``` ### 清除受污染的构建输出和缓存 从受感染配置编译出的构建产物会携带 payload。请删除它们, 以便它们能干净地重新生成: ``` rm -rf ~/Code/*/.next ~/Code/*/dist ~/Code/*/build ``` 同时清除可能执行或存储了受感染安装包的工具链缓存: ``` rm -rf ~/Library/Caches/typescript # TypeScript auto-acquired @types cache rm -rf ~/.npm/_npx # npx exec cache (re-created cleanly on demand) ``` ## 4. 阻止其重新运行(关闭触发路径) 如果编辑器功能不断自动运行安装并重新触发加载器,那么仅仅清理文件是不够的。 已知存在两种触发路径: ### a) TypeScript Automatic Type Acquisition (ATA) 当您打开/编辑 TS 文件时,编辑器会自动运行 `npm install @types/...`。请在 您的编辑器 `settings.json` 中禁用它: ``` { "typescript.disableAutomaticTypeAcquisition": true } ``` ### b) 在启动时执行 `npm exec` 包的 MCP servers 编辑器的 MCP 集成可以在每次编辑器启动时通过 `npm exec` 启动包。审查您的 MCP 配置并禁用任何您不完全信任的 server,然后 确认它们都没有生成加载器(请参阅下文的验证步骤)。 ### c) 全局阻止安装生命周期脚本(深度防御) ``` npm config set ignore-scripts true pnpm config set ignore-scripts true ``` ## 5. 重新安装 Node 工具链(推荐) 如果加载器在清理配置后依然存在 — 即,在源文件干净的情况下,它仍然会从新的触发中 生成 — 则注入可能存在于 Node/npm 执行 层中,而不是在您可以 grep 到的文件中。重新安装 Node 会替换该层, 而不会清除您的操作系统或项目(以 Homebrew 为例): ``` # 首先停止所有正在运行的任务 pkill -9 -f "global\['_V'\]" # 移除 node 及其所有缓存 brew uninstall --ignore-dependencies node rm -rf ~/.npm ~/.npmrc ~/.node-gyp ~/Library/Caches/typescript rm -rf /opt/homebrew/lib/node_modules # 重新安装 brew install node ``` 之后全新安装您的全局包(仅限您实际使用的工具),例如: ``` npm install -g pnpm # 以及你自己的必备工具,尽量保持最简 ``` 建议将 Prisma、Next、Tailwind、ESLint 和 Prettier 等工具作为项目 `devDependencies` 保留,而不是全局安装 — 这样可以减少 exec 的受攻击面。 ## 6. 验证您已彻底清理 运行定时监控,**同时触发所有操作**(打开您的编辑器,打开项目, 让 MCP servers 启动,编辑 TS/配置文件): ``` for i in $(seq 1 36); do echo "$(date +%H:%M:%S) count=$(ps -Awwo command | grep -c "global\['_V'\]")" sleep 5 done ``` 一个干净的结果是在整个时间窗口内的每一行都显示 `count=0`,包括触发器 启动时。同时重新运行第 2 节中的源码扫描 — 它应该什么也不返回 (或者仅返回故意设置的扫描脚本)。 再次检查之前被感染的配置在您于编辑器中 编辑后 **没有** 被重新写入恶意代码: ``` grep -c "global\['!'\]='10'" ~/Code/path/to/postcss.config.mjs # expect 0 ``` ## 7. 凭据 — 无论情况如何,都请执行此操作(从另一台干净的设备上) 加载器的目的是窃取凭据(不断重复的 keychain 提示就是它在尝试 读取您的 GitHub 凭据)。清理机器 **并** 不能撤销任何 已经离开机器的泄露信息。从手机或另一台受信任的计算机上: - **GitHub:** 更改您的密码;撤销 **所有** Personal Access Tokens、SSH keys 和已授权的 OAuth apps;重新检查 2FA。 - **npm / pnpm:** 更新 token。检查您的 npm 账户中是否有在您不知情的情况下发布的任何包版本 (该家族可以通过您的 token 重新发布)。 - **机器上保存的任何其他内容:** 服务器/VPS 的 SSH keys 和 root 密码、secrets 管理器、对象存储 keys、云提供商凭据。 在本地删除过期的 keychain 条目,以便您的工具能重新存储新的凭据: ``` security delete-internet-password -a -s github.com ``` ## 8. 阻止再次感染(人们常忽略的部分) 如果被注入的配置文件 **被提交到了 git**,那么恶意软件就会存在于您的历史记录中, 并且可能存在于您的远程仓库中 — 因此,全新的 `clone`/`pull` 会再次使您感染。对于每个仓库: ``` cd ~/Code/path/to/repo git status git diff # confirm only the payload is removed git log --oneline -5 -- # see when it was introduced git show HEAD: | grep -c "global\['!'\]='10'" # 0 = remote/committed copy is clean ``` - 如果提交的/远程的副本仍然包含 payload,请同时在那里进行清理,提交 修复,并执行 push。审查受影响组织中的每个仓库。 - 检查任何其他克隆了这些仓库的机器、团队成员、CI runner 或服务器。 - 在远程仓库清理干净之前,请将每次 clone 视为再次感染的媒介。 ## 快速参考 — 一次性健康检查 ``` echo "=== running loaders ==="; ps -Awwo pid,command | grep "global\['_V'\]" | grep -v grep echo "loader count: $(ps -Awwo command | grep -c "global\['_V'\]")" echo "=== keychain-theft processes ==="; ps -Awwo pid,command | grep "find-internet-password.*github" | grep -v grep echo "=== inline node -e processes ==="; pgrep -fl "node -e" | grep -v grep echo "=== source sweep ==="; grep -rIl "global\['!'\]='10'\|global\['_V'\]='A10'\|_\$_1e42" ~/Code \ --include="*.js" --include="*.mjs" --include="*.cjs" --include="*.ts" --include="*.tsx" 2>/dev/null \ | grep -vE "node_modules|/\.next/|\.infected\.bak" echo "=== done — all sections should be empty ===" ``` ## 注意事项与警告 - 混淆的字符串、数字常量和标记名称 **在不同变体之间会发生变化**。 请依赖结构特征(内联 `node -e`、字符串重组解码器、 配置尾部注入、访问 `github.com` 的 keychain)而不是确切的字节数据。 - 单一时间点的“0 进程”检查并不能作为证明。请在触发条件之间并 随着时间的推移进行验证。 - 如果加载器在清理配置 **并且** 完全重新安装 Node 并清除了 缓存后仍然存在,则持久化机制可能位于工具链之下。在这种情况下,彻底重装 OS 才是最可靠的途径 — 仅从已知早于感染时间的 commit 还原项目代码,并在审查完毕之前禁用脚本安装依赖。 - 绝不可商量的一步是从干净的设备上进行凭据轮换。即使其他一切都正常,也要执行此操作。 *欢迎自由分享。请根据您的环境调整路径和标记。*
标签:AMSI绕过, Cutter, DNS 反向解析, macOS安全, MITM代理, npm安全, Subfinder, 威胁检测, 库, 应急响应, 恶意软件清除, 暗色界面, 网络信息收集