Local accounts from the SAM hive: the RegRipper samparse plugin
9 min read
The SAM hive is the local account database. When you need to answer "who could log into this box, when did they last do it, and is any of it suspicious", the SAM hive is the source, and the RegRipper samparse plugin is the fastest way to turn its binary structures into a table. The word you have to keep in front of you the whole time is local. SAM knows about the accounts defined on this one machine. It knows nothing about domain accounts, which live in NTDS.dit on a domain controller. Treat the SAM hive as a per-host artifact and you will not over-claim.
Where the accounts live
Each local account is a subkey under:
SAM\Domains\Account\Users\<RID>
The RID subkeys are named as eight hex digits. 000001F4 is RID 500, 000001F5 is RID 501, and so on. samparse enumerates that path and keeps only the keys whose names match eight hex characters; everything else under Users (the Names subkey, for instance) is skipped.
Inside each RID key are two binary values that carry the account data: F and V. There is no friendly REG_SZ "username" value sitting in plain text. The username, the full name, the comment, the timestamps, the flags, the login count — all of it is packed into those two blobs, and you decode them by offset. This is why ad-hoc registry browsers are nearly useless here and why a plugin that knows the layout earns its keep.
What the F value holds
F is a fixed-layout structure. The fields samparse reads, and that this site's parser reads to match it, are:
- Offset 8 (8 bytes): last login, a FILETIME.
- Offset 24 (8 bytes): password last set, a FILETIME.
- Offset 40 (8 bytes): last failed login, a FILETIME.
- Offset 48 (4 bytes): the RID, little-endian.
- Offset 56 (2 bytes): the ACB account-control flags.
- Offset 64 (2 bytes): failed login count.
- Offset 66 (2 bytes): login count.
Those are the offsets the decoder actually uses. The classic RegRipper output also reports an account-expiration FILETIME from the same structure; it lives in the F block but our parser does not currently surface it, so if you need expiry specifically, read it out of RegRipper's output and do not assume the column exists everywhere.
A few cautions about the timestamps. The FILETIMEs are genuine, but their meaning is narrower than it looks. "Last login" and "last failed login" reflect interactive and network logons the SAM tracked locally; they are not a complete authentication log, and a value of zero (which renders as empty) means the event has not occurred, not that it happened at the epoch. "Password last set" is one of the more reliable fields and is often the cleanest evidence that an account was provisioned or had its credentials reset at a given time.
The login count at offset 66 is a cumulative counter, not a per-session log. A high count is a sign of an account in routine use. A count of zero on an enabled account is a flag worth chasing: it can mean the account was created and never used, which is exactly what a staged backdoor account looks like before it gets activated.
What the V value holds
V packs the variable-length strings. Its header is a series of (offset, length) pairs, and — this is the part that trips people up — each offset is relative to 0xCC, not to the start of the structure. So you read a 4-byte offset, add 0xCC, read the matching 4-byte length, and pull that many bytes of UTF-16LE.
The fields samparse extracts:
- Username: offset field at
0x0C, length field at0x10. - Full name: offset field at
0x18, length field at0x1C. - Comment: offset field at
0x24, length field at0x28.
The RID is read from F, not V; the plugin prefers the F-decoded RID and falls back to parsing the hex key name only if F is missing or unreadable. The comment field is worth reading every time. Built-in accounts ship with descriptive comments ("Built-in account for administering the computer/domain"), and an attacker who clones an account or sloppily creates one often leaves the comment blank or copies a mismatched one. A username and comment that do not agree is a small but real tell.
Put F and V together per RID and you get one row per local account: username, RID, full name, comment, decoded flags, last login, password-last-set, last failed login, login count, failed-login count. That is the samparse table.
The well-known RIDs
Some RIDs are fixed across every Windows install, and recognising them on sight saves time:
- 500 — Administrator. Always present, even when renamed. The RID does not change when the account is renamed, which is the whole point of looking at the RID rather than the name. An "Administrator" account that is not RID 500 is a decoy; the real one is whatever sits at 500.
- 501 — Guest. Normally disabled. An enabled Guest is an immediate finding.
- 503 — DefaultAccount. The DSMA (Default System Managed Account), introduced in Windows 10. Benign by default.
- 504 — WDAGUtilityAccount. Used by Application Guard. Also benign by default.
Locally created accounts start at 1000 and increment. So a clean baseline is: a 500, a 501, the system-managed accounts in the 500s, and then the real human and service accounts from 1000 up. Anything that breaks that pattern — a renamed 500, an enabled 501, an unexpected account in the 1000s with a zero login count — is where you spend your attention.
Decoding the account flags
The ACB flags at offset 56 are a bitfield. samparse (and this site's parser, which mirrors its flag map) decodes the bits that matter:
0x0001— Account disabled0x0002— Home directory required0x0004— Password not required0x0008— Temporary duplicate account0x0010— Normal user account0x0020— MNS logon user account0x0040— Interdomain trust account0x0080— Workstation trust account0x0100— Server trust account0x0200— Password does not expire0x0400— Account auto-locked
The three you read first in an investigation are Account disabled (0x0001), Account auto-locked (0x0400), and Password not required (0x0004). A 0x0004 on a human account is dangerous by itself — it means the account can authenticate with a blank password — and it is a favourite of lazy persistence. 0x0200, "Password does not expire", on a service-style account is normal; on a fresh account in the 1000s it is the kind of small convenience an attacker grants themselves.
A flag value with none of the recognised bits set renders as (none), which on a real account usually just means the structure was short or unreadable, not that the account is in some exotic state.
Group membership
Local accounts get their privileges from local groups, and those live in a different branch:
SAM\Domains\Builtin\Aliases\<RID>
Each alias key has a C value. The C structure carries the group name, a comment, and a list of member SIDs. As with V, the offsets are relative to a base — 0x34 for the strings — and the member SIDs are read as a packed list whose count and starting offset come out of the header. The sam_groups plugin walks each member SID, and for members that are local accounts it resolves the SID's RID back to the username it built from the Users keys. Members that are not local (domain SIDs, well-known SIDs) are left as raw SID strings, because SAM cannot resolve them — it does not have the data.
The group that matters most is Administrators (alias RID 0x220, 544). Read its membership every time. The expected occupants are RID 500 and whatever admin group the install uses. An extra local account in Administrators, especially one you have already flagged for a zero login count or a Password not required bit, is the join you are usually looking for. Cross-reference the membership table against the account table and the picture assembles itself.
What SAM can and cannot tell you
SAM is local. It does not contain domain accounts, domain group membership, or anything about authentication that happened against a domain controller. If a domain user logged into this host, you find traces of that in other artifacts — profile keys, the ProfileList, event logs — not in SAM's Users. Do not present a SAM-derived account list as "the users of this environment". It is the account list of one machine.
SAM also stores the password hashes — in the V structure, alongside the strings — but you cannot extract them from the SAM hive alone. The hashes are encrypted with the syskey (bootkey), and the bootkey is assembled from four obfuscated values under SYSTEM\ControlSet001\Control\Lsa (the JD, Skew1, GBG, and Data keys). On modern systems the SECURITY hive is also involved in the cached-credential and LSA-secret material. The practical consequence for acquisition: if hash extraction is in scope, you must grab SYSTEM (and usually SECURITY) alongside SAM, or you have collected a hive whose most sensitive contents you cannot read. We are not covering hash extraction or cracking here; the point is purely that SAM by itself is not sufficient, which informs what you collect. For the broader collection question, see which registry hive to grab.
Tools
- RegRipper's
samparseplugin. Parses every RID'sFandV, decodes the ACB flags, and prints the timestamps and counters. It is the reference implementation for SAM account parsing, and the plugin source is the canonical place to confirm an offset. The group-membership output covers the Builtin aliases. - Eric Zimmerman's RECmd with a SAM batch file, for CSV output that drops straight into a timeline.
- The parser on this site decodes the SAM hive in the browser, with the same F/V/C structures and the same ACB flag map, so you can analyze a SAM hive in your browser without a local toolchain. For the full list of what else runs against SAM and the other hives, see the RegRipper plugins reference.
The workflow that uses all of it
Pull the account table first. Note the RIDs against the well-known set, flag every enabled account, and read the flag column for Password not required and Password does not expire. Then pull sam_groups and read the Administrators membership. The accounts that appear in both your flagged list and the Administrators group are your shortlist. For each one, look at password-last-set and login count: a privileged account created recently, never logged in, with a non-expiring or blank-allowed password, is a planted backdoor until proven otherwise.
What SAM will not give you on its own is the rest of the story — the logon events, the domain context, the actual credentials. Those come from the event logs, the SECURITY hive, and the SYSTEM hive that holds the bootkey. SAM tells you the cast of local accounts and their basic state with high confidence. Pair it with the hives around it and you get the scene.