Registry Parser
All articles

Winlogon Shell and Userinit persistence: the RegRipper winlogon plugin

7 min read

Winlogon is the process that owns the interactive logon. Before the desktop exists, before Explorer paints a single icon, Winlogon reads a small set of registry values that tell it which user shell to start and which support binaries to run on the way there. Those values are a near-perfect persistence surface: they execute at every interactive logon, they run in the right session as the right user, and outside of a handful of canonical strings they should never change. The RegRipper winlogon plugin exists to pull those few values out of the SOFTWARE hive so you can eyeball them against a known-good baseline in seconds. Shell, Userinit, and the Notify subkeys are the three places an attacker reaches for, and they are the three places this post is about.

Where it lives

Everything here is under one key in the SOFTWARE hive:

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon

The plugin reads that exact path (Microsoft\Windows NT\CurrentVersion\Winlogon) and pulls a small set of values out of it. The columns it emits are simply Field and Value, one row per value of interest:

  • Shell
  • Userinit
  • DefaultUserName
  • DefaultDomainName
  • AutoAdminLogon

The first two are the persistence-relevant ones. The last three are credential/auto-logon hygiene checks that ride along in the same key and are worth a glance while you are already there: a populated DefaultUserName/DefaultDomainName with AutoAdminLogon set to 1 means the box logs in automatically, and on older configurations a cleartext DefaultPassword value may sit alongside them. That is a finding in its own right, separate from any code-execution persistence.

There is a subtlety that catches people. There is also an HKCU\...\Winlogon per-user key, but the values that matter for system-wide logon persistence live in HKLM under SOFTWARE. The RegRipper plugin, and the parser on this site, work the SOFTWARE hive. If you are hunting a per-user variant, you need the NTUSER.DAT equivalent as well.

What "normal" looks like

This is the whole game with Winlogon. The clean baseline is so narrow that anomalies jump out the instant you know the reference strings.

Shell should be exactly explorer.exe. Not a path, not a list, not explorer.exe followed by anything. Just the bare image name. The moment you see Shell set to explorer.exe, C:\Users\Public\svc.exe or Shell pointing at something in %APPDATA%, you have found persistence. Winlogon happily launches every executable in that value, in order, so appending a second binary is the classic, low-effort move.

Userinit should be C:\Windows\system32\userinit.exe, — note the trailing comma. userinit.exe is the binary that runs logon scripts and then launches the shell; the value is a comma-delimited list, and the canonical clean value is that one path with a trailing comma and nothing after it. The attack mirrors the Shell attack: append a second path after the comma. C:\Windows\system32\userinit.exe,C:\ProgramData\update.exe runs update.exe at every logon, as the logging-on user, with userinit as the parent. The trailing comma is load-bearing as a baseline cue — when you see a second comma followed by a path, stop and pivot.

A couple of honorable mentions that the canonical Winlogon key has historically carried and that you should check by hand even though this particular plugin's row set is focused on Shell/Userinit/auto-logon:

  • Taskman. Defines the task manager binary launched from the shell. Normally absent. If present, it is pointing somewhere, and you want to know where. Any value here is worth treating as suspicious until proven benign.
  • GinaDLL. Legacy only. On pre-Vista systems Winlogon loaded a GINA (Graphical Identification and Authentication) DLL named by this value, and replacing it was a well-worn credential-theft and persistence trick. On Vista and later GINA was replaced by the credential provider model and GinaDLL should not exist. If you are looking at a modern hive and GinaDLL is present, that is an artifact someone put there. If you are looking at an XP-era hive, treat it the way you would treat any other DLL load path.

I would not assert that every build ships with identical strings down to the byte — service packs and OEM images do wander — but the shape above (Shell = bare explorer.exe, Userinit = the single system32 path with a trailing comma, Taskman/GinaDLL absent) is the baseline to anchor on, and deviations are the thing to chase.

The Notify subkeys

The second RegRipper plugin in this family, winlogon_notify, handles the notification packages. These live one level down:

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Notify\

Each subkey under Notify is a notification package. Historically these DLLs registered to receive Winlogon events — logon, logoff, lock, unlock, startup, shutdown — and got loaded into the Winlogon process to handle them. The plugin enumerates the subkeys and, for each, reads the DllName value, emitting two columns: Package (the subkey name) and DllName (the DLL it loads).

That is the entire attack: register a subkey under Notify, point DllName at your DLL, and it loads into a SYSTEM-context process at logon. As with GINA, the Notify mechanism is a legacy Winlogon feature that was deprecated after Windows XP/2003 — modern Windows does not use SLDLL-style notification packages the way XP did. So on a modern hive, a populated Notify subkey pointing at a DLL outside the usual system locations is a strong tell. On legacy hives you will see legitimate entries (crypt32chain, cryptnet, cscdll, ScCertProp, sclgntfy, SensLogn, termsrv, wlballoon were common), so you are looking for the package that does not belong: an unusual name, a DllName in a writable directory, a DLL with no signature.

When you find one, pivot to the DLL itself. Pull it from the MFT, check its signature, check its compile timestamp against the subkey's LastWrite. The subkey LastWrite tells you when the persistence was planted; that timestamp is often the most useful single value on the screen.

Why this surface is worth the thirty seconds

Winlogon persistence is cheap to plant and cheap to find, which is exactly why it is worth checking on every SOFTWARE hive you touch. The values execute early, with high privilege, at every interactive logon, and the clean baseline is so small that the check is almost free. An attacker who appends to Userinit gets code execution as every user who logs in, running under a parent (userinit.exe) that looks unremarkable in process telemetry. That is a good deal for them and a fast win for you if you know the reference strings.

This is MITRE ATT&CK T1547.004 — Boot or Logon Autostart Execution: Winlogon Helper DLL, and it sits in the same logon-autostart family as the Run keys and Image File Execution Options. Do not work Winlogon in isolation. The same intrusion that touched Userinit very often also dropped a Run-key value or an IFEO Debugger hijack as a backup, and the cluster of artifacts is more convincing than any single one. Read the Run keys and IFEO persistence notes for that adjacent surface, and if the box is also running a suspicious service, the services plugin covers the registry side of that vector. Triage all of the SOFTWARE-hive autostart keys in one pass.

A practical workflow: open the SOFTWARE hive, run winlogon and winlogon_notify, and read four things — is Shell bare explorer.exe, does Userinit end at the single system32 path and its trailing comma, are Taskman/GinaDLL absent, and is Notify empty (or only the legacy packages on a legacy box). If all four hold, Winlogon is clean and you move on. If any one breaks, you have a lead with a LastWrite timestamp and a binary path to pivot on.

Tools

  • RegRipper's winlogon plugin. Reads the Winlogon key and surfaces Shell, Userinit, and the auto-logon values. Output is one line per value. The plugin source is short; read it once so you know exactly which values are and are not being pulled, then check the rest (Taskman, GinaDLL on legacy) by hand if your build carries them.
  • RegRipper's notification-package handling for the Notify subkeys and their DllName values.
  • Eric Zimmerman's RECmd with a Winlogon batch file produces the same fields in CSV if you are building a timeline.
  • You can analyze a SOFTWARE hive in your browser with the parser on this site, which surfaces the Winlogon and Notify values alongside the other persistence keys, with subkey LastWrite timestamps inline. See the RegRipper plugins reference for the full plugin catalog.

Whatever tool you use, the discipline is the same: memorize the clean baseline. Shell is explorer.exe. Userinit is C:\Windows\system32\userinit.exe, with the trailing comma. Notify is empty on modern Windows. Anything else is a binary you did not expect getting executed at logon, and that is the whole reason this key is on the map.

Further reading