A signed “PuTTY” installer that hides a malware loader inside Node.js

About this post: Thanks to MalwareHunterTeam for flagging this new payload. This analysis and write-up were produced by Claude (Opus 5.5), working as an agent in Claude Code on a REMnux analysis VM. Tools used: Malcat, msitools, osslsigncode, Python (pefile, capstone, pycryptodome), js-beautify, FLOSS, capa, ANY.RUN and Deception Pro.

TL;DR

A file called PuTTY.msi installs the real PuTTY SSH client, and it carries a valid Extended Validation (EV) signature from a Canadian company called "Code Blue Staffing Inc." Alongside PuTTY it drops a genuine, re-signed copy of Node.js with a hidden script baked into it. That script waits five minutes, checks which security product is watching, and then downloads its next stage from a Cloudflare Workers address. The next stage only ever lives in memory.

  • Lure: a signed PuTTY.msi that really does open PuTTY 0.85.
  • Loader: official Node.js 26.7.0 turned into a "single executable application" with an encrypted config and an extra hidden module.
  • Evasion: a 5-minute sleep, a config key that changes every calendar year, its own DNS lookups, ETW/AMSI patching and indirect system calls.
  • C2: machinegirl[.]carvelpolman[.]workers[.]dev hands out a module called get_module_luna over an encrypted key-exchange protocol. In our sandbox it did, and that module then called home to *.normanitavo[.]workers[.]dev.
  • Signer: Code Blue Staffing Inc., Certum EV, serial 71ec769262102d63e053d47269e7e635.
The chain: a signed MSI opens real PuTTY while a re-signed Node.js quietly fetches the next stage.
The chain: a signed MSI opens real PuTTY while a re-signed Node.js quietly fetches the next stage.

But it’s signed!

Code signing is how Windows tells you who published a file. An Extended Validation certificate is the premium version: the certificate authority checks that the company is real before issuing it. That’s why so many people treat a valid signature as a seal of approval.

Here the MSI, node.exe and the helper koffi.node are all signed by "Code Blue Staffing Inc." in Toronto, using a certificate Certum issued on 22 September 2026. Four weeks later it signed an installer that claims to be PuTTY by Simon Tatham. A staffing agency has no reason to ship an SSH client. A brand-new certificate that is immediately used to sign someone else’s product is a pattern we see again and again with signed malware. The certificate has been reported to the CA and added to the CertGraveyard.

The bundled putty.exe is the genuine one, still carrying Simon Tatham’s own signature. The installer opens it at the end through a small helper, decoy_launch.exe, so the victim sees exactly what they expected.

What the installer actually does

MSI files are small databases. You can read their tables to see what an installer will do before running it. This one copies four files to C:\ProgramData\NodeJS\, writes a few marker values under HKCU\Software\NodeJS and HKCU\Software\PuTTY, and has two "custom actions":

  • RunAgent starts node.exe with no arguments, in the background, as the logged-in user.
  • RunDecoy starts decoy_launch.exe putty.exe.

There is no Run key, shortcut or scheduled task, so the installer itself doesn’t set up persistence. Two details still matter. The Node.js files are marked permanent, so they stay behind after you uninstall "PuTTY". And the same custom action fires again on any repair or reinstall.

Node.js, but with a passenger

Node.js can be packaged as a single executable application (SEA). The developer bakes a script into node.exe as a resource, and when that exe starts it runs the script instead of waiting for input. This is a legitimate feature, and it is what makes the trick work: the file is the real Node.js 26.7.0. We compared it with the official download, and no machine code had been changed. The only differences are the injected script, the switch that turns SEA mode on, and three text strings where "Node.js" was renamed to "AppCore".

The hidden script (about 38 KB of minified JavaScript) brings along koffi, a popular library that lets JavaScript call Windows functions directly. The copy is byte-identical to the official koffi 3.0.2 package. With koffi, the script can allocate memory, flip it to executable and jump into it, all from JavaScript.

Settings that expire every New Year

The script’s settings are stored encrypted. The key is the SHA-256 hash of nm-cfg- plus the current year. So the config only decrypts on a machine whose clock says 2026; in 2027 the stager would look for a plaintext config.json next to itself and crash without one. This gives the operator a built-in expiry date and a way to re-arm the stager later. It also means anyone analysing it after New Year needs to set the clock back.

Decrypted, the config is short:

  • Link: hxxps[://]machinegirl[.]carvelpolman[.]workers[.]dev/
  • Request: get_module_luna, POST
  • Wait first: 300 seconds
  • User-Agent: a fake Chrome string claiming "Windows NT 11.0" (no such version exists)

The five-minute wait is aimed at sandboxes. Our ANY.RUN run lasted one minute, so it saw node.exe start and then nothing, and the verdict came from the PuTTY binary rather than from the loader.

Two ways to phone home

After the wait, the script looks at the DLLs loaded into its own process. If it spots SentinelOne, Bitdefender or Sophos, it plays it safe: it uses Node’s normal HTTPS code and does its own DNS lookups against public resolvers (Cloudflare, Google, Quad9, OpenDNS) or Cloudflare DNS-over-HTTPS, so the system resolver never sees the domain.

On every other machine it gets more aggressive. It decrypts a second hidden module (key: nm-httpc- plus the year) and loads it straight into memory. That module switches off two Windows monitoring features. It patches ETW (the event stream many security tools read) with a single "return" instruction, and it patches AMSI (the interface that lets antivirus scan scripts) so every scan answers "invalid argument". Its hidden strings name Windows’ WinINet and BCrypt libraries, including the elliptic-curve key-exchange functions, so it almost certainly performs the same encrypted exchange natively (we didn’t see it on the wire).

Which path the loader takes depends on which security product's DLLs it finds in its own process.
Which path the loader takes depends on which security product’s DLLs it finds in its own process.

Either way the conversation looks the same. The client and server each make a fresh key pair and agree on a shared secret (an "ECDH key exchange"). The request is then encrypted with AES-GCM, so even someone who can read the HTTPS traffic only sees random bytes. The server answers with the next stage.

Loading without touching disk

Whatever comes back is never written to a file. If it’s a Windows program (a PE file), the script maps it into memory itself, fixes up addresses, and overwrites its header with junk so memory scanners can’t spot the familiar "MZ" signature. It then calls the module’s start function on a background thread-pool worker. If it’s raw shellcode, the script just runs it.

To allocate and protect that memory, the script avoids the normal Windows functions that security tools watch. It builds its own small system-call stubs: tiny bits of code that ask the kernel directly, jumping through a syscall instruction inside ntdll so the call still appears to come from ntdll. Think of it as walking in through the staff entrance rather than the front door with the guard.

What happened when we let it run

We let it run in a long-lived sandbox (Deception Pro) with a simulated corporate user. Exactly as the code predicts, node.exe sat quietly for about five minutes, then contacted the carvelpolman worker. Eight seconds later the same process started talking to two new Workers addresses, bluepeak[.]normanitavo[.]workers[.]dev and cedar-stone[.]normanitavo[.]workers[.]dev, with a different network fingerprint. That fingerprint matches Windows’ WinHTTP library, which the loader itself never uses. In other words, the "luna" module arrived, started running inside node.exe, and opened its own command channel. The download was about 240 KB. From then on the module checked in every four to four and a half minutes, rotating between five subdomains of the same worker account (bluepeak, cedar-stone, harbor-atlas, common and fieldnote), a simple trick to make the traffic look less repetitive. The Node process stayed alive afterwards, and it had loaded Windows libraries for data protection (DPAPI), crash dumping and key storage. That could point to credential theft, but those libraries can also load for ordinary reasons, so we can’t say yet.

What we couldn’t see

The second stage itself is still a black box. It travels inside the key-exchange encryption on top of HTTPS, and the keys exist only in the memory of that one node.exe process, so even a perfect network capture doesn’t let us decrypt it. Getting the module would mean dumping that process’s memory while it runs. We also don’t yet know what the "luna" module does on its new channel. The sandbox run is still going and we’ll update this post if the traffic tells us more.

Advice

For regular users: download PuTTY only from its official page at chiark.greenend.org.uk/~sgtatham/putty/, or through winget. PuTTY is signed by Simon Tatham, so if the installer is signed by anyone else, stop. If you ran this file, check for C:\ProgramData\NodeJS\node.exe. Uninstalling "PuTTY" does not remove it.

For defenders:

  • Hunt for node.exe outside normal install paths, especially when it is signed by someone other than the OpenJS Foundation, and for HKCU\Software\NodeJS\Agent.
  • Flag Node processes that load koffi.node and then create private executable memory or thread-pool work.
  • Block or alert on *.carvelpolman[.]workers[.]dev and *.normanitavo[.]workers[.]dev. Look for direct DNS to public resolvers or DoH from non-browser processes.
  • Watch for MSI custom actions launching node.exe from ProgramData.

IOCs

Type Value
MSI SHA256 73ab77da60d7196bce61405c615f8cd03a3157abffaf4a2447bd79fb8082da9f
node.exe (SEA) 823d9e0d1086a5894e126dbff272259731208bd17dc47a0753849ee98818339e
koffi.node (re-signed) f384e2419f200e4eb8b49657be41a0e4327b044e7d571303b68463d417a2d544
decoy_launch.exe 2214430d1611a20f60cd656025b031d9476c5f2d9bdb421ec5d4e87551167453
httpc module (in memory) 7409a74bb808d35d03e4aac7e05991b3b2263a30bc225144d6f520ad264be636
C2 (loader) hxxps[://]machinegirl[.]carvelpolman[.]workers[.]dev/
C2 (stage 2) bluepeak, cedar-stone, harbor-atlas, common, fieldnote .normanitavo[.]workers[.]dev
Request string get_module_luna
Install path C:\ProgramData\NodeJS\
Registry HKCU\Software\NodeJS (Agent, Koffi), HKCU\Software\PuTTY (Installed, Decoy, Launch)
Signer Code Blue Staffing Inc., Certum EV Code Signing 2021 CA, serial 71ec769262102d63e053d47269e7e635
Build path C:\Users\user\Desktop\working\res5\fang\sub\no-mistakes\nodejs\dist\bundle.cjs

Wrapping up

Turning a real, untouched Node.js into a loader is a clever way to dodge "unknown binary" alarms: the big executable is genuine, and everything malicious is a few kilobytes of encrypted data riding inside it. Add an EV signature and a real PuTTY window, and very little looks out of place. If you’ve seen get_module_luna or the carvelpolman / normanitavo workers elsewhere, we’d love to hear about it. Stay safe, and check who signed your installers.