>samit_hota
Back to research
ETHICAL HACKING

Windows Persistence Demystified: Run Keys, Scheduled Tasks, Services, and WMI

Samit Hota·
#windows#persistence#threat-hunting#dfir

Most initial access vectors are temporary. A phish yields a web shell that gets recycled, or a stolen session token expires after a few hours. To turn a fleeting foothold into operational stability, an attacker must drop anchor. They rarely need custom rootkits or obscure zero-days to do this; Windows provides all the infrastructure required out of the box.

If you are waiting for your EDR to throw a high-severity alert on persistence, you are already losing. Attackers disguise their entry points inside legitimate administrative mechanisms, betting that security analysts won’t distinguish between a legitimate updater and a malicious launcher. To find them, you need to know exactly where these artifacts live on disk, how they modify the registry, and how to write hunt queries that surface anomalies across your fleet.

Registry Run Keys: The Lowest-Hanging Fruit

Registry Run keys remain the most common persistence vector because they require minimal effort and survive system reboots. When a user logs in, the Windows Interactive Logon Process (winlogon.exe) checks specific registry keys and executes any binary paths defined within their value data.

Attackers typically target two main locations:

  • Current User (HKCU): HKCU\Software\Microsoft\Windows\CurrentVersion\Run (does not require local administrative privileges).
  • Local Machine (HKLM): HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run (requires elevated privileges, executes for every user).

To simulate this mechanism, an attacker drops an executable into a readable location and writes a string value to the HKCU Run key:

reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "SecurityUpdate" /t REG_SZ /d "C:\Users\Public\Libraries\update.exe" /f

On disk, the executable sits in C:\Users\Public\Libraries\. In the registry, a new value SecurityUpdate is created under the targeted hive.

To hunt for new or modified Run keys across Microsoft Defender for Endpoint (KQL), run this query targeting suspicious executable paths and unquoted binary strings:

DeviceRegistryEvents
| where RegistryKey has_any (
    @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
    @"SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce"
)
| where ActionType in ("RegistryValueSet", "RegistryKeyCreated")
| where RegistryValueData matches regex @"(?i)(c:\\users\\public|c:\\programdata|c:\\windows\\temp|powershell|cmd\.exe|wscript|cscript)"
| project Timestamp, DeviceName, AccountName, RegistryKey, RegistryValueName, RegistryValueData, InitiatingProcessFolderPath

Scheduled Tasks: Hiding in System32\Tasks

Scheduled tasks allow execution triggered by time intervals, system events, or user actions (such as workstation unlock). Because legitimate software routinely installs scheduled tasks, malicious tasks blend in easily.

When a scheduled task is created, Windows writes two distinct artifacts:

  1. Disk: An XML definition file located in C:\Windows\System32\Tasks\ (or subfolders like C:\Windows\System32\Tasks\Microsoft\Windows\).
  2. Registry: Metadata written to HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree\ and HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tasks\.

An attacker can create a persistence task via schtasks.exe:

schtasks /create /tn "Updater\Maintenance" /tr "powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -Enc aT...==" /sc daily /st 09:00 /ru SYSTEM

Notice the task path: subfolder creation under TaskCache\Tree is a common tactic to imitate valid Microsoft paths.

To catch scheduled task creation in KQL, monitor both command-line invocations of schtasks.exe and Security Event ID 4698 (A scheduled task was created):

DeviceProcessEvents
| where ProcessCommandLine has "schtasks" and ProcessCommandLine has "/create"
| where ProcessCommandLine matches regex @"(?i)(powershell|cmd|cscript|wscript|mshta|rundll32|regsvr32|c:\\users\\public|c:\\windows\\temp)"
| project Timestamp, DeviceName, AccountName, FolderPath, ProcessCommandLine, InitiatingProcessParentFileName

If you collect Windows Security Logs via Microsoft Sentinel, query Event ID 4698 directly to extract the embedded XML payload:

SecurityEvent
| where EventID == 4698
| extend EventDataXML = parse_xml(EventData)
| extend TaskName = tostring(EventDataXML.EventData.Data[0].["#text"])
| extend Command = tostring(EventDataXML.EventData.Data[5].["#text"])
| where Command matches regex @"(?i)(powershell|cmd|cscript|wscript|c:\\users\\public|c:\\windows\\temp)"
| project TimeGenerated, Computer, SubjectUserName, TaskName, Command

Service Creation: Escalate and Persist in One Move

Windows Services are managed by the Service Control Manager (services.exe) and typically run with NT AUTHORITY\SYSTEM privileges. Creating a malicious service provides both persistence and privilege escalation.

When a service is registered:

  • Registry: A dedicated subkey is created at HKLM\SYSTEM\CurrentControlSet\Services\<ServiceName>.
  • Key Values: The path to the executable is stored in the ImagePath registry value.

An attacker creates a service pointing to a staged payload:

sc.exe create "WinAppSvc" binPath= "C:\Windows\Temp\svc_payload.exe" start= auto

If the binary is not a valid Windows Service binary (i.e., it lacks the ServiceMain callback function), services.exe will terminate the process after a timeout period, but the executable code will have already run during startup.

To detect service installation, rely on System Event ID 7045 (“A service was installed in the system”) or monitor telemetry from service creation registry keys:

DeviceEvents
| where ActionType == "ServiceInstalled"
| extend ServiceName = tostring(AdditionalFields.ServiceName)
| extend ServiceFolderPath = tostring(AdditionalFields.ImagePath)
| extend ServiceStartType = tostring(AdditionalFields.StartType)
| where ServiceFolderPath matches regex @"(?i)(c:\\users\\public|c:\\windows\\temp|c:\\programdata|powershell|cmd\.exe)"
| project Timestamp, DeviceName, AccountName, ServiceName, ServiceFolderPath, ServiceStartType

WMI Event Subscriptions: Living Off the Repository

WMI (Windows Management Instrumentation) event subscriptions are among the stealthiest persistence mechanisms on Windows. They run entirely in memory inside the WMI repository, leaving no standalone binary on disk if configured to execute inline scripts (e.g., VBScript or PowerShell via a ActiveScriptEventConsumer).

A WMI persistence mechanism requires three distinct objects within the root\subscription namespace:

  1. __EventFilter: Defines the trigger (e.g., system uptime reaching 300 seconds, or a user logging in).
  2. __EventConsumer: Defines the action (e.g., CommandLineEventConsumer running a command).
  3. __FilterToConsumerBinding: Binds the filter to the consumer.

All three objects are written directly to the WMI repository database file on disk: C:\Windows\System32\wbem\Repository\OBJECTS.DATA.

Because attackers rarely use native wmic.exe commands anymore, they assemble WMI subscriptions directly via PowerShell:

$Filter = Set-CimInstance -Namespace "root\subscription" -ClassName __EventFilter -Property @{
    Name = "SystemCheckFilter"
    EventNamespace = "root\cimv2"
    QueryLanguage = "WQL"
    Query = "SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System'"
}

$Consumer = Set-CimInstance -Namespace "root\subscription" -ClassName CommandLineEventConsumer -Property @{
    Name = "SystemCheckConsumer"
    CommandLineTemplate = "powershell.exe -NoP -NonI -W Hidden -Enc aT...=="
}

Set-CimInstance -Namespace "root\subscription" -ClassName __FilterToConsumerBinding -Property @{
    Filter = [ref]$Filter
    Consumer = [ref]$Consumer
}

To audit WMI persistence locally using PowerShell, enumerate all bindings in the root\subscription namespace:

Get-CimInstance -Namespace root\subscription -ClassName __FilterToConsumerBinding | Select-Object Filter, Consumer

For fleet-wide hunting with Defender for Endpoint telemetry (Sysmon Events 19, 20, and 21 map directly to WMI events):

DeviceEvents
| where ActionType in ("WmiEventFilterCreated", "WmiEventConsumerCreated", "WmiEventConsumerToFilterCreated")
| extend EventDetail = parse_json(AdditionalFields)
| project Timestamp, DeviceName, ActionType, AccountName, EventDetail

Building a Unified Persistence Baseline

Individual queries catch individual artifacts, but real-world hunting requires baselining. Legitimate applications install tasks, run keys, and services constantly. Your goal is not to eliminate every result from these queries, but to establish an operational baseline.

Focus your hunting workflows on these three core indicators across all four persistence vectors:

  1. Suspicious File Paths: Executables or scripts executing out of writeable non-standard directories like C:\Users\Public\, C:\ProgramData\, or C:\Windows\Temp\.
  2. Encoded Command Lines: Powershell invocations utilizing -EncodedCommand (-e, -enc) or -WindowStyle Hidden.
  3. Parent-Process Anomaly: Legitimate services spawning interactive shells, or wmiprvse.exe launching unexpected child processes.

Incorporate these queries into scheduled threat hunting sweeps rather than relying solely on automated detection rules. By searching raw telemetry for these persistence artifacts, you force attackers to burn their footholds long before they reach their final objectives.

Want a second set of eyes on your security posture?

Let's talk about where your real exposure is.

Book an advisory call