Драйверы режима ядра: Часть 6: Базовая техника: Работа с памятью. Использование системных куч — Архив WASM.RU

Все статьи

Драйверы режима ядра: Часть 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.

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

  1. Обращение к памяти сброшенной в файл подкачки при IRQL >= DISPATCH_LEVEL, как я уже говорил, приводят к краху системы.

    Имейте в виду, что если в момент обращения к подкачиваемой памяти она физически присутствует, то краха не будет, даже при IRQL >= DISPATCH_LEVEL. Но можете быть уверены, что рано или поздно ее не окажется на месте и тогда BSOD обеспечен


  2. Не стоит использовать неподкачиваемую память везде, где не попадя. Этот ресурс дороже, чем подкачиваемая память. Забирая его себе, Вы тем самым отнимаете его у тех, кому он нужен, возможно, больше чем Вам.


  3. Если Вы выделили память из любого системного пула, то вне зависимости от того, что дальше случится с Вашим драйвером, эта память не будет возвращена системе назад до тех пор, пока Вы явно не вызовите ExFreePool. Т.е. если драйвер не освободит выделенную ему память явно, то она так и останется бесполезно болтаться в системе, даже если драйвер будет выгружен. Я уже неоднократно говорил, что все выделяемые драйвером ресурсы должны быть явно возвращены назад.


  4. Выделяемая из системных пулов память не очищается системой (не заполняется нулями) и, возможно, будет содержать "мусор", оставленный предыдущими владельцами. Если Вы намереваетесь выполнять какие-то критичные к этому операции, например, манипулировать строками, то лучше ее явно очистить перед использованием.

Определить какой тип памяти (подкачиваемая или неподкачиваемая) Вам нужен очень просто. Если какой-либо код должен обращаться к памяти при IRQL >= DISPATCH_LEVEL, то нужно использовать только неподкачиваемую память. Причем, как сам код, так и данные должны располагаться в неподкачиваемой памяти. По умолчанию весь драйвер загружается в неподкачиваемую память, кроме секции с именем "INIT" и секций, имена которых начинаются с "PAGE". Если кроме этого вы не предпринимали никаких действий по изменению атрибутов памяти принадлежащих драйверу, например, не вызывали функцию MmPageEntireDriver, делающую весь образ драйвера подкачиваемым, то о самом драйвере беспокоится не стоит - он всегда будет присутствовать в физической памяти.

В предыдущих статьях мы достаточно подробно разобрали, при каком IRQL вызываются стандартные процедуры (DriverEntry, DriverUnload, DispatchXxx) драйвера.

Кроме того, в DDK в описании каждой функции указано при каком IRQL ее можно вызывать или при каком IRQL она вызывается системой, если это функция обратного вызова (callback). Например, в одной из следующих статей мы будем использовать функцию IoInitializeTimer. В описании этой функции сказано, что процедура, вызываемая системой при срабатывании таймера, выполняется при IRQL = DISPATCH_LEVEL. Это недвусмысленно говорит нам о том, что эта процедура и любая память, к которой она будет обращаться должны быть неподкачиваемыми.

На худой конец, если уж Вы совсем не уверены в текущем IRQL, его можно определить функцией KeGetCurrentIrql таким образом:


 invoke KeGetCurrentIrql
 .if eax < DISPATCH_LEVEL
     ; используем любую память
 .else
     ; используем только неподкачиваемую память
 .endif

6.2 Выделяем память из системного пула

В качестве примера рассмотрим очень простой драйвер SystemModules. Все действо будет происходить в процедуре DriverEntry. Мы быстренько выделим немного подкачиваемой памяти (Вы, несомненно, помните, что DriverEntry работает на IRQL = PASSIVE_LEVEL. Поэтому мы обойдемся подкачиваемой памятью.), что-нибудь полезное в нее запишем, освободим и заставим систему выгрузить драйвер.

Для экономии места я привожу только исходный текст процедуры DriverEntry. Это собственно и есть весь драйвер.


 ;@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.


     and cb, 0
     invoke ZwQuerySystemInformation, SystemModuleInformation, addr p, 0, addr cb

Для начала нам нужно определить, сколько места будет занимать интересующая нас информация. Вызвав ZwQuerySystemInformation так, как показано выше, мы получим STATUS_INFO_LENGTH_MISMATCH (что вполне естественно, т.к. размер переданного буфера равен нулю), а в переменной cb мы получим искомое количество байт. Таким способом можно узнать какой размер буфера необходим. Адрес переменной p, в данном случае, нужен только для нормальной работы этой функции: по нему все равно ничего записано не будет.


     .if cb != 0
         invoke ExAllocatePool, PagedPool, cb

Если размер требуемого буфера не равен нулю, мы выделяем необходимое количество памяти из подкачиваемого пула (об этом говорит первый параметр - PagedPool. Значение NonPagedPool будет означать запрос неподкачиваемой памяти). Функция ExAllocatePool даже проще чем ее аналог режима пользователя HeapAlloc. Всего два параметра. Первый определяет пул: подкачиваемый или неподкачиваемый, второй - размер требуемой памяти. Проще не бывает.


         .if eax != NULL

Если ExAllocatePool вернет ненулевое значение, то это указатель на выделенный буфер.

Когда будете изучать отладочную информачию выводимую драйвером в окно DebugView, обратите внимание на то, что адрес буфера, возвращенный ExAllocatePool, будет кратен размеру страницы. Дело в том, что если размер запрашиваемой памяти больше или равен размеру страницы (а в данном случае размер требуемой памяти значительно больше одной страницы), то выделенная область памяти будет начинаться с новой страницы.


             mov p, eax
             invoke ZwQuerySystemInformation, SystemModuleInformation, p, cb, addr cb

Сохраняем указатель на выделенный буфер в переменной p и вызываем ZwQuerySystemInformation еще раз, передавая ей указатель на буфер и его размер.


             .if eax == STATUS_SUCCESS
                 mov esi, p

Если ZwQuerySystemInformation возвращает STATUS_SUCCESS, то её удовлетворили параметры нашего буфера и теперь он содержит список системных модулей в виде массива структур SYSTEM_MODULE_INFORMATION (определена в файле include\w2k\native.inc).


 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 в цикле, передавая ей каждый раз буфер большего размера, до тех пор, пока размер буфера не окажется достаточным.


                 push dword ptr [esi]
                 pop dwNumModules

В самом первом двойном слове буфера, заполненного ZwQuerySystemInformation, содержится количество структур SYSTEM_MODULE_INFORMATION равное количеству модулей и сразу за ним (двойным словом) начинается их массив. Запоминаем количество модулей в переменной dwNumModules.


                 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 должно как раз хватить.

Обратите внимание на адрес буфера - он не будет кратен размеру страницы, т.к. его размер меньше страницы.


                     invoke memset, pMessage, 0, cb

На всякий случай затрем буфер нулями т.к. мы собираемся заполнить его символьной информацией. Это гарантирует нам, что строки будут завершаться нулями.


                     add esi, sizeof DWORD
                     assume esi:ptr SYSTEM_MODULE_INFORMATION

Пропускаем DWORD содержащий число модулей и регистр esi теперь указывает на первую структуру SYSTEM_MODULE_INFORMATION.


                     xor ebx, ebx
                     .while ebx < dwNumModules

Организуем цикл, повторяющийся dwNumModules раз. В цикле перебираем массив структур SYSTEM_MODULE_INFORMATION и ищем там структуры соответствующие модулям ntoskrnl.exe и ntice.sys.

В многопроцессорной системе модуль ntoskrnl.exe будет иметь имя ntkrnlmp.exe. А в системе с поддержкой PAE - ntkrnlpa.exe и ntkrpamp.exe, соответственно. Здесь я предполагаю, Вы не являетесь счастливым обладателем подобной системы.


                         lea edi, [esi].ImageName
                         movzx ecx, [esi].ModuleNameOffset
                         add edi, ecx

Поля ImageName и ModuleNameOffset содержат полный путь к модулю и относительное смещение имени модуля в пути, соответственно.


                         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 и т.п.


                         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).


                     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

Поскольку перед использованием мы обнулили буфер, то если первый его байт не равен нулю, значит, мы что-то нашли. Выводим содержимое буфера.


                     invoke ExFreePool, pMessage
                 .endif
             .endif
             invoke ExFreePool, p
         .endif
     .endif

Возвращаем выделенную из системных пулов память.


     mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
     ret

Заставляем систему выгрузить драйвер.

Как видите, использование системных пулов даже проще, чем работа с кучами в режиме пользователя. Задача только правильно определить тип пула.

Многие ZwXxx функции экспортируются также библиотекой ntdll.dll режима пользователя и являются простыми переходниками в режим ядра, где и происходит вызов соответствующей функции, которая и проделывает всю полезную работу. Примечательно то, что количество параметров и их смысл полностью совпадают. Из этой ситуации можно извлечь большую выгоду. Поскольку ошибки в ядре приводят к краху системы, можно сначала отладить код в режиме пользователя, а потом перенести его в драйвер с минимальными изменениями, а иногда даже и без изменений. Например, в нашем случае, будучи вызвана из ntdll.dll в режиме пользователя, функция ZwQuerySystemInformation вернет ту же самую информацию, что и одноименная функция, вызванная из ntoskrnl.exe в драйвере. Пользуясь этим нехитрым приемом, можно сэкономить немалое число перезагрузок.

Исходный код драйвера в архиве. Для компиляции требуется последняя версия KmdKit - берите на сайте.

2002-2013 (c) wasm.ru