typetechniquecreated2026-06-14updated2026-07-12obfuscationscriptdefense-evasion

Character-Skip Cipher PowerShell Obfuscation

A simple but effective static-avoidance technique used in PowerShell malware: sensitive strings (URLs, API calls, paths, headers) are hidden inside noise-padded literals. The decoder extracts every Nth character starting from a fixed offset, discarding the noise padding. The result is a string that evades naive keyword matching while remaining trivially reversible once the stride and offset are known.

What It Does

The attacker defines a decoder function that walks a string literal with a fixed stride (e.g., every 4th character starting at index 3). The literal itself contains three noise characters for every one real character, producing a string that looks like gibberish or placeholder text under casual inspection. When the decoder runs, it reassembles the plaintext command or URL.

Detection / Fingerprint

  • Look for PowerShell functions with loops that increment an index by a fixed value (4, 5, 8) and accumulate characters into a result variable.
  • The loop body often contains dead-code assignments (e.g., $mari=Compare-Object stormest beau) to pad the body and make regex-based extraction harder.
  • The outer wrapper is frequently a batch file (cmd.exe) that launches powershell.exe -windowstyle hidden with an inline script.
  • The decoded strings typically reveal Net.WebClient, IEX, DownloadFile, TLS settings, and User-Agent masquerades.

Implementation patterns observed

In the 402879ff4b36 sample, the decoder is:

function Beting ($antileu,$sank=0,$ugleset=0){
    $skyetkeye=3
    do {
        $crotc+=$antileu[$skyetkeye]
        $skyetkeye+=4
        $mari=Compare-Object stormest beau
    } until (!$antileu[$skyetkeye])
    $crotc
}

^[/intel/analyses/402879ff4b368a1dc489d8572137305c84b1983d9539d374f45f815bfa7c1177.html]

Key observations:

  • Stride = 4, offset = 3 (real characters at positions 3, 7, 11, 15, ...).
  • The $mari assignment is dead code — never referenced. Its only purpose is to complicate automated extraction.
  • The function name Beting and variable names (antileu, skyetkeye, crotc) are Danish, providing a linguistic fingerprint.

In the f7f089f7 NSIS sibling, the same operator deploys a stride-6 variant:

function planskreres ($Refractionalnurn){
    $Plettet='Jowari';
    $Plettet='Ploughfoot';
    ...
    $Refractional=5;
    while($maskeringers[$Refractional]) {
        $Systempartners+=$maskeringers[$Refractional];
        $Refractional+=6;
    }
    $Systempartners;
}

^[/intel/analyses/f7f089f7f7753da939649fe98a4d274e44b837a61b72d022897858e1998cc7c4.html]

Key observations:

  • Stride = 6, offset = 5 (real characters at positions 5, 11, 17, ...).
  • Dead-code variable $Plettet is reassigned 7× inside the decoder body, padding the function to complicate regex-based extraction.
  • The function name planskreres ("to plan") and variable names ($Opdateringsdisketten, $Folkemindesamlers) are Danish.
  • The outer PowerShell script is named Labourhoods.Mas — a nonsense filename designed to evade extension-based triage.

In the cf9a061d NSIS sibling, the operator increments to stride 8:

function Unurbanized ($deringa) {
    $Tainadrip157=$deringa;
    $Tainanskaffelseskurser='Care';
    ...
    $Dambrikken=7;
    while($Minimization[$Dambrikken]) {
        $Care+=$Minimization[$Dambrikken];
        $Dambrikken+=8;
    }
    $Care;
}

^[/intel/analyses/cf9a061d02b0601036e3fd138e6b59ee6cdba3e5a40f8472171a56771ace341c.html]

Key observations:

  • Stride = 8, offset = 7 (real characters at positions 7, 15, 23, ...).
  • Dead-code variable assignments ($Tainanskaffelseskurser='Care', $Gulvenes='Dimercaprol') pad the decoder body.
  • The outer PowerShell script is named Articulators.Inc — a masquerade filename suggesting a legitimate include file.
  • The decoded payload (2,670 characters) is larger than siblings 4 and 6, suggesting a more complex inner decryption routine.

In the 3c63a3c0 NSIS sibling (ninth confirmed), the operator reuses stride 6 but shifts the payload delivery from a standalone .ps1 file into a variable assignment inside a larger script (Rockendes.Pot). The decoder is triggered by IEX on the result of the skip-cipher:

$Frakendelsesperioden = "<14,885-char noise-padded literal>"
$Predesignated='RicksIChronEWorkaX'
... (dead-code assignments) ...
$Refractionalnurn=5
while($maskeringers[$Refractionalnurn]) {
    $Systempartners+=$maskeringers[$Refractionalnurn];
    $Refractionalnurn+=6;
}
$Systempartners

^[/intel/analyses/3c63a3c0670132e07fb90ca3c29ba35d898f6293bd40619a5611cb705e8b8212.html]

Key observations:

  • Stride = 6, offset = 5 (same arithmetic as f7f089f7 and 3a13583c).
  • The decoded payload (2,480 characters) contains New-Object byte[], hex-encoded nested payloads (Verandahs111 hex strings), and reflective execution patterns.
  • Danish variable names ($Femogtyvendedelenes, $Grundvidenskab, $chlorotrifluoroethylene, $Spisestedernes) continue the linguistic fingerprint.
  • The outer file is named Rockendes.Pot (nonsense) and the parent archive is named WORK_REPORT_FOR_YOUR_FILLING_AND_SUBMITTING_SCAN0012_PDF.com — a .com extension masquerade, first observed in this cluster.
  • Unlike f7f089f7, this variant does not wrap the decoder in a named function; the loop runs inline inside a larger variable-assignment block, then the result is passed to IEX or similar reflective execution.

Reproduce on Your Own VMs

Goal: Build a comparable character-skip obfuscator in PowerShell.

Steps:

  1. Choose a plaintext string, e.g., "https://example.com/payload".
  2. Generate a noise-padded literal by inserting three random characters before each real character.
  3. Write a decoder function that walks the literal with stride 4 and offset 3.
  4. Embed the decoder and the literal inside a batch→PowerShell wrapper.
  5. Execute and verify the decoded string matches the original.

Sample encoder (Python):

import random, string

def skip_cipher_encode(plaintext, stride=4, offset=3):
    noise = string.ascii_letters + string.digits + string.punctuation + ' '
    out = []
    for i, ch in enumerate(plaintext):
        for j in range(stride):
            if j == offset:
                out.append(ch)
            else:
                out.append(random.choice(noise))
    return ''.join(out)

print(skip_cipher_encode("https://example.com/payload"))

Sample decoder (PowerShell):

function DecodeSkip($s, $offset=3, $stride=4) {
    $out = ''
    for ($i = $offset; $i -lt $s.Length; $i += $stride) {
        $out += $s[$i]
    }
    $out
}

Verification: Run the encoder, embed the output into a PowerShell script with the decoder, and confirm the decoded string matches the original.

Defensive Countermeasures

  • String extraction: Generic string extractors will recover the noise-padded literals; post-processing with a stride decoder (configured with observed offsets) recovers plaintext.
  • Behavioral detection: Detect powershell.exe -windowstyle hidden with inline script bodies containing loop patterns that walk string literals with fixed strides.
  • Network: The decoded URLs are hardcoded and static; once identified, they can be blocked at the proxy or DNS layer.

Pages Where Observed

  • unclassified-danish-batch-ps-dropper — Danish-variable batch→PowerShell dropper using the Beting cipher.
  • /intel/analyses/78c5e8ca9474815c1cd85825b00d9be487a0e049fb827b12ef74bc57580cd3f5.html — NSIS SFX sibling with stride-6, offset-5 Danish PowerShell decoder using fresh variable names ($Spirobranchiateuddhismens, $forbrndingsprocessernes). Static-only.
  • /intel/analyses/3a13583c51add43997b963edaabc063c1abf9600d404e1d0e49bf4d09665d104.html — Eighth NSIS SFX sibling; stride-6, offset-5 decoder inside Prsteskabets.Bes with dead-code variable $Plettet reassigned 7×. English/Danish mixed dead-code names (Tinkler, Templardom, Skyggebilleder). Static-only.
  • /intel/analyses/3c63a3c0670132e07fb90ca3c29ba35d898f6293bd40619a5611cb705e8b8212.html — Ninth NSIS SFX sibling; stride-6, offset-5 decoder inside Rockendes.Pot (55 KB), inline loop (no named function), 2,480-char decoded payload with New-Object byte[], Verandahs111 hex strings, and Danish variable names ($Femogtyvendedelenes, $Grundvidenskab). Static-only.
  • /intel/analyses/11a563757a79333564f1eda8325816a621d4f404c282245c8a382f0ec9e2dcfb.htmlTenth NSIS SFX sibling; stride-6, offset-5 decoder inside Labourhoods.Mas (52 KB), named planskreres function with dead-code $Plettet reassigned 7×. Decoded payload (2,158 chars) reveals a snydendes hex-payload helper processing four XOR-keyed segments (94B3A8, 81A3B28BA9A2B3AAA38EA7A8A2AAA3, 85AAA7B5B5EAE696B3A4AAAFA5EAE695A3A7AAA3A2EAE687A8B5AF85AAA7B5B5EAE687B3B2A985AAA7B5B5, 9A). Unsigned (no certificate). Static-only.