Detect a hand-rolled character-table encoder in a script block
Matches PowerShell script-block logging where the recorded script both declares a literal character alphabet — a hand-rolled translation table, since .NET already ships Base64 and hex — and performs a per-character index or bit transform, which is the encode loop AN0927 describes. ScriptBlockText was chosen over the analytic's process_creation and network_connection sources because it is the only field in AN0927 that shows the encoder itself rather than its side effects. The analytic's real observable is a correlation: the encode routine, then egress with an asymmetric bytes_out:bytes_in ratio or fixed-size beacons inside MITRE's TimeWindow. Sigma expresses no ratio, no standard deviation and no join window, so this rule is the single-event precursor only, and the EntropyThreshold and TokenLengthThreshold knobs are left entirely to the analyst. UNVERIFIED AS AUTHORED — derived from MITRE ATT&CK DET0326, not hand-written and not tested by its author. Any lab result is on this rule's own page.Full descriptionShow less
The detection
The 4 SIEM queries below are previews produced by Siemphony’s own translator, not pySigma, and none has been executed against a real backend — review before you deploy.
detection.yml
title: Detect a hand-rolled character-table encoder in a script blockid: c2dbc459-235f-4f9f-a464-6bb60fc3fa88status: experimentaldescription: | Matches PowerShell script-block logging where the recorded script both declares a literal character alphabet — a hand-rolled translation table, since .NET already ships Base64 and hex — and performs a per-character index or bit transform, which is the encode loop AN0927 describes. ScriptBlockText was chosen over the analytic's process_creation and network_connection sources because it is the only field in AN0927 that shows the encoder itself rather than its side effects. The analytic's real observable is a correlation: the encode routine, then egress with an asymmetric bytes_out:bytes_in ratio or fixed-size beacons inside MITRE's TimeWindow. Sigma expresses no ratio, no standard deviation and no join window, so this rule is the single-event precursor only, and the EntropyThreshold and TokenLengthThreshold knobs are left entirely to the analyst. UNVERIFIED AS AUTHORED — derived from MITRE ATT&CK DET0326, not hand-written and not tested by its author. Any lab result is on this rule's own page.references: - https://attack.mitre.org/techniques/T1132/002 - https://attack.mitre.org/detectionstrategies/DET0326author: Siemphony pipeline (machine-derived from MITRE ATT&CK v19.2)date: 2026-08-16tags: - attack.command-and-control - attack.t1132.002logsource: product: windows service: powershelldetection: script_block: EventID: 4104 custom_alphabet: ScriptBlockText|contains: - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' - 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' - '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' - '0123456789abcdefghijklmnopqrstuvwxyz' transform_loop: ScriptBlockText|contains: - '-bxor' - '-band' - '-shr' - '-shl' - '.IndexOf(' - 'ToCharArray()' condition: script_block and custom_alphabet and transform_loopfalsepositives: - "In-house scripts that hand-roll base32 or base36 for licence keys, short-URL slugs or record-ID obfuscation. They declare exactly this kind of alphabet and index into it, and MITRE's KnownLegitEncoders knob exists for precisely these — suppress by script Path or ScriptBlockId rather than by weakening the alphabet list." - "Password and passphrase generators, and provisioning tooling that builds random tokens. They carry a full alphabet literal and call ToCharArray(), so all three selections are satisfied; without MITRE's EntropyThreshold and TokenLengthThreshold there is nothing inside a single 4104 event that separates a 16-character password from a 4 KB encoded payload." - "Checksum, CRC and simple integrity routines inside signed third-party modules, whose script blocks are logged verbatim when the module is dot- sourced. These use `-bxor` and `-shr` against a lookup table and will match if the table happens to be alphabetic."level: mediumSentinel · KQL
Run this as a search.
Event| where ((EventID == 4104 and (ScriptBlockText contains "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" or ScriptBlockText contains "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" or ScriptBlockText contains "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" or ScriptBlockText contains "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" or ScriptBlockText contains "0123456789abcdefghijklmnopqrstuvwxyz")) and (ScriptBlockText contains "-bxor" or ScriptBlockText contains "-band" or ScriptBlockText contains "-shr" or ScriptBlockText contains "-shl" or ScriptBlockText contains ".IndexOf(" or ScriptBlockText contains "ToCharArray()"))
Splunk · SPL
Run this as a search.
index=* ((EventID="4104" AND (ScriptBlockText="*ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*" OR ScriptBlockText="*abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ*" OR ScriptBlockText="*ABCDEFGHIJKLMNOPQRSTUVWXYZ234567*" OR ScriptBlockText="*0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ*" OR ScriptBlockText="*0123456789abcdefghijklmnopqrstuvwxyz*")) AND (ScriptBlockText="*-bxor*" OR ScriptBlockText="*-band*" OR ScriptBlockText="*-shr*" OR ScriptBlockText="*-shl*" OR ScriptBlockText="*.IndexOf(*" OR ScriptBlockText="*ToCharArray()*"))Elastic · ES|QL
Run this as a search.
FROM logs-*| WHERE ((event.code == 4104 AND (TO_LOWER(powershell.file.script_block_text) LIKE "*abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*abcdefghijklmnopqrstuvwxyz234567*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*0123456789abcdefghijklmnopqrstuvwxyz*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*0123456789abcdefghijklmnopqrstuvwxyz*")) AND (TO_LOWER(powershell.file.script_block_text) LIKE "*-bxor*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*-band*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*-shr*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*-shl*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*.indexof(*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*tochararray()*"))
Wazuh · XML rule
Deploy to your manager — this is a rule, not a search.
<group name="sigma,windows,"> <!-- Rule ids must be unique on your manager; 100000+ is the user range. --> <rule id="100000" level="7"> <!-- Set <if_sid> to the decoder/base rule for windows so this only evaluates relevant events. --> <field name="EventID" type="pcre2">(?i)^4104$</field> <field name="ScriptBlockText" type="pcre2">(?i)(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz|abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ|ABCDEFGHIJKLMNOPQRSTUVWXYZ234567|0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ|0123456789abcdefghijklmnopqrstuvwxyz)</field> <field name="ScriptBlockText" type="pcre2">(?i)(-bxor|-band|-shr|-shl|\.IndexOf\(|ToCharArray\(\))</field> <description>Detect a hand-rolled character-table encoder in a script block</description> <mitre> <id>T1132.002</id> </mitre> </rule></group>
Verdicts · reactions · comments
Community
Verdicts from engineers who actually deployed it, and the conversation around it.
Nobody has run this in a real environment and said what happened.
Verdicts from engineers who deployed it
0 castNo verdicts reported yet. The first one is worth more than the tenth — say what you ran it against.
Log in to report a verdict.
Log in to join the discussion.
No comments yet. Someone who deploys this will have something to say about it.