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 launchespowershell.exe -windowstyle hiddenwith 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
$mariassignment is dead code — never referenced. Its only purpose is to complicate automated extraction. - The function name
Betingand 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
$Plettetis 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
f7f089f7and3a13583c). - The decoded payload (2,480 characters) contains
New-Object byte[], hex-encoded nested payloads (Verandahs111hex 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 namedWORK_REPORT_FOR_YOUR_FILLING_AND_SUBMITTING_SCAN0012_PDF.com— a.comextension masquerade, first observed in this cluster. - Unlike
f7f089f7, this variant does not wrap the decoder in a namedfunction; the loop runs inline inside a larger variable-assignment block, then the result is passed toIEXor similar reflective execution.
Reproduce on Your Own VMs
Goal: Build a comparable character-skip obfuscator in PowerShell.
Steps:
- Choose a plaintext string, e.g.,
"https://example.com/payload". - Generate a noise-padded literal by inserting three random characters before each real character.
- Write a decoder function that walks the literal with stride 4 and offset 3.
- Embed the decoder and the literal inside a batch→PowerShell wrapper.
- 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 hiddenwith 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
Betingcipher. - /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.Beswith dead-code variable$Plettetreassigned 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 withNew-Object byte[],Verandahs111hex strings, and Danish variable names ($Femogtyvendedelenes,$Grundvidenskab). Static-only. - /intel/analyses/11a563757a79333564f1eda8325816a621d4f404c282245c8a382f0ec9e2dcfb.html — Tenth NSIS SFX sibling; stride-6, offset-5 decoder inside
Labourhoods.Mas(52 KB), namedplanskreresfunction with dead-code$Plettetreassigned 7×. Decoded payload (2,158 chars) reveals asnydendeshex-payload helper processing four XOR-keyed segments (94B3A8,81A3B28BA9A2B3AAA38EA7A8A2AAA3,85AAA7B5B5EAE696B3A4AAAFA5EAE695A3A7AAA3A2EAE687A8B5AF85AAA7B5B5EAE687B3B2A985AAA7B5B5,9A). Unsigned (no certificate). Static-only.