D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Автор soomerk
Статья написана для Конкурса статей #10
Всем доброго времени дня и ночи. Сегодня мы будем взаимодействовать с реестром без использования API, и немного поиздеваемся над ним. Сразу говорю, что это моя первая статья, и прошу судить не строго.
Что из себя представляет реестр ОС Windows?
По сути, это иерархическая база различных данных. На диске хранится в виде файлов кустов(к примеру, C:\Windows\System32\config\SOFTWARE). В рантайме иногда хранится в пространстве ядра, но чаще всего хранится в адресном пространстве процесса "Registry", поэтому при работе с реестром придётся аттачиться к процессу. Начало списка кустов реестра хранится в CmpHiveListHead.
Давайте разберемся, что там "под капотом" в файле.
Состоит regf файл из блоков - так называемых bin, представленных структурой HBIN. Их размер чаще всего равен 0x1000, но вообще может иметь любой размер, выровненный по размеру страницы.
C: Скопировать в буфер обмена
И на этом моменте запомните одну очень важную вещь - все поля по типу FileOffset, хранят в себе значение не от начала файла, а от первого блока - т.е. от 0x1000. Хранятся данные в "бинах" в виде наименьшей стандартной единице выделенной памяти - Cell'ов. Первые 4 байта содержат отрицательный размер выделенного места. Если размер изначально >= 0(т.е., после операции смены знака будет меньше 0), то "целл" является свободным(В дальнейшем под словом "целл" будет подразумеваться оффсет файла, содержащий само "тело" целла). После этих 4х байт идут уже основные данные - CELL_DATA. Их мы рассмотрим ниже. Целлы внутри бинов, как вы могли догадаться, расположены подряд.
Начинается regf файл с заголовка - так называемый BASE_BLOCK.
C: Скопировать в буфер обмена
Далее, начиная с оффсета 0x1000, начинается первый bin.
Глянем что там в рантайме!
Вся работа с реестром происходит в ntoskrnl, в функциях Cm и Hv. В роли объекта ключа реестра выступает структура CM_KEY_BODY с типом объекта CmKeyObjectType.
C: Скопировать в буфер обмена
Самое "важное" поле в данной структуре - KeyControlBlock. В ней находятся все необходимые нам данные.
C: Скопировать в буфер обмена
Такс.. KeyCell - это оффсет сырых данных нашего ключа. Вероятно, вы могли подумать, что для получения виртуального адреса, нам необходимо прибавить данное значения к базовому адресу файл маппинга. Но если мы откроем любую программу для просмотра регионов памяти, возникнут проблемы: с высокой долей вероятности, мы обнаружим что файл мапнут "по кускам"(если у вас не так - перезапустите ОС пару раз).
Ответ лежит в структуре куста реестра - HHIVE.
C: Скопировать в буфер обмена
Обратите внимание на названия последних двух полей. И они действительно играют важную роль в нашей текущей цели. Во ViewMap хранится адрес EPROCESS процесса реестра, в Storage находятся указатель на список свободных бинов, и самое главное, таблица "трансляции" целлов(DUAL::Map). К сведению, сама винда получает адрес целла с помощью функции HvpGetCellPaged(адрес которой вы можете заметить при дебаге в HHIVE, GetCellRoutine).
Принцип похож на таблицы трансляции адресов, за исключением того, что помимо адреса бина, мы получаем еще и отдельное смещение блока, и самый старший бит хранит в себе "тип" целла: Permanent(0) или Volatile(1). Следующие 10 бит хранят в себе индекс для первого уровня таблицы, еще 9 - второго уровня, и последние 12 бит - смещение.
CELL_DATA - сами данные, содержающиеся в целле. Идёт после первых четырех байт. В .pdb файлах представлена союзом структур. Начинаются они все с сигнатуры:
Спойлер:
_Сигнатуры:
CM_KEY_VALUE_SIG = "vk"
CM_KEY_NODE_SIG = "nk"
CM_FAST_LEAF_SIG = "lf"
CM_INDEX_LEAF_SIG = "li"
CM_HASH_LEAF_SIG = "lh"
CM_LINK_NODE_SIG = "lk"
CM_INDEX_ROOT_SIG = "ri"
CM_SECURITY_KEY_SIG = "sk"
CM_BIG_DATA_SIG = "db"
Половина из них идут под одну гребёнку - CM_INDEX/CM_KEY_INDEX.
Вернёмся еще раньше - к структуре CM_KEY_CONTROL_BLOCK. Для перечисления значений ключа мы уже на данном этапе можем использовать поле ValueList. В нём хранится количество значений, и целл, который хранит в себе список целлов значений. Давайте двигаться дальше. Получив адрес целла из KeyCell в KCB, мы попадём в структуру CM_KEY_NODE, играющую важнейшую роль.
C: Скопировать в буфер обмена
Тут содержится вся информация о ключе реестра. И обратите внимание на два поля - SubKeyLists и, прежне знакомый, ValueList. SubKeyLists, как вы могли понять из выше полученной информации, содержит в себе целл, который содержит в себе список целлов дочерних ключей. Способен собою этот список представлять 4 структуры: ri, li, lh и lf.
Начинаются они все с двух полей - ожидаемо, сигнатуры(рассмотрены выше) и количества элементов. Далее идёт массив вариативного размера, где и начинаются различия:
li - целлы с nk, непосредственно содержащие структуру дочернего ключа CM_KEY_NODE
ri - целлы, содержащие в себе целлы li
lh - принцип как у li. Помимо целла, в элементе массива содержится хеш имени ключа.
lf - принцип как у lh. Помимо целла, в элементе массива содержатся первые 4 символа имени ключа(т.е. вместо хеша).
C: Скопировать в буфер обмена
Но обратите внимание: SubKeyLists - это массив, из двух элементов. Тут всё просто - это разделение, как и ранее, на Permanent и Volatile ключи.
Что там с значениями ключей реестра? Целл из ValueList, сошлёт нас к сигнатуре "vk", соответствующей структуре CM_KEY_VALUE.
C: Скопировать в буфер обмена
Type мы можем глянуть в доке майкрософт, в разделе "Типы значений реестра". Data - целл, содержащий сырое значение... значения.
На данном этапе предлагаю притормозить. С базовой теорией - всё.
Практика.
Открываем WinDBG, виртуалку, и погнали!
Для начала предлагаю прописать dp nt!CmpHiveListHead для получения первого куста реестра. Получаем адрес ffffa08e7da4f640(указывает на CMHIVE.HiveList). Так как CMHIVE/HHIVE всегда выравнены по размеру страницы, обнулив младшие три байта пропишем: dt nt!CMHIVE ffffa08e7da4f000 -l HiveList.Flink. Перед вами все кусты реестра!
Для примера возьму куст SYSTEM, в связи с тем, что он находится в пространстве ядра(для простоты). Для примера, я взаранее создам значение в корневом ключе. Чтобы получить корневой ключ, читаем поле RootKcb, либо RootCell из HBASE_BLOCK и получаем наш целл(не забывайте про 4 байта размера, мы их пропускаем):
JavaScript: Скопировать в буфер обмена
Целл: 0x20
Куст: 0xffffa08e7da62000
Адрес целла: 0xffffa08e7f291020
Пропустив размер целла(4 байта): 0xffffa08e7f291024
Прописав команду
видим "nk" сигнатуру: смотрим выше - это структура CM_KEY_NODE.
Давайте глянем что нам родит
Смотрите на Name, - это на самом деле анси строка. Подглядев в db увидим имя корня - ROOT(стандартное имя корня куста реестра):
Давайте перечислим значения и дочерние ключи. Для этого, получаем целл списка значений и целл списка подключей(помним про Permanent и Volatile, рассмотрим лишь первое). Для начала значения:
Count == 1, в целле списка хранится массив целлов vk, перейдя по которым мы получим структуру CM_KEY_VALUE:
Сигнатура - корректна, значит всё ок. Имя ключа - test(смотреть на db), давайте получим данные, хранящиеся в Data. Type == 1 соответствует REG_SZ.
Записанные мною через regedit данные, мы видим перед своими глазами.
А теперь давайте... Перезапишем их.
Более чем получилось.
Немного экспериментов.
Перезапишем целл Data на 0xFFFFFFFF. Ключ реестра - пропал из регедита. Если попытаемся провернуть те же финты с одним из целлов в ValueList(именно в массиве) - получаем Critical Error в дебагере(увы, без экрана счастья). Хорошо, а если попробовать создать несколько значений реестра, и после перезаписи одного из целлов значения в 0xFFFFFFFF, сделать декремент количества значений?
Создам три ключа:
Кстати, не забывайте про ValueList в KCB. Насчет идеи выше - БСОД. Еще занятно, что реестр самовосстанавливается(однако когда тестировал на основной машине он какого-то хрена решил не восстанавливаться, так что не рискуйте...). А что если сделать "удаление" из массива целлов значений?
В данном случае, я тупо "вытолкнул" два ключа реестра вместо перезаписи в 0xFFFFFFFF. При этом, целлы значений не деаллоцируются, т.е. в дальнейшем проделав обратные операции(вернём целлы в массив), всё заработает.
Проделаем то же самое и с дочерними ключами. С ними будет чуть полегче - не надо делать двойную работу с KCB.
Посмотрим на целлы со списками подключей(первое - Permanent, второе - Volatile):
Это lh. Помните структуру _CM_KEY_FAST_INDEX?
Так как это массив вариативного размера, легче будет без структурки.
Перейдём по первому целлу в списке(зелёненький), и увидим следующее.
Что за зверьё такое, sk этот ваш?
C: Скопировать в буфер обмена
Попробуем получить второй ключ.
Получилось. Кстати, как вы могли заметить, ключи в списке отсортированы в алфавитном порядке, что немаловажно.
И чуть не забыл - получение KCB. Сделать это можно через _CMHIVE::KcbCacheTable, представляющей собой хеш-таблицу.
Насчёт хеш-функции. В целом, есть две функции CmpHash*, для двух видов строк соответственно. Их реверс проблем не составит.
Давайте подведём некоторые полученные из проведённых опытов знания:
Мы можем скрыть значение ключа перезаписав Data в 0xFFFFFFFF.
Для частичного удаления значения или дочернего ключа с сохранением выделенного под них места и их содержимого - убираем их из ValueList или SubKeyLists.
Пишем
А теперь давайте думать. Я предлагаю сделать следующее:
1. Каким-либо способом хукнуть запись файла(Получить хендл файла можно из _CMHIVE::FileHandles[0], прошу что-то менее банальное чем NTFS MajorFunctions)
2. Изменить реестр выше описанными методами для сокрытия "неприятных" ключей или значений.
3. При попытке записи на диск, будем проходиться по нашим ключам/значениям реестра, проверять, попадают ли текущие целлы(вы ведь помните, что это по сути просто смещение от 0x1000 после начала файла) на диск при данной операции записи основываясь на FilePointer, и возвращать данные в исходное состояние. После записи(условно CompletionRoutine) - снова удаляем ключи и значения из памяти.
Если вы ленивы(как и я), предлагаю получать родительский ключ некоторыми средствами винды в лице ObReferenceObject*(в данном случае - ByName). Также прошу не бросать помидоры за стиль кода. Попробуем скрыть "TestKey" в корне.
Для начала получим _CM_KEY_NODE и подключимся к процессу реестра:
C: Скопировать в буфер обмена
Давайте найдём наш ключ. Я не буду использовать способы быстрого нахождения, я даже не буду сейчас отсеивать различные варианты списка(lh, lf, ri, li). Просто пройдусь по массиву:
C: Скопировать в буфер обмена
Мы успешно удалили ключ. Не забудьте деаттачнуться от процесса. Также советую поэкспериментировать с kcb. Перед всеми манипуляциями с реестром необходимо сбросить WP бит в CR0, данное действие выполняет и сама винда.
Теперь давайте попробуем написать тестовый код для восстановления реестра при записи на диск(на данном этапе можно не париться с kcb и прочим, так как по сути на диск оно пишется как тупо файл маппинг, IRP_PAGING_IO).
Опять же, способ хука остаётся за вами. Так как работаем с определенным файлом куста на диске, можете попробовать потанцевать с FILE_OBJECT, может что и получиться.
За "внешние" данные в коде будет взята информация об операции записи в файл: размер(write_size) и байт-оффсет(byte_offset). Не буду повторять код на получение _CM_KEY_NODE. Необходимо чтобы программа помнила про целлы, которые удаляла. Также стоит помнить, что все ключи идут в алфавитном порядке. Для определения этого порядка буду использовать strcmp(что не очень хорошо).
C: Скопировать в буфер обмена
Те же действия можно проделать и со значениями.
Стоит понимать, что при проделывании всех выше сказанных операций, останутся слепы любые средства защиты, а самое главное - сам юзер.
В целом, на данном этапе можно закончить статью. Можно ещё со многим поэкспериментировать - это я оставляю на вас. Благодарю за прочтение!
Статья написана для Конкурса статей #10
Всем доброго времени дня и ночи. Сегодня мы будем взаимодействовать с реестром без использования API, и немного поиздеваемся над ним. Сразу говорю, что это моя первая статья, и прошу судить не строго.
Что из себя представляет реестр ОС Windows?
По сути, это иерархическая база различных данных. На диске хранится в виде файлов кустов(к примеру, C:\Windows\System32\config\SOFTWARE). В рантайме иногда хранится в пространстве ядра, но чаще всего хранится в адресном пространстве процесса "Registry", поэтому при работе с реестром придётся аттачиться к процессу. Начало списка кустов реестра хранится в CmpHiveListHead.
Давайте разберемся, что там "под капотом" в файле.
Состоит regf файл из блоков - так называемых bin, представленных структурой HBIN. Их размер чаще всего равен 0x1000, но вообще может иметь любой размер, выровненный по размеру страницы.
C: Скопировать в буфер обмена
Код:
//0x20 bytes (sizeof)
struct _HBIN
{
ULONG Signature; //0x0
ULONG FileOffset; //0x4
ULONG Size; //0x8
ULONG Reserved1[2]; //0xc
union _LARGE_INTEGER TimeStamp; //0x14
ULONG Spare; //0x1c
};
И на этом моменте запомните одну очень важную вещь - все поля по типу FileOffset, хранят в себе значение не от начала файла, а от первого блока - т.е. от 0x1000. Хранятся данные в "бинах" в виде наименьшей стандартной единице выделенной памяти - Cell'ов. Первые 4 байта содержат отрицательный размер выделенного места. Если размер изначально >= 0(т.е., после операции смены знака будет меньше 0), то "целл" является свободным(В дальнейшем под словом "целл" будет подразумеваться оффсет файла, содержащий само "тело" целла). После этих 4х байт идут уже основные данные - CELL_DATA. Их мы рассмотрим ниже. Целлы внутри бинов, как вы могли догадаться, расположены подряд.
Начинается regf файл с заголовка - так называемый BASE_BLOCK.
C: Скопировать в буфер обмена
Код:
//0x1000 bytes (sizeof)
struct _HBASE_BLOCK
{
ULONG Signature; //0x0
ULONG Sequence1; //0x4
ULONG Sequence2; //0x8
union _LARGE_INTEGER TimeStamp; //0xc
ULONG Major; //0x14
ULONG Minor; //0x18
ULONG Type; //0x1c
ULONG Format; //0x20
ULONG RootCell; //0x24
ULONG Length; //0x28
ULONG Cluster; //0x2c
UCHAR FileName[64]; //0x30
struct _GUID RmId; //0x70
struct _GUID LogId; //0x80
ULONG Flags; //0x90
struct _GUID TmId; //0x94
ULONG GuidSignature; //0xa4
ULONGLONG LastReorganizeTime; //0xa8
ULONG Reserved1[83]; //0xb0
ULONG CheckSum; //0x1fc
ULONG Reserved2[882]; //0x200
struct _GUID ThawTmId; //0xfc8
struct _GUID ThawRmId; //0xfd8
struct _GUID ThawLogId; //0xfe8
ULONG BootType; //0xff8
ULONG BootRecover; //0xffc
};
Далее, начиная с оффсета 0x1000, начинается первый bin.
Глянем что там в рантайме!
Вся работа с реестром происходит в ntoskrnl, в функциях Cm и Hv. В роли объекта ключа реестра выступает структура CM_KEY_BODY с типом объекта CmKeyObjectType.
C: Скопировать в буфер обмена
Код:
//0x68 bytes (sizeof)
struct _CM_KEY_BODY
{
ULONG Type; //0x0
struct _CM_KEY_CONTROL_BLOCK* KeyControlBlock; //0x8
struct _CM_NOTIFY_BLOCK* NotifyBlock; //0x10
VOID* ProcessID; //0x18
struct _LIST_ENTRY KeyBodyList; //0x20
ULONG Flags:16; //0x30
ULONG HandleTags:16; //0x30
union _CM_TRANS_PTR Trans; //0x38
struct _GUID* KtmUow; //0x40
struct _LIST_ENTRY ContextListHead; //0x48
VOID* EnumerationResumeContext; //0x58
ULONG RestrictedAccessMask; //0x60
};
Самое "важное" поле в данной структуре - KeyControlBlock. В ней находятся все необходимые нам данные.
C: Скопировать в буфер обмена
Код:
//0x138 bytes (sizeof)
//Windows 11 23H2
struct _CM_KEY_CONTROL_BLOCK
{
ULONGLONG RefCount; //0x0
ULONG ExtFlags:16; //0x8
ULONG Freed:1; //0x8
ULONG Discarded:1; //0x8
ULONG HiveUnloaded:1; //0x8
ULONG Decommissioned:1; //0x8
ULONG SpareExtFlag:1; //0x8
ULONG TotalLevels:10; //0x8
union
{
struct _CM_KEY_HASH KeyHash; //0x10
struct
{
struct _CM_PATH_HASH ConvKey; //0x10
struct _CM_KEY_HASH* NextHash; //0x18
struct _HHIVE* KeyHive; //0x20
ULONG KeyCell; //0x28
};
};
struct _EX_PUSH_LOCK KcbPushlock; //0x30
union
{
struct _KTHREAD* Owner; //0x38
LONG SharedCount; //0x38
};
UCHAR DelayedDeref:1; //0x40
UCHAR DelayedClose:1; //0x40
UCHAR Parking:1; //0x40
UCHAR LayerSemantics; //0x41
SHORT LayerHeight; //0x42
ULONG Spare1; //0x44
struct _CM_KEY_CONTROL_BLOCK* ParentKcb; //0x48
struct _CM_NAME_CONTROL_BLOCK* NameBlock; //0x50
struct _CM_KEY_SECURITY_CACHE* CachedSecurity; //0x58
struct _CHILD_LIST ValueList; //0x60
struct _CM_KEY_CONTROL_BLOCK* LinkTarget; //0x68
union
{
struct _CM_INDEX_HINT_BLOCK* IndexHint; //0x70
ULONG HashKey; //0x70
ULONG SubKeyCount; //0x70
};
union
{
struct _LIST_ENTRY KeyBodyListHead; //0x78
struct _LIST_ENTRY ClonedListEntry; //0x78
};
struct _CM_KEY_BODY* KeyBodyArray[4]; //0x88
union _LARGE_INTEGER KcbLastWriteTime; //0xa8
USHORT KcbMaxNameLen; //0xb0
USHORT KcbMaxValueNameLen; //0xb2
ULONG KcbMaxValueDataLen; //0xb4
ULONG KcbUserFlags:4; //0xb8
ULONG KcbVirtControlFlags:4; //0xb8
ULONG KcbDebug:8; //0xb8
ULONG Flags:16; //0xb8
ULONG Spare3; //0xbc
struct _CM_KCB_LAYER_INFO* LayerInfo; //0xc0
CHAR* RealKeyName; //0xc8
struct _LIST_ENTRY KCBUoWListHead; //0xd0
union
{
struct _LIST_ENTRY DelayQueueEntry; //0xe0
volatile UCHAR* Stolen; //0xe0
};
struct _CM_TRANS* TransKCBOwner; //0xf0
struct _CM_INTENT_LOCK KCBLock; //0xf8
struct _CM_INTENT_LOCK KeyLock; //0x108
struct _CHILD_LIST TransValueCache; //0x118
struct _CM_TRANS* TransValueListOwner; //0x120
union
{
struct _UNICODE_STRING* FullKCBName; //0x128
struct
{
ULONGLONG FullKCBNameStale:1; //0x128
ULONGLONG Reserved:63; //0x128
};
};
ULONGLONG SequenceNumber; //0x130
};
Такс.. KeyCell - это оффсет сырых данных нашего ключа. Вероятно, вы могли подумать, что для получения виртуального адреса, нам необходимо прибавить данное значения к базовому адресу файл маппинга. Но если мы откроем любую программу для просмотра регионов памяти, возникнут проблемы: с высокой долей вероятности, мы обнаружим что файл мапнут "по кускам"(если у вас не так - перезапустите ОС пару раз).
Ответ лежит в структуре куста реестра - HHIVE.
C: Скопировать в буфер обмена
Код:
//Windows 11 23H2
//0x600 bytes (sizeof)
struct _HHIVE
{
ULONG Signature; //0x0
struct _CELL_DATA* (*GetCellRoutine)(struct _HHIVE* arg1, ULONG arg2, struct _HV_GET_CELL_CONTEXT* arg3); //0x8
VOID (*ReleaseCellRoutine)(struct _HHIVE* arg1, struct _HV_GET_CELL_CONTEXT* arg2); //0x10
VOID* (*Allocate)(ULONG arg1, UCHAR arg2, ULONG arg3); //0x18
VOID (*Free)(VOID* arg1, ULONG arg2); //0x20
LONG (*FileWrite)(struct _HHIVE* arg1, ULONG arg2, struct CMP_OFFSET_ARRAY* arg3, ULONG arg4, ULONG arg5); //0x28
LONG (*FileRead)(struct _HHIVE* arg1, ULONG arg2, ULONG arg3, VOID* arg4, ULONG arg5); //0x30
VOID* HiveLoadFailure; //0x38
struct _HBASE_BLOCK* BaseBlock; //0x40
struct _CMSI_RW_LOCK FlusherLock; //0x48
struct _CMSI_RW_LOCK WriterLock; //0x50
struct _RTL_BITMAP DirtyVector; //0x58
ULONG DirtyCount; //0x68
ULONG DirtyAlloc; //0x6c
struct _RTL_BITMAP UnreconciledVector; //0x70
ULONG UnreconciledCount; //0x80
ULONG BaseBlockAlloc; //0x84
ULONG Cluster; //0x88
UCHAR Flat:1; //0x8c
UCHAR ReadOnly:1; //0x8c
UCHAR Reserved:6; //0x8c
UCHAR DirtyFlag; //0x8d
ULONG HvBinHeadersUse; //0x90
ULONG HvFreeCellsUse; //0x94
ULONG HvUsedCellsUse; //0x98
ULONG CmUsedCellsUse; //0x9c
ULONG HiveFlags; //0xa0
ULONG CurrentLog; //0xa4
ULONG CurrentLogSequence; //0xa8
ULONG CurrentLogMinimumSequence; //0xac
ULONG CurrentLogOffset; //0xb0
ULONG MinimumLogSequence; //0xb4
ULONG LogFileSizeCap; //0xb8
UCHAR LogDataPresent[2]; //0xbc
UCHAR PrimaryFileValid; //0xbe
UCHAR BaseBlockDirty; //0xbf
union _LARGE_INTEGER LastLogSwapTime; //0xc0
union
{
struct
{
USHORT FirstLogFile:3; //0xc8
USHORT SecondLogFile:3; //0xc8
USHORT HeaderRecovered:1; //0xc8
USHORT LegacyRecoveryIndicated:1; //0xc8
USHORT RecoveryInformationReserved:8; //0xc8
};
USHORT RecoveryInformation; //0xc8
};
UCHAR LogEntriesRecovered[2]; //0xca
ULONG RefreshCount; //0xcc
ULONG StorageTypeCount; //0xd0
ULONG Version; //0xd4
struct _HVP_VIEW_MAP ViewMap; //0xd8
struct _DUAL Storage[2]; //0x110
};
Обратите внимание на названия последних двух полей. И они действительно играют важную роль в нашей текущей цели. Во ViewMap хранится адрес EPROCESS процесса реестра, в Storage находятся указатель на список свободных бинов, и самое главное, таблица "трансляции" целлов(DUAL::Map). К сведению, сама винда получает адрес целла с помощью функции HvpGetCellPaged(адрес которой вы можете заметить при дебаге в HHIVE, GetCellRoutine).
Принцип похож на таблицы трансляции адресов, за исключением того, что помимо адреса бина, мы получаем еще и отдельное смещение блока, и самый старший бит хранит в себе "тип" целла: Permanent(0) или Volatile(1). Следующие 10 бит хранят в себе индекс для первого уровня таблицы, еще 9 - второго уровня, и последние 12 бит - смещение.
CELL_DATA - сами данные, содержающиеся в целле. Идёт после первых четырех байт. В .pdb файлах представлена союзом структур. Начинаются они все с сигнатуры:
Спойлер:
_Сигнатуры:
CM_KEY_VALUE_SIG = "vk"
CM_KEY_NODE_SIG = "nk"
CM_FAST_LEAF_SIG = "lf"
CM_INDEX_LEAF_SIG = "li"
CM_HASH_LEAF_SIG = "lh"
CM_LINK_NODE_SIG = "lk"
CM_INDEX_ROOT_SIG = "ri"
CM_SECURITY_KEY_SIG = "sk"
CM_BIG_DATA_SIG = "db"
Половина из них идут под одну гребёнку - CM_INDEX/CM_KEY_INDEX.
Вернёмся еще раньше - к структуре CM_KEY_CONTROL_BLOCK. Для перечисления значений ключа мы уже на данном этапе можем использовать поле ValueList. В нём хранится количество значений, и целл, который хранит в себе список целлов значений. Давайте двигаться дальше. Получив адрес целла из KeyCell в KCB, мы попадём в структуру CM_KEY_NODE, играющую важнейшую роль.
C: Скопировать в буфер обмена
Код:
//0x50 bytes (sizeof)
struct _CM_KEY_NODE
{
USHORT Signature; //0x0
USHORT Flags; //0x2
union _LARGE_INTEGER LastWriteTime; //0x4
UCHAR AccessBits; //0xc
UCHAR LayerSemantics:2; //0xd
UCHAR Spare1:5; //0xd
UCHAR InheritClass:1; //0xd
USHORT Spare2; //0xe
ULONG Parent; //0x10
ULONG SubKeyCounts[2]; //0x14
union
{
struct
{
ULONG SubKeyLists[2]; //0x1c
struct _CHILD_LIST ValueList; //0x24
};
struct _CM_KEY_REFERENCE ChildHiveReference; //0x1c
};
ULONG Security; //0x2c
ULONG Class; //0x30
ULONG MaxNameLen:16; //0x34
ULONG UserFlags:4; //0x34
ULONG VirtControlFlags:4; //0x34
ULONG Debug:8; //0x34
ULONG MaxClassLen; //0x38
ULONG MaxValueNameLen; //0x3c
ULONG MaxValueDataLen; //0x40
ULONG WorkVar; //0x44
USHORT NameLength; //0x48
USHORT ClassLength; //0x4a
WCHAR Name[1]; //0x4c
};
Тут содержится вся информация о ключе реестра. И обратите внимание на два поля - SubKeyLists и, прежне знакомый, ValueList. SubKeyLists, как вы могли понять из выше полученной информации, содержит в себе целл, который содержит в себе список целлов дочерних ключей. Способен собою этот список представлять 4 структуры: ri, li, lh и lf.
Начинаются они все с двух полей - ожидаемо, сигнатуры(рассмотрены выше) и количества элементов. Далее идёт массив вариативного размера, где и начинаются различия:
li - целлы с nk, непосредственно содержащие структуру дочернего ключа CM_KEY_NODE
ri - целлы, содержащие в себе целлы li
lh - принцип как у li. Помимо целла, в элементе массива содержится хеш имени ключа.
lf - принцип как у lh. Помимо целла, в элементе массива содержатся первые 4 символа имени ключа(т.е. вместо хеша).
C: Скопировать в буфер обмена
Код:
typedef struct _CM_INDEX {
HCELL_INDEX Cell;
union {
UCHAR NameHint[4]; //lf
ULONG HashKey; //lh
};
} CM_INDEX, *PCM_INDEX;
typedef struct _CM_KEY_FAST_INDEX { //lf и lh
USHORT Signature;
USHORT Count;
CM_INDEX List[1];
} CM_KEY_FAST_INDEX, *PCM_KEY_FAST_INDEX;
typedef struct _CM_KEY_INDEX { // li и ri
USHORT Signature;
USHORT Count;
HCELL_INDEX List[1];
} CM_KEY_INDEX, *PCM_KEY_INDEX;
Но обратите внимание: SubKeyLists - это массив, из двух элементов. Тут всё просто - это разделение, как и ранее, на Permanent и Volatile ключи.
Что там с значениями ключей реестра? Целл из ValueList, сошлёт нас к сигнатуре "vk", соответствующей структуре CM_KEY_VALUE.
C: Скопировать в буфер обмена
Код:
//0x18 bytes (sizeof)
struct _CM_KEY_VALUE
{
USHORT Signature; //0x0
USHORT NameLength; //0x2
ULONG DataLength; //0x4
ULONG Data; //0x8
ULONG Type; //0xc
USHORT Flags; //0x10
USHORT Spare; //0x12
WCHAR Name[1]; //0x14
};
Type мы можем глянуть в доке майкрософт, в разделе "Типы значений реестра". Data - целл, содержащий сырое значение... значения.
На данном этапе предлагаю притормозить. С базовой теорией - всё.
Практика.
Открываем WinDBG, виртуалку, и погнали!
Для начала предлагаю прописать dp nt!CmpHiveListHead для получения первого куста реестра. Получаем адрес ffffa08e7da4f640(указывает на CMHIVE.HiveList). Так как CMHIVE/HHIVE всегда выравнены по размеру страницы, обнулив младшие три байта пропишем: dt nt!CMHIVE ffffa08e7da4f000 -l HiveList.Flink. Перед вами все кусты реестра!
Для примера возьму куст SYSTEM, в связи с тем, что он находится в пространстве ядра(для простоты). Для примера, я взаранее создам значение в корневом ключе. Чтобы получить корневой ключ, читаем поле RootKcb, либо RootCell из HBASE_BLOCK и получаем наш целл(не забывайте про 4 байта размера, мы их пропускаем):
JavaScript: Скопировать в буфер обмена
Код:
//Простенький скрипт для windbg:
"use strict";
function CellAddress(HiveAddress, Cell) {
let Hive = host.createPointerObject(HiveAddress, "nt", "_HHIVE*");
let Map = Hive.Storage[(Cell >> 31) & 1].Map;
let Table = Map.Directory[(Cell >> 21) & 0x3FF];
let Entry = host.createPointerObject(Table.address.add(((Cell >> 12) & 0x1FF) * host.getModuleType("nt", "_HMAP_ENTRY").size), "nt", "_HMAP_ENTRY*");
let BinAddress = Entry.PermanentBinAddress.bitwiseAnd(~0x0f);
return BinAddress.add(Entry.BlockOffset).add(Cell & 0xFFF);
}
function invokeScript(Hive, Cell) {
host.diagnostics.debugLog(`${CellAddress(Hive, Cell)}\n`);
}
function initializeScript()
{
return [new host.apiVersionSupport(1, 9), new host.functionAlias(CellAddress, "getcell")];
}
//Для использования пропишите:
//.scriptload script_path
//@$getcell(адрес_куста, целл_индекс)
//Если вы работаете с кустом реестра, у которого ViewMap.ProcessTuple.ProcessReference != 0, необходим аттач к процессу "Registry"
Целл: 0x20
Куст: 0xffffa08e7da62000
Адрес целла: 0xffffa08e7f291020
Пропустив размер целла(4 байта): 0xffffa08e7f291024
Прописав команду
db 0xffffa08e7f291024
видим "nk" сигнатуру: смотрим выше - это структура CM_KEY_NODE.
Давайте глянем что нам родит
dt nt!_CM_KEY_NODE 0xffffa08e7f291024
Смотрите на Name, - это на самом деле анси строка. Подглядев в db увидим имя корня - ROOT(стандартное имя корня куста реестра):
Давайте перечислим значения и дочерние ключи. Для этого, получаем целл списка значений и целл списка подключей(помним про Permanent и Volatile, рассмотрим лишь первое). Для начала значения:
Count == 1, в целле списка хранится массив целлов vk, перейдя по которым мы получим структуру CM_KEY_VALUE:
Сигнатура - корректна, значит всё ок. Имя ключа - test(смотреть на db), давайте получим данные, хранящиеся в Data. Type == 1 соответствует REG_SZ.
Записанные мною через regedit данные, мы видим перед своими глазами.
А теперь давайте... Перезапишем их.
Более чем получилось.
Немного экспериментов.
Перезапишем целл Data на 0xFFFFFFFF. Ключ реестра - пропал из регедита. Если попытаемся провернуть те же финты с одним из целлов в ValueList(именно в массиве) - получаем Critical Error в дебагере(увы, без экрана счастья). Хорошо, а если попробовать создать несколько значений реестра, и после перезаписи одного из целлов значения в 0xFFFFFFFF, сделать декремент количества значений?
Создам три ключа:
Кстати, не забывайте про ValueList в KCB. Насчет идеи выше - БСОД. Еще занятно, что реестр самовосстанавливается(однако когда тестировал на основной машине он какого-то хрена решил не восстанавливаться, так что не рискуйте...). А что если сделать "удаление" из массива целлов значений?
В данном случае, я тупо "вытолкнул" два ключа реестра вместо перезаписи в 0xFFFFFFFF. При этом, целлы значений не деаллоцируются, т.е. в дальнейшем проделав обратные операции(вернём целлы в массив), всё заработает.
Проделаем то же самое и с дочерними ключами. С ними будет чуть полегче - не надо делать двойную работу с KCB.
Посмотрим на целлы со списками подключей(первое - Permanent, второе - Volatile):
Это lh. Помните структуру _CM_KEY_FAST_INDEX?
Так как это массив вариативного размера, легче будет без структурки.
Перейдём по первому целлу в списке(зелёненький), и увидим следующее.
Что за зверьё такое, sk этот ваш?
C: Скопировать в буфер обмена
Код:
struct _CM_KEY_SECURITY
{
USHORT Signature; //0x0
USHORT Reserved; //0x2
ULONG Flink; //0x4
ULONG Blink; //0x8
ULONG ReferenceCount; //0xc
ULONG DescriptorLength; //0x10
struct _SECURITY_DESCRIPTOR_RELATIVE Descriptor; //0x14
};
struct _SECURITY_DESCRIPTOR_RELATIVE
{
UCHAR Revision; //0x0
UCHAR Sbz1; //0x1
USHORT Control; //0x2
ULONG Owner; //0x4
ULONG Group; //0x8
ULONG Sacl; //0xc
ULONG Dacl; //0x10
};
Попробуем получить второй ключ.
Получилось. Кстати, как вы могли заметить, ключи в списке отсортированы в алфавитном порядке, что немаловажно.
И чуть не забыл - получение KCB. Сделать это можно через _CMHIVE::KcbCacheTable, представляющей собой хеш-таблицу.
Насчёт хеш-функции. В целом, есть две функции CmpHash*, для двух видов строк соответственно. Их реверс проблем не составит.
Давайте подведём некоторые полученные из проведённых опытов знания:
Мы можем скрыть значение ключа перезаписав Data в 0xFFFFFFFF.
Для частичного удаления значения или дочернего ключа с сохранением выделенного под них места и их содержимого - убираем их из ValueList или SubKeyLists.
Пишем
А теперь давайте думать. Я предлагаю сделать следующее:
1. Каким-либо способом хукнуть запись файла(Получить хендл файла можно из _CMHIVE::FileHandles[0], прошу что-то менее банальное чем NTFS MajorFunctions)
2. Изменить реестр выше описанными методами для сокрытия "неприятных" ключей или значений.
3. При попытке записи на диск, будем проходиться по нашим ключам/значениям реестра, проверять, попадают ли текущие целлы(вы ведь помните, что это по сути просто смещение от 0x1000 после начала файла) на диск при данной операции записи основываясь на FilePointer, и возвращать данные в исходное состояние. После записи(условно CompletionRoutine) - снова удаляем ключи и значения из памяти.
Если вы ленивы(как и я), предлагаю получать родительский ключ некоторыми средствами винды в лице ObReferenceObject*(в данном случае - ByName). Также прошу не бросать помидоры за стиль кода. Попробуем скрыть "TestKey" в корне.
Для начала получим _CM_KEY_NODE и подключимся к процессу реестра:
C: Скопировать в буфер обмена
Код:
UNICODE_STRING key_name;
InitUnicodeString(&key_name, L"\\REGISTRY\\MACHINE\\SOFTWARE");
_CM_KEY_BODY* key_object = 0;
ObReferenceObjectByName(&key_name, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, 0, 0, *CmKeyObjectType, KernelMode, 0, (PVOID*)&key_object);
_CM_KEY_CONTROL_BLOCK* kcb = key_object->KeyControlBlock;
_CMHIVE* hive = kcb->KeyHive;
_KAPC_STATE* apc_state = ExAllocatePoolWithTag(NonPagedPool, sizeof(KAPC_STATE), 'soom');
KeStackAttackProcess((_KPROCESS*)(hive->ViewMap.ProcessTuple->ProcessReference), apc_state);
_CM_KEY_NODE* key_node = ( _CM_KEY_NODE*)GetCellData(kcb->KeyCell);
Давайте найдём наш ключ. Я не буду использовать способы быстрого нахождения, я даже не буду сейчас отсеивать различные варианты списка(lh, lf, ri, li). Просто пройдусь по массиву:
C: Скопировать в буфер обмена
Код:
_CM_KEY_INDEX* key_leafs = ( _CM_KEY_INDEX*)GetCellData(key_node->SubKeyLists[0]);
for(uint8_t i = 0; i < key_leafs->Count; i++) {
_CM_KEY_NODE* key_node = ( _CM_KEY_NODE*)GetCellData(key_leafs->List[i]);
if(key_node->Signature != 0x6b6e ) continue;
if(!strncmp((char*)(key_node->Name), "TestKey", sizeof("TestKey"))) {
int after_key_no = key_leafs->Count - i - 1;
test_cell = key_leafs->List[i];
memcpy(&key_leafs->List[i], &key_leafs->List[i+1], after_key_no * sizeof(_CM_INDEX));
key_leafs->Count --;
key_node->SubKeyCounts[0] --;
break;
}
}
Мы успешно удалили ключ. Не забудьте деаттачнуться от процесса. Также советую поэкспериментировать с kcb. Перед всеми манипуляциями с реестром необходимо сбросить WP бит в CR0, данное действие выполняет и сама винда.
Теперь давайте попробуем написать тестовый код для восстановления реестра при записи на диск(на данном этапе можно не париться с kcb и прочим, так как по сути на диск оно пишется как тупо файл маппинг, IRP_PAGING_IO).
Опять же, способ хука остаётся за вами. Так как работаем с определенным файлом куста на диске, можете попробовать потанцевать с FILE_OBJECT, может что и получиться.
За "внешние" данные в коде будет взята информация об операции записи в файл: размер(write_size) и байт-оффсет(byte_offset). Не буду повторять код на получение _CM_KEY_NODE. Необходимо чтобы программа помнила про целлы, которые удаляла. Также стоит помнить, что все ключи идут в алфавитном порядке. Для определения этого порядка буду использовать strcmp(что не очень хорошо).
C: Скопировать в буфер обмена
Код:
if(key_node->SubKeyLists[0] > byte_offset && key_node->SubKeyLists[0] < (byte_offset + write_size)) {
_CM_KEY_INDEX* key_leafs = ( _CM_KEY_INDEX*)GetCellData(key_node->SubKeyLists[0]);
for(uint8_t i = 0; i < key_leafs->Count; i++) {
_CM_KEY_NODE* key_node = ( _CM_KEY_NODE*)GetCellData(key_leafs->List[i]);
if(key_node->Signature != 0x6b6e ) continue;
if(strcmp((char*)(key_node->Name), "TestKey", sizeof("TestKey")) < 0) {
int after_key_no = key_leafs->Count - i - 1;
reverse_memcpy(&key_leafs->List[i+1], &key_leafs->List[i], after_key_no * sizeof(_CM_INDEX));
key_leafs->List[i] = test_cell;
key_leafs->Count ++;
key_node->SubKeyCounts[0] ++;
break;
}
}
}
Те же действия можно проделать и со значениями.
Стоит понимать, что при проделывании всех выше сказанных операций, останутся слепы любые средства защиты, а самое главное - сам юзер.
В целом, на данном этапе можно закончить статью. Можно ещё со многим поэкспериментировать - это я оставляю на вас. Благодарю за прочтение!