Для большинства из нас реестр - это окно "Regedit" с древовидной структурой папок, и огромным кол-вом пар "ключ/значение". Однако на низком уровне это бинарные файлы в формате regf, которые хранятся на жёстком диске, чтобы была возможность запомнить текущие настройки системы при выключении машины. В данном цикле из трёх частей мы положим реестр на анатомический стол, и под микроскопом рассмотрим все его детали. Это увлекательное занятие раскроет двери в криминалистику так, что на поверхность всплывут ошибки, и как следствие - потенциальные угрозы. Чтобы из информации не получился винегрет, я разделил её по смыслу на следующие блоки:
Часть(1): Внутренняя структура файлов на диске
Часть(2): Алгоритм поиска файлов после проецирования в память
Часть(3): Формат и назначение файлов журнала .LOG\.LOG1\.LOG2
В этой части:
1. Общие сведения
Бинарный формат разделов реестра имеет сигнатуру "regf". Он довольно особенный, поскольку представляет древо узлов одновременно на диске и в памяти. По большому счёту, это сложная файловая система типа NTFS, с которой у regf много общего. За 30 лет существования формата майки так и не выпустили его официальную спеку, ведь учитывая область применения в контексте подсистемы безопасности, публичное обнародывание можно сравнить с выстрелом в ногу. Однако структуру всех компонентов раскрывают символы отладки PDB для ядра Ntoskrnl.exe, а учитывая повышенный интерес к этим бинарям экспертов по криминалистике, на свет появилось несколько неофициальных спецификаций от энтузиастов.
Автором одной из них является наш соотечественник Максим Суханов, и это наиболее внятное описание Regf на диске. Здесь уместно упоменуть и некоего исследователя Матеуш Юрчик из Кракова (Польша). По его словам, он потратил более 2-х лет на изучение внутренней кухни реестра, и нашёл в ней аж 57 уязвимостей! Пруфы на каждый баг он отправлял в Microsoft, которая заштопала дыры только в Win10.
Что касается ОС, то проецированием regf с диска в память, а так-же поддержкой остальных операций типа создание\запись\удаление разделов и ключей, управляет компонент ядра "Configuration Manager" (диспетчер конфигурации), и как следствие все Native-функции имеют префиксы
В зависимости от того, о каком уровне идёт речь, в качестве узла Hive может выступать и куст во вложенной папке корневого узла (в общем полная анархия и неразбериха в терминологии). Если при экспорте правой клавишей мыши в окне Regedit выбрать тип "Файлы кустов..", то получим бинарь с сигнатурой regf, который позже можно будет исследовать в любом HEX-редакторе, например "HxD".
2. Бэкап файлов SAM, System, Security
Прежде чем перейти к разбору формата REGF, нам нужен объект исследования, а потому рассмотрим несколько вариантов создания копий наиболее интересных кустов, к которым можно отнести все папки из раздела HKLM. Только проблема в доступе к ним - он полностью заблокирован не только смертному юзеру, но и самому админу. Это потому, что разделы создаются процессами служб в закрытой сессии(0), и соответственно прочитать их можно лишь с правами пользователя SYSTEM. Что мы можем сделать, оказавшись в такой ловушке?
1. Скопировать кусты реестра без запуска Win, загрузившись с флэшки или LiveCD.
Вариант безотказный и никогда не даёт осечки. Вот список путей, где хранятся основные файлы реестра. Обратите внимание, что куст HARDWARE создаётся при загрузки ОС динамически, так-что его бесполезно искать на диске. Причина банальна - между двумя вкл.машины, в систему могут быть добавлены новые физические устройства, а потому конфигуратору проще обновить весь список целиком, чем решать проблемы кривых настроек.
2. Запустив cmd.exe от админа, вскормить интерпретатору команду
Под катом она транзитом передаёт запрос службе в сессии(0), а потому при правильном подходе способ обычно срабатывает.
3. Есть ещё один довольно хитрый хак. Понятно, что для всех операций с реестром нужны права админа, однако кусты Hive являются объектами ядра, и как следствие доступ к ним ограничевает запись ACL (access control list) в токене текущего пользователя. Что примечательно, всего одной галкой мы можем сменить разрешения на доступ к объекту в его свойствах, как это представлено на рис.ниже. Например в дефолте содержимое куста SAM не отображается в правом окне Regedit, но если открыть полный доступ админу (а по факту назначить права SYSTEM конкретно для данного объекта), инфа воскреснет как феникс из пепла, и можно будет спокойно отправить весь куст на экспорт правой клавишей мыши.
Что-то подобное делает и утилита М.Руссиновича
У неё имеется своя служба "psexecvs" в сессии(0), а переданный ей параметр -sid представляет комбинацию из трёх ключей:
4. Если ни один из трёх вариантов не сработает, то расчехляем тяжёлую артиллерию в виде спец-утилит. Одна из них - крутая тулза "Windows Password Recovery" WРR. Правда незареганная её версия лишь отображает инфу, и при попытке сохранить предлагает нам расстаться с шекелями. Если вы плотно сидите в этой сфере, можно купить софтину за $65, но для разового пользования жалко честно заработанных, а потому придётся или искать альтернативы, или-же на свой страх и риск скачать крякнутую версию WPR с площадки rsload.net.
5. И наконец если у вас Win установлена на вирт.машине, можете просто открыть RAR'ом, 7-ZIP, или UltraISO файл образа жёсткого диска (*.vdi для VBox). На выходе получите все разделы харда, и отдельные его файлы, которые будут доступны для копирования без каких-либо ограничений. Так это выглядит в 7-ZIP:
3. Формат Regf-файлов на диске
Теперь, когда мы собрали бинарные файлы SAM, SYSTEM, SECURITY, SOFTWARE в папке Backup, можно приступать к анализу их генов. Да данном этапе желательно установить отладчик WinDbg с валидными символами PDB, или на худой конец дизассемблер IDA, что даст возможность запрашивать прототипы служебных структур менеджера конфигов CM.
Значит открываем любой из перечисленных файлов в hex-редакторе HxD, и сразу видим сигнатуру "regf". Если таковой нет в первых 4-х байтах бинарника, значит образ повреждён, и все последующие действия не имеют смысла.
3.1. Базовый блок
Посмотрев на файл с высоты птичьего полёта можно обнаружить, что начинается он с заголовка, который описывает структура HBASE_BLOCK размером ровно 4КБ. Поскольку мы имеем дело с файлом на диске, этот размер жёстко привязан к дефолтному размеру кластера HDD. Базовый блок описывает глобальные свойства всего файла regf, от чердака и до подвала.
3.2. Контейнеры HBIN
Сразу после базового блока начинается массив т.н. "контейнеров". Размер каждого из них тоже 4КБ, т.е. 1 кластер. Контейнеры имеют частный свой заголовок размером 0x20, который описывает структура HBIN. Здесь нужно отметить, что два последовательных контейнера могут объединяться в один размером уже 8КБ, или вообще 64КБ в союзе из 16-ти. Как результат, общий размер любого regf всегда будет кратен 4К. Длина контейнера указывается в поле "Size" его заголовка, и они начинаются исключительно на границе 0x1000 байт. Сигнатурой контейнера является 4-байтная строка hbin.
Отдельного внимания заслуживает поле
3.3. Ячейки CELL с данными
И наконец на самом нижнем уровне иерархии находятся ячейки для хранения полезных данных. Они заполняют собой контейнеры, и делятся на 4 типа. Проблема в том, что каждый тип ячеек описывает своя структура, т.е. они отличаются по содержимому. Тип можно определить по 2-байтной сигнатуре по смещению(4) от начала ячейки:
Размер этих тушканчиков не фиксирован (зависит от кол-ва данных), но внутри файла они всегда выравниваются на границу 8 байт. Фактический размер задаётся в первом дворде до сигнатуры - это число со знаком. Если значение отрицательное
Таким образом, чтобы получить реальный размер занятой ячейки, нужно прочитать её первый дворд, и применить к нему инструкцию
Но и это ещё не всё.. Чтобы "приукрасить" унылую жизнь прогеров, разработчики пошли ещё дальше. Так, для оптимизации поиска вложенных в корень кустов (а их может быть просто огромное кол-во в реестре), к основной структуре KeyNode привязали ещё 4 вспомогательных, что позволило выбирать один из 4-х алгоритмов поиска подкаталогов. Они тоже считаются самостоятельными ячейками KeyIndex, а их структуры можно найти по сигнатурами из списка ниже:
Таким образом, семейный портрет всех действующих лиц будет выглядеть примерно так..
На схеме ниже показано, как связываются структуры файла REGF между собой.
Здесь нужно запомнить одну простую истину: "Все оффсеты хранят относительный адрес, базой которого является константа 0x1000". Как уже упоминалось, значение 0x1000 это смещение первого HBIN относительно начала 0x00000000 самого бинарного файла на диске.
Например в поле
4. Практика - пишем парсер файлов REGF
Теперь соберём всё сказанное под один капот, и напишем небольшую утилиту.
Создавать очередной форк Regedit не имеет смысла, тем-более, что в открытом доступе лежат такие монстры как TotalReg П.Йосифовича, RegistryFinder от С.Филиппова, и прочие. Да и вообще изобретать свой велосипед с квадратными колёсами не есть гуд.
Поэтому я продемонстрирую только технику обхода кустов в консольном приложении, причём глубина вложений Depth будет равна всего 1. То-есть отобразим корневой куст, и перечислим лишь подкаталоги первого уровня, а для остальных покажем обычный счётчик. Здесь главное понять суть, а реализовать парсинг хоть до глубины "Марианской впадины" вообще не проблема.
В этом театре главную роль играет структура CM_KEY_NODE, которая описывает каталог как сущность на любом уровне, хоть 0, хоть 1000. Поэтому первое, что приходит на ум - это организовать последовательный поиск всех структур KeyNode в цикле, и... получим прокол! Выше уже упоминалось, что внутри файла царит полный хаос, и если ячейка с описанием головы куста лежит в начале/середине файла, то его хвост может торчать вообще чёрт знает где. Поэтому здесь нужен другой подход, который заключается в следующем:
1. Самая первая структура KEY_NODE с сигнатурой 'nk' всегда лежит в первом контейнере HBIN по смещению 0x1020 от начала файла REGF. Она описывает корневую папку куста, например: sam, system, security, software. Эту структуру мы должны использовать в качестве отправной точки, т.е. начинать создание дерева реестра с неё.
2. В структуре KEY_NODE имеется поле счётчика его подпапок
3. Чтобы реализовать теорию программно, мы должны предусмотреть цикл с рекурсией, т.е. вызовом процедуры самой себя. Если убрать рекурсию, то придётся выделять отдельные процедуры для каждой из найденных папок - в случае с реестром такой подход исключён, ведь вложенных диров у куста может быть не много, а очень много. К примеру у меня в кусте HKLM\SOFTWARE\Classes уютно расположились 5336 подпапки, и заранее не предусмотрев рекурсию в коде, можно даже не пытаться обойти их все.
На схеме ниже представлена логика парсера где видно, что гипотетический корневой узел имеет счётчик(2), а потому его поле
А вот собственно и исходник.
При запуске сразу появляется окно с предложением выбрать Regf-файл для анализа
Как результат получим такой лог где видно, что внутреннее имя куста вовсе не SOFТWARE, а CMI-CreateHive{GUID}. Это имя назначает кусту менеджер конфигурации при загрузке его с диска в память, и оно осталось в файле как артефакт. Кроме имени, в двух следующих столбцах имеем счётчик вложенных папок, и наличие у папки параметров, которые отображаются в правом окне Regedit. Обратите внимание на кол-во детей у куста "Classes", о чём я упоминал выше:
5. Заключение
Для криминалистики и поиска артефактов в реестре, важно использовать только файлы REGF на диске, поскольку в них остаются удалённые из реестра ключи, которые могут отсутствовать в памяти. Статья писалась с расчётом на то, чтобы заманить читателя в эту область, где можно найти много конфиденциальной инфы, например, из файла SAM. Это отдельная тема для разговора, и в последней части мы ещё вернёмся к ней, вытащив все пароли и явки из тёмных переулов файла на белый свет.
Темой для следующего разговора будет способ хранения кустов реестра в памяти работающей системы, а пока ставлю здесь точку. В скрепке найдёте инклуд с описанием всех структур менеджера конфигов, а так-же исходник с исполняемым файлом для тестов. Всем удачи, до скорого!
Часть(1): Внутренняя структура файлов на диске
Часть(2): Алгоритм поиска файлов после проецирования в память
Часть(3): Формат и назначение файлов журнала .LOG\.LOG1\.LOG2
В этой части:
1. Общие сведения
2. Бэкап защищённых файлов SAM\System\Security
3. Формат Regf-файлов на диске
4. Практика - пишем парсер
5. Заключение
1. Общие сведения
Бинарный формат разделов реестра имеет сигнатуру "regf". Он довольно особенный, поскольку представляет древо узлов одновременно на диске и в памяти. По большому счёту, это сложная файловая система типа NTFS, с которой у regf много общего. За 30 лет существования формата майки так и не выпустили его официальную спеку, ведь учитывая область применения в контексте подсистемы безопасности, публичное обнародывание можно сравнить с выстрелом в ногу. Однако структуру всех компонентов раскрывают символы отладки PDB для ядра Ntoskrnl.exe, а учитывая повышенный интерес к этим бинарям экспертов по криминалистике, на свет появилось несколько неофициальных спецификаций от энтузиастов.
Автором одной из них является наш соотечественник Максим Суханов, и это наиболее внятное описание Regf на диске. Здесь уместно упоменуть и некоего исследователя Матеуш Юрчик из Кракова (Польша). По его словам, он потратил более 2-х лет на изучение внутренней кухни реестра, и нашёл в ней аж 57 уязвимостей! Пруфы на каждый баг он отправлял в Microsoft, которая заштопала дыры только в Win10.
Что касается ОС, то проецированием regf с диска в память, а так-же поддержкой остальных операций типа создание\запись\удаление разделов и ключей, управляет компонент ядра "Configuration Manager" (диспетчер конфигурации), и как следствие все Native-функции имеют префиксы
Cm_xx(), или Hv_xx(). Последний подразумевает HIVE, что в дословном переводе звучит как Улей (встречается в большинстве доках и статьях), а по факту означает "Куст" раздела реестра. Как правило основных разделов 5, и каждый из них имеет свои узлы Hive: В зависимости от того, о каком уровне идёт речь, в качестве узла Hive может выступать и куст во вложенной папке корневого узла (в общем полная анархия и неразбериха в терминологии). Если при экспорте правой клавишей мыши в окне Regedit выбрать тип "Файлы кустов..", то получим бинарь с сигнатурой regf, который позже можно будет исследовать в любом HEX-редакторе, например "HxD".
2. Бэкап файлов SAM, System, Security
Прежде чем перейти к разбору формата REGF, нам нужен объект исследования, а потому рассмотрим несколько вариантов создания копий наиболее интересных кустов, к которым можно отнести все папки из раздела HKLM. Только проблема в доступе к ним - он полностью заблокирован не только смертному юзеру, но и самому админу. Это потому, что разделы создаются процессами служб в закрытой сессии(0), и соответственно прочитать их можно лишь с правами пользователя SYSTEM. Что мы можем сделать, оказавшись в такой ловушке?
1. Скопировать кусты реестра без запуска Win, загрузившись с флэшки или LiveCD.
Вариант безотказный и никогда не даёт осечки. Вот список путей, где хранятся основные файлы реестра. Обратите внимание, что куст HARDWARE создаётся при загрузки ОС динамически, так-что его бесполезно искать на диске. Причина банальна - между двумя вкл.машины, в систему могут быть добавлены новые физические устройства, а потому конфигуратору проще обновить весь список целиком, чем решать проблемы кривых настроек.
2. Запустив cmd.exe от админа, вскормить интерпретатору команду
reg save HKLM\SAM d:\backup\sam.Под катом она транзитом передаёт запрос службе в сессии(0), а потому при правильном подходе способ обычно срабатывает.
3. Есть ещё один довольно хитрый хак. Понятно, что для всех операций с реестром нужны права админа, однако кусты Hive являются объектами ядра, и как следствие доступ к ним ограничевает запись ACL (access control list) в токене текущего пользователя. Что примечательно, всего одной галкой мы можем сменить разрешения на доступ к объекту в его свойствах, как это представлено на рис.ниже. Например в дефолте содержимое куста SAM не отображается в правом окне Regedit, но если открыть полный доступ админу (а по факту назначить права SYSTEM конкретно для данного объекта), инфа воскреснет как феникс из пепла, и можно будет спокойно отправить весь куст на экспорт правой клавишей мыши.
Что-то подобное делает и утилита М.Руссиновича
psexec64.exe -sid c:\windows\regedit.exeУ неё имеется своя служба "psexecvs" в сессии(0), а переданный ей параметр -sid представляет комбинацию из трёх ключей:
-s запуск указанной программы с правами SYSTEM, -i консольная сессия, -d неинтерактивный режим. Помню когда-то на WinXP утилита у меня работала, но сейчас на х64 ничерта нефурычит, жалуясь на проблемы коннекта со своей службой. В общем пробуйте, может у вас получится.4. Если ни один из трёх вариантов не сработает, то расчехляем тяжёлую артиллерию в виде спец-утилит. Одна из них - крутая тулза "Windows Password Recovery" WРR. Правда незареганная её версия лишь отображает инфу, и при попытке сохранить предлагает нам расстаться с шекелями. Если вы плотно сидите в этой сфере, можно купить софтину за $65, но для разового пользования жалко честно заработанных, а потому придётся или искать альтернативы, или-же на свой страх и риск скачать крякнутую версию WPR с площадки rsload.net.
5. И наконец если у вас Win установлена на вирт.машине, можете просто открыть RAR'ом, 7-ZIP, или UltraISO файл образа жёсткого диска (*.vdi для VBox). На выходе получите все разделы харда, и отдельные его файлы, которые будут доступны для копирования без каких-либо ограничений. Так это выглядит в 7-ZIP:
3. Формат Regf-файлов на диске
Теперь, когда мы собрали бинарные файлы SAM, SYSTEM, SECURITY, SOFTWARE в папке Backup, можно приступать к анализу их генов. Да данном этапе желательно установить отладчик WinDbg с валидными символами PDB, или на худой конец дизассемблер IDA, что даст возможность запрашивать прототипы служебных структур менеджера конфигов CM.
Значит открываем любой из перечисленных файлов в hex-редакторе HxD, и сразу видим сигнатуру "regf". Если таковой нет в первых 4-х байтах бинарника, значит образ повреждён, и все последующие действия не имеют смысла.
3.1. Базовый блок
Посмотрев на файл с высоты птичьего полёта можно обнаружить, что начинается он с заголовка, который описывает структура HBASE_BLOCK размером ровно 4КБ. Поскольку мы имеем дело с файлом на диске, этот размер жёстко привязан к дефолтному размеру кластера HDD. Базовый блок описывает глобальные свойства всего файла regf, от чердака и до подвала.
Код:
struct HBASE_BLOCK ;//<--- sizeof = 0x1000
Signature db 'regf' ; 00
Sequence1 dd 0 ; 04 два счётчика - должны быть равны,
Sequence2 dd 0 ; 08 ^^^^^^^^^^^^ иначе остались незаписанные из памяти данные
TimeStamp dq 0 ; 0C время создания
Major dd 1 ; 14 основная версия Hive = всегда 1
Minor dd 0 ; 18 может быть = 3,4,5,6
Type dd 0 ; 1C 0 = основной файл
Format dd 0 ; 20 1 = прямая загрузка в память
RootCell dd 0 ; 24 смещение корневой ячейки(0) от начала полезных данных (обычно 0x20)
Length dd 0 ; 28 размер полезных данных в файле
Cluster dd 0 ; 2C размер сектора диска делённый на 512 (обычно 1)
FileName db 64 dup(0) ; 30 Unicode-имя куста реестра (макс 32 символа ascii)
RmId dq 0,0 ; 70 все Id = GUID
LogId dq 0,0 ; 80 ID файла журналов LOG для данного Hive
Flags dd 0 ; 90
TmId dq 0,0 ; 94
GuidSignature db 'rmtm' ; A4
LastWriteTime dq 0 ; A8 время последней записи в файл
OffRegSign dd 0 ; B0 строка 'OfRg' (offreg.dll)
OffRegFlags dd 0 ; B4
Reserved1 db 81*4 dup(0) ; B8
CheckSum dd 0 ; 1FC контрольная сумма всех полей выше
Padding rb 0xE00 ; 200 байты заполнения до 0x1000 (4096 байт)
ends
Реальный дамп - попробуйте наложить на него эту структуру
3.2. Контейнеры HBIN
Сразу после базового блока начинается массив т.н. "контейнеров". Размер каждого из них тоже 4КБ, т.е. 1 кластер. Контейнеры имеют частный свой заголовок размером 0x20, который описывает структура HBIN. Здесь нужно отметить, что два последовательных контейнера могут объединяться в один размером уже 8КБ, или вообще 64КБ в союзе из 16-ти. Как результат, общий размер любого regf всегда будет кратен 4К. Длина контейнера указывается в поле "Size" его заголовка, и они начинаются исключительно на границе 0x1000 байт. Сигнатурой контейнера является 4-байтная строка hbin.
Отдельного внимания заслуживает поле
FileOffset в заголовке - это смещение относительно первого HBIN в файле, но учитывая, что первый расположен сразу после HBASE_BLOCK, можно в качестве базы для FileOffset использовать константу 0x1000 (т.е. длина базового блока). Таким образом, FileOffset + 0x1000 = RealOffset (cмещение произвольного HBIN в бинарном файле).
Код:
struct HBIN ;<------- sizeof = 0x20
Signature db 'hbin' ; 00
FileOffset dd 0 ; 04 смещение от первого HBIN
Size dd 0 ; 08 размер контейнера
Reserved dq 0 ; 0C прозапас..
TimeStamp dq 0 ; 14 время последней записи
Spare dd 0 ; 1C выравнивание на границу 32 байт
ends
3.3. Ячейки CELL с данными
И наконец на самом нижнем уровне иерархии находятся ячейки для хранения полезных данных. Они заполняют собой контейнеры, и делятся на 4 типа. Проблема в том, что каждый тип ячеек описывает своя структура, т.е. они отличаются по содержимому. Тип можно определить по 2-байтной сигнатуре по смещению(4) от начала ячейки:
Код:
'nk' = KeyNode Описывает папку\узел реестра
'vk' = KeyValue Хранит пару ключ:значение
'sk' = KeySecurity Флаги доступа к узлу
'db' = BigData Инфа о больших массивах данных (Win10+)
Размер этих тушканчиков не фиксирован (зависит от кол-ва данных), но внутри файла они всегда выравниваются на границу 8 байт. Фактический размер задаётся в первом дворде до сигнатуры - это число со знаком. Если значение отрицательное
0xFFFFF000, значит ячейка занята и в ней лежит валидное значение. Если-же размер положительный 0x00000FFF, то возможно ключ был удалён с реестра, и соответственно эту ячейку можно перезаписать новыми данными.Таким образом, чтобы получить реальный размер занятой ячейки, нужно прочитать её первый дворд, и применить к нему инструкцию
NEG ассемблера. Возьмём размер из примера ниже 0xFFFFFF78 (см.жёлтый блок), и после NEG получим Size=0x88. Теперь где заканчивается одна, там сразу начинается следующая, и по волею судьбы она тоже типа 'nk', только размером уже 0xA8=0x58 байт. Как видим, в обоих случаях размер кратен 8-ми байтам, так-что дворд хранит значение учитывая себя и выравнивание Padding.Пример контейнера с заголовком HBIN, и первой ячейкой 'nk' в файле
Но и это ещё не всё.. Чтобы "приукрасить" унылую жизнь прогеров, разработчики пошли ещё дальше. Так, для оптимизации поиска вложенных в корень кустов (а их может быть просто огромное кол-во в реестре), к основной структуре KeyNode привязали ещё 4 вспомогательных, что позволило выбирать один из 4-х алгоритмов поиска подкаталогов. Они тоже считаются самостоятельными ячейками KeyIndex, а их структуры можно найти по сигнатурами из списка ниже:
Код:
'li' = IndexList Базовый вариант поиска - хранит просто оффсет ячейки KeyNode
'lf' = FeatList Аналогично 'li', только хранит ещё и первые 4 символа имени узла
'lh' = HashList Поиск имени по хэшу - хранит массив хэш-двордов
'ri' = RootIndex Линк на другие 'li,lf,lh' - сложен для парсинга. Win10+
;------ Заголовок до Offset у всех одинаковый --------
struct CM_KEY_INDEX
Size dd 0 ; размер ячейки
Signature dw 0 ; 'li,lf,lh,ri'
Count dw 0 ; кол-во элементов в массиве Offset
Offset dd 0 ; + 0x1000 = адрес искомого KEY_NODE
Name dd ? ; только для 'lf,lh'
ends ; 'lf' = NameHint (4 первых символа имени)
; 'lh' = NameHash (массив хэшей, только для Regf.Minor > 4)
Таким образом, семейный портрет всех действующих лиц будет выглядеть примерно так..
На схеме ниже показано, как связываются структуры файла REGF между собой.
Здесь нужно запомнить одну простую истину: "Все оффсеты хранят относительный адрес, базой которого является константа 0x1000". Как уже упоминалось, значение 0x1000 это смещение первого HBIN относительно начала 0x00000000 самого бинарного файла на диске.
Например в поле
RootCell структуры HBASE_BLOCK, как правило, лежит 0x20, и прибавив к нему базу получаем 0x1020, а это как-раз адрес первой ячейки после 20h-байтного заголовка 'hbin' (см.скрин редактора HxD выше). Аналогичным образом получаем валидные смещения и в полях FileOffset структур HBIN, SubKeyList + ValueList в KEY_NODE, List в KEY_INDEX, и DataOffset в KEY_VALUE. Ячейки одного узла могут быть хаотично разбросаны по всему файлу REGF (привет фрагментация реестра), а относительные указатели помогают собрать их в единое целое.4. Практика - пишем парсер файлов REGF
Теперь соберём всё сказанное под один капот, и напишем небольшую утилиту.
Создавать очередной форк Regedit не имеет смысла, тем-более, что в открытом доступе лежат такие монстры как TotalReg П.Йосифовича, RegistryFinder от С.Филиппова, и прочие. Да и вообще изобретать свой велосипед с квадратными колёсами не есть гуд.
Поэтому я продемонстрирую только технику обхода кустов в консольном приложении, причём глубина вложений Depth будет равна всего 1. То-есть отобразим корневой куст, и перечислим лишь подкаталоги первого уровня, а для остальных покажем обычный счётчик. Здесь главное понять суть, а реализовать парсинг хоть до глубины "Марианской впадины" вообще не проблема.
В этом театре главную роль играет структура CM_KEY_NODE, которая описывает каталог как сущность на любом уровне, хоть 0, хоть 1000. Поэтому первое, что приходит на ум - это организовать последовательный поиск всех структур KeyNode в цикле, и... получим прокол! Выше уже упоминалось, что внутри файла царит полный хаос, и если ячейка с описанием головы куста лежит в начале/середине файла, то его хвост может торчать вообще чёрт знает где. Поэтому здесь нужен другой подход, который заключается в следующем:
1. Самая первая структура KEY_NODE с сигнатурой 'nk' всегда лежит в первом контейнере HBIN по смещению 0x1020 от начала файла REGF. Она описывает корневую папку куста, например: sam, system, security, software. Эту структуру мы должны использовать в качестве отправной точки, т.е. начинать создание дерева реестра с неё.
2. В структуре KEY_NODE имеется поле счётчика его подпапок
SubKeyCount, и если в нём лежит нуль, значит у данного узла нет подпапок. Иначе, в соседнем поле SubKeyList будет прописан указатель на структуру KEY_INDEX для поиска вложенных папок, каждую из которых описывает своя KEY_NODE и что характерно, со-своим счётчиком вложенных SubKeyCount. Другими словами получаем матрёшку, и именно эта матрёшка поможет нам выстроить законченное древо.
C-подобный:
struct CM_KEY_NODE ;//<--------- Ячейка раздела реестра
Size dd 0 ;// 0xFFFFFxxx = занята, 0x00000xxx = свободна
Signature db 'nk'
Flags dw 0
LastWriteTime dq 0
Spare dd 0
Parent dd 0 ;// линк на родителя (в данном случае не нужен)
SubKeyCount dd 0,0 ;// счётчик подпапок (второй дворд резерв)
SubKeyList dd 0,0 ;// линк на структуры 'li,lf,lh,ri'
ValueListCount dd 0 ;// счётчик данных
ValueList dd 0 ;// линк на структуру 'vk' = ключ:значение
Security dd 0
Class dd 0
MaxNameLen dw 0
UserFlags dw 0
MaxClassLen dd 0
MaxValueNameLen dd 0
MaxValueDataLen dd 0
WorkVar dd 0
NameLength dw 0 ;// длина имени
ClassLength dw 0
Name db 0 ;// строка с именем узла реестра
ends
3. Чтобы реализовать теорию программно, мы должны предусмотреть цикл с рекурсией, т.е. вызовом процедуры самой себя. Если убрать рекурсию, то придётся выделять отдельные процедуры для каждой из найденных папок - в случае с реестром такой подход исключён, ведь вложенных диров у куста может быть не много, а очень много. К примеру у меня в кусте HKLM\SOFTWARE\Classes уютно расположились 5336 подпапки, и заранее не предусмотрев рекурсию в коде, можно даже не пытаться обойти их все.
На схеме ниже представлена логика парсера где видно, что гипотетический корневой узел имеет счётчик(2), а потому его поле
SubKeyList будет указывать именно на 2 структуры KEY_NODE первого уровня, из которых первая не имеет детей (счётчик нуль), а вторая всего одного, ..причём с тремя внуками уже на уровне(3). Обратите внимание, что каждый уровень возвращает управление только своему родителю Parent, так-что адреса возврата при рекурсии можно хранить в стеке, ну или в локальных переменных процедуры.А вот собственно и исходник.
При запуске сразу появляется окно с предложением выбрать Regf-файл для анализа
GetOpenFileName(), далее вычисляется его размер GetFileSize(), и выделив память VirtualAlloc(), файл копируется в буфер. Если сигнатура валидна, то выводим на консоль паспорт куста из HBASE_BLOCK, после чего обходим всё древо первого уровня. Исходник прокомментирован, а если что не понятно - спрашивайте:
C-подобный:
format pe64 console 6.0
include 'win64ax.inc'
include 'equates\reghive.inc'
entry start
;//-------------------
section '.data' data readable writeable
hFile dd 0
fSize dd 0
RegfBase dq 0
OffsetBase dq 0x1000
counter dd 1
stm SYSTEMTIME
ofn OPENFILENAME
filter db ' hive, regf, dat',0 ;// что видно в окне GetOpenFileName()
db '*.hive;*.regf;*.dat;sam;system;security;software',0,0 ;// маска с типами файлов
;//-------- Использовалось для отладки -----------
;// fName db 'D:\Backup\reg save\SAM',0
;// fName db 'D:\Backup\reg save\SECURITY',0
;// fName db 'D:\Backup\reg save\SYSTEM',0
;//-----------------------------------------------
align 16
indent db 64 dup(0)
fName rb 256
buff rb 512
;//-------------------
section '.text' code readable executable
start: sub rsp,8
invoke SetConsoleTitle,<'*** Regf file parser v0.1 *** @Marylin codeby.net',0>
invoke GetModuleHandle,0
invoke LoadIcon,rax,101
push rax
invoke FindWindow,0,<'*** Regf file parser v0.1 *** @Marylin codeby.net',0>
pop rbx
invoke SendMessage,rax,WM_SETICON,ICON_BIG,rbx
;//---- Заполнить структуру OPENFILENAME
mov [ofn.lpstrFilter],filter
mov [ofn.lpstrFile], fName ;// сюда получим имя файла
mov [ofn.nMaxFile], 256
mov [ofn.Flags], OFN_EXPLORER
invoke GetOpenFileName,ofn
;//---- Прочитать выбранный файл в память
invoke _lopen,fName,0 ;// 0 = of_Read
mov [hFile],eax
invoke GetFileSize,eax,0
mov [fSize],eax
invoke VirtualAlloc,0,[fSize],MEM_COMMIT,PAGE_READWRITE
mov [RegfBase],rax
add [OffsetBase],rax
invoke _lread,[hFile],rax,[fSize]
invoke _lclose,[hFile]
;//---- Тест на валидность сигнатуры
mov rax,[RegfBase]
cmp dword[eax],'regf'
jz @f
invoke VirtualFree,[RegfBase],0,MEM_RELEASE
invoke MessageBox,0,<'Unsupported format! This is not a REGF file.'>,0,30h
invoke exit,0
;//---- Всё ОК! Выводим паспорт файла HIVE
@@: cinvoke printf,<10,' ',10 dup(0x13),' %s',0>,fName
mov eax,[fSize]
shr eax,10
cinvoke printf,<' | size: %d Kb ',10 dup(0x13),0>,eax
mov rsi,[RegfBase]
push rsi rsi rsi
mov eax,dword[rsi+HBASE_BLOCK.Major]
mov ebx,dword[rsi+HBASE_BLOCK.Minor]
push rbx
cinvoke printf,<10,10,' HIVE version..: %d.%d',0>,eax,ebx
pop rbx
cmp ebx,5
jb @f
cinvoke printf,<' --> Support for hashing text strings',0>
@@: mov eax,[fSize] ;// вычисляем кол-во контейнеров HBIN
shr eax,12 ;// разделить на 4096
dec eax
cinvoke printf,<10,' HBIN count....: %d',0>,eax
pop rsi
mov eax,dword[rsi+HBASE_BLOCK.Length]
shr eax,10
cinvoke printf,<10,' Data length...: %d Kb',0>,eax
pop rsi
mov eax,dword[rsi+HBASE_BLOCK.CheckSum]
cinvoke printf,<10,' Checksum......: 0x%08X',0>,eax
pop rsi
lea rax,qword[rsi+HBASE_BLOCK.TimeStamp]
invoke FileTimeToSystemTime,rax,stm
movzx eax,word[stm.wDay]
movzx ebx,word[stm.wMonth]
movzx esi,word[stm.wYear]
movzx edi,word[stm.wHour]
movzx ebp,word[stm.wMinute]
cinvoke printf,<10,' Last write....: %02d.%02d.%d %02d:%02d',10,0>,\
eax,ebx,esi,edi,ebp
mov rsi,[OffsetBase]
add rsi,0x20 ;// корневой узел!
cmp word[rsi+CM_KEY_NODE.Signature],'nk'
je @f
cinvoke printf,<10,' ERROR! RootKey not found - invalid HIVE file format.',0>
jmp @exit
@@: push rsi
mov eax,dword[rsi+CM_KEY_NODE.SubKeyCount]
mov ecx,dword[rsi+CM_KEY_NODE.NameLength]
lea rsi,[rsi+CM_KEY_NODE.Name]
mov rdi,buff
rep movsb
mov word[edi],0
cinvoke printf,<10,' Root Key......: %s',\
10,' Root SubKey...: %d',\
10,' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',\
10,' SubKey list: ~~~~~~~~~~~~~~~~',10,0>,buff,eax
pop rsi
mov esi,dword[rsi+CM_KEY_NODE.SubKeyList]
add rsi,[OffsetBase]
stdcall ParseIndexNode,rsi,0
@exit: invoke VirtualFree,[RegfBase],0,MEM_RELEASE
cinvoke _getch
cinvoke exit,0
;//*******************************************
align 8
;//---- Обработка индексного узла (lf,lh,li,ri)
proc ParseIndexNode uses rbx rsi rdi, IndexAddr,Depth
local count dw 0
mov [IndexAddr],rcx
mov [Depth],rdx
mov rsi,[IndexAddr]
;//--- Проверка сигнатуры
movzx eax,word[rsi+4]
cmp ax,'lf'
je @stdIndex
cmp ax,'lh'
je @stdIndex
cmp ax,'li'
je @stdIndex
cmp ax,'ri'
je @riIndex
jmp @inRet
;//--- Получаем кол-во записей
@stdIndex:
movzx ecx,word[rsi+CM_KEY_INDEX.Count]
or ecx,ecx
jz @inRet
;//--- Перебираем все записи в цикле
add rsi,8 ;// пропускаем: Size + Sign + Count
@loop_entries:
push rcx rsi
;// Читаем смещение на дочерний NK (первые 4 байта записи)
mov eax,dword[rsi]
or eax,eax
jz @next
add rax,[OffsetBase] ;// Получаем адрес дочернего NK
stdcall ParseKeyNode,rax,[Depth] ;// Обрабатываем дочерний NK (рекурсивно)
@next: pop rsi rcx
;// Переход к следующей записи
cmp word[rsi-8+4],'lf' ;// смотрим сигнатуру из заголовка
je @lf_next
add rsi,8 ;// для 'lh' размер записи 8 байт
jmp @loop_check
@lf_next: add rsi, 12 ;// для 'lf' размер записи 12 байт
@loop_check:
loop @loop_entries
jmp @inRet
@riIndex:
;//--- 'RI' обрабатывается аналогично, но ведёт на другие индексы
movzx ecx,word[rsi+CM_KEY_INDEX.Count]
or ecx,ecx
jz @inRet
add rsi,8
@loop_ri: push rcx rsi
mov eax,dword[rsi]
or eax,eax
jz @next_ri
add rax,[OffsetBase]
stdcall ParseIndexNode,rax,[Depth] ;// рекурсия на индекс!
@next_ri: pop rsi rcx
add rsi,4 ;// 'ri' записи по 4 байта
loop @loop_ri
@inRet: ret
endp
;//--------------------
align 8
proc ParseKeyNode uses rbx rsi rdi, NodeAddr,Depth
mov [NodeAddr],rcx
mov [Depth],rdx
locals
child_count dd 0
subkey_list dd 0
endl
;//--- Тест сигнатуры
mov rsi,[NodeAddr]
cmp word[rsi+CM_KEY_NODE.Signature],'nk'
jne @nkRet
;//--- Формируем отступ (табуляции) по глубине
mov rcx,[Depth]
or ecx, ecx
jz @print_name
mov rdi,indent
mov al,9 ;// TAB
rep stosb
mov byte[rdi],0
;//--- Читаем имя ключа
@print_name:
movzx ecx,word[rsi+CM_KEY_NODE.NameLength]
or ecx,ecx
jz @no_name
push rsi
mov eax,dword[rsi+CM_KEY_NODE.SubKeyCount]
mov ebx,dword[rsi+CM_KEY_NODE.ValueListCount]
lea rsi,[rsi+CM_KEY_NODE.Name]
mov rdi,buff
rep movsb
mov word[rdi],0
pop rsi
cinvoke printf,<18 dup(' '),'%-20s SubKeys: %-4d Values: %d',10>,buff,eax,ebx
jmp @process_children
@no_name:
cinvoke printf,<' %s[Unnamed]',10>,indent
;//--- Получаем кол-во и список подразделов
@process_children:
mov eax,dword[rsi+CM_KEY_NODE.SubKeyCount]
mov [child_count],eax
or eax,eax
jz @nkRet
;//--- Переходим к обработке индексного узла
add rax,[OffsetBase]
mov rdx,[Depth]
inc edx ;// увеличиваем глубину для детей
stdcall ParseIndexNode,rax,edx
@nkRet: ret
endp
;//------------------
section '.idata' import data readable writeable
library kernel32,'kernel32.DLL', user32,'user32.DLL',\
msvcrt,'msvcrt.dll', comdlg32,'comdlg32.dll'
include 'api\kernel32.inc'
include 'api\user32.inc'
include 'api\msvcrt.inc'
include 'api\comdlg32.inc'
;//-----------------
section '.rsrc' data resource readable
directory RT_GROUP_ICON, icons,\
RT_ICON, my_icon
resource icons, 101, LANG_NEUTRAL,groups
resource my_icon,102, LANG_NEUTRAL,myicon
icon groups, myicon, 'reg.ico'
Как результат получим такой лог где видно, что внутреннее имя куста вовсе не SOFТWARE, а CMI-CreateHive{GUID}. Это имя назначает кусту менеджер конфигурации при загрузке его с диска в память, и оно осталось в файле как артефакт. Кроме имени, в двух следующих столбцах имеем счётчик вложенных папок, и наличие у папки параметров, которые отображаются в правом окне Regedit. Обратите внимание на кол-во детей у куста "Classes", о чём я упоминал выше:
5. Заключение
Для криминалистики и поиска артефактов в реестре, важно использовать только файлы REGF на диске, поскольку в них остаются удалённые из реестра ключи, которые могут отсутствовать в памяти. Статья писалась с расчётом на то, чтобы заманить читателя в эту область, где можно найти много конфиденциальной инфы, например, из файла SAM. Это отдельная тема для разговора, и в последней части мы ещё вернёмся к ней, вытащив все пароли и явки из тёмных переулов файла на белый свет.
Темой для следующего разговора будет способ хранения кустов реестра в памяти работающей системы, а пока ставлю здесь точку. В скрепке найдёте инклуд с описанием всех структур менеджера конфигов, а так-же исходник с исполняемым файлом для тестов. Всем удачи, до скорого!