Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #102, ИЮНЬ 2007 г.

Идем на перехват!

Deeoni$ (DeeoniS@gmail.com)

Хакер, номер #102, стр. 114

Перехват обращений к реестру в Windows Vista: практика

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

В предыдущем номере я постарался объять необъятное. Я не стремился на отведенных мне четырех страницах научить тебя писать драйверы, я хотел лишь подтолкнуть настоящих хакеров к изучению программирования в keel mode. Тот, кто за этот месяц «проштудировал» интернет на тему IRP-запросов, прерываний, IRQL, стеков устройств и т.д., без труда осилит этот материал и даже сможет написать модуль режима ядра, реализующий все здесь описанное. Итак, приступим.

Немного истории

Давным-давно, во времена Windows 2000, один программист, а по совместительству исследователь ОС Windows, создал маленькую утилиту под названием RegMon. В предыдущей статье я уже упоминал о ней, а сейчас расскажу чуть подробней. Эта утилита загружала свой драйвер режима ядра, которой перехватывал системные сервисы операционной системы. Надо сказать, что практически все прикладные API-функции, производящие какие-либо действия с системой, так или иначе в конечном итоге обращаются к этим сервисам.

Вызов функции ядра, прежде чем будет передан соответствующей NativeAPI ядра, предварительно проходит довольно сложную обработку. Сначала в третьем кольце вызывается соответствующая функция в Ntdll, где в регистр EAX помещается номер вызываемого системного сервиса, а в регистр EDX - указатель на передаваемые параметры. Затем вызывается прерывание 2Eh (в Windows XP - команда sysenter) и происходит переход процесса в нулевое кольцо, где управление передается записанному в IDT шлюзу прерывания. В этом месте окружение третьего кольца переключается на нулевое, выполняется смена стека на стек ядра и осуществляется перезагрузка сегментного регистра FS, который в нулевом кольце указывает на совершенно не такие структуры, как в третьем кольце. Затем управление передается обработчику прерывания 2Eh - функции ядра KiSystemService. Она копирует передаваемые системному сервису параметры в стек ядра и производит вызов NativeAPI-функции ядра, согласно содержимому ServiceDescriptorTable. Эта таблица находится в памяти ядра и представляет собой структуру, содержащую 4 таблицы системных сервисов (SST). Первая из них описывает сервисы, экспортируемые ядром (ntoskl.exe), вторая - графической подсистемой (win32k.sys), а остальные две зарезервированы на будущее и пока не используются.

Когда какое-либо приложение пытается получить доступ к реестру, оно вызывает API-функцию из Advapi32.dll. В случае когда приложение пытается создать ключ реестра, оно обращается к функции RegCreateKey или RegCreateKeyEx. В свою очередь, код этих функций обращается к шлюзу NtCreateKey, находящемуся в ntdll.dll. Почему я сказал «шлюз» вместо «функции»? Все очень просто. На самом деле, NtCreateKey не делает ничего, кроме вызова соответствующего системного сервиса. Схематично код этой функции выглядит примерно так:

Возможный код NtCreateKey

mov eax,29h

lea edx,[esp+4]

int 2Eh

ret

Как видно из приведенного примера, сначала в регистр eax заносится число 29h. Это число означает номер системного сервиса в таблице дескрипторов системных сервисов. То есть, если говорить проще, 41-ый элемент в этой таблице является указателем на точку входа в системный сервис, который создает ключ в реестре. После того как в eax загружено смещение, а в edx - указатель на передаваемые параметры, вызывается специальное прерывание 2Eh для перевода процессора в нулевое кольцо. По найденному в таблице системных сервисов смещению происходит переход на код, который дальше выполняет все необходимые действия.

Драйвер утилиты RegMon занимался тем, что подменял нужные ему смещения в этой таблице своими. Таким образом, когда какое-либо приложение пыталось обратиться к реестру, вызывался код драйвера RegMon'а, который, собрав нужную ему информацию, делал оригинальный вызов, чтобы не приводить системы в нерабочее состояние. Но с выходом Windows Vista эту возможность прикрыли, обосновав это тем, что этой технологией пользуются руткиты, а для антивирусов, файрволов и других подобных программ еще в Windows XP был предложен специальный механизм для перехвата обращений к реестру.

Фильтрация обращений к реестру

С выходом Windows XP появилось понятие registry filtering driver. Дословно оно переводится как «драйвер, фильтрующий обращения к реестру». Собственно, из названия вытекает и содержание – это драйвер режима ядра, который фильтрует обращения к системному реестру. Фильтрация обращений происходит за счет установки callback-вызова на функции обращения к реестру. Установить свой callback вызов можно с помощью следующей функции:

NTSTATUS

CmRegisterCallback(

IN PEX_CALLBACK_FUNCTION Function,

IN PVOID Context,

OUT PLARGE_INTEGER Cookie

);

Здесь Function – это указатель на функцию callback-вызова, который надо зарегистрировать. Context – указатель на некую структуру данных, которая содержит служебную информацию и определяется самим драйвером. Cookie – указатель на переменную типа LARGE_INTEGER, которая идентифицирует callback-рутину. В последствии этот параметр будет использоваться при снятии callback-вызова. Вызов CmRegisterCallback должен происходить при IRQL, меньшем или равном APC_LEVEL. В Windows Vista появилась новая функция для этих целей. Ее прототип представлен ниже.

NTSTATUS

CmRegisterCallbackEx(

IN PEX_CALLBACK_FUNCTION Function,

IN PCUNICODE_STRING Altitude,

IN PVOID Driver,

IN PVOID Context,

OUT PLARGE_INTEGER Cookie

PVOID Reserved

);

Как видно из описания функции, некоторые ее параметры схожи с предыдущими, но есть и новые. Altitude – это указатель на строку типа UNICODE_STRING, которая используется в драйверах мини-фильтрах. Driver – указатель на структуру объекта драйвера, который осуществляет вызов. Ну и, наконец, Reserved, название которого говорит само за себя.

Когда перехватывать обращения к реестру нам больше не нужно, следует вызвать функцию CmUnRegisterCallback.

NTSTATUS

CmUnRegisterCallback(

IN LARGE_INTEGER Cookie

);

Единственным ее параметром является переменная типа LARGE_INTEGER, указатель на которую мы передавали при вызове CmRegisterCallbackEx/CmRegisterCallback. Если все прошло удачно, функция вернет STATUS_SUCCESS. Если же параметр cookie не верен, результатом работы CmUnRegisterCallback будет STATUS_INVALID_PARAMETER. Вызов рутины должен происходить на IRQL, меньшем или равном APC_LEVEL.

Registry Callback Routine

Теперь, когда мы знаем, как зарегистрировать собственный callback вызов на обращение к реестру, пришла пора узнать формат этой самой callback-функции. Ее прототип должен выглядеть следующим образом:

NTSTATUS

RegistryCallback(

IN PVOID CallbackContext,

IN PVOID Argument1,

IN PVOID Argument2

);

Первый параметр - это указатель на переменную, которую мы передавали в функцию CmRegisterCallback или CmRegisterCallbackEx. Argument1 - это переменная типа REG_NOTIFY_CLASS, которая говорит нашей callback-рутине о том, вследствие чего произошел ее вызов: создание ключа, удаление параметра и т.д. Argument2 – это указатель на структуру, которая содержит в себе более подробную информацию о произошедшем. Тип этой структуры определяется переменной Argument1. Ниже приведена таблица, иллюстрирующая это соответствие.

Callbac-рутина может повлиять на ход выполнения операции. Так, если в ОС Windows XP и Windows 2003 RegistryCallback вернет STATUS_SUCCESS, то система продолжит выполнение операции, а если значение, при обработке которого макрос NT_SUCCESS выдаст FALSE, то система останавливает выполнение операции с определенным нами кодом ошибки.

В Windows Vista все немного иначе. Если RegistryCallback возвращает STATUS_SUCCES, то система продолжит выполнение операции. Если STATUS_CALLBACK_BYPASS, то система прекращает выполнение операции, но возвращает STATUS_SUCCES. И последний вариант, когда RegistryCallback вернет любое значение (за исключением STATUS_CALLBACK_BYPASS), при обработке которого макрос NT_SUCCESS возвращает FALSE. В этом случае результат аналогичен результату в Windows XP.

Следует заметить, что обращаться к структуре, указатель на которую мы получили в переменной Argument2, надо только в блоке tryexcept, чтобы случайно не получить голубой экран смерти. Еще одним немаловажным фактором является то, что callback-вызов всегда выполняется на IRQL, равном PASSIVE_LEVEL в контексте того потока, который инициировал обращение к системному реестру.

От теории к практике

Теперь у нас достаточно знаний, чтобы установить свою callback-функцию и обработать информацию о произошедшей операции. Как ты уже заметил, RegistryCallback может вызываться в двух случаях: до выполнения операции и после. Пусть нам надо перехватить момент перед созданием некоторого параметра в реестре и момент после удаления некоторого параметра. Перед тем как приступить непосредственно к написанию кода, рассмотрим еще пару структур данных.

В момент перед созданием или изменением какого-либо параметра реестра в Argument1 нам придет значение RegNtPreSetValueKey или RegNtSetValueKey. По сути, это одно и то же значение, просто программисты Майкрософт придумали ему разные названия. Этому значению соответствует структура REG_SET_VALUE_KEY_INFORMATION.

REG_SET_VALUE_KEY_INFORMATION

typedef struct _REG_SET_VALUE_KEY_INFORMATION {

PVOID Object;

PUNICODE_STRING ValueName;

ULONG TitleIndex;

ULONG Type;

PVOID Data;

ULONG DataSize;

PVOID CallContext;

PVOID ObjectContext;

PVOID Reserved;

} REG_SET_VALUE_KEY_INFORMATION, *PREG_SET_VALUE_KEY_INFORMATION;

Первым членом этой структуры является указатель на объект ключа реестра, в котором создается или изменяется параметр. ValueName – это указатель на строку, содержащую имя параметра, который будет изменен. TitleIndex зарезервировано для системного использования, драйвер должен игнорировать это значение. Type – это тип данных, которые будут записаны в этот параметр. Data – указатель на буфер с данными для записи в параметр. DataSize – размер буфера данных. CallContext и ObjectContext – это указатели на структуры данных, которые мы могли определить ранее при регистрации callback-функции.

Таким образом, перед тем как какое-либо приложение попытается создать или изменить тот или иной параметр в реестре, мы незамедлительно узнаем об этом и сможем даже модифицировать те данные, которые он хочет записать в реестр.

После удаления какого-либо параметра Argument1, нам придет значение RegNtPostDeleteValueKey. Ему соответствует структура REG_POST_OPERATION_INFORMATION.

REG_POST_OPERATION_INFORMATION

typedef struct _REG_POST_OPERATION_INFORMATION {

PVOID Object;

NTSTATUS Status;

PVOID PreInformation;

NTSTATUS RetuStatus;

PVOID CallContext;

PVOID ObjectContext;

PVOID Reserved;

} REG_POST_OPERATION_INFORMATION,*PREG_POST_OPERATION_INFORMATION;

Здесь наиболее интересны следующие значения. Status – результат выполненной операции. PreInformation – указатель на структуру с информацией, которая предшествовала операции, в нашем случае это указатель на структуру REG_DELETE_VALUE_KEY_INFORMATION. RetuStatus – значение, которое вернет система вызывающему потоку.

Теперь сам код. Первым делом зарегистрируем свой callback-вызов в системе. Сделать это можно, когда угодно, но я буду регистрировать его на этапе загрузки драйвера в систему.

DriverEntry

LARGE_INTEGER Cookie;

NTSTATUS DriverEntry (IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath )

{

NTSTATUS status;

//дополнительные действия по инициализации драйвера

status = CmRegisterCallback(RegistryCallback, NULL, &Cookie);

retu status;

}

Как видно из приведенного кода, мы просто вызвали CmRegisterCallback с «правильными» параметрами. Теперь займемся непосредственно самой функцией перехвата.

Рутина RegistryCallback и вспомогательные функции

NTSTATUS PreSetValueKey(IN PREG_SET_VALUE_KEY_INFORMATION info)

{

NTSTATUS status;

//выполняем нужные нам действия

retu status;

}

NTSTATUS PostDeleteValueKey(IN PREG_POST_OPERATION_INFORMATION info)

{

NTSTATUS status;

//выполняем нужные нам действия

retu status;

}

NTSTATUS RegistryCallback(IN PVOID CallbackContext, IN PVOID Argument1, IN PVOID Argument2)

{

NTSTATUS status;

switch ((REG_NOTIFY_CLASS)Argument1)

{

case RegNtPreSetValueKey:

status = PreSetValueKey((PREG_SET_VALUE_KEY_INFORMATION)Argument2);

break;

case RegNtPostDeleteValueKey:

status = PostDeleteValueKey((PREG_POST_OPERATION_INFORMATION)Argument2);

break;

default:

ststus = STATUS_SUCCESS;

break;

}

retu status;

}

Как мы убедились, не такое уж и сложное это дело - перехват обращений к реестру. Но просто перехватить операцию создания или удаления ключа/параметра реестра мало. Надо еще уметь хоть что-то сделать в этом перехвате.

Узнаем имя ключа

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

NTSTATUS

CmCallbackGetKeyObjectID(

IN PLARGE_INTEGER Cookie,

IN PVOID Object,

OUT OPTIONAL PULONG_PTR ObjectID,

OUT OPTIONAL PCUNICODE_STRING *ObjectName

);

Здесь Object – это указатель на объект, полученный в структуре информационного класса. ObjectID – указатель на переменную, куда запишется ID объекта, а ObjectName – указатель на строку типа UNICODE_STRING, которая после вызова функции будет содержать полное имя ключа.

CmCallbackGetKeyObjectID вызывается при IRQL, меньшем или равном APC_LEVEL, и работает только в Windows Vista.

Заключение

Вот и все. Теперь мы можем написать собственный RegMon, который будет работать в Windows Vista. Конечно, чтобы создать что-то более-менее приемлемое, надо изучить все тонкости программирования в режиме ядра и много практиковаться. Тем, кто уже имеет неплохой стаж в рассматриваемой области, эта статья, надеюсь, тоже пригодится, так как любимый всеми патч таблицы системных сервисов в Висте прикрыли, а отслеживать обращения к реестру никому не помешает.

Содержание
ttfb: 22.320985794067 ms