Идем на перехват!Deeoni$ (DeeoniS@gmail.com) Хакер, номер #102, стр. 114 Перехват обращений к реестру в Windows Vista: практика Если ты прочитал предыдущую статью и попробовал разобраться во всех тонкостях программирования в режиме ядра, то самое время перейти непосредственно к цели всего этого. Мы попробуем написать драйвер режима ядра, перехватывающий обращения к реестру и способный влиять на исход этой операции. В предыдущем номере я постарался объять необъятное. Я не стремился на отведенных мне четырех страницах научить тебя писать драйверы, я хотел лишь подтолкнуть настоящих хакеров к изучению программирования в kernel mode. Тот, кто за этот месяц «проштудировал» интернет на тему IRP-запросов, прерываний, IRQL, стеков устройств и т.д., без труда осилит этот материал и даже сможет написать модуль режима ядра, реализующий все здесь описанное. Итак, приступим. Немного истории Давным-давно, во времена Windows 2000, один программист, а по совместительству исследователь ОС Windows, создал маленькую утилиту под названием RegMon. В предыдущей статье я уже упоминал о ней, а сейчас расскажу чуть подробней. Эта утилита загружала свой драйвер режима ядра, которой перехватывал системные сервисы операционной системы. Надо сказать, что практически все прикладные API-функции, производящие какие-либо действия с системой, так или иначе в конечном итоге обращаются к этим сервисам. Вызов функции ядра, прежде чем будет передан соответствующей NativeAPI ядра, предварительно проходит довольно сложную обработку. Сначала в третьем кольце вызывается соответствующая функция в Ntdll, где в регистр EAX помещается номер вызываемого системного сервиса, а в регистр EDX - указатель на передаваемые параметры. Затем вызывается прерывание 2Eh (в Windows XP - команда sysenter) и происходит переход процесса в нулевое кольцо, где управление передается записанному в IDT шлюзу прерывания. В этом месте окружение третьего кольца переключается на нулевое, выполняется смена стека на стек ядра и осуществляется перезагрузка сегментного регистра FS, который в нулевом кольце указывает на совершенно не такие структуры, как в третьем кольце. Затем управление передается обработчику прерывания 2Eh - функции ядра KiSystemService. Она копирует передаваемые системному сервису параметры в стек ядра и производит вызов NativeAPI-функции ядра, согласно содержимому ServiceDescriptorTable. Эта таблица находится в памяти ядра и представляет собой структуру, содержащую 4 таблицы системных сервисов (SST). Первая из них описывает сервисы, экспортируемые ядром (ntoskrnl.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, надо только в блоке try\except, чтобы случайно не получить голубой экран смерти. Еще одним немаловажным фактором является то, что 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 ReturnStatus; PVOID CallContext; PVOID ObjectContext; PVOID Reserved; } REG_POST_OPERATION_INFORMATION,*PREG_POST_OPERATION_INFORMATION; Здесь наиболее интересны следующие значения. Status – результат выполненной операции. PreInformation – указатель на структуру с информацией, которая предшествовала операции, в нашем случае это указатель на структуру REG_DELETE_VALUE_KEY_INFORMATION. ReturnStatus – значение, которое вернет система вызывающему потоку. Теперь сам код. Первым делом зарегистрируем свой callback-вызов в системе. Сделать это можно, когда угодно, но я буду регистрировать его на этапе загрузки драйвера в систему. DriverEntry LARGE_INTEGER Cookie; NTSTATUS DriverEntry (IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath ) { NTSTATUS status;
//дополнительные действия по инициализации драйвера status = CmRegisterCallback(RegistryCallback, NULL, &Cookie); return status; } Как видно из приведенного кода, мы просто вызвали CmRegisterCallback с «правильными» параметрами. Теперь займемся непосредственно самой функцией перехвата. Рутина RegistryCallback и вспомогательные функции NTSTATUS PreSetValueKey(IN PREG_SET_VALUE_KEY_INFORMATION info) { NTSTATUS status; //выполняем нужные нам действия return status; } NTSTATUS PostDeleteValueKey(IN PREG_POST_OPERATION_INFORMATION info) { NTSTATUS status; //выполняем нужные нам действия return 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; } return 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. Конечно, чтобы создать что-то более-менее приемлемое, надо изучить все тонкости программирования в режиме ядра и много практиковаться. Тем, кто уже имеет неплохой стаж в рассматриваемой области, эта статья, надеюсь, тоже пригодится, так как любимый всеми патч таблицы системных сервисов в Висте прикрыли, а отслеживать обращения к реестру никому не помешает. |