Exploring NTDS.dit – Part 1: Cracking the Surface with DIT Explorer

Table of contents
NTDS.dit is the file housing the data for Windows Active Directory (AD). In this blog post, I’ll be diving into how the file is organized. I’ll also be walking through the new open-source tool DIT Explorer I developed for researching NTDS.dit and how it makes sense of this database to present a view of the directory. At the end you’ll find a list of references to documentation and other blogs that go into a little more detail on some of the topics covered in this article.
A Note About Names:
Schema objects in the directory have two names: the name proper and an LDAP name. The proper name is proper cased with dashes separating words, whereas the LDAP name is camel cased. For example, Object-Class and objectClass refer to the same attribute, just by different names. Throughout this article, I use both types of names to reference schema objects, depending on the context.
Introducing DIT Explorer
To facilitate my own research, I wrote a tool I call DIT Explorer. DIT Explorer opens a .dit file of your choosing, loads the directory schema, and presents the objects as a tree. It allows you to inspect the properties of objects in a manner similar to ADSI Edit. It also lets you see the database schema including tables, columns, and indexes, as well as export the raw table data for your own analysis.
Throughout this article I’ve included screenshots from DIT Explorer to demonstrate various points.
DIT Explorer is available for download on our GitHub repo. Feel free to grab a copy and follow along!
What is NTDS.dit?
So what is this file? The NTDS part stands for NT Directory Services, while the DIT part stands for Directory Information Tree. This file is used by the Directory System Agent, or DSA. The DSA is implemented, in part, within NTDSAI.DLL. The location of NTDS.dit and other DSA parameters are located under this registry path:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters
Note that if you are accessing the SYSTEM hive from an offline hive file, you’ll need to replace CurrentControlSet
in the path above with ControlSet001
. Here are some of the more interesting values:
Value Name | Description |
Configuration NC | NC containing configuration objects |
DSA Database file | Path of NTDS.dit |
Machine DN Name | DN of the NTDS settings object for this DC |
Root Domain | Domain hosted by this DC |
Figure 1- NTDS Registry Values
There are several other values in that registry key, such as the backup location and log file location, that may be of interest to you so take a look. The path to NTDS.dit is generally located here:
C:\Windows\NTDS\ntds.dit
The file itself is a database file created and accessed using the Extensible Storage Engine, or ESE. ESE is a database engine provided by Windows, implemented in ESENT.DLL, that exposes its functionality as a series of JET API functions. These functions allow you to open the file, query its contents, and even make modifications. Note that although the term JET is also used by Microsoft Access, ESE is a completely different engine, though, so don’t try opening the file in Access. JET stands for Joint Engine Technology, and refers to the API, not the implementation or the file format.
Acquiring NTDS.dit
To get started, let’s grab a copy of NTDS.dit since we know where to find it. There are a few caveats, however. First, on a running DC, the file is locked and cannot be accessed directly. Most approaches to acquiring this file rely on using a volume shadow copy, which essentially takes a snapshot of the file, optionally asking the DSA to ensure the file is in a consistent state. Note the emphasis on optionally, as this caveat will likely come back to bite you.

The next caveat to consider is that ESE writes entries to transaction log files alongside NTDS.dit to ensure its consistency. Take a look at the directory listing above. Digging through the ESE source code provided insight into some more of the file name extensions. The .chk file is the checkpoint log, .jrs is the log reserve file, and .jfm is the flush map file.
If the DSA is stopped abruptly, or the file is copied out from under it during a transaction, ESE can detect that something bad happened. This is great for a production environment where the file can be recovered or restored from backup. It’s not so great if you are acquiring the file through unintended methods. If you can, it’s best to grab the transaction logs sitting in the directory alongside NTDS.dit, as this will aid in recovery and improve your chances of having a usable NTDS.dit file. If you can’t, I’ll go over a workaround in the section Repairing a Damaged NTDS.dit.
Here are some common methods of acquiring NTDS.dit:
- Access the virtual hard drive directly for a DC based on a VM
- Use ntdsutil
- Use diskshadow
- Use vssadmin*
- Use wmic*
*Note that these methods will likely result in a copy of NTDS.dit that must be recovered or repaired. Remember that optionally part above? These methods don’t invoke the writers, so the copy of NTDS.dit you get will need to be repaired. For instructions on how to repair an ESE database, see Repairing a Damaged NTDS.dit below.
Using Ntdsutil
Running Ntdsutil presents you with a prompt. At this prompt, enter the following commands:
activate instance ntds
snapshot
create
The create
command creates a snapshot and prints its ID. Replace < id >
with this ID in the following command:
mount <id>
The mount
command prints the path where the snapshot is mounted. Navigate to the path containing NTDS.dit within this snapshot and copy the files from there.
Fun Fact: If you run ntdsutil /?
for help, it calls itself dsdbutil
. There is also a dsdbutil.exe that does the same thing.
Using Diskshadow
Diskshadow is a utility for working with shadow copies. When run, it presents you with a command prompt. Enter the following commands:
set context persistent
add volume C:
create
expose %VSS_SHADOW_1% z:
exit
This creates a shadow copy and mounts it as Z:
. Now simply copy the NTDS.dit and accompanying files from this volume.
Using Vssadmin
On a server, vssadmin allows you to create a shadow copy. Note that you can’t use vssadmin to create shadow copies on client versions of Windows.
vssadmin create shadow /for=C:
The command output prints a path similar to \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopyN
, where N
is a number. Copy the NTDS.dit file from this location using the copy
command:
copy \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopyN\Windows\NTDS\NTDS.dit <location of your choice>
You can replace the NTDS.dit part above with * to copy all files from that directory, including the transaction logs. Either way though, you’ll likely need to run the repair process described below.
Using wmic
WMIC is a command line utility that exposes the functionality of WMI, which conveniently includes the capability to create a shadow copy. Note that WMI can be accessed remotely, so you could use this approach to create a shadow copy on a remote system. To use this utility to create a shadow copy, issue the following command, replacing C:\ with the appropriate volume if necessary:
wmic shadowcopy call create volume=C:\
The command prints the ID of the shadow copy. To get the path of this shadow copy, replace < shadow ID >
with this ID, including the enclosing curly braces:
wmic shadowcopy where ID="<shadow ID>" get deviceobject
This gives you a path resembling \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopyN
that you can use to copy the NTDS.dit file:
copy \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy15\Windows\NTDS\NTDS.dit mydir\NTDS.dit
It’s worth noting that there is nothing magical about WMIC. It’s just the middleman, leaving WMI to do the heavy lifting. You can use this approach with other WMI interfaces such as PowerShell and DCOM.
Repairing a Damaged NTDS.dit
Esentutl is a command line utility for working with ESE databases. Note that this utility modifies the database, so if you are experimenting, you may want to make a backup copy of NTDS.dit and any transaction log files, if you have them. So, what can Esentutl do for us? You have a few options here. First, esent /g
assesses the condition of the database, detecting whether it was closed properly. It will tell you whether you can recover the database from the transaction logs using the /r option. If not, esent /p
will attempt to repair the database, which doesn’t require the log files but does discard data from transactions that were in progress at the time the file was acquired. To repair the file, use this command:
esentutl /p ntds.dit
You’ll get a warning saying that information from the transaction log will be discarded. At this point, you don’t really have a choice. This repairs the file so that you can use it with ESE and tools like DIT Explorer that use ESE.
Inside NTDS.dit
Now that you have the file, let’s take a look inside. Although Windows provides the database engine to access the file, there is no GUI or command line component. There are various tools available for working with ESE databases, both command line and GUI. I’ll be using DIT Explorer. Note that the structure of this database is not documented, at least not officially. Much of the information here is based on direct inspection of NTDS.dit, looking through the implementation in NTDSAI.DLL as well as some of the writings listed in the References section below. If you want to inspect the database manually, DIT Explorer provides a thin wrapper around ManagedEsent for accessing the ESE database tables directly as well as a directory object model that makes sense of these structures and presents the information as a directory tree.
Once you open NTDS.dit, using whatever tool you choose, you’ll see a few tables. In DIT Explorer, use View > Database Schema (F8)
from the main menu bar. Note that you can sort the list of columns by clicking the column headers. You can also right-click the list and copy items to the clipboard.

You’ll see a few tables. Here are some of the more interesting ones:
- datatable - Contains a record for every object in the directory
- link_table - Contains records for links between objects
- sd_table - Contains a row for each distinct security descriptor
Object Hierarchy
First up, let’s talk about datatable. It’s a lot to take in. At the top of the list (if you sorted by ID) you’ll see a few columns used for bookkeeping:
- DNT_col - Contains the DNT, or Distinguished Name Tag, which uniquely identifies each record
- PDNT_col - DNT of parent (0 for root)
- NCDNT_col - DNT of the enclosing naming context
- Ancestors_col - List of DNTs of ancestors
Every record in the database has a DNT. This means that every object has a DNT. But there are records for things that aren’t objects, at least not in the LDAP sense. This includes tombstones and recycled objects. According to the Active Directory documentation, the limit on the number of objects is close to 231, which makes sense seeing how the primary key is a 32-bit signed integer.
Almost every record has a value for PDNT_col and NCDNT_col. These hold the DNTs for the parent object and naming context, respectively. The naming context is essentially the partition an object belongs to. For example, the Schema container is a naming context. Although it is within the domain according to the PDNT_col and NCDNT_col, its children are not. This is indicated by the Instance-Type, which has the flag for 'head of naming context'. Its descendants have NCDNT_col set to the DNT for Schema, not the domain. You’ll also see this with DNS zones, as they are separate partitions.
The DSA enforces some uniqueness constraints within a naming context. No two objects can share a SAM-Account-Name. Note I said objects, not records. There’s an index on (NCDNT_col, SAM-Account-Type, SAM-Account-Name) that makes it easy for the DSA to search for an account within a domain, since each domain is its own naming context. This also means that if you try to get sneaky and hide a user account within a DNS partition, it won’t work, since it will have a different naming context. Note that the index doesn’t ensure uniqueness. It’s up to the DSA to check, but the index makes it easy to check. Object-SID also has an index, but duplicate SIDs are still a problem.
In the LDAP world, objects are identified by their distinguished name, or DN. This is like a file path, only in reverse. Instead of naming the root object at the beginning, a DN names the root object at the end. An RDN, or relative distinguished name, refers to the individual components of a distinguished name and identify an object within its parent. Each part of a DN or RDN has an attribute-name pair. For example, here is the DN of a user in my test directory:
CN=Mark S.,OU=MDR,OU=Severed Floor,OU=Kier\, PE,DC=dom,DC=local
Parts within a DN are separated by a comma. If the name contains a comma, it must be escaped with a preceding backslash. For example, OU=Kier\, PE refers to Kier, PE. The CN= label is the attribute name, followed by the value Mark S., which is the value of the cn attribute. This object is in a container named MDR, but it uses OU= instead of CN=. That’s because it’s an OU, and OU objects use the ou attribute for their name instead of cn. The RDN-Att-ID attribute of a class determines which attribute objects of that class use for their RDN. The DSA tracks this in the RDNtyp_col column for each object. Note that RDN-Att-ID is set on a class, but RDNtyp_col is set on objects of that class. To simplify lookup, the DSA stores the value of this attribute in the RDN attribute and slaps an index on the PDNT and RDN columns. (If you’re using DIT Explorer, you can view the indexes on the Index tab of the Database Schema viewer.) Objects within the same parent must have a unique name, where name is the value of whatever attribute is specified in the object class’s RDN-Att-ID.
This index makes it easy to find a named object within its parent, but what about locating objects within a subtree? That’s where Ancestors_col comes in. It holds a list of DNTs of all ancestors, starting with the DNT of the root and ending with the DNT of the current record. That last part threw me off, but I’m guessing it’s so that when you issue an LDAP subtree query, the root of that subtree is included in the search, making things a bit easier for the DSA. The column is stored as a LongBinary with each DNT encoded as a 32-bit little-endian value.
Attributes
After the bookkeeping columns, you’ll see a series of columns with the following format:
‘ATT’ <type code> <attribute id>
These are the attribute columns. Almost every attribute in the directory schema has its own database column. Unlike a lot of popular database engines, ESE allows you to store multiple values in a column for a single record. In DIT Explorer, you’ll notice the Multi value is True for these columns (even for attributes that are single-valued). This makes things much easier on the DSA, since it doesn’t have to deal with a more complex database schema using joins to other tables as you’d have to do in a traditional relational database.
So how do you know which column goes with which attribute? You need to figure out two things: the type code and the attribute ID, neither of which are readily apparent. Looking through NTDSAI.DLL, a few column names are hard-coded. Of particular note is the column named ATTc131102. It contains 6-digit values that resemble the number in the ATT columns. If you find the row containing 131102 in this column, you’ll notice a text field containing the text Attribute-ID. This makes sense. The DSA can use these hard-coded columns as a bootstrap when loading the database. If you convert these numbers to hex, you’ll notice a clear break at the 16-bit mark. More on that later. Also, while many of them are 6-digit numbers, some are shorter, and some are longer.
For a while I was content with this answer as it allowed me to further explore the database. Simply build a list of all columns, build a list of all records with a value in ATTc131102, and then join them together. In time, I was able to unlock the mysteries of Attribute-ID. In the Active Directory Schema documentation, each attribute is assigned an Attribute-ID in the form of an OID. While reading through the Directory Replication Service specification, I came across the section § 5.16.4 ATTRTYP-to-OID Conversion, which “describes the prefix mapping mechanism that allows the one-to-one mapping between OIDs and a 32-bit integer (ATTRTYP).” Here’s the short version:
- Look up the prefix in the prefix table toward the end of this article.
- Take the PrefixIndex, shift left 16 bits, then combine with the value of the last octet in the OID.
The prefix table uses the BER encoding of the OID, where the first two components of the OID are combined by multiplying the first component by 40 then adding the second component. Each subsequent octet is encoded as its value.
Applying the algorithm to a few attributes aligns with the ID in the ATTc131102 column as well as the Attribute-ID from the documentation. DIT Explorer decodes the values in NtdsDirectory.DecodePrefixedOid
. If you apply this algorithm to all fields containing OIDs, there are several using a prefix that isn’t in this table. For DIT Explorer, I enumerated every OID in a new directory, cross-referenced this with the Active Directory schema documentation, and added the missing prefixes.
As for the type code, or simply 'code', I queried the database for all attributes with all fields hard-coded into NTDSAI.DLL and noticed a strong correlation between the value of Attribute-Syntax. I documented the results in the following table, referencing [MS-ADTS] § 3.1.1.2.2.2 – LDAP Representations for the name.
Type Code | Syntax OID | Syntax Name | ESE Data Type | Interpretation |
b | 2.5.5.1 | DN | Int32 | Distinguished name tag |
c | 2.5.5.2 | OID Syntax | Int32 |
|
e | 2.5.5.4 | CaseIgnoreString |
|
|
f | 2.5.5.5 | IA5StringSyntax | LongBinary |
|
g | 2.5.5.6 | Numeric | Binary |
|
h | 2.5.5.7 | Object(OR-Name) |
| No examples |
i | 2.5.5.8 | Boolean | Int32 | 0 = False, non-zero = True |
j | 2.5.5.9 | Integer | Int32 | A 32-bit signed integer |
k | 2.5.5.10 | OctetString | LongBinary | Byte string |
l | 2.5.5.11 | UtcTime | Currency | Number of seconds since January 1, 1601 |
m | 2.5.5.12 | UnicodeString | LongText | UTF-16 text string |
n | 2.5.5.13 | PresentationAddress / CaseIgnoreString | Text |
|
p | 2.5.5.15 | SecurityDescriptor | LongBinary | Index into sd_table |
q | 2.5.5.16 | LargeInteger | Currency | 8-byte integer (not a floating point) |
r | 2.5.5.17 | SidString | LongBinary | Binary SID (see [MS-DTYP], ConvertSidToStringSid) |
Figure 4 - Syntax/Type Code Mapping
The syntax of an attribute determines both how it is stored in NTDS.dit as well as how the stored value is interpreted. Note that ESE only supports UTF-16 text columns, so the IA5StringSyntax (an ASCII string) above is stored in a LongBinary column, rather than a LongText column. The Integer and DN syntaxes both store the value as an Int32, but the DN syntax interprets the Int32 as a DNT, referencing another record using DNT_col. SidString is a tricky one. The column is typed as a LongBinary, but the value is actually an Int64 referencing a record in sd_table. Also note that the data type of the column doesn’t necessarily match how the value is represented in LDAP.
Now we have everything we need to determine which column goes with which attribute, or to derive a column name from an attribute. However, there are a few columns that appear to be missing.
Links
Some attributes reference other objects in the directory, such as the Member attribute to track members of a group. The DSA uses link_table for this. Let’s take a look at the data. You can use the Database Schema viewer in DIT Explorer to export the data in this table to a file.

The DNT columns seem pretty straightforward. These columns specify the endpoints of a link using the DNT from datatable. But how do we know what link attribute they correspond to?

Looking at the data, the only column other than the DNT columns with a value on every record is link_base, so this must be it. If you look at the schema for attributes, you’ll notice an attribute named Link-ID. If you’re using DIT Explorer, navigate to the Configuration\Schema container and use View > Columns… to show this attribute as a column. According to the documentation for Link-ID:
“An integer that indicates that the attribute is a linked attribute. An even integer is a forward link and an odd integer is a backward link.”
We’re almost there. This value doesn’t map directly to link_base. Instead, you’ll need to divide the value by 2 and round down. Since the forward and backward links are essentially the same data, the DSA only needs to maintain one record for each forward/backward link pair.

A quick note about the Member attribute. If you look through the data in the link table, you may expect a link object for every user, since most user accounts are in Domain Users by default. Go ahead and look. You won’t find them. In addition to the Member links, the DSA uses the Primary-Group-ID attribute for this. In the screenshot below, you may notice that one user is not like the others. The guest account has a primary group of Domain Guests. If you check the properties in Active Directory, you’ll notice that it is not a member of Domain Users. Keep in mind that this is how the DSA stores the information and doesn’t reflect the value of the Member attribute when queried via LDAP.

If you’re curious, [MS-ADTS] has a section specifically about how the linkID is generated. The short version is that if you create an attribute (via LDAP Add) and set linkID to 1.2.840.113556.1.2.50, it finds an unused even number to use as the ID. That OID is the Attribute-ID of the Link-ID attribute itself. If you set linkID to the attribute ID or the display name of an existing attribute, it creates it as a back link, or in other words, the linkID of that attribute +1.
Object Classes
So far, we’ve looked at objects as simply a record with values. An object class gives these records a bit of personality and purpose, specifying how they are composed and how they interact with the directory. Each object in the directory is assigned a class when it is created. For example, the User class describes objects representing user accounts that may be used for authentication, while the Secret class describes LSA secrets. Each class is represented within the directory as a Class-Schema object that comes with its own set of attributes, specified by Must-Contain, System-Must-Contain, May-Contain, and System-May-Contain. The documentation covers class creation in Defining a New Class. Both Class-Schema and Attribute-Schema objects are found in the Schema container under Configuration.
Each class is assigned a Governs-ID that serves a role similar to Attribute-ID in that it is a prefix-encoded OID that identifies the class. Other objects refer to an object class with its Governs-ID. The DSA uses the Object-Class (column ATTc0) to determine the class of an object. This is where an object begins to take form.

You’ll notice that every object has multiple values for this Object-Class attribute. You’ll also notice that every object has 2.5.6.0 as the last value for this attribute. Why is this? To understand this, we first have to talk about inheritance.
The directory supports class inheritance, meaning an object class can inherit from another. This allows a class to build on another, inheriting the attributes of the superclass (specifically the *-Contain attributes) while adding more specialized attributes. This is more precisely described in [MS-ADTS] § 3.1.1.2.4.2. A class declares its superclass (the one it inherits from) with the Sub-Class-Of attribute, which holds the Governs-ID of the superclass. This is a required attribute, meaning every class must have a superclass. If you walk this all the way to the top and make a list of all superclasses starting from a given object class, you get what’s called the superclass chain. What will you find at the top? The class at the top of the inheritance hierarchy is called, simply enough, Top. It names itself as its own superclass. (Since Sub-Class-Of is required, it has to have a superclass.) Keep this in mind, as it guarantees you’ll encounter a cycle while walking the superclass chain of any class.
The Object-Class attribute contains the entire superclass chain of an object, starting with the most specific subclass and ending with Top, the ultimate superclass. As this column is also indexed, it makes it easy for the DSA to query all records of a giver class or its subclasses. Revisiting the superclass chain in the screenshot, the most specific subclass is User (1.2.134.72.134.247.20.1.5.9), followed by Organizational-Person (2.5.6.7), then by Person (2.5.6.6), and finally by 2.5.6.0, which identifies the Top class. This is why 2.5.6.0 appears as the last Object-Class value for all objects.
This doesn’t tell the full story, though, as it only covers structural classes, determined by the Object-Class-Category. The directory also contains abstract and auxiliary classes. These are covered more in Structural, Abstract, and Auxiliary Classes in the documentation, but briefly, an abstract class cannot be instantiated and can only inherit another abstract class; an auxiliary class cannot be instantiated but can be used to group attributes and add those to a derived class; and a structural class can be instantiated, can inherit from a structural or abstract class, and can reference one or more auxiliary classes. Auxiliary classes are not listed in the Object-Class attribute as they are not part of the superclass chain. Instead, a class references auxiliary classes using the Auxiliary-Class attribute.

In this example, the User class references PosixAccount (1.3.6.1.1.1.2.0) and ShadowAccount (1.3.6.1.1.1.2.1) as auxiliary classes.
Digging through [MS-ADTS] § 3.1.1.2.4.1 reveals another category, the 88 class, with an Object-Class-Category of 0, but it doesn’t provide many more details than that. I did find more of an explanation in the .NET documentation for SchemaClassType, which I quote below:
"Classes defined before 1993 are not required to be included in another category; assigning classes to categories was not required in the X.500 1988 specification. Classes defined prior to the X.500 1993 standards default to the 1988 class. This type of class is specified by a value of 0 in the objectClassCategory attribute."
That explains the 88. Since these classes existed before the notion of class categories, they pretty much get a free pass and can act as though they are of any class category. Active Directory contains a few of these 88 classes:

Now that we understand how object classes are organized, we understand how to transform a record into the object. Just walk the superclass chain from Object-Class (checking for auxiliary class at each step), make a list of attributes referenced by *-Contains attributes of each class, then read those attributes from the record. Now we have an object.
Closely related to Object-Class is Object-Category. It references the Governs-ID of an object class, like Object-Class, but can only contain a single value. You may have noticed on the Irving B. account above, Object-Category is set to Person, which is in the superclass chain of User. Quoting the documentation:
"Each instance of an object class also has an objectCategory property, which is a single-valued property that contains the distinguished name of either the class of which the object is an instance or one of its superclasses. When an object is created, the system sets its objectCategory property to the value specified by the defaultObjectCategory property of its object class. An object's objectCategory property cannot be changed."
This is why objectCategory doesn’t always match objectClass. For example, a User object has a default category of Person, and a Domain object has a category of domainDNS. Since objectCategory doesn’t contain the superclass chain, queries using this attribute may be more precise, as the DSA won’t return objects of a subclass, unless that subclass has its defaultObjectCategory set to the same as the superclass, allowing queries to be more precise, if desired. Why do both attributes exist? The documentation states that objectClass wasn’t indexed prior to Windows Server 2008. Without an index, queries on objectClass would be slower than queries on objectCategory.
If you’ve spent much time in Active Directory Users and Computers, you may have noticed that it can be a bit restrictive on what objects may be created where. For example, it won’t let you create an Organizational-Unit under the Users container. This is determined, in part, by the Poss-Superiors and System-Poss-Superiors of a class. These attributes indicate the classes of objects that may serve as parents to instances of a class within the directory hierarchy. The Organizational-Unit class specifies its possible superiors as being Domain-DNS, Country, Organization, or Organizational-Unit, but not Container. This may provide a hint on where to look for objects of a certain class.
Security Descriptors
The last database table I’ll cover is sd_table. The columns seem fairly self-explanatory. The NT-Security-Descriptor column is an ID that references the sd_id column in this table. The actual SD is stored in binary form in the sd_value field. The sd_hash is the MD5 hash of sd_value. Presumably, the DSA hashes an SD, checks this table for an existing record, and increases the sd_refcount if found, or creates a new record if not. There doesn’t seem to be a provision in the database to allow for multiple SDs that have the same MD5 hash.
Going Through the Trash
When an object is deleted, the DSA doesn’t simply delete the database record. That would be far too simple. That would also make it difficult to communicate deletions to other DCs. Instead, the DSA converts it to a tombstone or to a recycled-object. In short, the DSA moves it to the Deleted Objects container, sets a couple attributes to mark it as deleted, and changes its RDN to, as [MS-ADTS] calls it, a "delete-mangled RDN", in quotes even.

What is a “delete-mangled RDN”? In the above example, Mr. Graner’s account was deleted for, well, reasons. Notice the Common-Name in the screenshot. It has Graner's presumed previous CN, then a line break (LF), then DEL: followed by a GUID. When this record is replicated to other DCs, they’ll know to delete the corresponding object. Note that, in this case, record and object aren’t the same thing. According to that same [MS-ADTS] article, once an object is deleted, it is no longer an object in the eyes of LDAP. This means that LDAP won’t return this record unless you specify special control options for the LDAP query. This makes it possible to find and restore items deleted by mistake by using an LDAP tool (such as ldp.exe) to use these special controls.
Here's a fun little experiment. Create a user, delete that user, then try to create another user with the same UPN and account name. What will happen? The DSA doesn’t have any problem with creating the new account. Remember that although the column is indexed, the index doesn’t ensure uniqueness. The DSA can see that a record (not an object) with the same account name already exists but that it is deleted and happily creates the new account. What happens when you restore the deleted account?

Looks like someone already thought of that. This is another reminder that the DSA performs additional processing and checks on directory operations, rather than simply presenting a front-end to the database.
Wrapping Up
In this article, I went over:
- datatable within NTDS.dit and how to map the columns to attributes
- Reconstructing the object hierarchy using DNT_col and PDNT_col
- Attribute-Schema and Class-Schema objects describing the schema of the directory
- The Object-Class attribute and how the superclass chain works
In future blog posts, I’ll go over how to enumerate users and groups, discover enterprise topology, and enumerate DNS, as well as how to extract credentials. I’ll also cover how to use the JET API to process NTDS.dit.
Glossary
Term | Description |
[MS-ADTS] | Active Directory Technical Specification |
CN | Common Name |
DC | Domain Controller |
DIT | Directory Information Tree |
DN | Distinguished Name |
DNS | Domain Name System |
DNT | Distinguished Name Tag |
DSA | Directory System Agent |
ESE | Extensible Storage Engine |
JET | Jet Engine Technology |
LDAP | Lightweight Directory Access Protocol |
NC | Naming Context |
NTDS | NT Directory Services |
OID | Object Identifier |
RDN | Relative Distinguished Name |
SD | Security Descriptor |
SID | Security Identifier |
UPN | User Principal Name |
References
Active Directory Documentation
- Active Directory Schema documentation - https://learn.microsoft.com/en-us/windows/win32/adschema/active-directory-schema
- Defining a New Class - https://learn.microsoft.com/en-us/windows/win32/ad/defining-a-new-class
- Structural, Abstract, and Auxiliary Classes - https://learn.microsoft.com/en-us/windows/win32/ad/structural-abstract-and-auxiliary-classes
- Object Class and Object Category - https://learn.microsoft.com/en-us/windows/win32/ad/object-class-and-object-category
- [MS-ADTS]: Active Directory Technical Specification § 3.1.1.2.2.2 – LDAP Representations - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/7cda533e-d7a4-4aec-a517-91d02ff4a1aa
- [MS-ADTS]: Active Directory Technical Specification § 3.1.1.2.3.1 Auto-Generated linkID - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/3c44bae8-c09a-439e-b266-6ffc7835d52d
- [MS-DRSR] Directory Replication Service (DRS) Remote Protocol 5.16.4 – ATTRTYP-to-OID Conversion - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-drsr/6f53317f-2263-48ee-86c1-4580bf97232c
- [MS-DTYP]: Windows Data Types § 2.4.2 SID - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/78eb9013-1c3a-4970-ad1f-2b1dad588a25
Command Line Tools
- Esentutl documentation - https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/hh875546(v=ws.11)
- Diskshadow documentation - https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/diskshadow
- Ntdsutil documentation - https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/cc753343(v=ws.11)
- Vssadmin documentation - https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/vssadmin
- Extensible Storage Engine developer documentation - https://learn.microsoft.com/en-us/windows/win32/extensible-storage-engine/extensible-storage-engine
Other Active Directory References
- MCM: Core Active Directory Internals - https://techcommunity.microsoft.com/blog/coreinfrastructureandsecurityblog/mcm-core-active-directory-internals/1785782
- Didier Stevens Practice ntds.dit File Overview - https://blog.didierstevens.com/2016/07/25/practice-ntds-dit-file-overview/
- Active Directory Fundamentals - https://rootdse.org/posts/active-directory-basics-1/#ntdsdit
OID Prefix Table
Here is the OID prefix table used by DIT Explorer. It is based on the table referenced in [MS-DRSR] Directory Replication Service (DRS) Remote Protocol 5.16.4 - ATTRTYP-to-OID Conversion and augmented with values from the Active Directory Schema. The Prefix Base column is the value of PrefixIndex shifted left 16. This column is useful when manually translating between an OID in the documentation and an attribute column. Just add the last part of the attribute’s OID to the prefix base to get the encoded attribute ID.
PrefixIndex | OID | Prefix Base |
0 | 2.5.4 | 0 |
1 | 2.5.6 | 65536 |
2 | 1.2.134.72.134.247.20.1.2 | 131072 |
3 | 1.2.134.72.134.247.20.1.3 | 196608 |
4 | 2.16.134.72.1.101.2.2.1 | 262144 |
5 | 2.16.134.72.1.101.2.2.3 | 327680 |
6 | 2.16.134.72.1.101.2.1.5 | 393216 |
7 | 2.16.134.72.1.101.2.1.4 | 458752 |
8 | 2.5.5 | 524288 |
9 | 1.2.134.72.134.247.20.1.4 | 589824 |
10 | 1.2.134.72.134.247.20.1.5 | 655360 |
… | ... | … |
19 | 0.9.146.38.137.147.242.44.100 | 1245184 |
20 | 2.16.134.72.1.134.248.66.3 | 1310720 |
21 | 0.9.146.38.137.147.242.44.100.1 | 1376256 |
22 | 2.16.134.72.1.134.248.66.3.1 | 1441792 |
23 | 1.2.134.72.134.247.20.1.5.182.88 | 1507328 |
24 | 2.5.21 | 1572864 |
25 | 2.5.18 | 1638400 |
26 | 2.5.20 | 1703936 |
27 | 1.3.6.1.4.1.1466.101.119 | 1769472 |
28 | 2.16.840.1.113730.3.2 | 1835008 |
29 | 1.3.6.1.4.1.250.1 | 1900544 |
30 | 1.2.840.113549.1.9 | 1966080 |
31 | 0.9.2342.19200300.100.4 | 2031616 |
32 | 1.2.840.113556.1.6.23 | 2097152 |
33 | 1.2.840.113556.1.6.18.1 | 2162688 |
34 | 1.2.840.113556.1.6.18.2 | 2228224 |
35 | 1.2.840.113556.1.6.13.3 | 2293760 |
36 | 1.2.840.113556.1.6.13.4 | 2359296 |
37 | 1.3.6.1.1.1.1 | 2424832 |
38 | 1.3.6.1.1.1.2 | 249036 |