Драйверы режима ядра: Часть 6: Базовая техника: Работа с памятью. Использование системных куч — Архив WASM.RU
В этой и нескольких следующих статьях будут рассмотрены некоторые базовые техники, без которых трудно представить себе создание мало-мальски полезного драйвера. Причем, я хочу подчеркнуть слово "некоторые", т.к. драйверы могут очень многое. Не всё что они могут, можно отнести к базовой технике, но даже того, что можно отнести к таковой будет достаточно для того, чтобы еще очень долго иметь возможность привлекать Ваше внимание к моей скромной персоне. Не уверен, что у меня хватит на это сил. Поэтому круг вопросов, которых мы коснемся, будет все же ограничен.
Поскольку умение работать с памятью является абсолютно необходимым и без освещения этого вопроса, хотя бы в общих чертах, невозможно перейти к остальным, то с него и начнем.
Пользовательским процессам система, точнее диспетчер памяти (Memory Manager), предоставляет довольно богатый API, для работы с памятью, в который входят три группы функций: операции со страницами виртуальной памяти, проецирование файлов в память и управление кучами (динамически распределяемыми областями памяти).
Компоненты ядра, и драйверы в том числе, имеют несколько более широкие возможности. Например, драйвер может выделить диапазон памяти непрерывный в физическом пространстве адресов. Имена функций предоставляемых диспетчером памяти драйверам начинаются с префикса Mm. Кроме того, существует набор функций исполнительной системы (Executive) для выделения и освобождения памяти из системных куч и для манипуляции ассоциативными списками (память из ассоциативных списков выделяется быстрее, чем из кучи, но только блоками заранее определенного фиксированного размера). Имена таких функций начинаются с префикса Ex.
6.1 Системные кучи
Системные кучи (к пользовательским кучам не имеют никакого отношения) представлены двумя так называемыми пулами памяти, которые, естественно, располагаются в системном адресном пространстве:
- Пул неподкачиваемой памяти (Nonpaged Pool). Назван так потому, что его страницы никогда не сбрасываются в файл подкачки, а значит, никогда и не подкачиваются назад. Т.е. этот пул всегда присутствует в физической памяти и доступен при любом IRQL. Одна из причин его существования в том, что обращение к такой памяти не может вызвать ошибку страницы (Page Fault). Такие ошибки приводят к краху системы, если происходят при IRQL >= DISPATCH_LEVEL.
- Пул подкачиваемой памяти (Paged Pool). Назван так потому, что его страницы могут быть сброшены в файл подкачки, а значит должны быть подкачаны назад при последующем к ним обращении. Эту память можно использовать только при IRQL строго меньше DISPATCH_LEVEL.
Оба пула находятся в системном адресном пространстве, а значит, доступны из контекста любого процесса. Для выделения памяти в системных пулах существует набор функций ExAllocatePoolXxx, а для возвращения выделенной памяти всего одна - ExFreePool.
Прежде чем мы воспользуемся одной из этих функций, хочу обратить Ваше внимание на несколько основополагающих моментов:
- Обращение к памяти сброшенной в файл подкачки при IRQL >= DISPATCH_LEVEL, как я уже говорил, приводят к краху системы.
Имейте в виду, что если в момент обращения к подкачиваемой памяти она физически присутствует, то краха не будет, даже при IRQL >= DISPATCH_LEVEL. Но можете быть уверены, что рано или поздно ее не окажется на месте и тогда BSOD обеспечен
- Не стоит использовать неподкачиваемую память везде, где не попадя. Этот ресурс дороже, чем подкачиваемая память. Забирая его себе, Вы тем самым отнимаете его у тех, кому он нужен, возможно, больше чем Вам.
- Если Вы выделили память из любого системного пула, то вне зависимости от того, что дальше случится с Вашим драйвером, эта память не будет возвращена системе назад до тех пор, пока Вы явно не вызовите ExFreePool. Т.е. если драйвер не освободит выделенную ему память явно, то она так и останется бесполезно болтаться в системе, даже если драйвер будет выгружен. Я уже неоднократно говорил, что все выделяемые драйвером ресурсы должны быть явно возвращены назад.
- Выделяемая из системных пулов память не очищается системой (не заполняется нулями) и, возможно, будет содержать "мусор", оставленный предыдущими владельцами. Если Вы намереваетесь выполнять какие-то критичные к этому операции, например, манипулировать строками, то лучше ее явно очистить перед использованием.
Определить какой тип памяти (подкачиваемая или неподкачиваемая) Вам нужен очень просто. Если какой-либо код должен обращаться к памяти при IRQL >= DISPATCH_LEVEL, то нужно использовать только неподкачиваемую память. Причем, как сам код, так и данные должны располагаться в неподкачиваемой памяти. По умолчанию весь драйвер загружается в неподкачиваемую память, кроме секции с именем "INIT" и секций, имена которых начинаются с "PAGE". Если кроме этого вы не предпринимали никаких действий по изменению атрибутов памяти принадлежащих драйверу, например, не вызывали функцию MmPageEntireDriver, делающую весь образ драйвера подкачиваемым, то о самом драйвере беспокоится не стоит - он всегда будет присутствовать в физической памяти.
В предыдущих статьях мы достаточно подробно разобрали, при каком IRQL вызываются стандартные процедуры (DriverEntry, DriverUnload, DispatchXxx) драйвера.
Кроме того, в DDK в описании каждой функции указано при каком IRQL ее можно вызывать или при каком IRQL она вызывается системой, если это функция обратного вызова (callback). Например, в одной из следующих статей мы будем использовать функцию IoInitializeTimer. В описании этой функции сказано, что процедура, вызываемая системой при срабатывании таймера, выполняется при IRQL = DISPATCH_LEVEL. Это недвусмысленно говорит нам о том, что эта процедура и любая память, к которой она будет обращаться должны быть неподкачиваемыми.
На худой конец, если уж Вы совсем не уверены в текущем IRQL, его можно определить функцией KeGetCurrentIrql таким образом:
Код (Text):
invoke KeGetCurrentIrql .if eax < DISPATCH_LEVEL ; используем любую память .else ; используем только неподкачиваемую память .endif6.2 Выделяем память из системного пула
В качестве примера рассмотрим очень простой драйвер SystemModules. Все действо будет происходить в процедуре DriverEntry. Мы быстренько выделим немного подкачиваемой памяти (Вы, несомненно, помните, что DriverEntry работает на IRQL = PASSIVE_LEVEL. Поэтому мы обойдемся подкачиваемой памятью.), что-нибудь полезное в нее запишем, освободим и заставим систему выгрузить драйвер.
Для экономии места я привожу только исходный текст процедуры DriverEntry. Это собственно и есть весь драйвер.
Код (Text):
;@echo off ;goto make ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; ; SystemModules - Выделяем память из системного пула и используем её. ; ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .386 .model flat, stdcall option casemap:none ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; В К Л Ю Ч А Е М Ы Е Ф А Й Л Ы ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: include \masm32\include\w2k\ntstatus.inc include \masm32\include\w2k\ntddk.inc include \masm32\include\w2k\native.inc include \masm32\include\w2k\ntoskrnl.inc includelib \masm32\lib\w2k\ntoskrnl.lib include \masm32\Macros\Strings.mac ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; В Ы Г Р У Ж А Е М Ы Й П Р И Н Е О Б Х О Д И М О С Т И К О Д ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .code INIT ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; DriverEntry ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: DriverEntry proc uses esi edi ebx pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING local cb:DWORD local p:PVOID local dwNumModules:DWORD local pMessage:LPSTR local buffer[256+40]:CHAR invoke DbgPrint, $CTA0("\nSystemModules: Entering DriverEntry\n") and cb, 0 invoke ZwQuerySystemInformation, SystemModuleInformation, addr p, 0, addr cb .if cb != 0 invoke ExAllocatePool, PagedPool, cb .if eax != NULL mov p, eax invoke DbgPrint, \ $CTA0("SystemModules: %u bytes of paged memory allocted at address %08X\n"), cb, p invoke ZwQuerySystemInformation, SystemModuleInformation, p, cb, addr cb .if eax == STATUS_SUCCESS mov esi, p push dword ptr [esi] pop dwNumModules mov cb, (sizeof SYSTEM_MODULE_INFORMATION.ImageName + 100)*2 invoke ExAllocatePool, PagedPool, cb .if eax != NULL mov pMessage, eax invoke DbgPrint, \ $CTA0("SystemModules: %u bytes of paged memory allocted at address %08X\n"), \ cb, pMessage invoke memset, pMessage, 0, cb add esi, sizeof DWORD assume esi:ptr SYSTEM_MODULE_INFORMATION xor ebx, ebx .while ebx < dwNumModules lea edi, [esi].ImageName movzx ecx, [esi].ModuleNameOffset add edi, ecx invoke _strnicmp, edi, $CTA0("ntoskrnl.exe", szNtoskrnl, 4), sizeof szNtoskrnl - 1 push eax invoke _strnicmp, edi, $CTA0("ntice.sys", szNtIce, 4), sizeof szNtIce - 1 pop ecx and eax, ecx .if ZERO? invoke _snprintf, addr buffer, sizeof buffer, \ $CTA0("SystemModules: Found %s base: %08X size: %08X\n", 4), \ edi, [esi].Base, [esi]._Size invoke strcat, pMessage, addr buffer .endif add esi, sizeof SYSTEM_MODULE_INFORMATION inc ebx .endw assume esi:nothing mov eax, pMessage .if byte ptr [eax] != 0 invoke DbgPrint, pMessage .else invoke DbgPrint, \ $CTA0("SystemModules: Found neither ntoskrnl nor ntice.\n") .endif invoke ExFreePool, pMessage invoke DbgPrint, $CTA0("SystemModules: Memory at address %08X released\n"), pMessage .endif .endif invoke ExFreePool, p invoke DbgPrint, $CTA0("SystemModules: Memory at address %08X released\n"), p .endif .endif invoke DbgPrint, $CTA0("SystemModules: Leaving DriverEntry\n") mov eax, STATUS_DEVICE_CONFIGURATION_ERROR ret DriverEntry endp ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: end DriverEntry :make set drv=SystemModules \masm32\bin\ml /nologo /c /coff %drv%.bat \masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj del %drv%.obj echo. pauseВ качестве чего-нибудь полезного мы возьмем список модулей загруженных в системное адресное пространство (в этот список войдут модули самой системы: ntoskrnl.exe, hal.dll и т.п. и драйверы устройств) и попытаемся найти в нем два модуля: ntoskrnl.exe и ntice.sys. Список системных модулей можно получить, вызвав функцию ZwQuerySystemInformation с информационным классом SystemModuleInformation. Описание этой функции можно найти в книге Гэри Неббета "Справочник по базовым функциям API Windows NT/2000". Кстати, ZwQuerySystemInformation уникальная функция. С её помощью можно получить просто огромное количество самой различной информации о системе.
Программы управления драйвером не будет. Используйте KmdManager (входит в пакет KmdKit) или аналогичную утилиту, а отладочные сообщения, выдаваемые драйвером, контролируйте с помощью утилиты DebugView (www.sysinternals.com) или консоли SoftICE.
Код (Text):
and cb, 0 invoke ZwQuerySystemInformation, SystemModuleInformation, addr p, 0, addr cbДля начала нам нужно определить, сколько места будет занимать интересующая нас информация. Вызвав ZwQuerySystemInformation так, как показано выше, мы получим STATUS_INFO_LENGTH_MISMATCH (что вполне естественно, т.к. размер переданного буфера равен нулю), а в переменной cb мы получим искомое количество байт. Таким способом можно узнать какой размер буфера необходим. Адрес переменной p, в данном случае, нужен только для нормальной работы этой функции: по нему все равно ничего записано не будет.
Код (Text):
.if cb != 0 invoke ExAllocatePool, PagedPool, cbЕсли размер требуемого буфера не равен нулю, мы выделяем необходимое количество памяти из подкачиваемого пула (об этом говорит первый параметр - PagedPool. Значение NonPagedPool будет означать запрос неподкачиваемой памяти). Функция ExAllocatePool даже проще чем ее аналог режима пользователя HeapAlloc. Всего два параметра. Первый определяет пул: подкачиваемый или неподкачиваемый, второй - размер требуемой памяти. Проще не бывает.
Код (Text):
.if eax != NULLЕсли ExAllocatePool вернет ненулевое значение, то это указатель на выделенный буфер.
Когда будете изучать отладочную информачию выводимую драйвером в окно DebugView, обратите внимание на то, что адрес буфера, возвращенный ExAllocatePool, будет кратен размеру страницы. Дело в том, что если размер запрашиваемой памяти больше или равен размеру страницы (а в данном случае размер требуемой памяти значительно больше одной страницы), то выделенная область памяти будет начинаться с новой страницы.
Код (Text):
mov p, eax invoke ZwQuerySystemInformation, SystemModuleInformation, p, cb, addr cbСохраняем указатель на выделенный буфер в переменной p и вызываем ZwQuerySystemInformation еще раз, передавая ей указатель на буфер и его размер.
Код (Text):
.if eax == STATUS_SUCCESS mov esi, pЕсли ZwQuerySystemInformation возвращает STATUS_SUCCESS, то её удовлетворили параметры нашего буфера и теперь он содержит список системных модулей в виде массива структур SYSTEM_MODULE_INFORMATION (определена в файле include\w2k\native.inc).
Код (Text):
SYSTEM_MODULE_INFORMATION STRUCT ;Information Class 11 Reserved DWORD 2 dup(?) Base PVOID ? _Size DWORD ? Flags DWORD ? Index WORD ? Unknown WORD ? LoadCount WORD ? ModuleNameOffset WORD ? ImageName CHAR 256 dup(?) SYSTEM_MODULE_INFORMATION ENDSДействительное количество байт скопированных в буфер вернется в переменной cb, но нам оно не нужно.
Здесь я предполагаю, что между двумя вызовами ZwQuerySystemInformation в системе не появится ни одного нового модуля. Это весьма вероятно, т.к. случается не часто. В учебном примере это простительно, но в коде своего драйвера Вы можете использовать более надежный способ: вызывать ZwQuerySystemInformation в цикле, передавая ей каждый раз буфер большего размера, до тех пор, пока размер буфера не окажется достаточным.
Код (Text):
push dword ptr [esi] pop dwNumModulesВ самом первом двойном слове буфера, заполненного ZwQuerySystemInformation, содержится количество структур SYSTEM_MODULE_INFORMATION равное количеству модулей и сразу за ним (двойным словом) начинается их массив. Запоминаем количество модулей в переменной dwNumModules.
Код (Text):
mov cb, (sizeof SYSTEM_MODULE_INFORMATION.ImageName + 100)*2 invoke ExAllocatePool, PagedPool, cb .if eax != NULL mov pMessage, eaxДля дальнейшей плодотворной работы нам потребуется еще один буфер, в который будут помещаться имена двух искомых модулей и кое-какая дополнительная информация. Мы предполагаем, что (sizeof SYSTEM_MODULE_INFORMATION.ImageName + 100)*2 должно как раз хватить.
Обратите внимание на адрес буфера - он не будет кратен размеру страницы, т.к. его размер меньше страницы.
Код (Text):
invoke memset, pMessage, 0, cbНа всякий случай затрем буфер нулями т.к. мы собираемся заполнить его символьной информацией. Это гарантирует нам, что строки будут завершаться нулями.
Код (Text):
add esi, sizeof DWORD assume esi:ptr SYSTEM_MODULE_INFORMATIONПропускаем DWORD содержащий число модулей и регистр esi теперь указывает на первую структуру SYSTEM_MODULE_INFORMATION.
Код (Text):
xor ebx, ebx .while ebx < dwNumModulesОрганизуем цикл, повторяющийся dwNumModules раз. В цикле перебираем массив структур SYSTEM_MODULE_INFORMATION и ищем там структуры соответствующие модулям ntoskrnl.exe и ntice.sys.
В многопроцессорной системе модуль ntoskrnl.exe будет иметь имя ntkrnlmp.exe. А в системе с поддержкой PAE - ntkrnlpa.exe и ntkrpamp.exe, соответственно. Здесь я предполагаю, Вы не являетесь счастливым обладателем подобной системы.
Код (Text):
lea edi, [esi].ImageName movzx ecx, [esi].ModuleNameOffset add edi, ecxПоля ImageName и ModuleNameOffset содержат полный путь к модулю и относительное смещение имени модуля в пути, соответственно.
Код (Text):
invoke _strnicmp, edi, $CTA0("ntoskrnl.exe", szNtoskrnl, 4), sizeof szNtoskrnl - 1 push eax invoke _strnicmp, edi, $CTA0("ntice.sys", szNtIce, 4), sizeof szNtIce - 1 pop ecxПоиск осуществляем простым сравнением имен модулей. Функция _strnicmp сравнивает две ANSI-строки независимо от регистра букв. Сравнивает она только то количество символов, которое передано в третьем параметре. В данном случае это не обязательно, т.к. имена модулей в структурах SYSTEM_MODULE_INFORMATION завершаются нулями и можно было бы использовать _stricmp. Я использую _strnicmp для пущей безопастности.
Кстати, ntoskrnl.exe экспортирует большое количество стандартных функций по работе со строками: strcmp, strcpy, strlen и т.п.
Код (Text):
and eax, ecx .if ZERO? invoke _snprintf, addr buffer, sizeof buffer, \ $CTA0("SystemModules: Found %s base: %08X size: %08X\n", 4), \ edi, [esi].Base, [esi]._Size invoke strcat, pMessage, addr buffer .endif add esi, sizeof SYSTEM_MODULE_INFORMATION inc ebx .endw assume esi:nothingЕсли любой из вышеозначенных модулей найден, с помощью функции _snprintf (и такая тоже есть в ядре) форматируем строку, содержащую имя модуля, его базовый адрес и размер и добавляем эту информацию в буфер.
Цифра 4 в макросах $CTA0 означает, что определяемая ими строка выравнивается по границе DWORD (здесь этого можно и не делать). А метки szNtoskrnl и szNtIce нужны для того, чтобы передать их в директиву sizeof. Кстати, вы можете менять местами метку и выравнивание в моих строковых макросах - они распознаются автоматически. Либо можете использовать только метку или только выравнивание (подробнее см. macros\Strings.mac).
Код (Text):
mov eax, pMessage .if byte ptr [eax] != 0 invoke DbgPrint, pMessage .else invoke DbgPrint, \ $CTA0("SystemModules: Found neither ntoskrnl nor ntice. Is it possible?\n") .endifПоскольку перед использованием мы обнулили буфер, то если первый его байт не равен нулю, значит, мы что-то нашли. Выводим содержимое буфера.
Код (Text):
invoke ExFreePool, pMessage .endif .endif invoke ExFreePool, p .endif .endifВозвращаем выделенную из системных пулов память.
Код (Text):
mov eax, STATUS_DEVICE_CONFIGURATION_ERROR retЗаставляем систему выгрузить драйвер.
Как видите, использование системных пулов даже проще, чем работа с кучами в режиме пользователя. Задача только правильно определить тип пула.
Многие ZwXxx функции экспортируются также библиотекой ntdll.dll режима пользователя и являются простыми переходниками в режим ядра, где и происходит вызов соответствующей функции, которая и проделывает всю полезную работу. Примечательно то, что количество параметров и их смысл полностью совпадают. Из этой ситуации можно извлечь большую выгоду. Поскольку ошибки в ядре приводят к краху системы, можно сначала отладить код в режиме пользователя, а потом перенести его в драйвер с минимальными изменениями, а иногда даже и без изменений. Например, в нашем случае, будучи вызвана из ntdll.dll в режиме пользователя, функция ZwQuerySystemInformation вернет ту же самую информацию, что и одноименная функция, вызванная из ntoskrnl.exe в драйвере. Пользуясь этим нехитрым приемом, можно сэкономить немалое число перезагрузок.
Исходный код драйвера в архиве. Для компиляции требуется последняя версия KmdKit - берите на сайте.
© Four-F
Драйверы режима ядра: Часть 6: Базовая техника: Работа с памятью. Использование системных куч
Дата публикации 2 окт 2003