PowerShell PSReflect Script

Detects PowerShell script block content containing PSReflect-style helper indicators, such as Add-Win32Type, New-InMemoryModule, or DllImport patterns, that may support dynamic Win32 API invocation from PowerShell.

Elastic rule (View on GitHub)

  1[metadata]
  2creation_date = "2021/10/15"
  3integration = ["windows"]
  4maturity = "production"
  5updated_date = "2026/05/03"
  6
  7[rule]
  8author = ["Elastic"]
  9description = """
 10Detects PowerShell script block content containing PSReflect-style helper indicators, such as Add-Win32Type,
 11New-InMemoryModule, or DllImport patterns, that may support dynamic Win32 API invocation from PowerShell.
 12"""
 13false_positives = [
 14    "Legitimate PSReflect use when reconstructed content, imported API set, script origin, launcher, user/host scope, and same-host effects align with an approved workflow"
 15]
 16from = "now-9m"
 17index = ["logs-windows.powershell*", "winlogbeat-*"]
 18language = "kuery"
 19license = "Elastic License v2"
 20name = "PowerShell PSReflect Script"
 21references = [
 22    "https://github.com/mattifestation/PSReflect/blob/master/PSReflect.psm1",
 23    "https://github.com/atc-project/atc-data/blob/master/docs/Logging_Policies/LP_0109_windows_powershell_script_block_log.md",
 24]
 25risk_score = 73
 26rule_id = "56f2e9b5-4803-4e44-a0a4-a52dc79d57fe"
 27severity = "high"
 28tags = [
 29    "Domain: Endpoint",
 30    "OS: Windows",
 31    "Use Case: Threat Detection",
 32    "Tactic: Execution",
 33    "Resources: Investigation Guide",
 34    "Data Source: PowerShell Logs",
 35]
 36timestamp_override = "event.ingested"
 37type = "query"
 38
 39query = '''
 40event.category:process and host.os.type:windows and
 41  powershell.file.script_block_text:(
 42    "New-InMemoryModule" or
 43    "Add-Win32Type" or
 44    psenum or
 45    DefineDynamicAssembly or
 46    DefineDynamicModule or
 47    "Reflection.TypeAttributes" or
 48    "Reflection.Emit.OpCodes" or
 49    "Reflection.Emit.CustomAttributeBuilder" or
 50    "Runtime.InteropServices.DllImportAttribute"
 51  ) and
 52  not user.id : "S-1-5-18"
 53'''
 54
 55note = """## Triage and analysis
 56
 57### Investigating PowerShell PSReflect Script
 58#### Possible investigation steps
 59
 60- Does the reconstructed script block show active PSReflect native-API wiring?
 61  - Why: script block logging can split one script; PSReflect is meaningful only after imports and follow-on logic are visible.
 62  - Focus: reconstruct with `powershell.file.script_block_id`, `powershell.sequence`, and `powershell.total`, then read `powershell.file.script_block_text` in `host.id` / `user.id` scope. $investigate_0
 63  - Implication: escalate when the full script defines New-InMemoryModule, Add-Win32Type, psenum, Reflection.Emit, DefineDynamicAssembly, DefineDynamicModule, or DllImportAttribute and calls wrapped APIs; lower suspicion when it is inert reference text, repository content, or a bounded lab example.
 64
 65- What native API objective does the script express?
 66  - Focus: Imported DLL/function pairs and call sites in reconstructed `powershell.file.script_block_text`.
 67  - Hint: renamed wrappers or helper-free variants can show the same behavior when DllImportAttribute, Reflection.Emit, VirtualAlloc, WriteProcessMemory, CreateRemoteThread, OpenProcessToken, or AdjustTokenPrivileges reveal native-API invocation.
 68  - Implication: escalate when call sites map to memory manipulation, token or privilege changes, service tampering, registry changes, or network control; lower urgency when imports stay limited to bounded diagnostics or application interop and later evidence does not contradict that use.
 69
 70- If endpoint process telemetry exists, how was PowerShell launched?
 71  - Why: script block events preserve deobfuscated content, not the command line or parent.
 72  - Focus: Recover the matching process via `host.id + process.pid` before interpreting `process.*` or `process.parent.*`; read `process.entity_id`, `process.command_line`, `process.parent.executable`, and `process.parent.command_line`. $investigate_1
 73  - Hint: expand the window if no process start appears; without process telemetry, keep later review scoped to `host.id`, `user.id`, `process.pid`, and alert time.
 74  - Implication: escalate when a document, browser, chat client, remote session, scheduled task, or user-writable script path launched the helper; lower suspicion when launcher and command fit the same recognized build, admin, interop, or assessment workflow as the script.
 75
 76- Does script origin fit the recovered workflow?
 77  - Focus: `file.path`, `file.directory`, `file.name`, missing file-origin fields, and recovered launch context.
 78  - Implication: escalate when fileless on a workstation or sourced from temp, download, mounted-share, or user-writable paths that do not fit the user; lower suspicion when it resolves to a stable repository, deployment path, or test harness matching the same workflow.
 79
 80- Do same-host effects prove the wrapped APIs were used?
 81  - Why: PSReflect is a helper pattern; the decisive follow-up is whether host activity matches the imported API family.
 82  - Focus: API-matched effects: child execution for process APIs, staged payloads for write/load APIs, persistence or configuration changes for service/registry APIs, and outbound activity for network APIs. Use same-PID events around `@timestamp` to reduce PID-reuse ambiguity.
 83    - $investigate_4
 84    - $investigate_5
 85    - $investigate_6
 86  - Implication: escalate when effects appear in recovered process scope or the `host.id`, `user.id`, `process.pid`, and time fallback scope; lower suspicion only when they align with the same bounded workflow. Missing network or endpoint effect telemetry is unresolved, not benign.
 87
 88- Does related alert scope change urgency?
 89  - Focus: 48-hour alerts for the same `user.id` showing repeated PowerShell, execution, defense-evasion, privilege-escalation, or credential-access activity; use same-`host.id` alerts only to confirm repeated script substrings or the same imported API set.
 90    - $investigate_2
 91    - $investigate_3
 92  - Implication: broaden the case when related activity shows repeated native-API abuse, follow-on execution, or spread outside the original host; keep scope local when repetition stays confined to one exact benign workflow already supported by local evidence.
 93
 94- What disposition is supported?
 95  - Implication: escalate when content, origin, launch, effects, or scope align on injection, privilege manipulation, persistence, remote activity, or egress that does not fit the user; close only when same-host telemetry proves one exact benign workflow and no contradictory findings remain. Outside confirmation may corroborate but not replace telemetry; if mixed or incomplete, preserve artifacts and escalate.
 96
 97### False positive analysis
 98
 99- Internal build, admin, interop, diagnostics, compatibility, or security-testing helpers can use PSReflect-style code. Confirm only when reconstructed `powershell.file.script_block_text`, imported API set, `file.path` or fileless delivery, launcher, `user.id`, `host.id`, and same-host effects all fit one bounded workflow without injection, persistence, egress, or privilege abuse. Change records, repository history, test schedules, or recurrence can support closure but not replace telemetry.
100- Before an exception, validate recurrence of the same `user.id`, `host.id`, stable `file.path` when file-backed, recovered launcher, and distinctive `powershell.file.script_block_text` substrings. Avoid exceptions on PSReflect helper strings, `user.name`, or host alone.
101
102### Response and remediation
103
104- If confirmed benign, document reconstructed script text, imported DLL/function set, origin, `user.id`, `host.id`, launch context, and same-host effect review before reversing containment. Create exceptions only for recurring workflows.
105- If suspicious but unconfirmed, preserve script block IDs/sequences, reconstructed text, API list, origin, process-start evidence, and same-host effect artifacts before response. Apply reversible containment such as destination restrictions or session limits; isolate only when corroboration shows likely injection, persistence, lateral movement, or credential misuse and the host role permits. Avoid destructive cleanup until scope is clearer.
106- If confirmed malicious, preserve the same artifacts plus related child-process, file-system, persistence, credential, and destination evidence before containment. Isolate the host and terminate PowerShell or child payloads after evidence capture; if direct response is unavailable, escalate with preserved artifacts. Block malicious scripts, payload hashes, and destinations, then review related users/hosts for the same script substrings, API set, or launch chain before eradication.
107- After containment, remove only scripts or payloads identified during investigation, undo persistence or configuration changes tied to the API objective, and remediate the delivery path or automation that launched the helper. If imports or same-host effects suggest credential theft, token abuse, or remote execution, reset affected credentials and review related auth/admin sessions before cleanup.
108- Post-incident hardening: restrict PowerShell interop and unsigned script distribution to controlled build, admin, or test systems; retain script block logging and endpoint process telemetry for `host.id + process.pid` recovery; document renamed-wrapper or helper-free reflection variants.
109"""
110
111setup = """## Setup
112
113PowerShell Script Block Logging must be enabled to generate the events used by this rule (e.g., 4104).
114Setup instructions: https://ela.st/powershell-logging-setup
115"""
116
117[[rule.filters]]
118
119[rule.filters.meta]
120negate = true
121[rule.filters.query.wildcard."file.path"]
122case_insensitive = true
123value = "?:\\\\ProgramData\\\\MaaS360\\\\Cloud Extender\\\\AR\\\\Scripts\\\\ASModuleCommon.ps1"
124
125[rule.investigation_fields]
126field_names = [
127    "@timestamp",
128    "user.name",
129    "user.id",
130    "user.domain",
131    "powershell.file.script_block_text",
132    "powershell.file.script_block_id",
133    "powershell.sequence",
134    "powershell.total",
135    "file.path",
136    "file.directory",
137    "file.name",
138    "process.pid",
139    "host.name",
140    "host.id",
141    "powershell.file.script_block_length"
142]
143
144[transform]
145
146[[transform.investigate]]
147label = "Script block fragments for the same script"
148description = ""
149providers = [
150  [
151    { excluded = false, field = "powershell.file.script_block_id", queryType = "phrase", value = "{{powershell.file.script_block_id}}", valueType = "string" },
152    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" }
153  ]
154]
155relativeFrom = "now-1h"
156relativeTo = "now"
157
158[[transform.investigate]]
159label = "Process events for the PowerShell instance"
160description = ""
161providers = [
162  [
163    { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" },
164    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
165    { excluded = false, field = "event.category", queryType = "phrase", value = "process", valueType = "string" }
166  ]
167]
168relativeFrom = "now-1h"
169relativeTo = "now"
170
171[[transform.investigate]]
172label = "Alerts associated with the user"
173description = ""
174providers = [
175  [
176    { excluded = false, field = "event.kind", queryType = "phrase", value = "signal", valueType = "string" },
177    { excluded = false, field = "user.id", queryType = "phrase", value = "{{user.id}}", valueType = "string" }
178  ]
179]
180relativeFrom = "now-48h/h"
181relativeTo = "now"
182
183[[transform.investigate]]
184label = "Alerts associated with the host"
185description = ""
186providers = [
187  [
188    { excluded = false, field = "event.kind", queryType = "phrase", value = "signal", valueType = "string" },
189    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" }
190  ]
191]
192relativeFrom = "now-48h/h"
193relativeTo = "now"
194
195[[transform.investigate]]
196label = "Child process events for the PowerShell PID"
197description = ""
198providers = [
199  [
200    { excluded = false, field = "event.category", queryType = "phrase", value = "process", valueType = "string" },
201    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
202    { excluded = false, field = "process.parent.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" }
203  ]
204]
205relativeFrom = "now-1h"
206relativeTo = "now"
207
208[[transform.investigate]]
209label = "File and registry events for the PowerShell PID"
210description = ""
211providers = [
212  [
213    { excluded = false, field = "event.category", queryType = "phrase", value = "file", valueType = "string" },
214    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
215    { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" }
216  ],
217  [
218    { excluded = false, field = "event.category", queryType = "phrase", value = "registry", valueType = "string" },
219    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
220    { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" }
221  ]
222]
223relativeFrom = "now-1h"
224relativeTo = "now"
225
226[[transform.investigate]]
227label = "Network events for the PowerShell PID"
228description = ""
229providers = [
230  [
231    { excluded = false, field = "event.category", queryType = "phrase", value = "network", valueType = "string" },
232    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
233    { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" }
234  ]
235]
236relativeFrom = "now-1h"
237relativeTo = "now"
238
239[[rule.threat]]
240framework = "MITRE ATT&CK"
241
242[[rule.threat.technique]]
243id = "T1059"
244name = "Command and Scripting Interpreter"
245reference = "https://attack.mitre.org/techniques/T1059/"
246
247[[rule.threat.technique.subtechnique]]
248id = "T1059.001"
249name = "PowerShell"
250reference = "https://attack.mitre.org/techniques/T1059/001/"
251
252[[rule.threat.technique]]
253id = "T1106"
254name = "Native API"
255reference = "https://attack.mitre.org/techniques/T1106/"
256
257[rule.threat.tactic]
258id = "TA0002"
259name = "Execution"
260reference = "https://attack.mitre.org/tactics/TA0002/"

Triage and analysis

Investigating PowerShell PSReflect Script

Possible investigation steps

  • Does the reconstructed script block show active PSReflect native-API wiring?

    • Why: script block logging can split one script; PSReflect is meaningful only after imports and follow-on logic are visible.
    • Focus: reconstruct with powershell.file.script_block_id, powershell.sequence, and powershell.total, then read powershell.file.script_block_text in host.id / user.id scope. $investigate_0
    • Implication: escalate when the full script defines New-InMemoryModule, Add-Win32Type, psenum, Reflection.Emit, DefineDynamicAssembly, DefineDynamicModule, or DllImportAttribute and calls wrapped APIs; lower suspicion when it is inert reference text, repository content, or a bounded lab example.
  • What native API objective does the script express?

    • Focus: Imported DLL/function pairs and call sites in reconstructed powershell.file.script_block_text.
    • Hint: renamed wrappers or helper-free variants can show the same behavior when DllImportAttribute, Reflection.Emit, VirtualAlloc, WriteProcessMemory, CreateRemoteThread, OpenProcessToken, or AdjustTokenPrivileges reveal native-API invocation.
    • Implication: escalate when call sites map to memory manipulation, token or privilege changes, service tampering, registry changes, or network control; lower urgency when imports stay limited to bounded diagnostics or application interop and later evidence does not contradict that use.
  • If endpoint process telemetry exists, how was PowerShell launched?

    • Why: script block events preserve deobfuscated content, not the command line or parent.
    • Focus: Recover the matching process via host.id + process.pid before interpreting process.* or process.parent.*; read process.entity_id, process.command_line, process.parent.executable, and process.parent.command_line. $investigate_1
    • Hint: expand the window if no process start appears; without process telemetry, keep later review scoped to host.id, user.id, process.pid, and alert time.
    • Implication: escalate when a document, browser, chat client, remote session, scheduled task, or user-writable script path launched the helper; lower suspicion when launcher and command fit the same recognized build, admin, interop, or assessment workflow as the script.
  • Does script origin fit the recovered workflow?

    • Focus: file.path, file.directory, file.name, missing file-origin fields, and recovered launch context.
    • Implication: escalate when fileless on a workstation or sourced from temp, download, mounted-share, or user-writable paths that do not fit the user; lower suspicion when it resolves to a stable repository, deployment path, or test harness matching the same workflow.
  • Do same-host effects prove the wrapped APIs were used?

    • Why: PSReflect is a helper pattern; the decisive follow-up is whether host activity matches the imported API family.
    • Focus: API-matched effects: child execution for process APIs, staged payloads for write/load APIs, persistence or configuration changes for service/registry APIs, and outbound activity for network APIs. Use same-PID events around @timestamp to reduce PID-reuse ambiguity.
      • $investigate_4
      • $investigate_5
      • $investigate_6
    • Implication: escalate when effects appear in recovered process scope or the host.id, user.id, process.pid, and time fallback scope; lower suspicion only when they align with the same bounded workflow. Missing network or endpoint effect telemetry is unresolved, not benign.
  • Does related alert scope change urgency?

    • Focus: 48-hour alerts for the same user.id showing repeated PowerShell, execution, defense-evasion, privilege-escalation, or credential-access activity; use same-host.id alerts only to confirm repeated script substrings or the same imported API set.
      • $investigate_2
      • $investigate_3
    • Implication: broaden the case when related activity shows repeated native-API abuse, follow-on execution, or spread outside the original host; keep scope local when repetition stays confined to one exact benign workflow already supported by local evidence.
  • What disposition is supported?

    • Implication: escalate when content, origin, launch, effects, or scope align on injection, privilege manipulation, persistence, remote activity, or egress that does not fit the user; close only when same-host telemetry proves one exact benign workflow and no contradictory findings remain. Outside confirmation may corroborate but not replace telemetry; if mixed or incomplete, preserve artifacts and escalate.

False positive analysis

  • Internal build, admin, interop, diagnostics, compatibility, or security-testing helpers can use PSReflect-style code. Confirm only when reconstructed powershell.file.script_block_text, imported API set, file.path or fileless delivery, launcher, user.id, host.id, and same-host effects all fit one bounded workflow without injection, persistence, egress, or privilege abuse. Change records, repository history, test schedules, or recurrence can support closure but not replace telemetry.
  • Before an exception, validate recurrence of the same user.id, host.id, stable file.path when file-backed, recovered launcher, and distinctive powershell.file.script_block_text substrings. Avoid exceptions on PSReflect helper strings, user.name, or host alone.

Response and remediation

  • If confirmed benign, document reconstructed script text, imported DLL/function set, origin, user.id, host.id, launch context, and same-host effect review before reversing containment. Create exceptions only for recurring workflows.
  • If suspicious but unconfirmed, preserve script block IDs/sequences, reconstructed text, API list, origin, process-start evidence, and same-host effect artifacts before response. Apply reversible containment such as destination restrictions or session limits; isolate only when corroboration shows likely injection, persistence, lateral movement, or credential misuse and the host role permits. Avoid destructive cleanup until scope is clearer.
  • If confirmed malicious, preserve the same artifacts plus related child-process, file-system, persistence, credential, and destination evidence before containment. Isolate the host and terminate PowerShell or child payloads after evidence capture; if direct response is unavailable, escalate with preserved artifacts. Block malicious scripts, payload hashes, and destinations, then review related users/hosts for the same script substrings, API set, or launch chain before eradication.
  • After containment, remove only scripts or payloads identified during investigation, undo persistence or configuration changes tied to the API objective, and remediate the delivery path or automation that launched the helper. If imports or same-host effects suggest credential theft, token abuse, or remote execution, reset affected credentials and review related auth/admin sessions before cleanup.
  • Post-incident hardening: restrict PowerShell interop and unsigned script distribution to controlled build, admin, or test systems; retain script block logging and endpoint process telemetry for host.id + process.pid recovery; document renamed-wrapper or helper-free reflection variants.

References

Related rules

to-top