Registry Parser
All articles

CurrentControlSet is a lie: control sets and the Select key

8 min read

Every Windows reference, every Microsoft KB, every malware writeup points you at paths like HKLM\SYSTEM\CurrentControlSet\Services. Then you mount the SYSTEM hive offline, type CurrentControlSet\Services into your parser, and get nothing. The key is not there. It was never there. CurrentControlSet is a runtime symbolic link the kernel materialises at boot; it does not exist on disk. What you have on disk is a Select key and a set of numbered control sets — ControlSet001, ControlSet002, and so on. Resolving which control set was "current" at acquisition is the first thing you do with a SYSTEM hive, and getting it wrong means analysing the wrong configuration.

This is one of those traps that everyone hits exactly once. This post is the explanation I wish I had the first time.

Live versus on-disk: what the kernel does at boot

On a running system, HKLM\SYSTEM is the loaded SYSTEM hive. The kernel reads the Select key, decides which numbered control set is active, and creates CurrentControlSet as a volatile symbolic link pointing at that control set. Code that reads HKLM\SYSTEM\CurrentControlSet\Services\Tcpip is really reading ...\ControlSet001\Services\Tcpip (or 002, or whatever Select named). The link exists only in the live registry's view. It is not a stored nk cell, so it never lands in the file. (If hive cells, nk records, and the rest of the on-disk vocabulary are unfamiliar, the regf format overview covers them.)

So when you load a SYSTEM hive from a triage image, a reg save, or a VSS snapshot, the root has children that look like this:

ControlSet001
ControlSet002        (may or may not be present)
Select
Setup
WPA
MountedDevices
...

No CurrentControlSet. That is not corruption and not a tool bug. The hive is faithful; the symbolic link simply was not a thing that gets written.

The Select key

SYSTEM\Select holds four REG_DWORD values. Each is a control set number, not a path:

  • Current — the control set the kernel is using right now (or was using when the hive was last flushed). 0x1 means ControlSet001, 0x2 means ControlSet002.
  • Default — the control set the system will boot into next, absent intervention. Normally identical to Current.
  • Failed — the control set that last failed to boot far enough to be marked good. 0x0 if none has.
  • LastKnownGood — the control set that last completed a successful boot and login, the one the "Last Known Good Configuration" recovery option rolls back to.

The mapping is mechanical: read Select\Current, prefix ControlSet, zero-pad the number to three digits. Current = 1 resolves to ControlSet001. Current = 2 resolves to ControlSet002. There is no ControlSet000; numbering starts at 1.

How this tool resolves it

This is not theoretical hand-waving — it is exactly what the SYSTEM-hive plugins here do before touching anything else. The helper that every SYSTEM-aware plugin calls (lib/plugins/helpers.ts) is short enough to read in full:

export async function currentControlSet(s: HiveSession): Promise<string> {
  const v = await s.getValue("Select", "Current");
  if (v && typeof v.value === "number" && v.value > 0) {
    return `ControlSet${String(v.value).padStart(3, "0")}`;
  }
  if ((await findKey(s, "ControlSet001")) !== null) return "ControlSet001";
  return "CurrentControlSet";
}

Three steps, in priority order:

  1. Read Select\Current. If it is a positive DWORD, format it as ControlSet plus the number padded to three digits (padStart(3, "0")). Current = 1 becomes ControlSet001. This is the correct, evidence-driven answer.
  2. If Select is unreadable or absent — a truncated hive, a Select key that did not survive — fall back to ControlSet001, but only if it actually exists. On the overwhelming majority of systems ControlSet001 is the current set, so this is a safe default rather than a guess pulled from nowhere.
  3. If even ControlSet001 is missing, return the literal "CurrentControlSet". At that point the hive is not a normal SYSTEM hive (or not a SYSTEM hive at all), and the string is left as-is so the failure is visible rather than silently swallowed.

Everything downstream — the services plugin, network configuration, mounted devices, the Control subtree — builds its paths on top of that resolved prefix. A plugin that wants the service list does not hardcode CurrentControlSet\Services; it resolves the control set first and reads ControlSet001\Services (or whichever number won). That is the whole point of the helper: write the plugin once against a logical "current" control set, and let the resolver rewrite it to the real on-disk name per hive.

Why naive paths fail

If your tooling takes a path like CurrentControlSet\Services\Tcpip\Parameters literally against a raw hive, the lookup fails at the very first path component, because there is no nk named CurrentControlSet under the root. Some tools paper over this by silently injecting a CurrentControlSet alias when they detect a SYSTEM hive; others make you do the rewrite by hand. Either way, the substitution has to happen somewhere, and if it does not, you get an empty result that looks exactly like "this key does not exist on this system" — a false negative that can sink an investigation. The rewrite is not optional. It is the difference between reading the configuration and reading nothing.

The practical rule: any path you copy out of Microsoft documentation, a blog, or a YARA-style detection that begins with CurrentControlSet must be mentally (or programmatically) rewritten to the resolved ControlSetNNN before it means anything against an offline hive. When you paste a path into the registry tree, this is handled for you — but know what is happening underneath, because the moment you script against the hive yourself, you own the rewrite.

Forensic signals in Select

Select is not just plumbing; the values carry meaning.

Failed. A non-zero Failed value means a control set was marked as having failed to reach a good boot. That points at a recent driver or service change that broke startup — a botched update, a misconfigured boot-start driver, or a service whose ErrorControl was 0x3 (Critical) and whose failure forced a Last Known Good rollback. In an intrusion timeline, a Failed set that suddenly appears around the time of a suspected persistence attempt is worth a hard look: an attacker who registered a malformed boot-start driver may have tripped exactly this mechanism.

LastKnownGood. When LastKnownGood differs from Current, the system has at some point fallen back to a known-good configuration. The control set named by LastKnownGood is a snapshot of a configuration that booted cleanly — which may predate a change you care about. If Current and LastKnownGood point at different numbers, you have two configurations to compare, and the delta between them is, by definition, a change that the system associated with a boot problem.

Default versus Current. These normally agree. When they diverge, someone or something has staged a different control set to boot into next. That is rare on healthy systems and worth explaining.

The recovery semantics matter for interpretation: Last Known Good restores only HKLM\SYSTEM\CurrentControlSet, not the rest of the registry. So a rollback that the Failed/LastKnownGood values record affected drivers and services but left, say, HKLM\SOFTWARE untouched. Do not assume a Last Known Good event reverted everything.

Why the differences between control sets matter

Multiple control sets are not redundant copies updated in lockstep. They drift. ControlSet002 may be a stale snapshot from before a configuration change, retained because it was the last-known-good at some earlier point. That makes the diff between two control sets a genuine forensic artifact:

  • A service or driver present in ControlSet001 but absent from ControlSet002 (or vice versa) tells you when it was introduced relative to the last good-boot snapshot.
  • An attacker modifying CurrentControlSet\Services to install persistence writes into the active set. The inactive set may still hold the pre-compromise configuration. Comparing the two can surface exactly what changed — the original ImagePath, the original Start value, a service that simply was not there before.
  • Misconfigurations cut the same way: a driver disabled in one set but enabled in another explains "it boots fine sometimes" without any malware in the picture.

So when a SYSTEM hive has more than one control set, do not just resolve Current and move on. Resolve it, analyse it, then diff it against the others. The non-current sets are free historical snapshots, and they cost you nothing but a second pass.

Practical checklist

When you load a SYSTEM hive:

  1. Read Select\Current, Default, Failed, and LastKnownGood. Write the four numbers down.
  2. Resolve Current to ControlSetNNN. That is your primary configuration.
  3. Rewrite every CurrentControlSet\... path you intend to query to that resolved prefix.
  4. If Failed is non-zero or LastKnownGood differs from Current, note it and figure out why.
  5. If a second control set exists, diff Services and Control between the sets.

CurrentControlSet, control sets, and the Select key: the takeaway

CurrentControlSet is a convenience the live kernel synthesises from the Select key; it has no on-disk reality. An offline SYSTEM hive gives you numbered control sets and a Select key that names which one was current. Read Select\Current, map the DWORD to ControlSetNNN, and rewrite your paths accordingly — that single resolution step, done correctly, is the foundation of every other piece of SYSTEM-hive analysis. Treat Failed and LastKnownGood as the forensic signals they are, and treat the non-current control sets as the historical snapshots they are.

You can parse a SYSTEM hive in your browser and watch the resolution happen, with nothing uploaded.

Further reading

The first SYSTEM hive that returns nothing for a CurrentControlSet path is a rite of passage. Now it is just a Select lookup and a rewrite.