Skip to content

NTDS.dit explained: how domain hashes are stored and dumped

A domain controller keeps every account hash in one ESE database. See how the datatable, the Password Encryption Key (PEK) and each cipher layer fit — and how a hash is unwrapped offline.

Published on 2 min read
NTDS.dit explained: how domain hashes are stored and dumped

On a domain controller every account hash in the forest lives in a single file: NTDS.dit. Unlike the registry hives it is a full ESE database (Extensible Storage Engine, also called JET Blue) — the same engine behind Exchange. Dumping it means parsing that database, then applying the same kind of layered decryption you saw in the SAM.

The ESE layout

An ESE file is a tree of fixed-size pages (8 KiB for NTDS). The pieces a dumper cares about:

  • The header (page 0) — version, revision, and page size.
  • The catalog (page 4) — defines every table, and for each table its columns (name, identifier, type, codepage).
  • The datatable — one giant table holding every directory object as a row.

Records are decoded in three regions: fixed-size columns, variable-size columns (length array up front), and tagged columns (a sparse index of present attributes). The columns we want carry cryptic names like ATTk589914 (unicodePwd / NT hash) and ATTr589970 (objectSid).

The Password Encryption Key

Hashes in NTDS are wrapped with a PEK rather than the boot key directly. The PEK list is stored in the pekList attribute of the domain root object and is encrypted under the boot key:

# up to Server 2012 R2
tmpKey = MD5(bootKey ‖ keyMaterial × 1000)
peks   = RC4(tmpKey, encryptedPek)

# Server 2016+
peks   = AES-CBC(bootKey, encryptedPek, keyMaterial)

A DC may hold several PEKs (after key rollover); each ciphered hash names its PEK index in its header byte.

Unwrapping a hash

For each account row, the unicodePwd (NT) and dBCSPwd (LM) blobs are peeled in two layers — exactly mirroring the SAM, but keyed by the PEK instead of the hashed boot key:

# layer 1 (PEK)
tmpKey = MD5(PEK[idx] ‖ keyMaterial)      # or AES on 2016+
inner  = RC4(tmpKey, encryptedHash)

# layer 2 (per-RID DES)
rid    = big-endian last 4 bytes of objectSid
hash   = removeDESLayer(inner, rid)

The RID comes from the account's objectSid. Only rows whose sAMAccountType marks them as a user, machine, or trust account are dumped; the datatable also holds groups, OUs, schema objects and much more.

Beyond NT hashes

The supplementalCredentials attribute additionally stores Kerberos keys (AES256/AES128/DES) and, where reversible encryption is enabled, cleartext passwords — derived with the same PEK unwrap. Those are what make a full NTDS.dit dump so valuable to an attacker and so worth protecting: it is, quite literally, every credential in the domain.

Defensive notes

NTDS.dit only leaves a DC through a backup, a volume shadow copy, or ntdsutil — so the controls that matter are tight backup custody, monitoring for shadow-copy creation and DRSUAPI replication (DCSync), and Tier-0 isolation of domain controllers.

Related articles

How secretsdump's -just-dc uses the DRSUAPI replication protocol to pull every domain hash from a DC without touching disk — the required rights, the flags, and what it outputs.
From the hashed boot key to a user's NT hash: the F and V structures, the RC4 vs AES storage formats, and the per-RID DES layer that wraps every Windows password hash.
The boot key is the root of all offline credential dumping. Here is where it lives, why it is scrambled across four registry keys, and how to reassemble it from the SYSTEM hive.