
Все данные каталога Active Directory хранятся в БД в файле ntds.dit. Подавляющее большинство приложений взаимодействуют с каталогом через прослойку DSA реализованную в ntdsa.dll. В свою очередь функции из ntdsa.dll не работают напрямую с ntds.dit, их функционал ограничен потребностями службы каталогов и они не могут дать нам представление о внутреннем устройстве БД Active Directory. Тем не менее ntds.dit представляет собой не что иное как БД JET Blue. В каждой версии windows (начиная с Windows 2000) есть всё необходимое для работы с этой БД.
В статье ниже я попробую осветить следующие вопросы:
- Какова структура БД?
- Как данных в ntds.dit получается «дерево»?
- Как реализовано членство в группах?
- Каков формат атрибута replPropertyMetaData и с какой точностью в метаданных репликации хранятся временные метки?
Что нужно, чтобы заглянуть в ntds.dit ?
Как минимум: esent.dll
Для «комфортного» использования из различных языков программирования над ESENT API имеются различные «обёртки». Я использовал Meneged Esent в связке с C#. На сайте проекта имеется много примеров — поэтому далее я постараюсь сконцентрироваться именно на содержимом ntds.dit.
Также следует обратить внимание, что у БД JET Blue есть такой параметр как PageSize. По-умолчанию, он равен 4096 (для тех версий esent.dll, с которыми я сталкивался). Так вот в ntds.dit размер страницы равен 8192 и этот параметр следует корректно установить до открытия БД.
У API ESENT есть несколько версий. Эти версии несовместимы! Так ntds.dit из Windows Server 2008 R2 на windows xp не откроется, только на Windoes 7. (Обратную совместимость я не проверял)
Вопрос первый: Какова структура БД?
Функция GetTableNames возвратит список таблиц в базе:
datatable — основная таблица со всеми данными каталога
hiddentable
link_table — согласно статье на technet таблица с информацией о связанных атрибутах (например MemberOf)
quota_rebuild_progress_table
quota_table
sdpropcounttable
sd_table — согласно статье на technet таблица содержит информацию о наследованных правилах доступа для каждого объекта каталога.
Остановимся подробнее на таблице datatable. Список её столбцов имеет вид (на скриншоте первые несколько строк):

Тупик? Нет!
Имена большинства колонок имеют формат ATT<одна латинская буква><цифровой код>. Так вот этот цифровой код — уникальный идентификатор атрибута Active Directory. В таблице есть одна строка, в которой значение цифрового кода столбца совпадает с его значением. Если вывести все значения типа Text из этой строки, то мы увидим, что это определение атрибута «attributeID».
Теперь ничего не мешает получить соответствие всех столбцов таблицы атрибутам Active Directory (таблица приведена для примера - она сокращена, иначе не помещалась в пост)
JET_COLUMNID | Column Name | Jet Column Type | AD atribute Name | |
---|---|---|---|---|
JET_COLUMNID(0x6) | ab_cnt_col | Long | Single-value | |
JET_COLUMNID(0x100) | Ancestors_col | LongBinary | Single-value | |
JET_COLUMNID(0x536) | ATTb131079 | Long | subRefs | Multi-value |
JET_COLUMNID(0x121) | ATTb131088 | Long | nCName | Multi-value |
JET_COLUMNID(0x2dd) | ATTb131108 | Long | dMDLocation | Multi-value |
JET_COLUMNID(0x42c) | ATTb1376270 | Long | documentAuthor | Multi-value |
JET_COLUMNID(0x169) | ATTb1376277 | Long | secretary | Multi-value |
JET_COLUMNID(0x2b8) | ATTb1376294 | Long | associatedName | Multi-value |
JET_COLUMNID(0x470) | ATTb33 | Long | roleOccupant | Multi-value |
JET_COLUMNID(0x203) | ATTb34 | Long | seeAlso | Multi-value |
JET_COLUMNID(0x2d2) | ATTb49 | Long | distinguishedName | Multi-value |
JET_COLUMNID(0x4cc) | ATTb50 | Long | uniqueMember | Multi-value |
JET_COLUMNID(0x206) | ATTb589856 | Long | domainPolicyObject | Multi-value |
JET_COLUMNID(0x250) | ATTb589864 | Long | fromServer | Multi-value |
JET_COLUMNID(0x205) | ATTb589881 | Long | defaultLocalPolicyObject | Multi-value |
JET_COLUMNID(0x1cc) | ATTb589921 | Long | preferredOU | Multi-value |
JET_COLUMNID(0x5a6) | ATTb590037 | Long | defaultClassStore | Multi-value |
JET_COLUMNID(0x270) | ATTb590038 | Long | nextLevelStore | Multi-value |
JET_COLUMNID(0x183) | ATTb590127 | Long | notificationList | Multi-value |
JET_COLUMNID(0x238) | ATTb590192 | Long | rIDManagerReference | Multi-value |
JET_COLUMNID(0x57c) | ATTb590193 | Long | fSMORoleOwner | Multi-value |
JET_COLUMNID(0x3c8) | ATTb590246 | Long | domainPolicyReference | Multi-value |
JET_COLUMNID(0x566) | ATTb590281 | Long | localPolicyReference | Multi-value |
JET_COLUMNID(0x3c1) | ATTb590295 | Long | trustParent | Multi-value |
JET_COLUMNID(0x4c1) | ATTb590296 | Long | domainCrossRef | Multi-value |
JET_COLUMNID(0x5bc) | ATTb590304 | Long | defaultGroup | Multi-value |
JET_COLUMNID(0x57f) | ATTb590318 | Long | siteServer | Multi-value |
JET_COLUMNID(0x532) | ATTb590338 | Long | physicalLocationObject | Multi-value |
JET_COLUMNID(0x563) | ATTb590341 | Long | ipsecPolicyReference | Multi-value |
JET_COLUMNID(0x24f) | ATTb590361 | Long | dynamicLDAPServer | Multi-value |
JET_COLUMNID(0x1a4) | ATTb590381 | Long | parentCA | Multi-value |
JET_COLUMNID(0x233) | ATTb590448 | Long | ipsecOwnersReference | Multi-value |
JET_COLUMNID(0x345) | ATTq590722 | Currency | aCSNonReservedTxSize | Multi-value |
JET_COLUMNID(0x398) | ATTq591137 | Currency | aCSMaxTokenBucketPerFlow | Multi-value |
JET_COLUMNID(0x19b) | ATTq591138 | Currency | aCSMaximumSDUSize | Multi-value |
JET_COLUMNID(0x4d3) | ATTq591139 | Currency | aCSMinimumPolicedSize | Multi-value |
JET_COLUMNID(0x244) | ATTq591140 | Currency | aCSMinimumLatency | Multi-value |
JET_COLUMNID(0x12f) | ATTq591141 | Currency | aCSMinimumDelayVariation | Multi-value |
JET_COLUMNID(0x16e) | ATTq591142 | Currency | aCSNonReservedPeakRate | Multi-value |
JET_COLUMNID(0x344) | ATTq591143 | Currency | aCSNonReservedTokenSize | Multi-value |
JET_COLUMNID(0x4d4) | ATTq591144 | Currency | aCSNonReservedMaxSDUSize | Multi-value |
JET_COLUMNID(0x343) | ATTq591145 | Currency | aCSNonReservedMinPolicedSize | Multi-value |
JET_COLUMNID(0x5ac) | ATTq591191 | Currency | mS-SQL-Memory | Multi-value |
JET_COLUMNID(0x213) | ATTq591204 | Currency | mS-SQL-Status | Multi-value |
JET_COLUMNID(0x4d5) | ATTq591220 | Currency | mS-SQL-Size | Multi-value |
JET_COLUMNID(0x2c5) | ATTq591266 | Currency | msDS-Cached-Membership-Time-Stamp | Multi-value |
JET_COLUMNID(0x548) | ATTq591456 | Currency | msWMI-Int8Default | Multi-value |
JET_COLUMNID(0x52a) | ATTq591457 | Currency | msWMI-Int8Max | Multi-value |
JET_COLUMNID(0x53f) | ATTq591458 | Currency | msWMI-Int8Min | Multi-value |
JET_COLUMNID(0x214) | ATTq591459 | Currency | msWMI-Int8ValidValues | Multi-value |
JET_COLUMNID(0x2c1) | ATTq591520 | Currency | lastLogonTimestamp | Multi-value |
JET_COLUMNID(0x2cc) | ATTq591794 | Currency | msDS-LastSuccessfulInteractiveLogonTime | Multi-value |
JET_COLUMNID(0x462) | ATTq591795 | Currency | msDS-LastFailedInteractiveLogonTime | Multi-value |
JET_COLUMNID(0x440) | ATTq591835 | Currency | msDS-MaximumPasswordAge | Multi-value |
JET_COLUMNID(0x2a5) | ATTq591836 | Currency | msDS-MinimumPasswordAge | Multi-value |
JET_COLUMNID(0x200) | ATTq591841 | Currency | msDS-LockoutObservationWindow | Multi-value |
JET_COLUMNID(0x2e7) | ATTq591842 | Currency | msDS-LockoutDuration | Multi-value |
JET_COLUMNID(0x3d0) | ATTq591879 | Currency | msDS-USNLastSyncSuccess | Multi-value |
JET_COLUMNID(0x49a) | ATTq591922 | Currency | msDS-ClaimValueType | Multi-value |
JET_COLUMNID(0x476) | ATTq592002 | Currency | msKds-UseStartTime | Multi-value |
JET_COLUMNID(0x477) | ATTq592003 | Currency | msKds-CreateTime | Multi-value |
JET_COLUMNID(0x3a0) | ATTq592007 | Currency | msDS-GeoCoordinatesAltitude | Multi-value |
JET_COLUMNID(0x3a1) | ATTq592008 | Currency | msDS-GeoCoordinatesLatitude | Multi-value |
JET_COLUMNID(0x3a2) | ATTq592009 | Currency | msDS-GeoCoordinatesLongitude | Multi-value |
JET_COLUMNID(0x43b) | ATTr589945 | LongBinary | securityIdentifier | Multi-value |
JET_COLUMNID(0x1c9) | ATTr589970 | LongBinary | objectSid | Multi-value |
JET_COLUMNID(0x276) | ATTr590433 | LongBinary | sIDHistory | Multi-value |
JET_COLUMNID(0x443) | ATTr590491 | LongBinary | syncWithSID | Multi-value |
JET_COLUMNID(0x5e4) | ATTr591234 | LongBinary | mS-DS-CreatorSID | Multi-value |
JET_COLUMNID(0x50a) | ATTr591668 | LongBinary | msDS-QuotaTrustee | Multi-value |
JET_COLUMNID(0x1c2) | ATTr591978 | LongBinary | msAuthz-CentralAccessPolicyID | Multi-value |
JET_COLUMNID(0x5) | cnt_col | Long | Single-value | |
JET_COLUMNID(0x1) | DNT_col | Long | Single-value | |
JET_COLUMNID(0x5f1) | extendedprocesslinks_col | LongBinary | Single-value | |
JET_COLUMNID(0x9) | IsVisibleInAB | UnsignedByte | Single-value | |
JET_COLUMNID(0x8) | NCDNT_col | Long | Single-value | |
JET_COLUMNID(0x3) | OBJ_col | UnsignedByte | Single-value | |
JET_COLUMNID(0x2) | PDNT_col | Long | Single-value | |
JET_COLUMNID(0x4) | RDNtyp_col | Long | Single-value | |
JET_COLUMNID(0xa) | recycle_time_col | Currency | Single-value | |
JET_COLUMNID(0x7) | time_col | Currency | Single-value |
Остался один «нерасшифрованый» атрибут ATTc0 — это ссылка на «objectClass».
Обратите внимание — все колонки, хранящие атрибуты каталога Active Directory заведены как Multi-value в не зависимости от схемы.
Вопрос второй: Как данных в ntds.dit получается «дерево»?
Название столбца «DNT_col» провоцирует предположить, что он как-то связан с distinguishedName объекта (как позже выяснилось — они равны)
Значения колонки DNT_col начинаются с 1, причём строка с DNT_col=1 соответствует интересному объекту со значенем атрибута name="$NOT_AN_OBJECT1$"
Строка с DNT_col=2 содержит в себе атрибуты объекта с именем "$ROOT_OBJECT$" (Не путать с RootDSE)
С DNT_col=6 начинаются определения objectClass'ов
Опять тупик? Опять нет!
Пойдём другим путём.
Поиск по колонке ATTm589825 (name) значения типа Text «Administrator» вернул запись с DNT_col=3841 и PDNT_col=1951
Поиск по колонке ATTm589825 (name) значения типа Text «Users» (контейнер, в котором расположена учётная запись стандартного администратора) вернул запись с DNT_col=1951 и PDNT_col=1944
Вот она связь! столбец PDNT_col содержит идентификатор DNT_col родительского объекта.
В строке с DNT_col=1944 ( PDNT_col=1943) — оказался объект домена второго уровня (ntds.dit был взять с тестового контроллера домена второго уровня)
В строке с DNT_col=1943 ( PDNT_col=2) — объект домена первого уровня.
Если вы когда-нибудь задавались вопросом: «Почему в при подключении к dc=contoso,dc=com не отображаются объекты из cn=Configuration,dc=contoso,dc=com?» то вам следует обратить внимание на колонку NCDNT_col — она содержит ссылки на объект контекста именования. (причём, объект домена первого уровня не имеет контекста именования — моэет по этому он и не виден?). Для Объектов в контекстах именования «dc=contoso,dc=com» и «cn=Configuration,dc=contoso,dc=com» значение этой колонки отличается.
Не менее интересно как из этих чисел получается полное distinguishedName объекта? Какой аттрибут обекта является относительным уникальным именем (RDN) и используется для построения полного distinguishedName.
Колонка RDNtyp_col содержит идентификатор атрибута (attributeID), содержащего RDN.
В колонке CNT_col содержится количество объектов, связанных с текущим. Эта колонка используется механизмом Link Cleaner
Если бит в колонке OBJ_col установлен в 1, значит данная строка описывает объект каталога. Иначе — это объект-фантом.
Как реализован SubTree поиск?
Понятно, что при такой структуре для поиска OneLavel достаточно поискать среди записей с PDNT_col равной DN базы поиска, а вот как поискать по SubTree? Обходить все ветви? Нет это слишком сложно.
Присмотримся к значениям в колонке с говорящим именем Ancestors_col. В глаза бросается, что чем глубже в иерархии расположен объект, тем длиннее значение в этой колонке. Каждый уровень вложенности добавляет к длине 4 байта. Это не что иное как список родительских объектов в порядке очерёдности.
Причём список начинается с dn=2, т.е. с "$ROOT_OBJECT$"
Кто занимается поддержанием Ancestors_col в актуальном состоянии? Согласно документу при перемещении объекта в новое место изменяется только его PDNT_col, а значение Ancestors_col обновляется механизмом SDProp заодно с пересчётом новых правил доступа к объекту.
Вопрос третий: Как реализовано членство в группах?
Почему c 2003-го сервера появилась возможность влючать в группу брактически неограниченное число прользователей, и при этом в строковый или числовой multi-value атрибут не удастся записать боле ~1200 значений?
Да потому, что «member» и «memberOf» определены как ссылки. В таблице datatable нет столбцов, соответствующих этим атрибутам. Значения этих атрибутов реализованы как соответствия в отдельной таблице link_table.
Быть может именно поэтому при удалении группы (без использования RecucleBin) мы теряем сведения о её членах — объект удалённой группы получает новый идентификатор в DNT_col при перемещении в контейнер удалённых объектов (а связь группы и её члена построена именно по этому идентификатору)
Посмотрим на столбцы таблицы link_table
JET_COLUMNID(0x2) | backlink_DNT | Long | Single-value |
JET_COLUMNID(0x3) | link_base | Long | Single-value |
JET_COLUMNID(0x100) | link_data | LongBinary | Single-value |
JET_COLUMNID(0x4) | link_deactivetime | Currency | Single-value |
JET_COLUMNID(0x5) | link_deltime | Currency | Single-value |
JET_COLUMNID(0x1) | link_DNT | Long | Single-value |
JET_COLUMNID(0x80) | link_metadata | Binary | Single-value |
JET_COLUMNID(0x7) | link_ncdnt | Long | Single-value |
JET_COLUMNID(0x101) | link_ndesc | Long | Single-value |
JET_COLUMNID(0x6) | link_usnchanged | Currency | Single-value |
Названия и тип столбцов намекают, а анализ содержащихся в них данных подтверждает, что:
link_DNT — Содержит идентификатор dn (соответствует DNT_col из datatable) объекта, связь которого описывается (например, объекта группы)
backlink_DNT — Содержит идентификатор dn связанного с ним объекта (например, объекта пользователя)
link_ncdnt — Содержит идентификатор dn контекста именования, в котором расположены участники связи.
link_usnchanged — Содержит USN последнего изменения всязи
link_data — Я видел это поле заполненным только для связей объектов NTDS Settings. Да! Объекты NTDS Settings имеют связи с объектами разделов каталога повидимому посредством этих связей задаются параметры репликации различных разделов каталога.
link_deltime — Содержит время удаления связи.
link_metadata — содержит метаданные, необходимые для репликации внесённых изменений. Заполеннными значения этой колонки я видел только на Windows 2012. Christoffer Andersson пишет, что колонка содержит структуру DS_REPL_VALUE_META_DATA и здесь я с ним категорически не согласен. Структура DS_REPL_VALUE_META_DATA используется на более высоком уровне — её возвращают вызовы ntdsa.dll. Данные, содержащаеся в этой колонке имеют размер меньше, чем необходимо под структуру DS_REPL_VALUE_META_DATA. Пока данная колонка остаётся для меня загадкой. Впринципе метаданные репликации для аттрибутов-связей можно получить из построенного атрибута msDS-ReplValueMetaData, но это не раскрывает внутреннего механизма обработки и хранения этих данных.
Вопрос четвёртый: Каков формат атрибута replPropertyMetaData и с какой точностью в метаданных репликации хранятся временные метки?
В этом атрибуте хранятся метаданные репликации в двоичном виде:

Что хранится в первых 8-ми байтах данной структуры я пока не разобрался.
Следует обратить внимание на тот факт, что в структуре replPropertyMetaData перечисляются только реплицируемые атрибуты со значениями. Так, например, вы не увидите в replPropertyMetaData идентификатора атрибута logonTimestamp.
Тут есть интересный момент: в структуре replPropertyMetaData для объектов безопасности Active Directory (пользователей, групп и компьютеров) присутствует атрибут objectSid.
Кстати, чтобы расшифровать значение этого атрибута — не обязательно так напрягаться — существует построенный атрибут msDS-ReplAttributeMetaData — он собой предстваляет не что иное как разобранное значение replPropertyMetaData.
Временные метки в Active Directory это 64-битные беззнаковые целые числа, указывающие количество секунд, прошедших с 00:00 1 января 1601 года. Отсюда вытекает интересный нюанс: при правке одного атрибута одного и того же объекта на двух контроллерах домена в течение 1 секунды (например при пакетной обработке большого числа пользователей) можно получить неожиданный результат.
По данным статьи на technet выигрывает версия атрибута от контроллера с меньшим GUID.
Использованная информация:
technet.microsoft.com/en-us/library/cc772829%28v=ws.10%29.aspx
blogs.chrisse.se/2012/02/11/how-the-active-directory-data-store-really-works-inside-ntds-dit-part-1
blogs.chrisse.se/2012/02/15/how-the-active-directory-data-store-really-works-inside-ntds-dit-part-2
blogs.chrisse.se/2012/02/20/how-the-active-directory-data-store-really-works-inside-ntds-dit-part-3
blogs.chrisse.se/2012/02/28/how-the-active-directory-data-store-really-works-inside-ntds-dit-part-4
gexeg.blogspot.ru/2009/12/active-directory.html
www.ntdsxtract.com
Все эксперименты ставились над ntds.dit из Windows Server 2012 Eng
UPD. На закуску маленькая утилитка, которую написал чтобы изучать ntds.dit (быть может кому сгодится).