🧠 本文完全由人类撰写:除入侵指标(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 编码的自执行混淆存根被注入到了 _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 — 将 `process.env` 通过 `JSON.stringify` 序列化后发送回来。
- 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 仓库,我可能已经上当了。