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, 威胁检测, 库, 应急响应, 恶意软件清除, 暗色界面, 网络信息收集