Skip to content

Inside NTDS.dit: the ESE database and the PEK

A domain controller stores every account hash in an ESE database. Here is how the datatable is laid out, how the Password Encryption Key is derived, and how each hash is unwrapped.

Published on 2 min read
Inside NTDS.dit: the ESE database and the PEK

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

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.
The SECURITY hive stores service passwords, the machine account, DPAPI keys, and offline logon caches. Here is how the LSA key unlocks them — and why DCC2 is salted but still dumpable.