Hunting Windows Persistence: Registry, Scheduled Tasks & WMI Subscriptions
Persistence investigations look for configuration that causes code or services to run again, such as at logon, startup, or in response to an event. This walkthrough inventories common Windows Run keys, scheduled tasks, services, and permanent WMI event subscriptions. It also uses Microsoft Sysinternals Autoruns to broaden the inventory.
Treat an unusual path, unsigned file, task name, or service account as a lead to investigate, not proof of malicious activity. Compare the configuration with an approved baseline, software inventory, owner, execution context, and change history. These commands inspect configuration; they do not establish that a file is malicious or remove persistence.
Persistence hunt workflow: inventory common locations, correlate each entry with its trigger and execution context, preserve evidence, then follow the incident leadβs approved response plan.
Step 1: Inventory Registry Run and RunOnce Keys
Inspect Machine and User Logon Entries
Registry (T1547.001)Windows has four standard machine and user Run/RunOnce locations. This inventory also checks the common 32-bit software view. RunOnce entries are one-time startup or logon entries, so their absence does not establish that the host has no persistence. Preserve the raw value and expand environment variables only for easier review.
$RunPaths = @( 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run', 'HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnce', 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run', 'HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce', 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Run', 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\RunOnce')
$RunEntries = foreach ($RegistryPath in $RunPaths) { if (Test-Path -LiteralPath $RegistryPath) { $Key = Get-ItemProperty -LiteralPath $RegistryPath foreach ($Property in $Key.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' }) { $RawValue = [string]$Property.Value [pscustomobject]@{ RegistryPath = $RegistryPath ValueName = $Property.Name RawValue = $RawValue ExpandedValue = [Environment]::ExpandEnvironmentVariables($RawValue) } } }}
$RunEntries | Sort-Object RegistryPath, ValueName | Format-Listβ― View Expected Console Output
Review each entry's raw command, expanded path, owner, signature, hash, and surrounding change history.Finding no entries in these locations does not mean that no other persistence mechanism exists.Step 2: Review Scheduled Task Definitions
Inventory Task Actions, Triggers, and Run-As Context
Scheduled Tasks (T1053.005)Inventory enabled and disabled tasks, including tasks stored under Microsoft-named folders. Attackers can choose those paths, and legitimate third-party tasks may be outside them. Review the action, arguments, triggers, principal, task path, and registration details together; do not filter by folder name alone.
Get-ScheduledTask | ForEach-Object { $Task = $_ foreach ($Action in $Task.Actions) { [pscustomobject]@{ TaskPath = $Task.TaskPath TaskName = $Task.TaskName State = $Task.State RunAs = $Task.Principal.UserId ActionType = $Action.CimClass.CimClassName Execute = $Action.Execute Arguments = $Action.Arguments ClassId = $Action.ClassId } }} | Sort-Object TaskPath, TaskName | Format-List:: Detailed local inventory; do not remove lines by matching the word Microsoft.schtasks.exe /query /fo LIST /v
:: Export a specific task definition for evidence review.schtasks.exe /query /tn "\TaskPath\TaskName" /xmlβ― View Expected Console Output
For each task, correlate its action and trigger with the registered owner,run-as identity, file metadata, signer, and expected software behavior.COM-handler actions may not have an executable path in the Execute field;review their ClassId and the referenced component as well.Step 3: Review Services and Unquoted Image-Path Candidates
Inventory Service Configuration and Binary Paths
Windows Services (T1543.003)Review all services rather than labeling every non-System32 path as suspicious. The check below flags a possible unquoted executable path containing spaces for manual review. That flag is not proof of exploitability: validate the parsed image path, service configuration, directory permissions, start mode, account, and change history.
$ServiceInventory = Get-CimInstance -ClassName Win32_Service | ForEach-Object { $PathName = [string]$_.PathName $UnquotedCandidate = $false
if ($PathName -and $PathName -notmatch '^\s*"' -and $PathName -match '^\s*(?<Image>.+?\.exe)(?:\s|$)') { $UnquotedCandidate = $Matches['Image'] -match '\s' }
[pscustomobject]@{ Name = $_.Name DisplayName = $_.DisplayName State = $_.State StartMode = $_.StartMode StartName = $_.StartName ProcessId = $_.ProcessId UnquotedPathCandidate = $UnquotedCandidate PathName = $PathName }}
$ServiceInventory | Sort-Object UnquotedPathCandidate -Descending | Format-List# Run against a file collected to the analyst workstation using approved procedures.$EvidenceFile = 'D:\Cases\INC-2026-1005\files\service-image.exe'
Get-FileHash -LiteralPath $EvidenceFile -Algorithm SHA256Get-AuthenticodeSignature -LiteralPath $EvidenceFile | Select-Object Status, StatusMessage, SignerCertificateβ― View Expected Console Output
NotSigned is a signature status, not a malware verdict. A valid signature alsodoes not prove that a file is safe; validate publisher, hash, path, and behavior.Step 4: Inspect Permanent WMI Event Subscriptions
Correlate WMI Filters, Consumers, and Bindings
WMI (T1546.003)A permanent subscription links an event filter to a consumer through a binding. Review the filter query and event namespace alongside consumer behavior and the binding references. The examples include both standard script and command-line consumers; investigate other consumer classes and relevant namespaces according to the host and incident evidence.
$Namespace = 'root\subscription'
Get-CimInstance -Namespace $Namespace -ClassName __EventFilter | Select-Object Name, QueryLanguage, Query, EventNamespace, CreatorSID
Get-CimInstance -Namespace $Namespace -ClassName CommandLineEventConsumer | Select-Object Name, ExecutablePath, CommandLineTemplate, CreatorSID
Get-CimInstance -Namespace $Namespace -ClassName ActiveScriptEventConsumer | Select-Object Name, ScriptingEngine, ScriptFileName, ScriptText, CreatorSID
Get-CimInstance -Namespace $Namespace -ClassName __FilterToConsumerBinding | Select-Object Filter, Consumer, CreatorSIDβ― View Expected Console Output
Check that each binding points to an existing filter and consumer.Review WQL triggers, execution content, namespaces, and CreatorSID values.Treat an unfamiliar subscription as a lead until its owner and purpose are validated.Step 5: Create a Broad Autoruns Inventory
Export Autoruns Entries and Review the Results
Full InventoryAutoruns covers many additional autostart locations. Create the case directory first, then export all categories for all user profiles with CSV output, hashes, signature verification, and normalized UTC timestamps. The command below does not enable VirusTotal lookup or file submission. Check the field names in the exported CSV before building filters around them.
mkdir C:\DFIR_Case_001autorunsc.exe -accepteula -a * -c -h -s -t * > C:\DFIR_Case_001\autoruns.csv$AutorunsCsv = 'C:\DFIR_Case_001\autoruns.csv'$Entries = Import-Csv -LiteralPath $AutorunsCsv
# Inspect the exported column names and a few entries before filtering.$Entries | Select-Object -First 10 | Format-List *β― View Expected Console Output
Review entries against an approved baseline and software inventory.Use the displayed signer, hash, entry location, and image path as investigation leads.For VirusTotal integration, follow organizational policy: Autoruns can query hashes,and its VirusTotal submission option can upload files not previously scanned.