Статья Анатомия реестра Windows[1] - формат файлов REGF на диске

Для большинства из нас реестр - это окно "Regedit" с древовидной структурой папок, и огромным кол-вом пар "ключ/значение". Однако на низком уровне это бинарные файлы в формате regf, которые хранятся на жёстком диске, чтобы была возможность запомнить текущие настройки системы при выключении машины. В данном цикле из трёх частей мы положим реестр на анатомический стол, и под микроскопом рассмотрим все его детали. Это увлекательное занятие раскроет двери в криминалистику так, что на поверхность всплывут ошибки, и как следствие - потенциальные угрозы. Чтобы из информации не получился винегрет, я разделил её по смыслу на следующие блоки:

Часть(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:

RegeditHive.webp

В зависимости от того, о каком уровне идёт речь, в качестве узла Hive может выступать и куст во вложенной папке корневого узла (в общем полная анархия и неразбериха в терминологии). Если при экспорте правой клавишей мыши в окне Regedit выбрать тип "Файлы кустов..", то получим бинарь с сигнатурой regf, который позже можно будет исследовать в любом HEX-редакторе, например "HxD".

RegSaveAs.webp


Rng.webp


2. Бэкап файлов SAM, System, Security

Прежде чем перейти к разбору формата REGF, нам нужен объект исследования, а потому рассмотрим несколько вариантов создания копий наиболее интересных кустов, к которым можно отнести все папки из раздела HKLM. Только проблема в доступе к ним - он полностью заблокирован не только смертному юзеру, но и самому админу. Это потому, что разделы создаются процессами служб в закрытой сессии(0), и соответственно прочитать их можно лишь с правами пользователя SYSTEM. Что мы можем сделать, оказавшись в такой ловушке?

1. Скопировать кусты реестра без запуска Win, загрузившись с флэшки или LiveCD.
Вариант безотказный и никогда не даёт осечки. Вот список путей, где хранятся основные файлы реестра. Обратите внимание, что куст HARDWARE создаётся при загрузки ОС динамически, так-что его бесполезно искать на диске. Причина банальна - между двумя вкл.машины, в систему могут быть добавлены новые физические устройства, а потому конфигуратору проще обновить весь список целиком, чем решать проблемы кривых настроек.

hVer.webp

2. Запустив cmd.exe от админа, вскормить интерпретатору команду reg save HKLM\SAM d:\backup\sam.
Под катом она транзитом передаёт запрос службе в сессии(0), а потому при правильном подходе способ обычно срабатывает.

regSave.webp

3. Есть ещё один довольно хитрый хак. Понятно, что для всех операций с реестром нужны права админа, однако кусты Hive являются объектами ядра, и как следствие доступ к ним ограничевает запись ACL (access control list) в токене текущего пользователя. Что примечательно, всего одной галкой мы можем сменить разрешения на доступ к объекту в его свойствах, как это представлено на рис.ниже. Например в дефолте содержимое куста SAM не отображается в правом окне Regedit, но если открыть полный доступ админу (а по факту назначить права SYSTEM конкретно для данного объекта), инфа воскреснет как феникс из пепла, и можно будет спокойно отправить весь куст на экспорт правой клавишей мыши.

SAM.webp

Что-то подобное делает и утилита М.Руссиновича 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.

WPR.webp

5. И наконец если у вас Win установлена на вирт.машине, можете просто открыть RAR'ом, 7-ZIP, или UltraISO файл образа жёсткого диска (*.vdi для VBox). На выходе получите все разделы харда, и отдельные его файлы, которые будут доступны для копирования без каких-либо ограничений. Так это выглядит в 7-ZIP:

vdi.webp


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

Реальный дамп - попробуйте наложить на него эту структуру
sam_regf.webp


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' в файле
sam_hbin.webp

Но и это ещё не всё.. Чтобы "приукрасить" унылую жизнь прогеров, разработчики пошли ещё дальше. Так, для оптимизации поиска вложенных в корень кустов (а их может быть просто огромное кол-во в реестре), к основной структуре 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)

Таким образом, семейный портрет всех действующих лиц будет выглядеть примерно так..

HIVE.webp

На схеме ниже показано, как связываются структуры файла 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 (привет фрагментация реестра), а относительные указатели помогают собрать их в единое целое.

Struct.webp


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, так-что адреса возврата при рекурсии можно хранить в стеке, ну или в локальных переменных процедуры.

Parse.webp

А вот собственно и исходник.
При запуске сразу появляется окно с предложением выбрать 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 yg140.servegame.com',0>
          invoke  GetModuleHandle,0
          invoke  LoadIcon,rax,101
          push    rax
          invoke  FindWindow,0,<'*** Regf file parser v0.1 ***  @Marylin yg140.servegame.com',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", о чём я упоминал выше:

Result.webp


5. Заключение

Для криминалистики и поиска артефактов в реестре, важно использовать только файлы REGF на диске, поскольку в них остаются удалённые из реестра ключи, которые могут отсутствовать в памяти. Статья писалась с расчётом на то, чтобы заманить читателя в эту область, где можно найти много конфиденциальной инфы, например, из файла SAM. Это отдельная тема для разговора, и в последней части мы ещё вернёмся к ней, вытащив все пароли и явки из тёмных переулов файла на белый свет.

Темой для следующего разговора будет способ хранения кустов реестра в памяти работающей системы, а пока ставлю здесь точку. В скрепке найдёте инклуд с описанием всех структур менеджера конфигов, а так-же исходник с исполняемым файлом для тестов. Всем удачи, до скорого!
 

Вложения

Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab