Skip to content

07 · Persistence

Hunting Windows Persistence: Registry, Tasks, WMI and More

Find attacker persistence on Windows: Run keys, services, scheduled tasks, WMI, Winlogon, IFEO, COM hijacking and BITS, mapped to MITRE ATT&CK, with tools.

Published on 10 min read

Once attackers get in, they make sure they can come back after a reboot, a password change or a patched vulnerability. This guide walks through the autostart locations that Windows intrusions use most, maps each one to its MITRE ATT&CK technique, and shows where to look on a live host or a disk image. You will finish with a repeatable triage workflow and a way to separate real persistence from the hundreds of benign autostarts on a normal machine.

Persistence map

MechanismATT&CKPrimary locationKey logs
Run / RunOnce keysT1547.001SOFTWARE and NTUSER.DAT: ...\CurrentVersion\Run, RunOnceSysmon 13
Startup foldersT1547.001...\Start Menu\Programs\Startup (per user and all users)Sysmon 11
Windows servicesT1543.003SYSTEM\ControlSet00x\Services\<name>7045, 4697
Scheduled tasksT1053.005C:\Windows\System32\Tasks, TaskCache in SOFTWARE4698, TaskScheduler 106/140/200
WMI event subscriptionT1546.003C:\Windows\System32\wbem\Repository\OBJECTS.DATAWMI-Activity 5861, Sysmon 19-21
Winlogon Userinit / ShellT1547.004SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinlogonSysmon 13
IFEO Debugger, SilentProcessExitT1546.012...\Image File Execution Options\<exe>, ...\SilentProcessExit\<exe>Sysmon 13
AppInit_DLLsT1546.010SOFTWARE\Microsoft\Windows NT\CurrentVersion\WindowsSysmon 13
COM hijackingT1546.015UsrClass.dat: Software\Classes\CLSID\{GUID}Sysmon 13
BITS jobsT1197C:\ProgramData\Microsoft\Network\Downloader\Bits-Client 3/59/60
Accessibility featuresT1546.008System32\sethc.exe, utilman.exe or their IFEO keysPrefetch, file hashes, Sysmon 1
Local account creationT1136.001SAM hive4720, 4722, 4732
DLL search order hijackingT1574.001Application folders, writable PATH entriesSysmon 7

ATT&CK IDs are current at the time of writing; MITRE occasionally renames or merges sub-techniques (recent versions, for example, broadened T1574.001 to cover DLL side-loading as well). Check attack.mitre.org when you put IDs in a report.

Registry autostarts

Run and RunOnce keys

The classic. Values under these keys launch at logon:

  • HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run and \RunOnce (all users)
  • HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run and \RunOnce (32-bit view on 64-bit Windows)
  • HKCU\Software\Microsoft\Windows\CurrentVersion\Run and \RunOnce, stored in each user's NTUSER.DAT
  • ...\Policies\Explorer\Run, normally only written by Group Policy

RunOnce values are deleted before they execute (unless prefixed with !), so a one-shot payload may leave no trace in the hive afterwards. Values themselves carry no timestamp: the only time you get is the key's last write time, which reflects the latest change to any value in that key. On Windows 8 and later, Explorer\StartupApproved\Run records whether an entry is enabled or disabled in Task Manager. Full details are on the Run keys and Startup folders page.

Winlogon Userinit and Shell

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon holds Userinit (default C:\Windows\system32\userinit.exe, with a trailing comma) and Shell (default explorer.exe). Anything appended to these values runs at every interactive logon. A per-user Shell value in NTUSER.DAT under the same path is also honoured. Compare exact strings: an extra comma-separated path in Userinit is easy to miss in a GUI.

IFEO Debugger and SilentProcessExit

Image File Execution Options (HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\<name.exe>) exists for debugging. A Debugger value makes Windows launch the debugger instead of the target, with the target path as an argument, so Debugger = C:\ProgramData\x.exe on notepad.exe runs the payload whenever Notepad starts.

The quieter variant is SilentProcessExit: set GlobalFlag to 0x200 on the IFEO key, then create ...\CurrentVersion\SilentProcessExit\<name.exe> with ReportingMode = 1 and MonitorProcess pointing to the payload. The payload runs when the monitored process exits. Both keys are covered on the IFEO and Winlogon page.

AppInit_DLLs

AppInit_DLLs under HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows (and its WOW6432Node twin), with LoadAppInit_DLLs = 1, loads the listed DLLs into every process that loads user32.dll. On Windows 8 and later the mechanism is disabled when Secure Boot is on, so its presence on a modern Secure Boot machine is more a sign of intent than of working persistence. Any non-empty value is still worth an explanation.

COM hijacking

When a program instantiates a COM object, Windows looks up the CLSID in HKCU\Software\Classes\CLSID before HKLM\SOFTWARE\Classes\CLSID. An attacker without admin rights can register the same CLSID under the user hive with an InprocServer32 (DLL) or LocalServer32 (EXE) pointing to their payload, and it loads every time Explorer or a scheduled task instantiates that object. On disk, HKCU\Software\Classes lives in UsrClass.dat, not NTUSER.DAT, so collect both. Hunt for CLSIDs present in a user's UsrClass.dat that also exist in HKLM with a different server path, and for servers under user-writable folders like AppData.

Services

Services live under HKLM\SYSTEM\CurrentControlSet\Services\<name>; on an offline hive, resolve the active control set through Select\Current. The values that matter:

  • ImagePath: the executable or driver, including arguments. Look for cmd.exe /c, powershell, paths in Temp, ProgramData or user profiles, and random names.
  • Parameters\ServiceDll (sometimes directly under the service key): the DLL loaded by a shared svchost.exe service. Replacing it on a legitimate service is stealthier than creating a new one.
  • Start: 0 boot, 1 system, 2 automatic, 3 manual, 4 disabled.
  • ObjectName: the account it runs as, usually LocalSystem.
  • FailureActions and FailureCommand: a recovery command can itself be a persistence hook.

Installation leaves 7045 in the System log (service name, image path, start type, account) and, when Audit Security System Extension is on, 4697 in Security. PsExec-style lateral movement produces the same events, which is why the lateral movement guide leans on them too. See the services page.

# Live: services with their binary and account
Get-CimInstance Win32_Service |
  Select-Object Name, State, StartMode, StartName, PathName |
  Sort-Object PathName

Scheduled tasks

A task on Vista and later leaves three traces:

  1. An XML definition in C:\Windows\System32\Tasks\<folder>\<name> (no extension), with Actions, Triggers, Principals and RegistrationInfo.
  2. Registry keys in the SOFTWARE hive under Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache: Tree\<path> (values Id, Index, SD) and Tasks\{GUID} (Path, Actions, Triggers, DynamicInfo).
  3. Events: Security 4698 (created, with the task XML, if Audit Other Object Access Events is on), 4702 (updated), 4699 (deleted), and in Microsoft-Windows-TaskScheduler/Operational 106 (registered), 140 (updated), 141 (deleted), 200 (action started) and 201 (action completed).

The HAFNIUM "Tarrask" technique hides a task by deleting its SD value under TaskCache\Tree. The task keeps running but disappears from schtasks /query and the Task Scheduler console. Any Tree subkey without an SD value is a finding; also compare the Tree, Tasks and XML file lists against each other, since a mismatch indicates tampering. The Author and Date fields in the XML are written by the creator and can be forged. Details on the scheduled tasks page.

# Live: every task with its action, including hidden-flag tasks
Get-ScheduledTask | ForEach-Object {
  [pscustomobject]@{
    Path    = $_.TaskPath + $_.TaskName
    State   = $_.State
    Hidden  = $_.Settings.Hidden
    Execute = ($_.Actions | ForEach-Object { "$($_.Execute) $($_.Arguments)" }) -join ' | '
  }
}

# Tree entries missing their SD value (Tarrask-style hiding)
Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree' -Recurse |
  Where-Object { $_.GetValue('Id') -and ($_.GetValueNames() -notcontains 'SD') } |
  Select-Object Name

Get-ScheduledTask will not show a task whose SD was removed, which is exactly why the registry check matters.

WMI event subscriptions

A permanent WMI subscription combines three objects, usually in root\subscription:

  • an __EventFilter holding a WQL query (for example a trigger a few minutes after boot, or on a process start),
  • an __EventConsumer subclass that acts, typically CommandLineEventConsumer (runs a command) or ActiveScriptEventConsumer (runs VBScript or JScript),
  • a __FilterToConsumerBinding that links the two.

The consumer runs inside WmiPrvSE.exe as SYSTEM, with no file in any autostart folder. Everything is stored in the CIM repository, C:\Windows\System32\wbem\Repository\OBJECTS.DATA together with INDEX.BTR and the MAPPING*.MAP files. On Windows 10 and later, 5861 in Microsoft-Windows-WMI-Activity/Operational logs each new permanent binding with the filter query and consumer details; Sysmon 19, 20 and 21 capture filter, consumer and binding activity when configured. Deleted subscriptions can survive in unallocated pages of OBJECTS.DATA. See the WMI repository page.

# Live enumeration
Get-CimInstance -Namespace root/subscription -ClassName __EventFilter |
  Select-Object Name, Query
Get-CimInstance -Namespace root/subscription -ClassName __EventConsumer |
  Select-Object __CLASS, Name, CommandLineTemplate, ScriptText
Get-CimInstance -Namespace root/subscription -ClassName __FilterToConsumerBinding |
  Select-Object Filter, Consumer
# Offline: parse the repository copied from an image
python PyWMIPersistenceFinder.py ./Repository/OBJECTS.DATA

Other locations worth checking

Startup folders

Shortcuts or scripts dropped in C:\Users\<user>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup or C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp run at logon. Files there have normal NTFS timestamps, which gives you a creation time that Run key values lack. Also check User Shell Folders for a redirected Startup path.

BITS jobs

Background Intelligent Transfer Service jobs can download a payload and, through bitsadmin /SetNotifyCmdLine, run a command when the job completes or errors. Jobs can be kept alive for long periods and retried, which makes them a quiet persistence and download mechanism. Evidence sits in the BITS database (qmgr.db on Windows 10 and later, qmgr0.dat/qmgr1.dat before) and in Microsoft-Windows-Bits-Client/Operational (3 created, 59 transfer started with URL, 60 stopped). See the BITS jobs page.

Accessibility feature backdoors

Replacing C:\Windows\System32\sethc.exe (Sticky Keys, Shift five times) or utilman.exe (Win+U) with cmd.exe, or pointing an IFEO Debugger at them, gives a SYSTEM shell from the logon screen, including over RDP. Check the hashes and version info of sethc.exe, utilman.exe, osk.exe, Magnify.exe, Narrator.exe, DisplaySwitch.exe and AtBroker.exe against a clean build of the same version, look for IFEO keys on those names, and review Prefetch for them executing at unusual times.

New local accounts

A new local administrator is persistence that survives any cleanup of malware. Look for 4720 (created), 4722 (enabled) and 4732 (added to a local group such as Administrators) in Security, and parse the SAM hive for account creation times, last logon and group membership (see the SAM user accounts page). Accounts can be hidden from the logon screen with a 0 DWORD under HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList\<name>, and names ending in $ are skipped by some listing commands.

DLL search order hijacking

When an application loads a DLL by name without a full path, Windows searches a sequence of directories, starting with the application's own folder. Dropping a malicious DLL with the expected name in that folder, or in a writable directory earlier in the search order, gets it loaded by a legitimate and often signed process. KnownDLLs (HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs) are protected from this. Hunt for DLLs that normally live in System32 appearing next to executables in ProgramData, AppData or Temp, for unsigned DLLs beside signed binaries, and for Sysmon 7 image loads from unusual paths. Amcache can help date when the DLL first appeared.

Tools

Autoruns offline

Sysinternals Autoruns covers most registry and file autostarts and flags unsigned entries. autorunsc can analyze an offline Windows directory with -z, pointing at the Windows folder and a user profile of a mounted image.

# Live, all entry types, CSV with hashes and signature checks
autorunsc.exe -accepteula -nobanner -a * -c -h -s > autoruns_live.csv

# Offline image mounted as F:
autorunsc.exe -accepteula -nobanner -a * -c -h -s -z F:\Windows F:\Users\alice > autoruns_offline.csv

Offline mode depends on the hives it can read, so run it once per user profile of interest.

KAPE and RECmd

KAPE collects the hives, task XML, WMI repository and logs in one pass, then its !EZParser module runs Eric Zimmerman's tools. RECmd with a batch file (such as Kroll_Batch.reb shipped in its BatchExamples folder) extracts known persistence keys from every hive into one CSV, including hive transaction logs when present.

kape.exe --tsource C: --tdest E:\tout --target KapeTriage --mdest E:\mout --module !EZParser
RECmd.exe -d E:\tout\C --bn .\BatchExamples\Kroll_Batch.reb --csv E:\out\registry

Velociraptor

For fleets, Velociraptor runs the same checks everywhere at once. Useful artifacts include Windows.Sysinternals.Autoruns, Windows.Persistence.PermanentWMIEvents, Windows.System.TaskScheduler, Windows.System.Services and Windows.Sys.StartupItems. Stacking results across hosts is often faster than examining one machine in depth. The acquisition and triage guide covers deployment.

Common mistakes

  • Only checking HKLM. HKCU Run keys, per-user COM objects in UsrClass.dat and user Startup folders need admin-free access and are favourites for that reason. Parse every user's hives, including service accounts.
  • Forgetting WOW6432Node. 32-bit Run keys, IFEO and AppInit entries live in a separate branch on 64-bit systems.
  • Trusting signatures. Attackers point autostarts at signed LOLBins (rundll32, mshta, regsvr32) with malicious arguments. Read the command line, not just the binary.
  • Over-reading key timestamps. A last write time dates the last change to the key, not to the value you care about.
  • Missing tasks hidden from the API. Enumerate TaskCache and the Tasks folder directly, not just schtasks.
  • Stopping at the first finding. Attackers usually set more than one mechanism; keep going through the whole list.

Triage workflow and baseline comparison

  1. Collect hives (SYSTEM, SOFTWARE, SAM, every NTUSER.DAT and UsrClass.dat, with transaction logs), System32\Tasks, the WMI Repository folder, BITS database, Startup folders and all event logs. The registry forensics guide explains the hive layout.
  2. Enumerate with Autoruns (live or -z), RECmd batch output, task XML plus TaskCache, and a WMI parser.
  3. Check the logs for 7045, 4697, 4698, TaskScheduler 106/200, WMI-Activity 5861, Bits-Client 3/59, 4720/4732, following the event log forensics guide.
  4. Compare against a baseline: Autoruns output from a clean machine built from the same image, or stacked results from the fleet. Entries that appear on one or two hosts out of hundreds float to the top.
  5. Filter by time: keep entries whose key last write time, file creation time or install event falls inside the intrusion window.
  6. Validate each candidate: hash and signature of the target, file path, Prefetch or Amcache for execution, and the account that created it.
  7. Document each mechanism with its ATT&CK ID, location, creation time and evidence of execution, then remove all of them together during remediation so the attacker cannot use one to restore the others.

Area 07

Persistence

The artifact cheat sheets behind this guide: location by Windows version, what each one proves and the tools to parse it.

Related articles