Potential PowerShell Obfuscation via Character Array Reconstruction
editPotential PowerShell Obfuscation via Character Array Reconstruction
editDetects PowerShell scripts that reconstructs strings from char[] arrays, index lookups, or repeated ([char]NN)+ concatenation/join logic. Attackers use character-array reconstruction to hide commands, URLs, or payloads and evade static analysis and AMSI.
Rule type: esql
Rule indices: None
Severity: high
Risk score: 73
Runs every: 5m
Searches indices from: now-9m (Date Math format, see also Additional look-back time)
Maximum alerts per execution: 100
References: None
Tags:
- Domain: Endpoint
- OS: Windows
- Use Case: Threat Detection
- Tactic: Defense Evasion
- Data Source: PowerShell Logs
- Resources: Investigation Guide
Version: 11
Rule authors:
- Elastic
Rule license: Elastic License v2
Investigation guide
editTriage and analysis
Investigating Potential PowerShell Obfuscation via Character Array Reconstruction
Possible investigation steps
- What hidden text is reconstructed at the alert-marked sites?
-
Focus:
powershell.file.script_block_text,powershell.file.script_block_length, and alert-localEsql.script_block_pattern_countandEsql.script_block_tmpreconstruction markers. - Implication: escalate when reconstructed strings reveal execution, download, staging, persistence, credential, or policy-control intent; lower concern only when decoded text is limited to static formatting, localization, or configuration constants in an otherwise readable generated script.
- Is the full source script block available for pivots?
- Why: ES|QL preserves alert evidence, but split 4104 events and omitted source fields can hide the decoded action or later pivot keys.
-
Focus: same
host.idandpowershell.file.script_block_id, ordered bypowershell.sequenceand checked againstpowershell.total, with each fragment’spowershell.file.script_block_text. !{investigate{"description":"","label":"Script block fragments for the same script","providers":[[{"excluded":false,"field":"powershell.file.script_block_id","queryType":"phrase","value":"{{powershell.file.script_block_id}}","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"}]],"relativeFrom":"now-1h","relativeTo":"now"}} -
Hint: if fragments do not match
powershell.total, querylogs-windows.powershell_operational*before closing. Record sourceprocess.pidandfile.path. - Implication: escalate when complete reconstruction exposes hidden stages, payload material, or omitted execution context; keep unresolved when missing fragments or source fields could contain the decoded action or pivot key.
- Which source event and launch context explain this PowerShell?
- Why: endpoint launch context must be recovered before parentage or process-scoped pivots are trusted.
-
Focus: if endpoint process telemetry is available, use source
process.pidplushost.idaround@timestampto recoverprocess.entity_id,process.command_line, andprocess.parent.executable. !{investigate{"description":"","label":"Process events for the PowerShell PID","providers":[[{"excluded":false,"field":"event.category","queryType":"phrase","value":"process","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"},{"excluded":false,"field":"process.pid","queryType":"phrase","value":"{{process.pid}}","valueType":"string"}]],"relativeFrom":"now-1h","relativeTo":"now"}} -
Hint: do not require powershell.exe; hosted PowerShell can still produce 4104. If endpoint process telemetry is missing, bound later pivots to
host.id,user.id, and a tight alert window. - Implication: escalate when the script is fileless, sourced from user-writable or delivery paths, launched by a browser/document/remoting/scheduled-task parent, or run with a command line that does not fit the actor; lower concern only when recovered source and launch evidence identify one generator or updater workflow and decoded intent stays non-executing.
- Does decoded content add obfuscation, execution, staging, or persistence?
-
Focus:
powershell.file.script_block_text, decoded strings, sourcefile.path, and, with endpoint file or registry telemetry,registry.path. Use same-PID artifact events around@timestampto validate writes or registry changes. !{investigate{"description":"","label":"File and registry events for the PowerShell PID","providers":[[{"excluded":false,"field":"event.category","queryType":"phrase","value":"file","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"},{"excluded":false,"field":"process.pid","queryType":"phrase","value":"{{process.pid}}","valueType":"string"}],[{"excluded":false,"field":"event.category","queryType":"phrase","value":"registry","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"},{"excluded":false,"field":"process.pid","queryType":"phrase","value":"{{process.pid}}","valueType":"string"}]],"relativeFrom":"now-1h","relativeTo":"now"}} -
Hint: scope file or registry review with recovered
process.entity_idor fallbackhost.id+user.id+ tight alert window. Useregistry.data.stringsonly afterregistry.pathpoints to persistence or policy state. -
Implication: escalate when decoded content feeds
Invoke-Expression, reflection, Base64, decompression, dynamic member access, payload writes, or persistence/policy registry changes; lower concern only when decoded values are static data and available endpoint telemetry does not contradict that. Missing endpoint file or registry telemetry leaves follow-on activity unresolved. - Do decoded or recovered process destinations fit the decoded intent?
-
Focus: if endpoint network telemetry is available, DNS lookup_result and connection fields:
dns.question.name,dns.resolved_ip,destination.ip, anddestination.port. -
Hint: scope network review with same-PID events around
@timestamp, or with recoveredprocess.entity_idwhere available. Correlate DNSdns.resolved_ipto connectiondestination.ip. Missing network telemetry is unresolved, not benign. !{investigate{"description":"","label":"Network and DNS events for the PowerShell PID","providers":[[{"excluded":false,"field":"event.category","queryType":"phrase","value":"network","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"},{"excluded":false,"field":"process.pid","queryType":"phrase","value":"{{process.pid}}","valueType":"string"}],[{"excluded":false,"field":"event.category","queryType":"phrase","value":"dns","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"},{"excluded":false,"field":"process.pid","queryType":"phrase","value":"{{process.pid}}","valueType":"string"}]],"relativeFrom":"now-1h","relativeTo":"now"}} - Implication: escalate when decoded URLs, domains, or IPs lead to rare public infrastructure, direct IP access, nonstandard ports, or destinations unrelated to the recovered workflow; lower concern when destinations are internal, proxy, or vendor services aligned with the same generated-script or updater workflow.
- If local evidence is suspicious or unresolved, does the pattern recur beyond this host or user?
-
Focus: related alerts for the same
user.idin the last 48 hours, comparing decoded strings, reconstruction pattern, andfile.path. !{investigate{"description":"","label":"Alerts associated with the user","providers":[[{"excluded":false,"field":"event.kind","queryType":"phrase","value":"signal","valueType":"string"},{"excluded":false,"field":"user.id","queryType":"phrase","value":"{{user.id}}","valueType":"string"}]],"relativeFrom":"now-48h/h","relativeTo":"now"}} -
Hint: if the user view is sparse, pivot to same-
host.idalerts and compare decoded indicators or source path. !{investigate{"description":"","label":"Alerts associated with the host","providers":[[{"excluded":false,"field":"event.kind","queryType":"phrase","value":"signal","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"}]],"relativeFrom":"now-48h/h","relativeTo":"now"}} - Implication: broaden scope when the same decoded indicator or reconstruction pattern appears on unrelated hosts or accounts; keep scope local when confined to one recovered workflow. Do not close solely because no prior alerts exist.
- Escalate for hidden execution, download, staging, persistence, credential, policy-control, or defense-evasion behavior; close only when alert-local and recovered evidence bind one benign generated-script or updater workflow with no contradictions; preserve and escalate when decoding, fragments, or conditional endpoint telemetry remain incomplete.
False positive analysis
-
Generated build, packaging, localization, templating, vendor bootstrap, or updater scripts can legitimately rebuild strings from character codes. Confirm only when decoded content resolves to static data, expected generator output, installation, or update logic;
file.path,user.id, andhost.idfit that workflow; recoveredprocess.command_lineorprocess.parent.executablesupports it; and any available destination evidence reaches vendor, proxy, or internal update services rather than staging infrastructure. Without change or ownership records, use telemetry-only confirmation: the same source path, actor/host scope, and decoded-string purpose recur across prior alerts from this rule. Recurrence can support a future exception, but should not be the primary reason to close the first alert. -
Before creating an exception, anchor it to indexed alert fields such as
user.id,host.id, file-backedfile.path, and a tightly boundedpowershell.file.script_block_textpattern that represents the confirmed generator or updater. Do not useEsql.script_block_pattern_countorEsql.script_block_tmpin exceptions because they are alert-local summaries rather than stable exception anchors.
Response and remediation
-
If confirmed benign, record the evidence that proved the workflow first: decoded script purpose, validated
file.pathor repeated fileless pattern,user.id,host.id, and any recovered endpoint process or destination evidence. Then reverse temporary containment and create a narrow exception only after the same workflow recurs consistently. -
If suspicious but unconfirmed, preserve evidence first: export the alert, source 4104 event, ordered script fragments, decoded strings, source
process.pid, and any recoveredprocess.entity_id, command line, parent, file, registry, DNS, or destination artifacts. Apply reversible containment tied to the findings, such as heightened monitoring, temporary destination blocking, or host isolation when the host role allows it, then escalate before deleting artifacts or resetting accounts. -
If confirmed malicious, preserve the same script, source-event, process, artifact, and destination evidence before destructive actions. Isolate the host when business impact allows, record the malicious PowerShell
process.entity_idbefore termination when it was recovered, block confirmed malicious domains, URLs, IPs, and hashes, and remove only files, registry changes, scheduled tasks, or other persistence tied to the decoded script. Reset credentials only when the investigation shows account misuse beyond local execution. - Post-incident hardening: retain PowerShell script-block logging and endpoint telemetry needed for process, file, registry, DNS, and network recovery; constrain PowerShell automation to signed or centrally managed workflows where feasible; record the benign workflow or malicious artifact set so repeat alerts can be handled with the same evidence standard.
Setup
editSetup
PowerShell Script Block Logging must be enabled to generate the events used by this rule (e.g., 4104). Setup instructions: https://ela.st/powershell-logging-setup
Rule query
editfrom logs-windows.powershell_operational* metadata _id, _version, _index
| where event.code == "4104"
// Filter for scripts that contain the "char" keyword using MATCH, boosts the query performance
| where powershell.file.script_block_text : "char"
// replace the patterns we are looking for with the 🔥 emoji to enable counting them
// The emoji is used because it's unlikely to appear in scripts and has a consistent character length of 1
| eval Esql.script_block_tmp = replace(
powershell.file.script_block_text,
"""(char\[\]\]\(\d+,\d+[^)]+|(\s?\(\[char\]\d+\s?\)\+){2,})""",
"🔥"
)
// count how many patterns were detected by calculating the number of 🔥 characters inserted
| eval Esql.script_block_pattern_count = length(Esql.script_block_tmp) - length(replace(Esql.script_block_tmp, "🔥", ""))
// keep the fields relevant to the query, although this is not needed as the alert is populated using _id
| keep
Esql.script_block_pattern_count,
Esql.script_block_tmp,
powershell.file.*,
file.path,
powershell.sequence,
powershell.total,
_id,
_version,
_index,
host.name,
host.id,
agent.id,
user.id,
process.pid
// Filter for scripts that match the pattern at least once
| where Esql.script_block_pattern_count >= 1
Framework: MITRE ATT&CKTM
-
Tactic:
- Name: Defense Evasion
- ID: TA0005
- Reference URL: https://attack.mitre.org/tactics/TA0005/
-
Technique:
- Name: Obfuscated Files or Information
- ID: T1027
- Reference URL: https://attack.mitre.org/techniques/T1027/
-
Sub-technique:
- Name: Command Obfuscation
- ID: T1027.010
- Reference URL: https://attack.mitre.org/techniques/T1027/010/
-
Technique:
- Name: Deobfuscate/Decode Files or Information
- ID: T1140
- Reference URL: https://attack.mitre.org/techniques/T1140/
-
Tactic:
- Name: Execution
- ID: TA0002
- Reference URL: https://attack.mitre.org/tactics/TA0002/
-
Technique:
- Name: Command and Scripting Interpreter
- ID: T1059
- Reference URL: https://attack.mitre.org/techniques/T1059/
-
Sub-technique:
- Name: PowerShell
- ID: T1059.001
- Reference URL: https://attack.mitre.org/techniques/T1059/001/