Registry Parser
All articles

Windows services as persistence: the RegRipper services plugin

8 min read

Windows services are the persistence mechanism that survives reboots, runs before any user logs in, and runs as LocalSystem if the attacker asks nicely. Every service on the box is defined in the SYSTEM hive, which means the RegRipper services plugin is reading the same data that the Service Control Manager reads at boot. If something is configured to launch as a service, it is in there. Your job is to tell the legitimate thousand-odd services apart from the one that should not exist, and the SYSTEM hive gives you everything you need to do it offline.

Where services live

Service definitions sit under SYSTEM\CurrentControlSet\Services\<name>. Each subkey is a service or driver: the key name is the short service name (the one sc.exe and net start use), not the friendly display name. Under each key you get a set of values that the SCM consumes at boot, plus optional subkeys, the most interesting of which is Parameters.

The services plugin enumerates every subkey under CurrentControlSet\Services, pulls the values from each, and skips keys with no values (driver stubs and empty placeholders). For each real service it surfaces a row with the short name, DisplayName, ImagePath, the Parameters\ServiceDll, ObjectName, Start, and Type. That column set is deliberate: those seven fields are where persistence and hijacks show up.

The values that matter

Start is the load-order control. It is a REG_DWORD and takes five values:

  • 0 — Boot. Loaded by the boot loader. Disk, filesystem, low-level drivers.
  • 1 — System. Loaded during kernel init.
  • 2 — Auto. Started automatically by the SCM at boot. This is the one attackers want. An auto-start service launches with no user interaction, every boot.
  • 3 — Demand. Started manually or on dependency. Lots of legitimate services live here.
  • 4 — Disabled. Will not start.

A malicious service that wants reliable persistence is almost always Start=2. A Start=3 malicious service needs something else to trigger it, which is more work and less reliable, so it is rarer. Start=4 on a security product is a tell that someone disabled your EDR or Windows Defender service; compare it against a known-good baseline.

Type is also a REG_DWORD. The values you care about:

  • 0x1 — Kernel driver.
  • 0x2 — File system driver.
  • 0x10 (16) — Own-process Win32 service. Runs in its own services.exe-spawned process.
  • 0x20 (32) — Shared-process Win32 service. Runs inside a shared host, i.e. svchost.exe.

Bits like 0x100 (interactive) can be OR-ed in. The split that matters for triage is driver (0x1/0x2) versus user-mode service (0x10/0x20), because it tells you whether ImagePath points at a .sys driver or an .exe, and whether you should be chasing a ServiceDll.

ImagePath is the binary the SCM launches. For an own-process service it is the path to the .exe (often REG_EXPAND_SZ with %SystemRoot%). For a driver it is the .sys file, usually under System32\drivers. For a shared-process service it is %SystemRoot%\System32\svchost.exe -k <groupname> — and that is the trap. svchost.exe is signed, legitimate, and identical across thousands of services. The actual code is somewhere else.

ServiceDll is where that somewhere-else lives. For svchost-hosted services the DLL to load is named under SYSTEM\CurrentControlSet\Services\<name>\Parameters\ServiceDll. The plugin reads Parameters separately and pulls ServiceDll into its own column precisely because ImagePath hides it. If you only ever look at ImagePath, every svchost service looks identical and benign. The ServiceDll is the thing that actually executes, and it is the thing an attacker swaps. The code comment in the plugin says it plainly: this is "a common malware hiding spot the ImagePath (svchost.exe) hides."

ObjectName (surfaced as "Run as") is the account the service runs under: LocalSystem, NT AUTHORITY\LocalService, NT AUTHORITY\NetworkService, or a named account. A service running as LocalSystem is running with the highest privilege available on the box. A service configured to run as a specific domain or local user account is unusual enough to look at twice — attackers occasionally do this to run under a compromised account, and it is rare in legitimate built-in services.

How attackers use this surface

This is MITRE ATT&CK T1543.003, Create or Modify System Process: Windows Service. Three variants, all visible in the SYSTEM hive:

New service. The attacker creates a brand-new service key pointing ImagePath at their binary, Start=2, ObjectName=LocalSystem. Crude but extremely common — sc create, New-Service, or the equivalent API. PsExec famously does this: it drops PSEXESVC as a service to get its payload running as SYSTEM. The tell is a service whose name you do not recognise, whose ImagePath is in a non-standard directory, with a recent key LastWrite.

Service hijack. The attacker takes an existing, legitimate, disabled or rarely-used service and repoints its ImagePath (or flips Start from 4 to 2). The service name stays trustworthy; the binary behind it does not. This evades a defender who is only checking for unfamiliar service names.

ServiceDll swap. The most evasive of the three. The attacker leaves ImagePath as svchost.exe, leaves the service name alone, and only changes Parameters\ServiceDll to point at a malicious DLL. From the outside the service looks completely normal. The malicious code rides inside a legitimate, signed svchost.exe process. This is why a dedicated ServiceDll column exists, and why you compare it against what that service's DLL should be.

The CurrentControlSet gotcha

CurrentControlSet is not a real key in the hive on disk. It is a runtime symlink the kernel populates at boot, pointing at one of the numbered ControlSet00N keys. When you load a SYSTEM hive offline, CurrentControlSet may not resolve the way it does on a live system. The plugin handles this with a currentControlSet helper that reads Select\Current and resolves to the right ControlSet00N — so the rows you see reflect the control set that was actually current at the time the hive was captured.

That resolution is exactly why you should also look at the other control set yourself. A typical hive has ControlSet001 and ControlSet002. Select\Current names the one in use; Select\LastKnownGood names the last-good fallback. If an attacker modified a service and the system later rolled back to LastKnownGood, or if the malicious change only landed in one control set, comparing ControlSet001\Services against ControlSet002\Services will surface the discrepancy. A service that exists in one set but not the other, or one whose Start, ImagePath, or ServiceDll differs between sets, is worth running down. The plugin reports the current set; the diff is a manual step, but it is a cheap one.

Spotting the anomalies

Once the table is in front of you, the triage patterns are mechanical:

  • ImagePath in a user-writable path. Legitimate service binaries live in System32, System32\drivers, or a vendor directory under Program Files. An ImagePath (or ServiceDll) pointing into %APPDATA%, %LOCALAPPDATA%, \Users\<name>\, \Temp\, \ProgramData\ (a softer signal), or C:\Windows\ root rather than System32 is anomalous. Services do not normally launch binaries out of user-writable directories.
  • Unsigned or oddly-named binaries. The hive only gives you the path, not the signature — but the path is enough to pivot. Pull the binary referenced by ImagePath/ServiceDll and check its signature out of band. Random-looking eight-character DLL names, or a binary named to impersonate a system file but living in the wrong directory, are classic.
  • Recently-created service keys. Every service key carries a LastWrite timestamp. Sort by it. A cluster of service keys written around your suspected intrusion window, against a backdrop of keys that have not changed since OS install, is a strong lead. The caveat: LastWrite updates on any value change, so a recent timestamp is necessary but not sufficient — it tells you the key was touched, not that it was created.
  • svchost service with a ServiceDll outside System32. A shared-process service whose ImagePath is the normal svchost.exe -k line but whose ServiceDll resolves anywhere other than System32 is the ServiceDll-swap pattern, full stop.

Pairing with the svchost plugin

The services plugin shows you, per service, which ServiceDll a svchost service loads. The companion svchost plugin comes at it from the other direction: it reads SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost, which defines the service groups — the -k netsvcs, -k LocalService, and so on that the ImagePath references — and lists which services are supposed to belong to each group. Cross-referencing the two answers a question neither answers alone: is this service hosted in the group it claims, and is that group's membership what it should be? A service that points its ImagePath at -k netsvcs but is not a member Microsoft ships in netsvcs, or a custom group name that does not appear in the standard svchost configuration, is a fabricated host group — a known persistence trick. Run both plugins, line them up.

Tools

  • RegRipper's services plugin. Enumerates the services key and decodes Start, Type, ImagePath, and the ObjectName. Read the source to confirm exactly which fields your version emits — plugin output has shifted across RegRipper releases.
  • Eric Zimmerman's RECmd with a services batch file. Same data, CSV out, easy to diff between control sets.
  • The parser on this site surfaces the same seven fields — name, display name, ImagePath, ServiceDll, run-as account, Start, and Type — in its tree view, so you can analyze a SYSTEM hive in your browser without standing up a Perl environment.

For the full set of what RegRipper extracts from each hive, see the RegRipper plugins reference. And because services are only one persistence surface, pair this analysis with the Run keys and IFEO post — an attacker who failed to get a service past you may have fallen back to a Run key, and vice versa.

The bottom line

A service is the most durable persistence an attacker can plant and the SYSTEM hive records every detail of it offline. Start=2 for the auto-launch, ObjectName=LocalSystem for the privilege, ImagePath or ServiceDll for the actual code. Read all seven fields, never trust a svchost ImagePath on its own, resolve CurrentControlSet and then diff the control sets by hand, and sort by LastWrite. The malicious service is rarely subtle once you know which columns lie to you.