作者遭遇假招聘题库投递恶意脚本,改为先用本地 LLM 静态扫描未知仓库(git clone --no-checkout + 纯读文件),避免在编辑器中触发恶意代码。
五月,一个"招聘方"给我发了一个 Web3 岗位的上机作业。README 写得很正规,Next.js 结构看起来也很合理,公司听起来也很真实。但隐藏在构建工具里的,是一个 postinstall 脚本,它会解码一段 base64 blob,并从硬编码的 IP 下载第二阶段 payload。如果我当时做了 99% 的求职者都会做的事——git clone 然后 npm install 然后用编辑器打开——那么等我读完第一行代码时,信息窃取木马就已经在我电脑上运行了。
这还不是最后一个。这类虚假招聘方诱饵已经形成了一个产业,而且 payload 几乎从不藏在 src/ 里。它藏在人们会略过的地方:生命周期脚本、配置文件、一个只有奇怪函数的"工具"文件。所以我改变了默认行为。现在每个未知仓库在编辑器触碰之前,都会先经过本地 LLM 的分类审查。不执行任何代码,不安装任何依赖,只做静态读取。
零号法则:永远不要让仓库执行任何东西
整个流程的关键在于让仓库保持 inert(惰性)。有两种安全获取文件的方式:
# Option 1: clone without checkout, inspect the tree first
git clone --no-checkout https://github.com/some-org/take-home-task.git
cd take-home-task
git ls-tree -r HEAD --name-only
# Option 2: download the tarball, no git hooks, no clone at all
curl -L https://github.com/some-org/take-home-task/archive/refs/heads/main.tar.gz \
| tar -xz -C ./quarantine/
我更倾向于 tarball。它无法执行任何东西,而且解压到 quarantine/ 目录能让我保持谨慎。还需要明确说一点:不要在编辑器中打开带有会自动运行任务的插件的文件夹。VS Code 会愉快地执行 workspace 设置、启动配置,有些扩展还会"好心"帮你运行 npm install。用 cat、bat 或下面的 LLM 流程来读取文件。
第一步:无聊的检查,能捕捉大部分 payload
在任何 AI 介入之前,三个简单的检查就能捕捉大多数这类攻击活动:
# Lifecycle scripts are the #1 delivery mechanism
cat package.json | jq '.scripts | with_entries(
select(.key | test("install|prepare|prepublish|postpack")))'
# Dependencies present in package.json but missing from the lockfile
# (a classic evasion: the malicious dep gets resolved fresh at install time)
jq -r '.dependencies, .devDependencies | keys[]?' package.json | sort > deps.txt
jq -r '.packages | keys[]' package-lock.json | sed 's|node_modules/||' | sort > locked.txt
comm -23 deps.txt locked.txt
# Long encoded blobs anywhere in the tree
rg -n --max-columns=200 '[A-Za-z0-9+/]{120,}={0,2}' --glob '!*.lock' --glob '!*.map'
lockfile 不匹配的检查比人们想象的更重要。我剖析过的几个攻击活动,提供了看起来干净的 lockfile 和有问题的 package.json,或者只在脚本中引用了一个 typosquatted 包。当我构建 Argus Lens(lens.noctis.biz)时——一个专门针对这类仓库的扫描器——deps-missing-from-lockfile 实际上是整个工具中最高信号的检查之一。
第二步:把可疑文件喂给本地模型
正则能帮你找到候选对象。判断力才是模型真正发挥作用的地方,而这必须是一个本地模型,因为有时我在处理 NDA 下的仓库,或者我不想让仓库的 URL 离开我的机器。
我在 WSL2 上用 Ollama 运行 qwen2.5-coder,有两个尺寸:1.5b 用于快速扫描所有文件,7b 用于小模型标记出可疑内容时的进一步分析。prompt 是一个分类器,不是聊天:
triage_file() {
local file="$1"
ollama run qwen2.5-coder:7b <<EOF
You are a supply-chain malware analyst. Classify the following file
from an UNTRUSTED repository. Do not summarize what the code claims
to do. Focus on what it actually does.
Answer in exactly this format:
VERDICT: CLEAN | SUSPICIOUS | MALICIOUS
SIGNALS: <comma-separated list, or "none">
EXPLANATION: <max 3 sentences>
Signals to look for:
- decoding of base64/hex strings followed by eval, Function, or child_process
- network calls to raw IPs or unusual domains at import/build time
- reading of environment variables, keychains, browser profile paths,
.ssh, .aws, or wallet files
- code that only runs during install/build, not at runtime
- obfuscation: string array shuffling, charCode arithmetic, packed code
FILE: ${file}
---
$(cat "$file")
EOF
}
然后就是遍历候选文件的循环:
rg -l 'child_process|eval\(|Function\(|fromCharCode|atob|Buffer\.from' \
--glob '!node_modules' quarantine/ | while read -r f; do
echo "=== $f"
triage_file "$f"
done
严格的输出格式在这里发挥了真正的作用。小模型会啰嗦,而"第一行给出 VERDICT"把一个啰嗦的模型变成了可以 grep 和脚本化的东西。
哪些信号真正重要
把这些仓库喂给这个流程过了几十次之后(以及构建了 spectr-ai,我的开源合约审计器,它教会了我很多关于为安全工作提示小模型的知识),真正能把真实 payload 和噪音区分开的信号是:
安装时执行。合法项目很少需要 beyond 原生模块构建的 postinstall。一个会联网或解码字符串的 postinstall 几乎等于板上钉钉的有罪判决。
清单中有但 lockfile 中没有的依赖。上文已经讨论过。这意味着攻击者希望在你的机器上重新进行解析。
编码 blob 加解码器。单独的 base64 字符串通常没问题(内联图片、测试固件)。但如果在 eval、new Function 或 child_process.exec 能触及范围内的 base64 字符串就不是了。
环境变量收割。遍历 process.env,或者构建通往 ~/.ssh、浏览器扩展文件夹或钱包数据目录(如 Local Storage/leveldb)的路径。一个 take-home CRUD 应用没有任何正当理由要知道 MetaMask 把它的状态存在哪里。
effort 不对称。应用代码是 boilerplate 质量,但有一个配置或辅助文件是密集的、压缩过的、或者奇怪地 sophistication。攻击者复制应用但手工制作 payload,接缝会露出来。
1.5b 模型会漏掉东西。作为对许多文件快速过滤是可以的,但我见过它把一个 charCode 混淆的 dropper 标记为"字符串操作工具"。7b 能捕捉我扔给它的大部分东西,但一个有决心的攻击者如果针对开放模型测试他们的 payload,最终会绕过这个。这没关系。我不是在试图建造一个完美的先知,我是在确保那些懒惰的、批量生产的诱饵(这也是大部分)在我不执行任何东西的情况下,两分钟内被捕捉到。
还有:模型读的是你给它的东西。如果你只扫描 .js 文件,payload 就会藏在 .node 二进制文件或构建配置中。先把网撒宽,然后再分类。
整个流程每个未知仓库花我大约三分钟,完全离线运行,而且已经回报了两次。便宜的保险。
你在安装前真的会检查陌生人的仓库吗,还是 npm install 仍然是无意识地发生?