Skip to content

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.

Windows persistence hunt workflow covering registry entries, scheduled tasks, services, WMI subscriptions, evidence review, and approved response

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

01

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.

Terminal window
$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

02

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.

Terminal window
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
❯ 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

03

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.

Terminal window
$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
❯ View Expected Console Output
NotSigned is a signature status, not a malware verdict. A valid signature also
does not prove that a file is safe; validate publisher, hash, path, and behavior.

Step 4: Inspect Permanent WMI Event Subscriptions

04

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.

Terminal window
$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

05

Export Autoruns Entries and Review the Results

Full Inventory

Autoruns 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.

Terminal window
mkdir C:\DFIR_Case_001
autorunsc.exe -accepteula -a * -c -h -s -t * > C:\DFIR_Case_001\autoruns.csv
❯ 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.

References

Comments