В первой части был рассмотрен формат файла REGF на диске, а теперь перейдём к архитектуре реестра в памяти. На первый взгляд всё просто - взял файл с диска, выделил для него страницы, и спроецировал туда всё содержимое. Однако в реале всё гораздо сложнее. Это огромный механизм и представляет собой аналог пейджинга страниц с таблицами
В этой части:
1. Поддержка целостности реестра
Чтобы образ куста на диске и в памяти был всегда идентичным, используется механизм ленивого сброса LazyFlush. Суть его в том, что изменение ключей не записываются сразу на диск, а копятся в специальном буфере. Период сброса буфера жёстко задан в системе, и составляет 5 сек после правки ключа. В качестве пруфов, можно в отладчике запросить у ядра его переменную размером дворд "FlushInterval" и как видим, в ней лежит значение(5).
Когда система или софт функцией
Здесь нужно упоменуть ещё об одном типе сброса - это принудительный ForcedFlush. Иногда приложению жизненно важно, чтобы изменения попали на диск немедленно, например при установке критического обновления системы, или настройке безопасности. Для таких случаев предусмотрена
2. Иерархия структур реестра в памяти
Базовой структурой любого куста является CMHIVE. Она начинается с достаточно объёмной/вложенной HHIVE, которая занимает 0x600 байт на Win10, и чуть меньше 0x598 на Win7 - эта связь напоминает родственные пары объектов EPROCESS/KPROCESS. Поскольку HHIVE выделяется как компонент более крупной CMHIVE, указатели на них в памяти фактически совпадают.
HHIVE управляет аспектами низкого уровня - это выделение/освобождение памяти для контейнеров HBIN файла, а также синхронизация куста с его образом на диске. В свою очередь CMHIVE обрабатывает уже более абстрактную инфу о реестре - это кэш дескрипторов безопасности, указатели на высокоуровневые объекты типа "блок управления ключом" KCB (key control block), и прочее. Найти эти структуры можно по сигнатуре
Общую суть происходящего в памяти хорошо раскрывает HHIVE.
Со смещения
По смещению
Отдельно внимания заслуживают флаги по смещению
3. Доступ к проекциям кустов HIVE
В родительской структуре CMHIVE имеются несколько кольцевых списков LIST_ENTRY, которые позволяют перечислять себе-подобных двигаясь вперёд Flink, или назад Blink от текущей позиции. Cреди них выделяется список в поле HiveList - используя хранящийся в нём указатель мы сможем узнать адреса буквально всех кустов реестра в памяти, а точнее их структур CMHIVE. Но для этого нужно каким-то образом заполучить адрес хотя-бы одной из этих LIST_ENTRY, чтобы использовать её как начальную точку отсчёта.
Для решения этой проблемы в ядре имеется переменная
Для работы с кустами реестра, WinDbg имеет специальное расширение
Теперь имея адреса можно узнать, в какой пул ядерной памяти промаплены структуры реестра - выгружаемый в файл подкачки Paged, или постоянно находящийся в памяти NonPaged. Как видим это PagedPool, причём отладчик вернул даже 4-байтный тэг
Так-же можем запросить и структуру паспорта куста HBASE_BLOCK.
4. Поиск ячеек KEY_NODE / KEY_VALUE
До этого момента мы исследовали окружение, а теперь рассмотрим, как работает механизм доступа к ячейкам в памяти, где хранятся данные. Уже известно, что файл куста на диске начинается с блока с сигнатурой 'regf' и размером
Этот эксперимент доказывает, что содержимое кустов на диске и в памяти отличается, а потому здесь нужен иной подход. Если следовать логике, то вирт.память состоит из 4КБ страниц, а для адресации к ним используется PAGE_TABLE. В случае с реестром всё аналогично, только менеджер конфигурации использует теперь свою таблицу HMAP_TABLE, при чём для каждого куста system\sam\security\etc, он создаёт отдельную HMAP.
Адрес в таблице 32-битный, а указатель на неё лежит в поле Storage структуры HHIVE (в самом её конце). Фактически имеются две одинаковые таблицы HMAP - менеджер выбирает их старшим битом адреса.
Значит чтобы добраться до этих таблиц в отладчике, нужно: (1)найти адрес куста Hive командой
Поскольку старший бит выбирает одну из двух таблиц HMAP, под адрес остаётся 31-бит, а это макс. 2ГБ памяти. В каталоге HMAP_DIRECTORY всего 1024 указателя на одну из 1024 таблиц HMAP_TABLE. Итого получаем размер каталога
Однако в реальной ситуации размеры кустов обычно не превышает 2МБ (за некоторым исключением), а потому используется всего одна HMAP_TABLE, а не все 1024 штуки. Но если таблица единственная, значит и в каталоге HMAP_DIRECTORY достаточно одного указателя, что сокращает его размер с 8КБ до 8 байт.
Для поддержки такой оптимизации, в структуре DUAL имеется поле SmallDir (см.логи выше). Если в этом поле лежит нуль, значит размер куста больше 2МБ, и менеджер использует стандартную схему с несколькими HMAP_TABLE, иначе переключается на упрощённую, как на рис.ниже.
Например в первой структуре "Stable" видно, что размер куста
Обратите внимание на поле BinAddress в структуре HMAP_ENTRY - младшие 4 бита в нём являются флагами, а потому нужно сбросить их в нуль. В данном случае флаги
Это поле было введено на случай, когда размер контейнера HBIN превышает 4КБ, т.е. несколько смежных склеены в один. Тогда BinAddress - это указатель на начало расширенного блока с сигнатурой 'hbin', а BlockAddress - одни из экземляров вирт.страниц на границе 4КБ. В данном случае у первых двух полей одинаковое значение (не считая флагов во втором), а значит это блок стандартного размера 4КБ, который и указан в поле
5. Кэш грязных страниц
Выше упоминалось, что все изменения в реестре (т.е. создание/удаление новых ключей), как правило, вступают в силу после перезагрузки ОС. До этого они находятся словно в подвешенном "Dirty-состоянии", а система общается с ним через кэш файловой системы. Отладчик предоставляет нам несколько мощных инспекторов, чтобы заглянуть в потроха этого механизма.
•
•
Если предыдущая показывает грязные блоки конкретного куста, то
Если-же без расширений отладчика, то инфа о кэше грязных страниц лежит в структуре HHIVE. Здесь непосредственно хранятся векторы грязных страниц, которые управляют состоянием кеша на низком уровне. Все поля, которые относятся к кэшу начинаются с Dirty_xx, а потому можно найти их по маске:
1. DirtyVector: это структура RTL_BITMAP, в которой всего 2 поля - SizeOfBitMap хранит размер буфера в битах, а Buffer - это указатель на битовую карту в памяти. Каждый бит в этом буфере представляет один блок HBIN: если бит взведён(1) значит блок грязный и ожидает записи на диск, 0=чистый. Это сердце механизма.
2. DirtyAlloc: тоже размер буфера как и SizeOfBitmap, но уже не в битах, а в байтах. Например здесь
3. DirtyCount: просто счётчик кол-ва грязных блоков HBIN (быстрый способ оценить масштаб изменений). Если он нуль, остальные поля Dirty* можно игнорировать.
5.1. Лист пустых блоков
Перед операцией записи в реестр новых значений, диспетчеру необходимо найти свободные ячейки в блоках HBIN. Для быстрого их поиска, во-вложенной в HHIVE структуре DUAL предусмотрены специальные поля с приставкой Free_xx.
1. FreeDisplay: состоит из 24 структур FREE_DISPLAY, каждая из которых соответствует определённому диапазону размеров ячеек. Указывает, какие страницы в кусте Hive потенциально могут содержать свободные ячейки заданной длины. Сама структура имеет 2 поля: RealVectorSize информирует, какого размера ячейку можно выделить в указанной в Display странице памяти. То-есть это верхний порог размера ячейки в байтах. Если под новую ячейку требуется больше байт, то система ищет подходящие в следующих 23-х структурах.
Поле "RealVectorSize" может хранить и невалидные значения, как например экземпляры 2,3 ниже.
Во-первых, валидные размеры всегда должны быть степенью двойки, а во-вторых размер не должен превышать значение в поле Length (общий размер куста), которое в данном случает равно
2. FreeSummary: это двоичная маска с подсказкой. Действительны в поле только младшие 24-бита. Они указывают, в какой из 24-х структур FREE_DISPLAY могут находиться свободные ячейки. Нуль в этом поле означает, что в кусте нет свободных ячеек, и нужно выделять дополнительный блок HBIN.
3. FreeBins: кольцевой список с указателями на следующий\предыдущий блок полностью пустых ячеек.
6. Постфикс
Реестр Windows это не чёрный ящик, а хорошо документированная (пусть и не официально) система. Каждый байт в файле REGF имеет своё место и назначение. Вооружившись WinDbg, знанием структур и терпением, можно анализировать его содержимое на любом уровне - от дампа памяти, и до бинарного файла на диске. Реестр - это как слоёный пирог: чтобы добраться до начинки, нужно аккуратно пройти через все слои. Но если вы это сделали, награда стоит усилий.
Отдельное спасибо:
В следующей\заключительной части мы рассмотрим внутренние особенности куста реестра SAM, где хранятся личные сведения о всех пользователях системы, их правах, паролях, и многое другое. До скорого, всем удачи, пока!
PAGE_DIRECTORY\TABLE\ENTRY. Знание этих принципов поможет искать различного рода улики внутри "живого" реестра, где инфа актуальна именно на данный момент. Для экспериментов понадобится отладчик ядра WinDbg в связке с утилитой "LiveKD" из пакета SysInternals М.Руссиновича. Если отладчика нет, то придётся вам поверить мне наслово.В этой части:
1. Поддержка целостности реестра
2. Иерархия структур реестра в памяти
3. Доступ к проекциям кустов HIVE
4. Поиск ячеек KEY_NODE / KEY_VALUE
5. Кэш модифицированных страниц
6. Постфик
1. Поддержка целостности реестра
Чтобы образ куста на диске и в памяти был всегда идентичным, используется механизм ленивого сброса LazyFlush. Суть его в том, что изменение ключей не записываются сразу на диск, а копятся в специальном буфере. Период сброса буфера жёстко задан в системе, и составляет 5 сек после правки ключа. В качестве пруфов, можно в отладчике запросить у ядра его переменную размером дворд "FlushInterval" и как видим, в ней лежит значение(5).
Код:
0: kd> x nt!cmplazyflush* ;<---- Поиск по маске (функции, структуры, переменные)
fffff800`023fb2a8 nt!CmpLazyFlushHiveCount
fffff800`02538b00 nt!CmpLazyFlushWorker
fffff800`023fb2b0 nt!CmpLazyFlushCount
fffff800`0222afe4 nt!CmpLazyFlushDpcRoutine
fffff800`02414098 nt!CmpLazyFlushPending
fffff800`02489a80 nt!CmpLazyFlushTimer
fffff800`023fadac nt!CmpLazyFlushIntervalInSeconds ;<-------
fffff800`0253a918 nt!CmpLazyFlush
fffff800`02489a40 nt!CmpLazyFlushDpc
0: kd> dd nt!CmpLazyFlushIntervalInSeconds L1
fffff800`023fadac 00000005 ;<----- Интервал сброса буфера в сек
Когда система или софт функцией
RegSetValue() вносит изменения в реестр, тут-же включается таймер, по истечении которого управление получает асинхронная функция nt!CmpLazyFlush(). Код её обработчика инициирует фоновую запись на диск только тех страниц памяти, у которых в записях PTE взведён бит "Dirty" (грязная). Важно понять, что таймер срабатывает по прошествии 5 сек уже после того, как реестр был изменён - если изменений нет, таймер не запускается. Другими словами интервал это период ожидания, а не частота опроса. Если в течении 5 сек происходят новые изменения, таймер сбрасывается в нуль и начинает отсчёт заново. Такой подход является наиболее эффективным способом управления записью на диск, т.к. не блокирует работу приложений.Здесь нужно упоменуть ещё об одном типе сброса - это принудительный ForcedFlush. Иногда приложению жизненно важно, чтобы изменения попали на диск немедленно, например при установке критического обновления системы, или настройке безопасности. Для таких случаев предусмотрена
RegFlushKey(). Это дорогая операция, т.к. весь куст блокируется до завершения сброса на диск, что может вызвать заметные тормоза в системе. Поэтому майки настоятельно рекомендуют эту функцию только в крайних случаях, и всегда полагаться на штатный механизм ленивого сброса. При выкл или ребуте машины, Win всегда выполняет принудительный сброс CmShutdownSystem(), чтобы гарантировать целостность всех данных.2. Иерархия структур реестра в памяти
Базовой структурой любого куста является CMHIVE. Она начинается с достаточно объёмной/вложенной HHIVE, которая занимает 0x600 байт на Win10, и чуть меньше 0x598 на Win7 - эта связь напоминает родственные пары объектов EPROCESS/KPROCESS. Поскольку HHIVE выделяется как компонент более крупной CMHIVE, указатели на них в памяти фактически совпадают.
HHIVE управляет аспектами низкого уровня - это выделение/освобождение памяти для контейнеров HBIN файла, а также синхронизация куста с его образом на диске. В свою очередь CMHIVE обрабатывает уже более абстрактную инфу о реестре - это кэш дескрипторов безопасности, указатели на высокоуровневые объекты типа "блок управления ключом" KCB (key control block), и прочее. Найти эти структуры можно по сигнатуре
0xBEE0BEE0 в первом дворде, а выравнивание их в памяти составляет 16 байт.Общую суть происходящего в памяти хорошо раскрывает HHIVE.
Со смещения
0x18 до 0x40 лежат 8 указателей на внутренние обработчики ядра для сл.операций - на них основан общий механизм проецирования кустов в память.HvpGetCellPaged()- преобразует индекс ячейки в вирт.адрес в памяти. В первой части говорилось, что поля Offset и List в структурах хранят индексы ячеек внутри файла. Но после загрузки в память, индексы нужно превратить в RVA-адреса - этим и занимается данная функция. Это самая востребованная API из всех.CmpAllocate()- смотрит на размер файла куста реестра, и выделяет ему память в ядре.CmpFree()- обратная второму пункту операция, аCmpFileSetSize()устанавливает новый размер файла, после записи куста на диск.CmpFileRead\Write\Flush()- базовые операции с образом файла на диске.
Код:
0: kd> dt _hhive fffff8a000024010
nt!_HHIVE
+0x000 Signature : 0xbee0bee0 ;<------------------------- Сигнатура
+0x008 GetCellRoutine : 0xfffff800`02513930 nt!HvpGetCellPaged
+0x010 ReleaseCellRoutine : (null)
+0x018 Allocate : 0xfffff800`024f3388 nt!CmpAllocate
+0x020 Free : 0xfffff800`024e8904 nt!CmpFree
+0x028 FileSetSize : 0xfffff800`02542910 nt!CmpFileSetSize
+0x030 FileWrite : 0xfffff800`0253a854 nt!CmpFileWrite
+0x038 FileRead : 0xfffff800`02552db0 nt!CmpFileRead
+0x040 FileFlush : 0xfffff800`025427b8 nt!CmpFileFlush
+0x048 HiveLoadFailure : (null)
+0x050 BaseBlock : 0xfffff8a0`00025000 _HBASE_BLOCK
+0x058 DirtyVector : _RTL_BITMAP
+0x068 DirtyCount : 0
+0x06c DirtyAlloc : 0x103c
+0x070 BaseBlockAlloc : 0x1000
+0x074 Cluster : 1
+0x078 Flat : 0
+0x079 ReadOnly : 0
+0x07a DirtyFlag : 0x1
+0x07c HvBinHeadersUse : 0
+0x080 HvFreeCellsUse : 0x48ce8
+0x084 HvUsedCellsUse : 0xfd4f38
+0x088 CmUsedCellsUse : 0
+0x08c HiveFlags : 0x200
+0x090 CurrentLog : 4
+0x094 LogSize : [2] 0x40000
+0x09c RefreshCount : 0
+0x0a0 StorageTypeCount : 2
+0x0a4 Version : 5
+0x0a8 Storage : [2] _DUAL
0: kd>
По смещению
0x50 можно найти адрес структуры HBASE_BLOCK требуемого куста в памяти, а по смещению 0х70 её размер. В диапазоне 0x58...0x6С структура хранит несколько полей с префиксом "Dirty", описывающих состояние синхронизации куста с образом на диске. Поле "DirtyFlag" по смещению 0x7A является триггером для таймера ленивого сброса "LazyFlush".Отдельно внимания заслуживают флаги по смещению
0x8C - они описывают общее состояние куста, а их двоичная маска выглядит так:
Код:
HIVE_VOLATILE = 0x000001 ; непостоянный куст, существует только в памяти, например HARDWARE
HIVE_NOLAZYFLUSH = 0x000002 ; ручной сброс на диск, например для критически-важного куста SAM (безопасность)
HIVE_PRELOADED = 0x000010 ; один из системных кустов, например SYSTEM или SOFTWARE
HIVE_IS_UNLOADING = 0x000020 ; в данный момент происходит загрузка\выгрузка куста
HIVE_UNLOAD_STARTED = 0x000040 ; начался процесс выгрузки памяти на диск
HIVE_ALL_REFS_DROPPED = 0x000080 ; все ссылки на куст через KCB были удалены
HIVE_INTERNALS_FLAG = 0x000200 ; внутренний флаг ядра! взводится, например, отладчиком
HIVE_ON_PRELOADED_LIST = 0x000400 ; задействован LIST_ENTRY в поле PreloadedHiveList структуры CMHIVE
HIVE_FILE_READ_ONLY = 0x008000 ; файл загружен в память с флагом REG_OPEN_READ_ONLY
HIVE_SECTION_BACKED = 0x020000 ; файл грузится в память не целиком, а отдельными блоками HBIN
HIVE_DIFFERENCING = 0x080000 ; куст с функцией сравнения результатов (только REGF v1.6 Win10+)
HIVE_IMMUTABLE = 0x100000 ; флаг запрета на изменения куста
3. Доступ к проекциям кустов HIVE
В родительской структуре CMHIVE имеются несколько кольцевых списков LIST_ENTRY, которые позволяют перечислять себе-подобных двигаясь вперёд Flink, или назад Blink от текущей позиции. Cреди них выделяется список в поле HiveList - используя хранящийся в нём указатель мы сможем узнать адреса буквально всех кустов реестра в памяти, а точнее их структур CMHIVE. Но для этого нужно каким-то образом заполучить адрес хотя-бы одной из этих LIST_ENTRY, чтобы использовать её как начальную точку отсчёта.
Для решения этой проблемы в ядре имеется переменная
nt!CmpHiveListHead. Таким образом, чтобы узнать адрес структуры, которая была первой загружена в память, нужно прочитать значение этой переменной, и отнять от неё смещение поля HiveList (отличается на Win7\10\11). Вот цепочка команд в отладчике: (1)запрос переменной, (2)определить смещение поля, (3)отобразить CMHIVE или HHIVE, т.к. адрес у них одинаковый.
Код:
0: kd> dq nt!CmpHiveListHead L1
fffff800`027580e8 fffff8a0`0000f5e8 ;<---- Значение переменной ядра
0: kd> dt _cmhive
nt!_CMHIVE
+0x000 Hive : _HHIVE
+0x598 FileHandles : [6] Ptr64 Void
+0x5c8 NotifyList : _LIST_ENTRY
+0x5d8 HiveList : _LIST_ENTRY ;<--------- Переменная указывает сюда, оффсет 0x5d8
.......
0: kd> dt _cmhive fffff8a0`0000f5e8-5d8 ;<---- Прыгаем от неё в начало структуры
nt!_CMHIVE
+0x000 Hive : _HHIVE
+0x598 FileHandles : [6] 0xffffffff`80000198 Void
+0x5c8 NotifyList : _LIST_ENTRY [ 0xfffff8a0`002ce5f0 - 0x0 ]
+0x5d8 HiveList : _LIST_ENTRY [ 0xfffff8a0`000635e8 - 0xfffff8a0`0000f5e8 ] ;<---- Следующая структура..
+0x5e8 PreloadedHiveList : _LIST_ENTRY [ 0xfffff8a0`000245f8 - 0xfffff8a0`000245f8 ]
+0x5f8 HiveRundown : _EX_RUNDOWN_REF
+0x600 ParseCacheEntries : _LIST_ENTRY [ 0xfffff8a0`0000bf28 - 0xfffff8a0`0000c878 ]
+0x610 KcbCacheTable : 0xfffff8a0`00064000 _CM_KEY_HASH_TABLE_ENTRY
+0x618 KcbCacheTableSize : 0x400
+0x61c Identity : 1
+0x620 HiveLock : 0xfffffa80`0c7200e0 _FAST_MUTEX
+0x628 ViewLock : _EX_PUSH_LOCK
+0x630 ViewLockOwner : (null)
+0x638 ViewLockLast : 0xd
+0x63c ViewUnLockLast : 0x19
+0x640 WriterLock : 0xfffffa80`0c720150 _FAST_MUTEX
+0x648 FlusherLock : 0xfffffa80`0c720188 _ERESOURCE
+0x650 FlushDirtyVector : _RTL_BITMAP
+0x660 FlushOffsetArray : (null)
+0x668 FlushOffsetArrayCount : 0
+0x66c FlushHiveTruncated : 0
+0x670 FlushLock2 : 0xfffffa80`0c720118 _FAST_MUTEX
+0x678 SecurityLock : _EX_PUSH_LOCK
+0x680 MappedViewList : _LIST_ENTRY [ 0xfffff8a0`00024690 - 0xfffff8a0`00024690 ]
+0x690 PinnedViewList : _LIST_ENTRY [ 0xfffff8a0`000246a0 - 0xfffff8a0`000246a0 ]
+0x6a0 FlushedViewList : _LIST_ENTRY [ 0xfffff8a0`000246b0 - 0xfffff8a0`000246b0 ]
+0x6b0 MappedViewCount : 0
+0x6b2 PinnedViewCount : 0
+0x6b4 UseCount : 0
+0x6b8 ViewsPerHive : 0x40e
+0x6c0 FileObject : (null)
+0x6c8 LastShrinkHiveSize : 0x103b000
+0x6d0 ActualFileSize : 0x1040000
+0x6d8 FileFullPath : _UNICODE_STRING ""
+0x6e8 FileUserName : _UNICODE_STRING ""
+0x6f8 HiveRootPath : _UNICODE_STRING "\REGISTRY\MACHINE\SYSTEM"
+0x708 SecurityCount : 0xe1
+0x70c SecurityCacheSize : 0xe6
+0x710 SecurityHitHint : 207
+0x718 SecurityCache : 0xfffff8a0`00071010 _CM_KEY_SECURITY_CACHE_ENTRY
+0x720 SecurityHash : [64] _LIST_ENTRY [ 0xfffff8a0`00056018 - 0xfffff8a0`00056018 ]
+0xb20 UnloadEventCount : 0
+0xb28 UnloadEventArray : (null)
+0xb30 RootKcb : (null)
+0xb38 Frozen : 0
+0xb40 UnloadWorkItem : (null)
+0xb48 UnloadWorkItemHolder : _CM_WORKITEM
+0xb70 GrowOnlyMode : 0
+0xb74 GrowOffset : 0
+0xb78 KcbConvertListHead : _LIST_ENTRY [ 0xfffff8a0`00024b88 - 0xfffff8a0`00024b88 ]
+0xb88 KnodeConvertListHead : _LIST_ENTRY [ 0xfffff8a0`00024b98 - 0xfffff8a0`00024b98 ]
+0xb98 CellRemapArray : (null)
+0xba0 Flags : 0x30c
+0xba8 TrustClassEntry : _LIST_ENTRY [ 0xfffff8a0`00024bb8 - 0xfffff8a0`00024bb8 ]
+0xbb8 FlushCount : 0xfd
+0xbc0 CmRm : 0xfffff8a0`0588c700 _CM_RM
+0xbc8 CmRmInitFailPoint : 0
+0xbcc CmRmInitFailStatus : 0
+0xbd0 CreatorOwner : (null)
+0xbd8 RundownThread : (null)
+0xbe0 LastWriteTime : 0x0
0: kd>
Для работы с кустами реестра, WinDbg имеет специальное расширение
!reg. Если ввести эту команду без параметров, получим хелп, среди которого есть аргумент dumppool. Он проделывает указанные выше операции, и циклическим обход возвращает адреса всех кустов Hive реестра. У себя на машине я получил такой лог:
Код:
0: kd> !reg dumppool
dumping hive at fffff8a00000f010 (NONAME)
Stable Length = 1000
1 pages present
Volatile Length = 1000
1 pages present
dumping hive at fffff8a000024010 (SYSTEM)
Stable Length = 103b000
4155 pages present
Volatile Length = 28000
40 pages present
dumping hive at fffff8a000063010 (NONAME)
Stable Length = 15000
21 pages present
Volatile Length = 5000
5 pages present
dumping hive at fffff8a0006b92b0 (emRoot\System32\Config\SOFTWARE)
Stable Length = 3fb7000
16311 pages present
Volatile Length = 6000
6 pages present
dumping hive at fffff8a001c09010 (emRoot\System32\Config\SECURITY)
Stable Length = 5000
5 pages present
Volatile Length = 1000
1 pages present
dumping hive at fffff8a001cf9010 (\SystemRoot\System32\Config\SAM)
Stable Length = 18000
24 pages present
Volatile Length = 0
dumping hive at fffff8a001e6b010 (rofiles\LocalService\NTUSER.DAT)
Stable Length = 3c000
60 pages present
Volatile Length = 0
dumping hive at fffff8a00294c010 (\Microsoft\Windows\UsrClass.dat)
Stable Length = 1ed000
493 pages present
Volatile Length = 0
dumping hive at fffff8a00294e010 (\??\C:\Users\Marylin\ntuser.dat)
Stable Length = 17c000
380 pages present
Volatile Length = 2000
2 pages present
dumping hive at fffff8a005825010 (kVolume1\EFI\Microsoft\Boot\BCD)
Stable Length = 8000
8 pages present
Volatile Length = 0
dumping hive at fffff8a00996e010 (temRoot\System32\Config\DEFAULT)
Stable Length = 3b000
59 pages present
Volatile Length = 1000
1 pages present
dumping hive at fffff8a0099ad010 (files\NetworkService\NTUSER.DAT)
Stable Length = 3b000
59 pages present
Volatile Length = 0
Total pages present = 21632
0: kd>
Теперь имея адреса можно узнать, в какой пул ядерной памяти промаплены структуры реестра - выгружаемый в файл подкачки Paged, или постоянно находящийся в памяти NonPaged. Как видим это PagedPool, причём отладчик вернул даже 4-байтный тэг
CM10, который можно использовать в качестве маски для поиска регионов памяти.
Код:
0: kd> !pool fffff8a001cf9010 ;<----------------------- Базовый адрес куста SAM
Pool page fffff8a001cf9010 region is Paged pool ;<------ Выгружаемая в pagefile.sys память
*fffff8a001cf9000 size: c00 previous size: 0 (Allocated) *CM10
Pooltag CM10: Internal Configuration manager allocations, Binary: nt!cm
fffff8a001cf9c00 size: 30 previous size: c00 (Allocated) Ntf0
fffff8a001cf9c30 size: 10 previous size: 30 (Free) IoNm
fffff8a001cf9c40 size: 70 previous size: 10 (Allocated) CMnb Process: fffffa8010a66b00
fffff8a001cf9cb0 size: a0 previous size: 70 (Allocated) AlSe
fffff8a001cf9d50 size: b0 previous size: a0 (Allocated) ObSc
fffff8a001cf9e00 size: b0 previous size: b0 (Allocated) NtFS
fffff8a001cf9eb0 size: 150 previous size: b0 (Allocated) Clfs
Так-же можем запросить и структуру паспорта куста HBASE_BLOCK.
Код:
0: kd> !reg baseblock fffff8a001cf9010
FileName : \SystemRoot\System32\Config\SAM
Signature: HBASE_BLOCK_SIGNATURE
Sequence1: 167
Sequence2: 167
TimeStamp: 1dce257`d24d504b
Major : 1
Minor : 3
Type : HFILE_TYPE_PRIMARY
Format : HBASE_FORMAT_MEMORY
RootCell : 20
Length : 18000
Cluster : 1
CheckSum : 3af90e6b
4. Поиск ячеек KEY_NODE / KEY_VALUE
До этого момента мы исследовали окружение, а теперь рассмотрим, как работает механизм доступа к ячейкам в памяти, где хранятся данные. Уже известно, что файл куста на диске начинается с блока с сигнатурой 'regf' и размером
0x1000, а сразу за ним идут контейнеры HBIN, которые заполняют ячейки. Однако если взять адрес HBASE_BLOCK в памяти и посмотреть на дамп за его пределами, то никаких контейнеров там уже нет.
Код:
0: kd> !reg dumppool
dumping hive at fffff8a000024010 (SYSTEM)
Stable Length = 103b000
4155 pages present
.......
0: kd> dt _hhive fffff8a000024010 BaseBlock
nt!_HHIVE
+0x050 BaseBlock: 0xfffff8a0`00025000 _HBASE_BLOCK
0: kd> db 0xfffff8a0`00025000 ;<--- Сигнатура блока валидна ------ vvvv
fffff8a0`00025000 72 65 67 66 da a7 00 00-da a7 00 00 a4 14 82 ee regf............
fffff8a0`00025010 87 e2 dc 01 01 00 00 00-05 00 00 00 00 00 00 00 ................
fffff8a0`00025020 01 00 00 00 20 00 00 00-00 b0 03 01 01 00 00 00 .... ...........
fffff8a0`00025030 53 00 59 00 53 00 54 00-45 00 4d 00 00 00 00 00 S.Y.S.T.E.M.....
0: kd> db 0xfffff8a0`00025000+1000 ;<----- Здесь должен начинаться HBIN, но видим мусор!
fffff8a0`00026000 00 01 38 03 43 4d 33 39-88 1b 00 00 44 0f b7 c0 ..8.CM39....D...
fffff8a0`00026010 e8 6b f1 f8 ff 48 8b 57-58 48 8b cb 48 8b 92 88 .k...H.WXH..H...
fffff8a0`00026020 1b 00 00 e8 b0 82 00 00-ba 21 00 00 00 48 8b cf .........!...H..
fffff8a0`00026030 03 01 35 07 43 4d 33 39-c9 01 41 b8 25 00 00 00 ..5.CM39..A.%...
0: kd>
Этот эксперимент доказывает, что содержимое кустов на диске и в памяти отличается, а потому здесь нужен иной подход. Если следовать логике, то вирт.память состоит из 4КБ страниц, а для адресации к ним используется PAGE_TABLE. В случае с реестром всё аналогично, только менеджер конфигурации использует теперь свою таблицу HMAP_TABLE, при чём для каждого куста system\sam\security\etc, он создаёт отдельную HMAP.
Адрес в таблице 32-битный, а указатель на неё лежит в поле Storage структуры HHIVE (в самом её конце). Фактически имеются две одинаковые таблицы HMAP - менеджер выбирает их старшим битом адреса.
1. Stable (постоянная) - это основная таблица, данные в которой точно соответствуют файлу REGF на диске. Когда срабатывает механизм сброса "LazyFlush", содержимое всей этой таблицы сохраняется обратно на диск. Чтобы изменения ключей вступили в силу требуется перезагрузка, хотя можно просто кильнуть процесс Еxplorer.ехе в диспетчере задач, и ОС сама его перезапустит, не забыв обновить и реестр.
2. Volatile (временная) - это пространство в памяти, которое не привязано к файлу на диске. Её существование объясняет, почему некоторые изменения не требуют ребута всей ОС. Она хранит временные ключи, которые создаются динамически, но не нужны после остановки системы. К примеру куст
HKLM\HARDWARE практически полностью является Volatile. Или песочницы Sandbox - при запуске они создают себе ключи в реестре, которые автоматически уничтожаются после закрытия приложения.Значит чтобы добраться до этих таблиц в отладчике, нужно: (1)найти адрес куста Hive командой
!reg dumppool, (2)получить смещение поля HHIVE.Storage, (3)прибавить это смещение к адресу куста, (4)отобразить обе структуры DUAL по полученному адресу:
Код:
0: kd> !reg dumppool
........
dumping hive at fffff8a000024010 (SYSTEM) <---- Пусть будет куст HKLM\SYSTEM
Stable Length = 103b000
Volatile Length = 28000
........
0: kd> dt _hhive storage
nt!_HHIVE
+0x0a8 Storage: [2] _DUAL ;<------- Смещение поля равно 0xA8
0: kd> dt -a2 _dual fffff8a000024010+a8 ;<------ Адрес + оффсет
nt!_DUAL
[0] @ fffff8a0000240b8
---------------------------------------
+0x000 Length : 0x103b000 ;<-------------------- Смотри лог "dumppool"
+0x008 Map : 0xfffff8a0`0002e000 _HMAP_DIRECTORY
+0x010 SmallDir : (null) _HMAP_TABLE
+0x018 Guard : 0xffffffff
+0x020 FreeDisplay : [24] _FREE_DISPLAY
+0x260 FreeSummary : 0x7ffdff
+0x268 FreeBins : _LIST_ENTRY [ 0xfffff8a0`01ef0790 - 0xfffff8a0`01609ea0 ]
[1] @ fffff8a000024330
---------------------------------------
+0x000 Length : 0x28000
+0x008 Map : 0xfffff8a0`00024340 _HMAP_DIRECTORY
+0x010 SmallDir : 0xfffff8a0`0006a000 _HMAP_TABLE
+0x018 Guard : 0xffffffff
+0x020 FreeDisplay : [24] _FREE_DISPLAY
+0x260 FreeSummary : 0x110803
+0x268 FreeBins : _LIST_ENTRY [ 0xfffff8a0`00024598 - 0xfffff8a0`00024598 ]
0: kd>
Поскольку старший бит выбирает одну из двух таблиц HMAP, под адрес остаётся 31-бит, а это макс. 2ГБ памяти. В каталоге HMAP_DIRECTORY всего 1024 указателя на одну из 1024 таблиц HMAP_TABLE. Итого получаем размер каталога
1024х8=8КБ. Теперь записей HMAP_ENTRY в таблице второго уровня 512 штук, и каждая адресует 1 контейнер HBIN размером 4КБ (одна страница вирт.памяти). Таким образом, одна HMAP_TABLE может адресовать 512х4096=2МБ памяти, а 1024 таблиц - ровно 2ГБ.Однако в реальной ситуации размеры кустов обычно не превышает 2МБ (за некоторым исключением), а потому используется всего одна HMAP_TABLE, а не все 1024 штуки. Но если таблица единственная, значит и в каталоге HMAP_DIRECTORY достаточно одного указателя, что сокращает его размер с 8КБ до 8 байт.
Для поддержки такой оптимизации, в структуре DUAL имеется поле SmallDir (см.логи выше). Если в этом поле лежит нуль, значит размер куста больше 2МБ, и менеджер использует стандартную схему с несколькими HMAP_TABLE, иначе переключается на упрощённую, как на рис.ниже.
Например в первой структуре "Stable" видно, что размер куста
0x103b000=16.2МБ, а потому поле "SmallDir" у неё сброшено. Зато размер второй "Volatile" равен 0x28000=160КБ, и в SmallDir уже лежит 32-битный адрес 0x0006А000. При этом по указателю на каталог HMAP_DIRECTORY система запишет это-же значение, которое выбирает таблицу, с первой записью HMAP_ENTRY в ней:
Код:
0: kd> dqs 0xfffff8a0`00024340 L2 ;<--------- Линк на HMAP_DIRECTORY
fffff8a0`00024340 fffff8a0`0006a000 ;<----- Первый и единственный указатель в ней
fffff8a0`00024348 8f8949c9`ffffffff :<----- (мусор, невалидный указатель)
0: kd> dt _hmap_entry fffff8a0`0006a000 ;<----- Начало HMAP_TABLE
nt!_HMAP_ENTRY
+0x000 BlockAddress : 0xfffff8a0`00054000 ;<-- 32-байтная запись в ней
+0x008 BinAddress : 0xfffff8a0`00054009
+0x010 CmView : (null)
+0x018 MemAlloc : 0x1000
0: kd> db 0xfffff8a0`00054000 L90 ;<---------- И точно! это блок HBIN с ячейкой KeyNode(nk)
fffff8a0`00054000 68 62 69 6e 00 00 00 00-00 10 00 00 00 00 00 00 hbin............
fffff8a0`00054010 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fffff8a0`00054020 98 ff ff ff 6e 6b 30 00-80 65 86 cd 31 e3 dc 01 ....nk0..e..1...
fffff8a0`00054030 00 00 00 00 20 00 00 00-00 00 00 00 00 00 00 00 .... ...........
fffff8a0`00054040 ff ff ff ff ff ff ff ff-01 00 00 00 08 02 00 80 ................
fffff8a0`00054050 88 00 00 80 ff ff ff ff-00 00 00 00 00 00 00 00 ................
fffff8a0`00054060 22 00 00 00 4c 00 00 00-00 00 00 00 11 00 00 00 "...L...........
fffff8a0`00054070 43 75 72 72 65 6e 74 43-6f 6e 74 72 6f 6c 53 65 CurrentControlSe
fffff8a0`00054080 74 00 00 00 00 00 00 00-10 ff ff ff 73 6b 00 00 t...........sk..
0: kd>
Обратите внимание на поле BinAddress в структуре HMAP_ENTRY - младшие 4 бита в нём являются флагами, а потому нужно сбросить их в нуль. В данном случае флаги
0x9=1001. Вот их скрытый смысл:
Код:
MAP_ENTRY_NEW_ALLOC = 0001 ; Это именно блок HBIN, а не что-то другое
MAP_ENTRY_DISCARDABLE = 0010 ; Блок пустой и состоит из одной ячейки
MAP_ENTRY_TRIMMED = 0100 ; Страница помечана как "обрезанная" ???
MAP_ENTRY_DUMMY = 1000 ; Страница выделена из пула ядра
Это поле было введено на случай, когда размер контейнера HBIN превышает 4КБ, т.е. несколько смежных склеены в один. Тогда BinAddress - это указатель на начало расширенного блока с сигнатурой 'hbin', а BlockAddress - одни из экземляров вирт.страниц на границе 4КБ. В данном случае у первых двух полей одинаковое значение (не считая флагов во втором), а значит это блок стандартного размера 4КБ, который и указан в поле
MemAlloc=0x1000.5. Кэш грязных страниц
Выше упоминалось, что все изменения в реестре (т.е. создание/удаление новых ключей), как правило, вступают в силу после перезагрузки ОС. До этого они находятся словно в подвешенном "Dirty-состоянии", а система общается с ним через кэш файловой системы. Отладчик предоставляет нам несколько мощных инспекторов, чтобы заглянуть в потроха этого механизма.
•
!reg dirtyvector - основная команда для данной задачи. Она показывает, какие блоки куста были изменены в памяти, но ещё не записаны на диск. Параметром является адрес структуры HHIVE (см. dumppool). Так получим битовую карту, где каждый бит соответствует одному 4К-блоку HBIN. Установленный бит означает, что данные в этом блоке были модифицированы и ожидают сброса на диск через механизмы кеш-менеджера, или LazyFlush. Это и есть "подвешенное" состояние.
Код:
0: kd> !reg dirtyvector fffff8a000024010
HSECTOR_SIZE = 200
HBLOCK_SIZE = 1000
PAGE_SIZE = 1000
DirtyAlloc = 0x103c ;<-------------- Размер битовой карты
DirtyCount = 0x0 ;<-------------- Кол-во грязных блоков
Buffer = 0xfffff8a00002c000 ;<--- Указатель на карту в памяти
Address 32k 32k
0x 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0x 8000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0x 10000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0x 18000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
....................
0x 1030000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0x 1038000 00000000 00000000 00000000
0: kd>
•
!defwrites - глобальное состояние системного кэш-менеджераЕсли предыдущая показывает грязные блоки конкретного куста, то
!defwrites дампит глобальное состояние всей системы кеширования. Она не имеет параметров, а основными полями в логе являются CcTotalDirtyPages - это общее кол-во грязных страниц в системе, и CcDirtyPageThreshold - макс.кол-во грязных, после которого система начнёт активно сбрасывать их на диск.
Код:
0: kd> !defwrites
*** Cache Write Throttle Analysis ***
CcTotalDirtyPages: 78 ( 312 Kb)
CcDirtyPageThreshold: 520727 ( 2082908 Kb)
MmAvailablePages: 3303471 (13213884 Kb)
MmThrottleTop: 450 ( 1800 Kb)
MmThrottleBottom: 80 ( 320 Kb)
MmModifiedPageListHead.Total: 79191 ( 316764 Kb)
Write throttles not engaged
0: kd>
Если-же без расширений отладчика, то инфа о кэше грязных страниц лежит в структуре HHIVE. Здесь непосредственно хранятся векторы грязных страниц, которые управляют состоянием кеша на низком уровне. Все поля, которые относятся к кэшу начинаются с Dirty_xx, а потому можно найти их по маске:
Код:
0: kd> dt _hhive fffff8a001cf5010 Dirty*
nt!_HHIVE
+0x058 DirtyVector : _RTL_BITMAP
+0x068 DirtyCount : 0
+0x06c DirtyAlloc : 0x18
+0x07a DirtyFlag : 0x1
0: kd> dt _hhive fffff8a001cf5010 DirtyVector.
nt!_HHIVE
+0x058 DirtyVector :
+0x000 SizeOfBitMap : 0xc0
+0x008 Buffer : 0xfffff8a0`01acac80 -> 0
0: kd> db 0xfffff8a0`01acac80 L18
fffff8a0`01acac80 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fffff8a0`01acac90 00 00 00 00 00 00 00 00 ........
0: kd>
1. DirtyVector: это структура RTL_BITMAP, в которой всего 2 поля - SizeOfBitMap хранит размер буфера в битах, а Buffer - это указатель на битовую карту в памяти. Каждый бит в этом буфере представляет один блок HBIN: если бит взведён(1) значит блок грязный и ожидает записи на диск, 0=чистый. Это сердце механизма.
2. DirtyAlloc: тоже размер буфера как и SizeOfBitmap, но уже не в битах, а в байтах. Например здесь
0х18=24, и если умножить на кол-во бит в байте, то получим 24*8=192, что хексах будет SizeOfBitmap=0xC0.3. DirtyCount: просто счётчик кол-ва грязных блоков HBIN (быстрый способ оценить масштаб изменений). Если он нуль, остальные поля Dirty* можно игнорировать.
5.1. Лист пустых блоков
Перед операцией записи в реестр новых значений, диспетчеру необходимо найти свободные ячейки в блоках HBIN. Для быстрого их поиска, во-вложенной в HHIVE структуре DUAL предусмотрены специальные поля с приставкой Free_xx.
Код:
0: kd> dt _hhive fffff8a001cf5010 Storage.
nt!_HHIVE
+0x0a8 Storage : [2]
+0x000 Length : 0x18000
+0x008 Map : 0xfffff8a0`01cf50c8 _HMAP_DIRECTORY
+0x010 SmallDir : 0xfffff8a0`01cf9000 _HMAP_TABLE
+0x018 Guard : 0xffffffff
+0x020 FreeDisplay : [24] _FREE_DISPLAY
+0x260 FreeSummary : 0x1a821f
+0x268 FreeBins : _LIST_ENTRY [ 0xfffff8a0`01cf5320 - 0xfffff8a0`01cf5320 ]
0: kd>
1. FreeDisplay: состоит из 24 структур FREE_DISPLAY, каждая из которых соответствует определённому диапазону размеров ячеек. Указывает, какие страницы в кусте Hive потенциально могут содержать свободные ячейки заданной длины. Сама структура имеет 2 поля: RealVectorSize информирует, какого размера ячейку можно выделить в указанной в Display странице памяти. То-есть это верхний порог размера ячейки в байтах. Если под новую ячейку требуется больше байт, то система ищет подходящие в следующих 23-х структурах.
Поле "RealVectorSize" может хранить и невалидные значения, как например экземпляры 2,3 ниже.
Во-первых, валидные размеры всегда должны быть степенью двойки, а во-вторых размер не должен превышать значение в поле Length (общий размер куста), которое в данном случает равно
0x18000 (см.выше).
Код:
0: kd> dt -a4 fffff8a001cf5010+a8 _dual freedisplay.
nt!_DUAL
[0] @ fffff8a001cf50b8
-----------------------------------------
+0x020 FreeDisplay : [24]
+0x000 RealVectorSize : 0x100 <----- Valid
+0x008 Display : _RTL_BITMAP
[1] @ fffff8a001cf5330
-----------------------------------------
+0x020 FreeDisplay : [24]
+0x000 RealVectorSize : 0
+0x008 Display : _RTL_BITMAP
[2] @ fffff8a001cf55a8
-----------------------------------------
+0x020 FreeDisplay : [24]
+0x000 RealVectorSize : 0x800002e0 <----- Invalid
+0x008 Display : _RTL_BITMAP
[3] @ fffff8a001cf5820
-----------------------------------------
+0x020 FreeDisplay : [24]
+0x000 RealVectorSize : 0x1cf5840 <----- Invalid
+0x008 Display : _RTL_BITMAP
.........
2. FreeSummary: это двоичная маска с подсказкой. Действительны в поле только младшие 24-бита. Они указывают, в какой из 24-х структур FREE_DISPLAY могут находиться свободные ячейки. Нуль в этом поле означает, что в кусте нет свободных ячеек, и нужно выделять дополнительный блок HBIN.
3. FreeBins: кольцевой список с указателями на следующий\предыдущий блок полностью пустых ячеек.
6. Постфикс
Реестр Windows это не чёрный ящик, а хорошо документированная (пусть и не официально) система. Каждый байт в файле REGF имеет своё место и назначение. Вооружившись WinDbg, знанием структур и терпением, можно анализировать его содержимое на любом уровне - от дампа памяти, и до бинарного файла на диске. Реестр - это как слоёный пирог: чтобы добраться до начинки, нужно аккуратно пройти через все слои. Но если вы это сделали, награда стоит усилий.
Отдельное спасибо:
• М.Руссиновичу и команде Sysinternals за WinDbg, расширение !reg и бесценную инфу о внутренностях Windows.
• Разработчикам ReactOS за открытые исходные коды, которые служат отличной документацией.
• Всем читателям, кто дочитал до этого места. Вы настоящие энтузиасты низкоуровневого анализа.
В следующей\заключительной части мы рассмотрим внутренние особенности куста реестра SAM, где хранятся личные сведения о всех пользователях системы, их правах, паролях, и многое другое. До скорого, всем удачи, пока!