Potential PowerShell Obfuscation via Character Array Reconstruction
Detects 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.
Elastic rule (View on GitHub)
1[metadata]
2creation_date = "2025/04/14"
3integration = ["windows"]
4maturity = "production"
5updated_date = "2026/04/30"
6
7[rule]
8author = ["Elastic"]
9description = """
10Detects PowerShell scripts that reconstructs strings from char[] arrays, index lookups, or repeated ([char]NN)+
11concatenation/join logic. Attackers use character-array reconstruction to hide commands, URLs, or payloads and evade
12static analysis and AMSI.
13"""
14from = "now-9m"
15language = "esql"
16license = "Elastic License v2"
17name = "Potential PowerShell Obfuscation via Character Array Reconstruction"
18risk_score = 73
19rule_id = "85e2d45e-a3df-4acf-83d3-21805f564ff4"
20severity = "high"
21tags = [
22 "Domain: Endpoint",
23 "OS: Windows",
24 "Use Case: Threat Detection",
25 "Tactic: Defense Evasion",
26 "Data Source: PowerShell Logs",
27 "Resources: Investigation Guide",
28]
29timestamp_override = "event.ingested"
30type = "esql"
31
32query = '''
33from logs-windows.powershell_operational* metadata _id, _version, _index
34| where event.code == "4104"
35
36// Filter for scripts that contain the "char" keyword using MATCH, boosts the query performance
37| where powershell.file.script_block_text : "char"
38
39// replace the patterns we are looking for with the 🔥 emoji to enable counting them
40// The emoji is used because it's unlikely to appear in scripts and has a consistent character length of 1
41| eval Esql.script_block_tmp = replace(
42 powershell.file.script_block_text,
43 """(char\[\]\]\(\d+,\d+[^)]+|(\s?\(\[char\]\d+\s?\)\+){2,})""",
44 "🔥"
45)
46
47// count how many patterns were detected by calculating the number of 🔥 characters inserted
48| eval Esql.script_block_pattern_count = length(Esql.script_block_tmp) - length(replace(Esql.script_block_tmp, "🔥", ""))
49
50// keep the fields relevant to the query, although this is not needed as the alert is populated using _id
51| keep
52 Esql.script_block_pattern_count,
53 Esql.script_block_tmp,
54 powershell.file.*,
55 file.path,
56 powershell.sequence,
57 powershell.total,
58 _id,
59 _version,
60 _index,
61 host.name,
62 host.id,
63 agent.id,
64 user.id,
65 process.pid
66
67// Filter for scripts that match the pattern at least once
68| where Esql.script_block_pattern_count >= 1
69'''
70
71note = """## Triage and analysis
72
73### Investigating Potential PowerShell Obfuscation via Character Array Reconstruction
74
75#### Possible investigation steps
76
77- What hidden text is reconstructed at the alert-marked sites?
78 - Focus: `powershell.file.script_block_text`, `powershell.file.script_block_length`, and alert-local `Esql.script_block_pattern_count` and `Esql.script_block_tmp` reconstruction markers.
79 - 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.
80
81- Is the full source script block available for pivots?
82 - Why: ES|QL preserves alert evidence, but split 4104 events and omitted source fields can hide the decoded action or later pivot keys.
83 - Focus: same `host.id` and `powershell.file.script_block_id`, ordered by `powershell.sequence` and checked against `powershell.total`, with each fragment's `powershell.file.script_block_text`. $investigate_0
84 - Hint: if fragments do not match `powershell.total`, query `logs-windows.powershell_operational*` before closing. Record source `process.pid` and `file.path`.
85 - 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.
86
87- Which source event and launch context explain this PowerShell?
88 - Why: endpoint launch context must be recovered before parentage or process-scoped pivots are trusted.
89 - Focus: if endpoint process telemetry is available, use source `process.pid` plus `host.id` around `@timestamp` to recover `process.entity_id`, `process.command_line`, and `process.parent.executable`. $investigate_1
90 - 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.
91 - 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.
92
93- Does decoded content add obfuscation, execution, staging, or persistence?
94 - Focus: `powershell.file.script_block_text`, decoded strings, source `file.path`, and, with endpoint file or registry telemetry, `registry.path`. Use same-PID artifact events around `@timestamp` to validate writes or registry changes. $investigate_2
95 - Hint: scope file or registry review with recovered `process.entity_id` or fallback `host.id` + `user.id` + tight alert window. Use `registry.data.strings` only after `registry.path` points to persistence or policy state.
96 - 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.
97
98- Do decoded or recovered process destinations fit the decoded intent?
99 - Focus: if endpoint network telemetry is available, DNS lookup_result and connection fields: `dns.question.name`, `dns.resolved_ip`, `destination.ip`, and `destination.port`.
100 - Hint: scope network review with same-PID events around `@timestamp`, or with recovered `process.entity_id` where available. Correlate DNS `dns.resolved_ip` to connection `destination.ip`. Missing network telemetry is unresolved, not benign. $investigate_3
101 - 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.
102
103- If local evidence is suspicious or unresolved, does the pattern recur beyond this host or user?
104 - Focus: related alerts for the same `user.id` in the last 48 hours, comparing decoded strings, reconstruction pattern, and `file.path`. $investigate_4
105 - Hint: if the user view is sparse, pivot to same-`host.id` alerts and compare decoded indicators or source path. $investigate_5
106 - 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.
107
108- 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.
109
110### False positive analysis
111
112- 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`, and `host.id` fit that workflow; recovered `process.command_line` or `process.parent.executable` supports 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.
113- Before creating an exception, anchor it to indexed alert fields such as `user.id`, `host.id`, file-backed `file.path`, and a tightly bounded `powershell.file.script_block_text` pattern that represents the confirmed generator or updater. Do not use `Esql.script_block_pattern_count` or `Esql.script_block_tmp` in exceptions because they are alert-local summaries rather than stable exception anchors.
114
115### Response and remediation
116
117- If confirmed benign, record the evidence that proved the workflow first: decoded script purpose, validated `file.path` or 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.
118- If suspicious but unconfirmed, preserve evidence first: export the alert, source 4104 event, ordered script fragments, decoded strings, source `process.pid`, and any recovered `process.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.
119- 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_id` before 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.
120- 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.
121"""
122
123setup = """## Setup
124
125PowerShell Script Block Logging must be enabled to generate the events used by this rule (e.g., 4104).
126Setup instructions: https://ela.st/powershell-logging-setup
127"""
128
129[rule.investigation_fields]
130field_names = [
131 "@timestamp",
132 "user.id",
133 "powershell.file.script_block_text",
134 "powershell.file.script_block_id",
135 "powershell.sequence",
136 "powershell.total",
137 "powershell.file.script_block_entropy_bits",
138 "powershell.file.script_block_surprisal_stdev",
139 "powershell.file.script_block_length",
140 "file.path",
141 "process.pid",
142 "host.name",
143 "host.id",
144 "Esql.script_block_tmp",
145 "Esql.script_block_pattern_count"
146]
147
148[transform]
149
150[[transform.investigate]]
151label = "Script block fragments for the same script"
152description = ""
153providers = [
154 [
155 { excluded = false, field = "powershell.file.script_block_id", queryType = "phrase", value = "{{powershell.file.script_block_id}}", valueType = "string" },
156 { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" }
157 ]
158]
159relativeFrom = "now-1h"
160relativeTo = "now"
161
162[[transform.investigate]]
163label = "Process events for the PowerShell PID"
164description = ""
165providers = [
166 [
167 { excluded = false, field = "event.category", queryType = "phrase", value = "process", valueType = "string" },
168 { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
169 { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" }
170 ]
171]
172relativeFrom = "now-1h"
173relativeTo = "now"
174
175[[transform.investigate]]
176label = "File and registry events for the PowerShell PID"
177description = ""
178providers = [
179 [
180 { excluded = false, field = "event.category", queryType = "phrase", value = "file", valueType = "string" },
181 { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
182 { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" }
183 ],
184 [
185 { excluded = false, field = "event.category", queryType = "phrase", value = "registry", valueType = "string" },
186 { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
187 { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" }
188 ]
189]
190relativeFrom = "now-1h"
191relativeTo = "now"
192
193[[transform.investigate]]
194label = "Network and DNS events for the PowerShell PID"
195description = ""
196providers = [
197 [
198 { excluded = false, field = "event.category", queryType = "phrase", value = "network", valueType = "string" },
199 { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
200 { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" }
201 ],
202 [
203 { excluded = false, field = "event.category", queryType = "phrase", value = "dns", valueType = "string" },
204 { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
205 { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" }
206 ]
207]
208relativeFrom = "now-1h"
209relativeTo = "now"
210
211[[transform.investigate]]
212label = "Alerts associated with the user"
213description = ""
214providers = [
215 [
216 { excluded = false, field = "event.kind", queryType = "phrase", value = "signal", valueType = "string" },
217 { excluded = false, field = "user.id", queryType = "phrase", value = "{{user.id}}", valueType = "string" }
218 ]
219]
220relativeFrom = "now-48h/h"
221relativeTo = "now"
222
223[[transform.investigate]]
224label = "Alerts associated with the host"
225description = ""
226providers = [
227 [
228 { excluded = false, field = "event.kind", queryType = "phrase", value = "signal", valueType = "string" },
229 { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" }
230 ]
231]
232relativeFrom = "now-48h/h"
233relativeTo = "now"
234
235[[rule.threat]]
236framework = "MITRE ATT&CK"
237
238[[rule.threat.technique]]
239id = "T1027"
240name = "Obfuscated Files or Information"
241reference = "https://attack.mitre.org/techniques/T1027/"
242
243[[rule.threat.technique.subtechnique]]
244id = "T1027.010"
245name = "Command Obfuscation"
246reference = "https://attack.mitre.org/techniques/T1027/010/"
247
248[[rule.threat.technique]]
249id = "T1140"
250name = "Deobfuscate/Decode Files or Information"
251reference = "https://attack.mitre.org/techniques/T1140/"
252
253[rule.threat.tactic]
254id = "TA0005"
255name = "Defense Evasion"
256reference = "https://attack.mitre.org/tactics/TA0005/"
257
258[[rule.threat]]
259framework = "MITRE ATT&CK"
260
261[[rule.threat.technique]]
262id = "T1059"
263name = "Command and Scripting Interpreter"
264reference = "https://attack.mitre.org/techniques/T1059/"
265
266[[rule.threat.technique.subtechnique]]
267id = "T1059.001"
268name = "PowerShell"
269reference = "https://attack.mitre.org/techniques/T1059/001/"
270
271[rule.threat.tactic]
272id = "TA0002"
273name = "Execution"
274reference = "https://attack.mitre.org/tactics/TA0002/"
Triage 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.
- Focus:
-
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_0 - 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_1 - 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_2 - 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.
- Focus:
-
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_3 - 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.
- Focus: if endpoint network telemetry is available, DNS lookup_result and connection fields:
-
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_4 - Hint: if the user view is sparse, pivot to same-
host.idalerts and compare decoded indicators or source path. $investigate_5 - 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.
- Focus: related alerts for the same
-
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.
Related rules
- Potential PowerShell Obfuscation via Backtick-Escaped Variable Expansion
- Potential PowerShell Obfuscation via Concatenated Dynamic Command Invocation
- Potential PowerShell Obfuscation via String Concatenation
- Potential Process Injection via PowerShell
- Deprecated - Potential PowerShell Obfuscated Script