Windows registry forensics is one of the highest-yield areas of endpoint investigation, and the reason is structural: the registry is where Windows reads "what should I run when…" answers, so attackers who want persistence have to write there. This post walks the seven registry locations we check first on a suspected-compromised host, framed as defence against the MITRE ATT&CK persistence techniques behind them. It is meant for authorised triage on systems you administer — for continuous monitoring, use Sysmon, Microsoft Defender for Endpoint, or a SIEM agent.
Key Takeaways
- Seven registry surfaces account for the vast majority of registry-based persistence: Run/RunOnce keys, services, COM hijacking, shell extensions, Winlogon/AppInit load points, file association handlers, and LSA providers.
- The reason attackers reach for the registry is structural — Windows itself reads these keys to decide what to launch. Anything that breaks that pact also breaks Windows, which is why the surface is unlikely to shrink.
- Each surface maps to a MITRE ATT&CK sub-technique (T1547 Boot or Logon Autostart Execution, T1546.015 COM Hijacking, T1546.009 AppCert DLLs, etc.).
- Baseline a clean image. Compare every audited host against it. The new keys are the suspicious ones; the existing list of Microsoft entries is large and irrelevant.
- For production monitoring, use Sysmon's RegistryEvent rules and forward them to a SIEM. The PowerShell snippets below are for authorised investigation, not for monitoring fleets.
Environment
- Windows 10/11 and Windows Server 2019/2022.
- Windows PowerShell 5.1 or PowerShell 7.4.
- Local administrator rights — most of HKLM is unreadable otherwise.
- Sysmon recommended for continuous detection (separate from this triage).
The Problem
Triage time on a suspect host is finite. Most of the registry is irrelevant; a few keys are heavily abused. Looking everywhere wastes hours. Looking only at the Run keys misses three-quarters of the modern persistence surface. The right answer is a short checklist of locations that account for most real-world cases, plus a sense of what each one is for so you can tell a legitimate entry from a planted one.
The Solution
1 — Run and RunOnce keys (MITRE T1547.001)
The classics, and still common. Five locations are worth checking; only the first four are widely known:
$runKeys = @(
'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\Microsoft\Windows\CurrentVersion\RunOnceEx',
'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Run',
'HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Windows\Load',
'HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Windows\Run'
)
$runKeys | ForEach-Object {
$key = $_
Get-ItemProperty $_ -ErrorAction SilentlyContinue |
Select-Object @{Name='Key';Expression={$key}}, * -ExcludeProperty PS*
}
The HKCU variants do not require elevation to write, which makes them attractive to malware running as a standard user. RunOnce entries delete themselves after execution, which makes them ideal for staging payloads — short-lived, harder to catch.
2 — Services (MITRE T1543.003)
Services run as SYSTEM by default and look indistinguishable from legitimate Windows components in the GUI. The interesting key per service is ImagePath (the binary) and ObjectName (the account). Everything below HKLM:\SYSTEM\CurrentControlSet\Services is a candidate:
Get-ChildItem 'HKLM:\SYSTEM\CurrentControlSet\Services' |
ForEach-Object {
$p = Get-ItemProperty $_.PsPath -ErrorAction SilentlyContinue
if ($p.ImagePath -and $p.ImagePath -match 'temp|appdata|users\\public|programdata') {
[PSCustomObject]@{
Service = $_.PsChildName
ImagePath = $p.ImagePath
ObjectName = $p.ObjectName
Start = $p.Start
Type = $p.Type
}
}
}
A service binary living in %TEMP% or %PROGRAMDATA% on a production server is essentially always wrong. Cross-reference any hit against the signed-binary check from the process investigation guide.
3 — COM hijacking (MITRE T1546.015)
COM hijacking abuses the lookup order between HKCU and HKLM. A class registered in HKCU shadows the same CLSID in HKLM when the user-mode component does the lookup — and Windows itself does a lot of COM lookups. The HKCU CLSID hive should normally be sparse:
Get-ChildItem 'HKCU:\Software\Classes\CLSID' -ErrorAction SilentlyContinue |
ForEach-Object {
$inproc = Get-ItemProperty (Join-Path $_.PsPath 'InprocServer32') -ErrorAction SilentlyContinue
if ($inproc) {
[PSCustomObject]@{
CLSID = $_.PsChildName
Dll = $inproc.'(default)'
Threading = $inproc.ThreadingModel
}
}
}
On a clean workstation this query usually returns very few rows, often zero. Anything pointing at a DLL outside C:\Program Files or the Windows directory is worth investigating.
4 — Winlogon and AppInit_DLLs (MITRE T1547.004, T1546.010)
Three values matter here. Shell and Userinit under Winlogon determine what runs at logon; AppInit_DLLs under Windows NT is injected into every process that loads user32.dll:
Get-ItemProperty 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon' |
Select-Object Shell, Userinit
Get-ItemProperty 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Windows' |
Select-Object AppInit_DLLs, LoadAppInit_DLLs, RequireSignedAppInit_DLLs
Shell should be explorer.exe and Userinit should be C:\Windows\system32\userinit.exe,. Anything else is either a deliberate kiosk configuration or an attack. AppInit_DLLs should be empty on modern Windows; it is a legacy mechanism still enabled on some systems.
5 — Shell extensions (MITRE T1546.001)
Shell extensions load whenever Explorer runs. The interesting paths are the Approved key (which lists the registered handlers) and the per-class context-menu handlers:
Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved' |
Select-Object * -ExcludeProperty PS*
Get-ChildItem 'Registry::HKEY_CLASSES_ROOT\*\shellex\ContextMenuHandlers' -ErrorAction SilentlyContinue |
Where-Object PsChildName -notmatch '^(Open With|Sharing|Compatibility)$' |
Select-Object PsPath, PsChildName
Most shell extensions are signed Microsoft or vendor components. An unsigned DLL appearing here on a managed host is worth a closer look.
6 — File association handlers (MITRE T1546.001)
Modifying the handler for .exe, .dll, .lnk, or .bat at the HKCU level redirects execution of those file types for the affected user. The HKLM defaults should match the documented values; HKCU overrides should be examined:
'.exe','.dll','.cmd','.bat','.ps1','.lnk' | ForEach-Object {
$ext = $_
$hklm = (Get-ItemProperty "HKLM:\Software\Classes\$ext" -ErrorAction SilentlyContinue)."(default)"
$hkcu = (Get-ItemProperty "HKCU:\Software\Classes\$ext" -ErrorAction SilentlyContinue)."(default)"
if ($hkcu -and $hkcu -ne $hklm) {
[PSCustomObject]@{
Extension = $ext
HKLMHandler = $hklm
HKCUHandler = $hkcu
}
}
}
An HKCU override that differs from HKLM for a system extension is a strong signal. Carbanak and several other groups have used this technique to silently intercept Office file openings.
7 — LSA providers (MITRE T1547.002)
LSA Security Support Provider entries handle authentication. A malicious SSP loaded into LSASS can intercept credential material. The relevant values live under the LSA key:
Get-ItemProperty 'HKLM:\System\CurrentControlSet\Control\Lsa' |
Select-Object 'Security Packages','Authentication Packages','Notification Packages'
Get-ItemProperty 'HKLM:\System\CurrentControlSet\Control\Lsa\OSConfig' -ErrorAction SilentlyContinue |
Select-Object * -ExcludeProperty PS*
Expected packages are kerberos, msv1_0, schannel, wdigest, tspkg, pku2u, and cloudAP. Anything else, particularly an unrecognised name, is significant. APT29 has used LSA provider abuse as part of their credential theft toolkit.
Pulling it together — a single triage pass
Wrapping the seven checks above in a single function gives you a one-command first pass. Pipe the result to Export-Csv for sharing or to a SIEM ingestion path. Always treat the output as input to a human decision, not as automatic verdicts:
function Invoke-RegistryPersistenceSweep {
$now = Get-Date
$hits = New-Object System.Collections.Generic.List[object]
foreach ($key in $runKeys) {
Get-ItemProperty $key -ErrorAction SilentlyContinue |
Select-Object * -ExcludeProperty PS* |
ForEach-Object {
$_.PSObject.Properties |
Where-Object Name -notmatch '^PS' |
ForEach-Object { $hits.Add([PSCustomObject]@{
Time = $now
Source = "Run/$key"
Name = $_.Name
Value = $_.Value
})}
}
}
# … repeat for the remaining six surfaces …
return $hits
}
Frequently Asked Questions
Is the registry the only place to look for persistence?
No. Scheduled tasks, WMI event subscriptions, services, startup folders, and Group Policy preferences all coexist with the registry as persistence anchors. The registry covers a large fraction but not all of them. Pair this guide with the scheduled-task detection post linked below.
Can I monitor the registry continuously without polling?
Yes — Sysmon with RegistryEvent rules emits an event for every create/modify on the keys you specify. Defender for Endpoint includes registry telemetry natively. Both are dramatically better than periodic PowerShell sweeps for monitoring at scale.
Why HKCU as well as HKLM?
HKCU does not require admin rights to modify. Malware running as a standard user lands there first, and several COM and file-association hijacks specifically depend on the HKCU-over-HKLM precedence in user-mode lookups.
What about the BootExecute and Image File Execution Options keys?
Both are also legitimate persistence surfaces — T1037.002 and T1546.012 respectively. They are less common in commodity malware but worth adding to the checklist for higher-confidence investigations. We omitted them from the seven above to keep the first pass short.
How do I tell legitimate from malicious in these locations?
Two heuristics carry most of the weight: signed Microsoft or vendor binaries living in well-known directories are almost always legitimate; unsigned binaries in user-writable directories are almost always not. Beyond that, compare against a clean-image baseline. The diff is the answer.
Conclusion
Registry-based persistence is not exotic; it is the predictable result of Windows reading "what should I run when X happens" from a small set of well-documented keys. Triage is about knowing which of those keys actually matter, what the expected values are, and where to look first. Seven surfaces, one baseline, a routine sweep — that is most of what you need for the first pass. Anything beyond it is for the SIEM and the EDR.
Related Posts
- Windows Security: Detecting Malicious Scheduled Tasks — the other persistence surface most often paired with the registry.
- Windows Security: Best Practices for Securing Windows Services — defensive hardening for the service surface used in section 2.
- PowerShell Quick Guide: Process Investigation — pair with registry triage to confirm the binaries actually running.
Authoritative reference: MITRE ATT&CK Persistence tactic (TA0003).
0 comments:
Post a Comment