typetechniqueconfidencehighcreated2026-06-13updated2026-06-13obfuscationevasionscriptnodejsmalware-familyresearch-target

poem-word-list-steganography

What it does

A custom steganography technique where a malware author composes a fixed-length word list (typically 256 words to map directly to byte values 0x00-0xFF) written as natural-language prose — often a poem or story fragment. Embedded binary payloads are then encoded as sequences of words from this list. At runtime the malware splits the prose into an array, looks up each word to recover its index, and writes the index values back to disk as bytes.

This produces a carrier script that appears to contain harmless English text but actually hides executable payloads inside what looks like poetry. Static string analysis tools see prose, not MZ headers or base64 blobs. ^[/intel/analyses/d0ca14b3ad12100898d69afacfecfbdb186fe1bd801f69aecf355413bf6e502b.html]

Detection / fingerprint

  • A JavaScript/Node.js script that defines a large string of space-separated words (often 256) alongside another large string of space-separated words that reuses the same vocabulary heavily.
  • The presence of a lookup function that splits the first string, searches for words from the second string, and writes index & 0xFF to a buffer.
  • The "poem" string will have exactly 256 unique words (or 256 total words) when split on spaces.
  • The encoded payload strings will reuse the same vocabulary thousands of times, producing extremely long lines (tens of thousands of characters) with low Shannon entropy despite large file size.
  • No base64, no hex, no URL-safe encoding alphabet — the obfuscation is purely lexical.

Implementation patterns observed

const wlist = "word0 word1 ... word255";   // 256 words
const exe = "word53 word7 word201 ...";    // payload as word indices

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);
}

Observed in the letsdiskusscom cluster where the word list is an original 256-word English poem and four PE files plus a BAT script are encoded this way. ^[/intel/analyses/d0ca14b3ad12100898d69afacfecfbdb186fe1bd801f69aecf355413bf6e502b.html]

Reproduce on your own VMs

  1. Create a 256-word list:
    cat > words.txt <<'EOF'
    gentle hush that wraps the midnight air ... (256 words total)
    EOF
    
  2. Encode a file:
    with open('words.txt') as f:
        words = f.read().split()
    assert len(words) == 256
    with open('payload.exe', 'rb') as f:
        data = f.read()
    encoded = ' '.join(words[b] for b in data)
    with open('encoded.txt', 'w') as f:
        f.write(encoded)
    
  3. Verify: The encoded text should decode back to the original file byte-for-byte.
  4. Carrier: Wrap the word list and encoded string in a Node.js script using the writePositionsToFile pattern above.

Defensive countermeasures

  • Script-length anomaly detection: Flag Node.js or JScript files > 1 MB with extremely long lines and low vocabulary diversity.
  • Word-frequency analysis: If a script contains a space-separated string where > 90% of tokens fall within a 256-word vocabulary, treat as suspicious regardless of natural-language appearance.
  • Child-process monitoring: Any node.exe that writes files to %ProgramData% and then spawns an EXE from that directory is worth alerting on.

Pages where observed