PowerShell PInvoke Process Injection API Chain

Original Source: [splunk source]
Name:PowerShell PInvoke Process Injection API Chain
id:3f1a2b4c-d5e6-7890-abcd-ef1234567890
version:1
date:2026-04-22
author:Teoderick Contreras, Splunk
status:production
type:TTP
Description:The following analytic detects PowerShell Script Block Logging (Event ID 4104) evidence of a complete P/Invoke process-injection API chain at either the compile phase or the execution phase. Compile phase: Detects inline .NET class definitions created via `Add-Type -TypeDefinition` where the embedded C# source includes `[DllImport]` declarations and full P/Invoke signatures such as `extern <ReturnType> <FunctionName>`. This reduces false positives from comments, string literals, and arbitrary text references. Execution phase: Detects PowerShell static method invocation patterns using `::MethodName(` syntax, anchored with execution-context indicators such as `[IntPtr]::Zero` or `Marshal]::Copy`. Injection technique coverage: 1. Self-injection shellcode runner - VirtualAlloc + VirtualProtect + CreateThread 2. Remote thread injection - OpenProcess + VirtualAllocEx/VirtualAlloc + WriteProcessMemory + CreateRemoteThread 3. APC injection - OpenProcess + VirtualAllocEx/VirtualAlloc + WriteProcessMemory + QueueUserAPC 4. Thread-context hijacking - OpenThread + SuspendThread + GetThreadContext + WriteProcessMemory + SetThreadContext + ResumeThread 5. Process hollowing - CreateProcess + VirtualAllocEx/VirtualAlloc + WriteProcessMemory + SetThreadContext + ResumeThread 6. Section-map injection - NtCreateSection + NtMapViewOfSection (+ CreateRemoteThread in this analytic) 7. Reflective DLL loading - VirtualAlloc + GetProcAddress + GetModuleHandle + VirtualProtect + CreateThread 8. DLL injection - OpenProcess + VirtualAllocEx/VirtualAlloc + WriteProcessMemory + GetModuleHandle + GetProcAddress + CreateRemoteThread Including the execution phase ensures coverage when an adversary loads a pre-compiled assembly from memory or disk and bypasses Add-Type entirely, since the static method call pattern is still captured in the script block regardless of how the class was originally compiled.
Data_source:
  • -Powershell Script Block Logging 4104
search:`powershell`
EventCode=4104
(
ScriptBlockText = "*add-type*"
ScriptBlockText="*DllImport*"
(
(
ScriptBlockText="*extern IntPtr VirtualAlloc*"
ScriptBlockText="*extern bool VirtualProtect*"
ScriptBlockText="*extern IntPtr CreateThread*"
)
OR
(
ScriptBlockText="*extern IntPtr OpenProcess*"
ScriptBlockText="*extern IntPtr VirtualAlloc*"
ScriptBlockText="*extern bool WriteProcessMemory*"
ScriptBlockText="*extern IntPtr CreateRemoteThread*"
)
OR
(
ScriptBlockText="*extern IntPtr OpenProcess*"
ScriptBlockText="*extern IntPtr VirtualAlloc*"
ScriptBlockText="*extern bool WriteProcessMemory*"
ScriptBlockText="*extern bool QueueUserAPC*"
)
OR
(
ScriptBlockText="*extern IntPtr OpenThread*"
ScriptBlockText="*extern uint SuspendThread*"
ScriptBlockText="*extern bool GetThreadContext*"
ScriptBlockText="*extern bool WriteProcessMemory*"
ScriptBlockText="*extern bool SetThreadContext*"
ScriptBlockText="*extern uint ResumeThread*"
)
OR
(
ScriptBlockText="*extern bool CreateProcess*"
ScriptBlockText="*extern IntPtr VirtualAlloc*"
ScriptBlockText="*extern bool WriteProcessMemory*"
ScriptBlockText="*extern bool SetThreadContext*"
ScriptBlockText="*extern uint ResumeThread*"
)
OR
(
ScriptBlockText="*extern int NtCreateSection*"
ScriptBlockText="*extern int NtMapViewOfSection*"
ScriptBlockText="*extern IntPtr CreateRemoteThread*"
)
OR
(
ScriptBlockText="*extern IntPtr VirtualAlloc*"
ScriptBlockText="*extern IntPtr GetProcAddress*"
ScriptBlockText="*extern IntPtr GetModuleHandle*"
ScriptBlockText="*extern IntPtr CreateThread*"
ScriptBlockText="*extern bool VirtualProtect*"
)
OR
(
ScriptBlockText="*extern IntPtr OpenProcess*"
ScriptBlockText="*extern IntPtr VirtualAlloc*"
ScriptBlockText="*extern bool WriteProcessMemory*"
ScriptBlockText="*extern IntPtr GetProcAddress*"
ScriptBlockText="*extern IntPtr CreateRemoteThread*"
)
OR
(
ScriptBlockText="*::VirtualAlloc*"
ScriptBlockText="*::VirtualProtect*"
ScriptBlockText="*::CreateThread*"
(
ScriptBlockText="*[IntPtr]::Zero*"
OR
ScriptBlockText="*Marshal]::Copy*"
)
)
OR
(
ScriptBlockText="*::OpenProcess*"
ScriptBlockText="*::VirtualAlloc*"
ScriptBlockText="*::WriteProcessMemory*"
ScriptBlockText="*::CreateRemoteThread*"
ScriptBlockText="*[IntPtr]::Zero*"
)
OR
(
ScriptBlockText="*::OpenProcess*"
ScriptBlockText="*::VirtualAlloc*"
ScriptBlockText="*::WriteProcessMemory*"
ScriptBlockText="*::QueueUserAPC*"
ScriptBlockText="*[IntPtr]::Zero*"
)
OR
(
ScriptBlockText="*::OpenThread*"
ScriptBlockText="*::SuspendThread*"
ScriptBlockText="*::GetThreadContext*"
ScriptBlockText="*::WriteProcessMemory*"
ScriptBlockText="*::SetThreadContext*"
ScriptBlockText="*::ResumeThread*"
)
OR
(
ScriptBlockText="*::CreateProcess*"
ScriptBlockText="*::VirtualAlloc*"
ScriptBlockText="*::WriteProcessMemory*"
ScriptBlockText="*::SetThreadContext*"
ScriptBlockText="*::ResumeThread*"
)
OR
(
ScriptBlockText="*::NtCreateSection*"
ScriptBlockText="*::NtMapViewOfSection*"
ScriptBlockText="*::CreateRemoteThread*"
)
OR
(
ScriptBlockText="*::VirtualAlloc*"
ScriptBlockText="*::GetProcAddress*"
ScriptBlockText="*::GetModuleHandle*"
ScriptBlockText="*::VirtualProtect*"
ScriptBlockText="*::CreateThread*"
)
OR
(
ScriptBlockText="*::OpenProcess*"
ScriptBlockText="*::VirtualAlloc*"
ScriptBlockText="*::WriteProcessMemory*"
ScriptBlockText="*::GetModuleHandle*"
ScriptBlockText="*::GetProcAddress*"
ScriptBlockText="*::CreateRemoteThread*"
)
)
)
| fillnull
| stats count min(_time) as firstTime
max(_time) as lastTime
by dest signature signature_id user_id vendor_product EventID
Guid Opcode Name Path ProcessID ScriptBlockId ScriptBlockText
| `security_content_ctime(firstTime)`
| `security_content_ctime(lastTime)`
| `powershell_pinvoke_process_injection_api_chain_filter`


how_to_implement:The following analytic requires PowerShell operational logs to be imported. Modify the powershell macro as needed to match the sourcetype or add index. This analytic is specific to 4104, or PowerShell Script Block Logging.
known_false_positives:No false positives have been identified at this time.
References:
  -https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.dllimportattribute
  -https://www.ired.team/offensive-security/code-injection-process-injection
  -https://www.broadcom.com/support/security-center/protection-bulletin/vip-keylogger-spreads-via-multi-org-impersonation-campaign
  -https://attack.mitre.org/techniques/T1055/
  -https://attack.mitre.org/techniques/T1620/
drilldown_searches:
name:'View the detection results for - "$dest$"'
search:'%original_detection_search% | search dest = "$dest$"'
earliest_offset:'$info_min_time$'
latest_offset:'$info_max_time$'
name:'View risk events for the last 7 days for - "$dest$"'
search:'| from datamodel Risk.All_Risk | search normalized_risk_object IN ("$dest$") | stats count min(_time) as firstTime max(_time) as lastTime values(search_name) as "Search Name" values(risk_message) as "Risk Message" values(analyticstories) as "Analytic Stories" values(annotations._all) as "Annotations" values(annotations.mitre_attack.mitre_tactic) as "ATT&CK Tactics" by normalized_risk_object | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)`'
earliest_offset:'7d'
latest_offset:'0'
tags:
  analytic_story:
    - 'VIP Keylogger'
  asset_type:Endpoint
  mitre_attack_id:
    - 'T1055.001'
    - 'T1055.003'
    - 'T1055.004'
    - 'T1055.012'
    - 'T1055.013'
    - 'T1059.001'
    - 'T1620'
  product:
    - 'Splunk Enterprise'
    - 'Splunk Enterprise Security'
    - 'Splunk Cloud'
  security_domain:endpoint

tests:
name:'True Positive Test'
 attack_data:
  data: https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1059.001/vip_injection_pwh/pwh_net_inline.log
  source: XmlWinEventLog:Microsoft-Windows-PowerShell/Operational
  sourcetype: XmlWinEventLog
manual_test:None

Related Analytic Stories


VIP Keylogger