Статья ASM. Драйверы WDM (3) – Пример Legacy драйвера

Ознакомьтесь с предыдущими частями «Введение в IRQL» и «Объекты драйверов».
Пробежавшись по макушкам фундаментальных основ, пришло время заняться практикой. Начнём, пожалуй, с Legacy, которых и по сей день водится с избытком в современных ОС. Как упоминалось ранее, они отличаются от WDM тем, что не поддерживают технологию Plug&Play, хотя в остальном нисколько не уступают последним. Другими словами, Legacy требуют ручной установки, не дружат с программным управлением питания, ну и так по мелочам. На Win7 можно было в меню «Диспетчера устройств» выбрать «Показать скрытые», и получить увесистый их список. Но начиная с Win10 единую базу убрали, а девайсы распихали по соответствующим пунктам, спрятав от нас подальше. Таким образом тема не-PnP-драйверов актуальна, а потому стоит уделить ей внимание.

DevMgr.png

Обосновано-ли использование ассемблера для драйверов?
Несмотря на то, что львиная доля ядра Ntoskrnl.exe написана на чистом Си и плюсах, это всё-же языки высокого уровня. В системе достаточно мест, где они не смогут справиться с задачей по определению, например прямое взаимодействие с регистрами ЦП. Поэтому отдельные части диспетчера памяти и планировщика потоков написаны именно на ассме, где он и выражает себя в полной мере. Ну и конечно это размеры бинарей на выходе из компилятора, которые будут как-минимум в 2 раза меньше сишных. Но есть и минусы – отсутствует реализованный на стороне компилятора сборщик мусора, автоанализ ошибок, заголовки с описанием структур, и прочее.

Кстати, как утверждает руководитель отдела Azure в Microsoft М.Руссинович, начиная с Win11 разрабы отказываются уже и от сишки, в пользу самобытного языка Rust. Написанные на нём модули можно найти по суффиксу(_rs) в именах, например так:

Код:
C:\>cd windows\system32
C:\Windows\System32> dir win32k*  <--------// или:  dir *_rs*

 Том в устройстве C имеет метку SYSTEM
 Серийный номер тома: 5C91-038E
 Содержимое папки C:\Windows\System32

21.11.2019  08:24    3 126 272 win32k_rs.sys  <------//

C:\Windows\System32>


В этой части:
  1. Структура драйвера
  2. Точка входа DriverEntry()
  3. Базовые процедуры DispatchRoutine
  4. Ожидание команд от юзера DeviceIoControl()
  5. Выводы


1. Структура файлов *.sys

Формат двоичных файлов драйвера Win ничем не отличается от привычных нам исполняемых *.exe, поскольку у них один прародитель РЕ (Portable-Executable). Если вскрыть их в любом РЕ-вьювере, можно обнаружить те-же секции кода/данных/импорта/экспорта и прочих. Просто в поле «Subsystem» опционального заголовка выставляется флаг «Native=1».

Код:
;//---- Subsystem --------

IMAGE_SUBSYSTEM_UNKNOWN                   = 0   ;// Неизвестная подсистема
IMAGE_SUBSYSTEM_NATIVE                    = 1   ;// Драйверы устройств и собственные процессы Win
IMAGE_SUBSYSTEM_WINDOWS_GUI               = 2   ;// Подсистема графического интерфейса (GUI)
IMAGE_SUBSYSTEM_WINDOWS_CUI               = 3   ;// Подсистема консоли Windows (CUI)
IMAGE_SUBSYSTEM_EFI_APPLICATION           = 10  ;// Приложение EFI (Extensible Firmware Interface)
IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER   = 11  ;// Драйвер EFI с загрузочными службами
IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER        = 12  ;// Драйвер EFI со службами времени выполнения
IMAGE_SUBSYSTEM_EFI_ROM                   = 13  ;// Образ EFI ROM
IMAGE_SUBSYSTEM_XBOX                      = 14  ;// Xbox
IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION  = 16  ;// Загрузочное приложение Windows.

Второй момент – это текстовые строки. Дело в том, что ядро понимает только Unicode, хотя бывают и редкие исключения (например теги в пулах памяти, или отладочные сообщения). В приложениях юзера ANSI-строки адресуются явно, а здесь нужно заворачивать их в структуру UNICODE_STRING, и передавать получателю адрес этой структуры:

C-подобный:
struct UNICODE_STRING
   Length          dw  0  ;// реальная длина строки
   MaximumLength   dw  0  ;// длина строки +2 (терминальный нуль)
   Buffer          dq  0  ;// указатель на строку в памяти
ends

В ядре имеются более 50 функций, которые ожидают в аргументах линки на данную структуру – найти их в отладчике можно по маске x nt!*UnicodeString*. Это сравнение строк, преобразование из Unicode в ANSI, или из текста в число, и многое другое. Но в данном случае нам понадобится лишь RtlInitUnicodeString(), которая заполнит поля структуры валидными значениями:

Код:
0: kd> x nt!*UnicodeString*

fffff800`02cddd00  nt!RtlInitUnicodeString
fffff800`02f84b90  nt!RtlCreateUnicodeString
fffff800`02f84c38  nt!RtlFreeUnicodeString

fffff800`02fef770  nt!RtlUnicodeStringToAnsiString
fffff800`02fb55c8  nt!RtlAnsiStringToUnicodeString

fffff800`02f676b8  nt!RtlIntegerToUnicodeString
fffff800`02f69ec0  nt!RtlUnicodeStringToInteger
fffff800`02fb5d8c  nt!RtlInt64ToUnicodeString

fffff800`02f67010  nt!RtlCompareUnicodeString
fffff800`02ce3340  nt!RtlCopyUnicodeString
fffff800`02f679f0  nt!RtlHashUnicodeString
.............
0: kd>

Ну и в третьих – это невыгружаемые в своп критически важные секции кода/данных, а у остальных иногда выставляется атрибут «Discardable» (не нужны после загрузки образа в память). Например релоки загрузчик использует для правки абсолютных адресов, а по окончании этого процесса к секции .reloc никто не обращается от слова совсем. Чтобы она не болталась без дела в памяти, её без последствий можно просто затереть новыми данными. Аналогичная ситуация и с секцией INIT, которая представляет собой запакованный архив – после анпака можно без зазрения совести отправить её в корзину.

DrvSections.png

На запрос !dh (dump header) отладчик сбрасывает в лог РЕ-заголовок файла, ожидая аргументом базу образа в памяти (см. lmsm), при этом ключ /f оставляет в логе только информацию о заголовке, /s только секции, а в дефолте (без ключей) и то и другое:

Код:
0: kd> lmsm m i*
start              end                 module name
fffff880`03bcf000  fffff880`03bed000   i8042prt   (deferred)
fffff880`0161b000  fffff880`01625000   IaNVMeF    (deferred)
fffff880`00e89000  fffff880`00e91000   intelide   (deferred)
fffff880`00e40000  fffff880`00e4a000   iusb3hcs   (deferred)

0: kd> !dh /f fffff880`03bcf000

FILE HEADER VALUES
    8664  machine (х64)
       8  number of sections
4A5BC11D  time date stamp Tue Jul 14 04:19:57 2009
      F0  size of optional header
      22  characteristics
             Executable
             App can handle >2gb addresses

OPTIONAL HEADER VALUES
     20B  magic #
    9.00  linker version
   15600  size of code
    4400  size of initialized data
   18070  address of entry point

         ----- new -----
00010000  image base
    1000  section alignment
     200  file alignment
       1  subsystem (Native)    <----------------//
    6.01  operating system version
   1E000  size of image
     400  size of headers
   25E29  checksum

0000000000040000 size of stack reserve
0000000000001000 size of stack commit
0000000000100000 size of heap reserve
0000000000001000 size of heap commit

0: kd>


2. Точка входа DriverEntry()

Процедура DriverEntry() первой получает управление от диспетчера ввода-вывода.
Её код должен проделать следующие операции:
  1. IoCreateDevice() – создать подчинённое устройство.
  2. IoCreateSymbolicLink() – зарегистрировать устройство в пространстве имён, чтобы можно было найти его по имени.
  3. В поле «DriverUnload» структуры DRIVER_OBJECT записать указатель на процедуру выгрузки драйвера из памяти.
  4. В массив «IRP_MJ_xx» структуры DRIVER_OBJECT записать линки на процедуры обслуживания запросов на в/в (выборочно).
  5. Если всё прошло успешно, в регистре EAX вернуть диспетчеру значение STATUS_SUCCESS=0.
Здесь нужно отметить, что только Legacy драйвера создают устройство прямо на точке-входе, а для WDM предусмотрена спец.процедура AddDevice() (рассмотрим в сл.части). Связано это с тем, что диспетчер PnP проверяет обязательное поле в структуре DRIVER_OBJECT. DriverExtension.AddDevice, где и должен лежать указатель на процедуру добавления устройства (создав, она сразу аттачит девайс в стек драйверов). Но сейчас мы во власти Legacy и драйвер у нас монолитный, а потому AddDevice() не нужна.

Код:
0: kd> !drvobj i8042prt 3
Driver object (fffffa8002ad9e70) is for: \Driver\i8042prt
Device object list:   fffffa8002adc640,  fffffa8002aae3d0

DriverEntry:   fffff88001033070  i8042prt!GsDriverEntry
DriverStartIo: fffff8800101d6f8  i8042prt!I8xStartIo
DriverUnload:  fffff8800102fae0  i8042prt!I8xUnload
AddDevice:     fffff8800102ef80  i8042prt!I8xAddDevice  <--------//

Поскольку DriverEntry() выполняется в контексте системного потока «System», она не должна занимать много времени, и проделав на одном дыхании 5 этих пунктов, тут-же отчитаться перед диспетчером. С этого момента ОС отправляет дров в пул неактивных потоков в наборе «WorkingSet», и он даже не попадает в очередь планировщика. То-есть наступает полный штиль, до прихода на его имя запроса IRP (io-request-packet).

У процедуры 2 аргумента: в первый диспетчер передаёт линк на созданную им где-то структуру DRIVER_OBJECT, а во второй – указатель на куст в реестре, куда дров может сохранить свои настройки (пока нам не нужен). Вот пример реализации сказанного:

C-подобный:
format   PE native
entry    DriverEntry
include  'win32ax.inc'
include  'equates\wdm32.inc'

;//*******************
section '.data' data readable writeable notpageable  ;//<---- атрибут невыгружаемой секции

DevName    du  '\Device\Codeby',0   ;// имя устройства (unicode)
linkName   du  '\??\Codeby',0       ;// ссылка на него

pDevName   UNICODE_STRING
plinkName  UNICODE_STRING

pDevObj    dd  0     ;// IoCreateDevice() вернёт сюда указатель на DEVICE_OBJECT
Status     dd  0     ;// переменная под статус операции

;//*******************
section '.text' code readable executable notpageable

proc  DriverEntry, pDrvObject, pRegPath

;// Оформляем строки в юникод
       invoke  RtlInitUnicodeString, pDevName,  DevName
       invoke  RtlInitUnicodeString, plinkName, linkName
      cinvoke  DbgPrint,<'Init string   OK!',0>

;// Cоздаём вирт. псевдо-устройство
       invoke  IoCreateDevice,[pDrvObject],0,pDevName,\
                              FILE_DEVICE_UNKNOWN,\
                              FILE_DEVICE_SECURE_OPEN,0,pDevObj
       or      eax,eax
       je      @f
       mov     [Status],eax
      cinvoke  DbgPrint,<'CreateDevice error:  %08x',0>,eax
       jmp     @exit
@@:   cinvoke  DbgPrint,<'CreateDevice  OK!  %08x',0>,[pDevObj]

;// Cоздаём ссылку на устройство в пространстве имён
       invoke  IoCreateSymbolicLink, plinkName, pDevName
       or      eax,eax
       je      @f
       mov    [Status],eax
      cinvoke  DbgPrint,<'Create link ERROR! %08x',0>,eax
       jmp     @exit
@@:   cinvoke  DbgPrint,<'Create link   OK!',0>

;// Регистрируем нужные процедуры Dispatch
       mov     esi,[pDrvObject]
       mov    [esi + DRIVER_OBJECT.DriverUnload] , CodebyUnload
       mov    [esi + DRIVER_OBJECT.IRP_MJ_CREATE], CodebyCreate
       mov    [esi + DRIVER_OBJECT.IRP_MJ_CLOSE] , CodebyClose
       mov    [esi + DRIVER_OBJECT.IRP_MJ_DEVICE_CONTROL],CodebyControl

;// Инициализация закончена - посылаем диспетчеру Ok!
       mov    [Status],STATUS_SUCCESS
@exit: mov     eax,[Status]
       ret
endp

Как видим нет ничего особенного – главное обзавестись инклудом с описанием полей в структурах, от чего я вас избавил прицепив его в скрепку к статье. Теперь если запустить утилиту WinObj из пакета «SysinternalsSuite» Руссиновича (у меня Explorer от Four-F), можно с удивлением обнаружить в ней наш девайс «Codeby». По этому имени позже мы сможем открыть его на чтение/запись юзер-функцией CreateFile().

WinObj.png


3. Процедуры обслуживания запросов DispatchRoutine

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

Такой расклад приводит к тому, что отлавливать баги приходится по несколько дней, и хорошо, если это даст положительный результат. Ловля блох методом исключений даёт отличный профит на практике, т.е. добавили первый функционал – потестили, потом второй ..третий, исключая таким образом потенциальных виновников. Именно по этой тропе мы и проследуем с вами.

За некоторым исключением, почти все процедуры принимают от диспетчера 2 аргумента – это указатель на структуру DEVICE_OBJECT устройства, а так-же указатель на пакет запроса IRP, который собственно и адресован драйверу. В последнем хранится буквально вся информация о запросе на ввод-вывод:

Код:
struct _IRP
   Type                  dw  0
   Size                  dw  0
   Reserved              dd  0   
   MdlAddress            dq  0
   Flags                 dq  0
   AssociatedIrp         dq  0
   ThreadListEntry       LIST_ENTRY
   IoStatus              IO_STATUS_BLOCK
   RequestorMode         db  0
   PendingReturned       db  0
   StackCount            db  0
   CurrentLocation       db  0
   Cancel                db  0
   CancelIrql            db  0
   ApcEnvironment        db  0
   AllocationFlags       db  0
   UserIosb              dq  0
   UserEvent             dq  0
   Overlay               dq  2 dup(0)
   CancelRoutine         dq  0
   UserBuffer            dq  0
   DriverContext         dq  4 dup(0)
   Thread                dq  0
   AuxiliaryBuffer       dq  0
   ListEntry             LIST_ENTRY
   CurrentStackLocation  dq  0     ;<-----// IO_STACK_LOCATION
   OriginalFileObject    dq  0
ends

Прочитав поле CurrentStackLocation получим линк на одноимённую структуру, где можно найти запрашиваемые коды операций Major/Minor, и что более важно – это параметры текущего запроса. Для них выделяется блок размером в 32-байта, а содержимое блока напрямую зависит от одного из 28-ми кодов IRP_MJ_xx. В своей процедуре DriverEntry() я договорился с диспетчером, что буду обслуживать всего три базовых запроса Create/Close/IoControl, а потому меня будут интересовать только их параметры.

Код:
struct IO_STACK_LOCATION
   MajorFunction      db  0   <----//
   MinorFunction      db  0   <----//
   Flags              db  0
   Control            db  0
   Reserved           dd  0
   Parameters         db  32 dup(0) ------//------+
   DeviceObject       dq  0                       |
   FileObject         dq  0                       |
   CompletionRoutine  dq  0                      ///
   Context            dq  0                       |
ends                                              |
                                                  |
0: kd> dt _io_stack_location Parameters. –v       V

   +0x008 Parameters  : union <unnamed-tag>, 38 elements, 0x20 bytes

      +0x000 Create             : struct <unnamed-tag>, 5 elements, 0x20 bytes
      +0x000 Read               : struct <unnamed-tag>, 3 elements, 0x18 bytes
      +0x000 Write              : struct <unnamed-tag>, 3 elements, 0x18 bytes
      +0x000 DeviceIoControl    : struct <unnamed-tag>, 4 elements, 0x20 bytes

      +0x000 DeviceCapabilities : struct <unnamed-tag>, 1 elements, 0x08 bytes
      +0x000 ReadWriteConfig    : struct <unnamed-tag>, 4 elements, 0x20 bytes
      +0x000 FileSystemControl  : struct <unnamed-tag>, 4 elements, 0x20 bytes
.........

0: kd> dt _io_stack_location Parameters.DeviceIoControl.
   +0x008 Parameters
      +0x000 DeviceIoControl
         +0x000 OutputBufferLength  : Uint4B
         +0x008 InputBufferLength   : Uint4B
         +0x010 IoControlCode       : Uint4B
         +0x018 Type3InputBuffer    : Ptr64

0: kd> dt _io_stack_location Parameters.Create.
   +0x008 Parameters
      +0x000 Create
         +0x000 SecurityContext  : Ptr64
         +0x008 Options          : Uint4B
         +0x010 FileAttributes   : Uint2B
         +0x012 ShareAccess      : Uint2B
         +0x018 EaLength         : Uint4B

0: kd> dt _io_stack_location Parameters.Read.
   +0x008 Parameters
      +0x000 Read
         +0x000 Length      : Uint4B
         +0x008 Key         : Uint4B
         +0x010 ByteOffset  : LARGE_INTEGER
0: kd>

Самая простая (и важная) процедура из всех – это Unload().
Как и следует из её названия, она озадачена выгрузкой образа драйвера из системной памяти. Код процедуры должен отменить всё, что было проделано внутри DriverEntry(). Если в своём примере я создавал ссылку при помощи IoCreateSumbolicLink(), значит должен затереть её чз IoDeleteSumbolicLink(), и т.д.

Кстати помимо официального способа выгрузки драйверов (посредством регистрации процедуры Unload) есть ещё и топорный – просто возвращаем диспетчеру в/в STATUS_DEVICE_CONFIGURATION_ERROR=0xC0000182, и он сам правильно разрулит ситуацию. Обратите внимание, что все процедуры DispatchXX обязательно должны быть выровнены в памяти на границу 16-байт:

C-подобный:
align  16
proc   CodebyUnload, pDrvObject
       invoke  IoDeleteSymbolicLink, plinkName
       invoke  IoDeleteDevice, [pDevObj]

      cinvoke  DbgPrint,<'Unload driver OK!',0>
       mov     eax,STATUS_SUCCESS
       ret
endp

Здесь я планирую обрабатывать только запросы IRP_MJ_CREATE/CLOSE/CONTROL – это стандартный минимум для Legacy драйверов, хотя в WDM обязательны ещё POWER+PNP. Когда софт пользователя откроет девайс «Codeby» чз CreateFile(), диспетчер пошлёт IRP в наш коллбек MJ_CREATE, а у драйвера появится возможность отловить этот момент. Дальнейшие действия зависят уже от назначения и функциональных особенностей драйвера, я-же просто выведу мессагу DbgPrint(). Аналогично и с процедурой MJ_CLOSE, только она перехватывает юзерскую CloseHandle() при освобождении дескриптора устройства.

На выходе нужна функция IoCompleteRequest() – сигнал диспетчеру, что «в Багдаде всё спокойно» и мы успешно обработали запрос. В модели WDM её аргумент IO_NO_INCREMENT=0 указывает, до какого значения нужно поднять приоритет запроса IRQL следующему драйверу в стеке. Здесь мы передаём нуль, т.е. оставить прежний (возможные варианты см.в скрепке).

C-подобный:
align  16
proc   CodebyCreate, pDevObj, pIrp
      cinvoke  DbgPrint,<'Dispatch Create! IRP: %08x',0>,[pIrp]
       invoke  IoCompleteRequest, [pIrp], IO_NO_INCREMENT
       mov     eax,STATUS_SUCCESS
       ret
endp
;//----------------
align  16
proc   CodebyClose,  pDevObj, pIrp
      cinvoke  DbgPrint,<'Dispatch Close!  IRP: %08x',0>,[pIrp]
       invoke  IoCompleteRequest, [pIrp], IO_NO_INCREMENT
       mov     eax,STATUS_SUCCESS
       ret
endp

Если заглянуть сейчас во-вьювер «DeviceTree», он покажет паспорт нашего мини-драйвера, в том числе: базу загрузки образа, его размер в памяти, обрабатываемые коды IRP_MJ_xx, отсутствие процедуры AddDevice(), и факт его монолитности, поскольку ни к кому не приаттачен в стеке драйверов.

osrDrv.png


4. Ожидание команд от юзера DeviceIoControl()

И вот со скоростью черепахи мы добрались до основной процедуры драйвера CodebyControl() – я специально оставил её на дессерт. Она реагирует на вызов DeviceIoControl() юзера, и ожидает в аргументе код самой операции IOCTL. Этот красавец с мускулатурой бодибилдера способен на многое, и является основным интерфейсом для взаимодействия с драйверами из пользовательского режима. Вот прототип этой Win32-API:

C++:
BOOL DeviceIoControl (
   HANDLE  hDevice             ;// дескриптор открытого устройства от CreateFile()
   DWORD   dwIoControlCode     ;// код IOCTL операции для выполнения
   LPVOID  lpInBuffer          ;// указатель на буфер, для передачи доп.данных драйверу
   DWORD   nInBufferSize       ;//   ..размер этого буфера
   LPVOID  lpOutBuffer         ;// указатель на буфер, для приёма ответа от драйвера
   DWORD   nOutBufferSize      ;//   ..размер этого буфера
   LPDWORD lpBytesReturned     ;// указатель на переменную, куда вернётся кол-во принятых байт
   LPOVERLAPPED lpOverlapped   ;// структура для асинхронной операции (Null)
);

После успешного вызова этой API, драйверу придёт такая инфа:

Код:
0: kd> dt _io_stack_location Parameters.DeviceIoControl.
   +0x008 Parameters
      +0x000 DeviceIoControl
         +0x000 OutputBufferLength  : Uint4B
         +0x008 InputBufferLength   : Uint4B
         +0x010 IoControlCode       : Uint4B  <--------//
         +0x018 Type3InputBuffer    : Ptr64

Особого внимания заслуживает здесь 32-битный код операции, который состоит аж из шести частей.
В старшей половине с битами [31:16] кодируется тип устройства, которому адресуется запрос. В системе зарегано 64 типа с константами FILE_DEVICE_xx. Если мы обращаемся к одному из них, то старший бит(31) в коде запроса должен быть сброшен (см.рис.ниже). Если-же это придуманный нами какой-то левый девайс, то соответственно бит нужно будет взвести. В данном случае, при создании устройства чз IoCreateDevice() я указал системную константу FILE_DEVICE_UNKNOWN=0х22, и соответственно обращаться к нему должен именно под этим ником в коде IOCTL.

C++:
NTSTATUS IoCreateDevice(
  [in]   PDRIVER_OBJECT  DriverObject,
  [in]   ULONG           DeviceExtensionSize,
  [in]   PUNICODE_STRING DeviceName,
  [in]   DEVICE_TYPE     DeviceType,    <------// FILE_DEVICE_UNKNOWN см. DriverEntry() выше
  [in]   ULONG           DeviceCharacteristics,
  [in]   BOOLEAN         Exclusive,
  [out]  PDEVICE_OBJECT  *DeviceObject
);

Таким образом, чтобы обратиться к своему устройству «Codeby» из юзермоды, в аргументе DeviceIoControl() я должен указать IOCTL=0x00220004, что подразумевает DEVICE_UNKNOWN, неограниченные права ANY_ACCESS, код операции 0x01, и метод передачи BUFFERED. Иначе диспетчер в/в не сможет найти адресат, и в лучшем случае тупо проигнорирует наш запрос. Имейте в виду, что драйверы плохо относятся к самодеятельности и народному творчеству, а потому лучше всегда придерживаться общих правил на уровне каждого бита.

ioctl.png

В своём коллбеке CodebyControl() и беру из структуры IO_STACK_LOCATION переданный мне юзером код операции, и по его значению могу выполнять различные задачи. В данном случае юзер посылает только 0x01, но если учитывать разрядность всего поля 11-бит, то получаем 2^11=2048 возможных вариантов. Текущее значение IRQL я беру здесь напрямую из структуры процессора PCR, хотя можно было вызвать функцию ядра KeGetCurrentIrql(). На системах х32 доступ к этой структуре открывает сегментный регистр fs, а на системах х64 регистр gs:

C-подобный:
align  16
proc   CodebyControl, pDevObj, pIrp  ;// 0x220004
       mov     esi,[pIrp]
       mov     esi,[esi + _IRP.CurrentStackLocation]
       lea     esi,[esi + IO_STACK_LOCATION.Parameters]
       mov     eax,[esi + PARAM_IO_CONTROL.IoControlCode]

       mov     ebx,[fs:KPCR.Irql]

      cinvoke  DbgPrint,<'Dispatch IoControl',10,\
                         'IOCTL: %08x  IRP: %08x  IRQL: %d',0>,eax,[pIrp],ebx

       invoke  IoCompleteRequest, [pIrp], IO_NO_INCREMENT
       mov     eax,STATUS_SUCCESS
       ret
endp

Собрав всё вместе получаем следующую картину где видно, что драйвер благополучно похукал вызовы всех Win32API на стороне юзера Create/IoControl/Close, после чего диспетчер выгрузил его тушку из системной памяти ядра. Обратите внимание на адрес пакета IRP 0x81e0b140 – в рамках трёх операций внутри одного процесса диспетчер не удаляет его, а тупо перезаписывает содержимое новыми данными.

xpTest.png


5. Заключение

По некоторым причинам я проводил тесты на вирт.машине WinXP x32, хотя переделать исходник под х64 не составит особого труда. Тут главной целью было донести до читателя суть, а реализация уже дело второе. Позже рассмотрим организацию драйверов WDM, работу с памятью, и физическими устройствами, а пока интриги больше нет – расходимся. В скрепку положил пример для компиляции ассемблером FASM, инклуды с основными структурами ядра для х32/64, а так-же софт, который использовал в данной статье. Всем удачи, пока!
 

Вложения

  • Soft_Includes.ZIP
    461,5 КБ · Просмотры: 5

Pavel Shuhray

Green Team
14.12.2016
19
5
BIT
59
На самом деле я Rust учил, он не показался особенно сложным, но и не вдохновляет, бюрократия (вдохновляет Factor)
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!