Слежение за вызовом функций Native API

Дата публикации 12 ноя 2004

Слежение за вызовом функций Native API — Архив WASM.RU

ПРЕДИСЛОВИЕ

Ну что ж, пришло время рассказать об одной очень полезной, я бы даже сказал, в некоторых случаях, чрезвычайно необходимой возможности, любезно предоставляемой нам внутренними механизмами Windows. Да, тема уже не нова и порядком избита, так было написано много статей и даже целых книг, вышедших из-под пера таких громких авторов и специалистов, как Джеффри Рихтер, Мэт Питрек, Свен Шрайбер, Марк Руссинович и др.

Да, кстати, спешу заметить, что практическое применение материал статьи будет иметь только в Windows линейки NT. К линейке 9Х это не относится ни коим образом.

Для того, что бы наиболее эффективно усвоить материал статьи, Вам необходимо следующее:

  • Знание языка С
  • Установленная на компьютере Windows NT  2K/XP/2K3)
  • Хотя бы поверхностное представление о процессах, потоках и основных системных структурах Windows NT.
  • Знать, как функционирует процессор x86 в защищенном режиме
  • Иметь комплект DDK(мы будем работать с DDK 2K Build 2195).
  • SoftIce. Я пользуюсь айсом от DriverStudio v2.7. Необходимо взять полную версию студии, а не пользоваться вырипаным айсом, выложенным на  сайте, иначе не оберётесь проблем с совместимостью. Впрочем, в форуме этот вопрос обсуждался уже не раз.

                                                           ЧТО ИМЕЕМ?

Насколько мне довелось просветиться из книг и статей, написанных Рихтером (почему я прикопался именно к Рихтеру? Да потому что его книги и статьи переведены на русский, широко распространены, более доступны, да и более того, его методика является неким стандартом де-факто), автор занимался исследованием и разработкой механизмов перехвата функций API непосредственно в пользовательском режиме. Что же мы имели в этом случае?

А вот что:

Метод

Плюсы

Минусы

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

1. Довольно простая схема реализации (потому как полностью описана в книге Джеффри Рихтера “Windows для профессионалов  4-е издание, 2004, Русская редакция ”). Гарантия невозможности повреждения содержимого адресного пространства случайного процесса.   

 2. В случае ошибки грохается только подопытный процесс/процессы, а не вся система.

1. Необходимость прибегнуть к извращениям типа использования некоторых методов внедрения собственного кода в адресное пространство чужого процесса (установка глобальных хуков (SetWindowsHookEx()), загрузка DLL через реестр, непосредственное внедрение   кода в адресное пространство процесса-кролика (WriteProcessMemory(), CreateRemoteThread()…).

2. Невозможность произвести данные манипуляции с любым процессом, поскольку Win NT снабжает практически все системные объекты дескрипторами безопасности, определяющими возможность и степень владения одного объекта другим.

3. Что, если перехватываемая системная функция А базируется на другой системной функции В и процессу вдруг вздумалось напрямую обратиться к функции В? Придется искать такие вот функции В и тоже рисовать для них перехватчик.

4. Возможность организовать слежение за функциями, вызываемыми только лишь “обработанными” процессами, а не всеми процессами.

Модификация части кода библиотек, содержащих отслеживаемые функции(метод сплайсинга кода, к примеру, путём внедрения в начала функций машинной инструкции Jmp 0xXXXXXXXX на тело нашего обработчика).

Не требуется производить модификацию таблиц импорта модулей процесса. !В Win 9X метод даёт возможность отслеживать обращения к функциям системных библиотек из всех процессов.

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

2.Для осуществления данного метода необходимо каким-то образом модифицировать сегменты кода  библиотек, имеющие атрибуты “ReadExecute”, но, возможность есть.

И, наконец, ещё один вариант – это использование специально предоставляемого Windows отладочного механизма (Debug API). С этим я думаю, и так всё понятно. Кстати, на сайте wasm.ru имеется достаточное количество статей, описывающих все вышеуказанные методы перехвата. Особо стоит отметить статью 90210/HI-TECH, там написано практически всё обо всём.

            А теперь давайте представим себе ситуацию, когда нам нужно “отмониторить”  к примеру сразу все процессы, для того, что бы получить общую картину пользования некоторыми ресурсами и выполняемых над ними операций (создания/открытия/чтения/

записи каких либо файлов, реестра,  изменение атрибутов защиты страниц памяти какого- либо процесса и т.п.). Представили? А теперь внимательно ещё раз посмотрите на таблицу  и скажите, пожалуйста, реально ли это сделать, прибегнув к вышеуказанным методам? Конечно, среди вас найдутся  оптимисты, скажут, одной левой, но, больше чем уверен, это будет бравадой лишь на словах, потому как, в действительности, оценив ситуацию, сказать такое вряд ли будет правильно. Однако есть способ проще, а главное – намного эффективнее. 

                                                           NATIVE API

            Как известно, Windows 2000 , была спроектирована таким образом, что бы дать возможность исполняться не только Win32 приложениям, но так же древним Win16, MsDos, Os/2v 1.x и POSIX. Короче говоря, ось имеет три встроенных по умолчанию подсистемы, каждая из которых исполняет свои образы.  Реально, никто нам не мешает добавить в систему поддержку приложений иных операционных систем, всего лишь нужно написать и прикрутить модуль требуемой подсистемы. И все-таки как  же это работает? Для ответа на данный вопрос посмотрим на рисунок ниже.

 

Если запускаемая программа не является Win32 приложением, а программой другой ос, тогда Win2k будет искать для нее соответствующий образ поддержки, который поймёт и запустит данное приложение. Если не найдёт, то вернёт ошибку соответственно. Поясним данную схему. Любой вызов, к примеру, приложения OS/2 будет конвертирован модулем поддержки данного приложения в вызов соответствующей функции родной библиотеки Win32-подсистемы KERNEL32.DLL. KERNEL32.DLL в свою очередь направит вызов одной из функции модуля NTDLL.DLL

Хотя, например, ничего не мешает Win32 приложению напрямую обратиться к   функциям модуля ntdll.dll или даже минуя последний, напрямую к шлюзу int 0x2e.


  Только, это должно быть очень специфичное приложение и уж конечно оно должно   знать, что делает. Тем не менее, такие программы в WinNT не редкость и именуются  они сервисами, точнее, сервисными приложениями. Примером может служить небезызвестный Svchost.exe. Такие приложения имеют полное право на        существование только в среде WinNT. В Win9x они работать не могут.

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

Код (Text):
  1.  
  2. .text:77F84F40                 public ZwWriteFile                  ; NtDll!ZwWriteFile
  3. .text:77F84F40 ZwWriteFile     proc near
  4. .text:77F84F40
  5. .text:77F84F40 arg_0       = byte ptr  4
  6. .text:77F84F40
  7. .text:77F84F40                 mov  eax, 0EDh           ; NtWriteFile
  8. .text:77F84F45                 lea      edx, [esp+arg_0]
  9. .text:77F84F49                 int      2Eh            
  10. .text:77F84F4B                 retn 24h
  11. .text:77F84F4B ZwWriteFile     endp

Разберём данный фрагмент кода. Функция ZwWriteFile() принимает на стек 9 аргументов размером в 4 байта.

Исчерпывающую информацию почти обо всех функциях Native Api Вы    можете найти в книге Гари Нэббета “Справочник по базовым функциям API Windows NT/2000”, издательский дом “Вильямс”,2002. Но, я подчеркиваю  почти , поскольку  книга писалась во времена Win2K, то, соответственно, в ней описаны   только те функции, которые существовали лишь в данной версии Windows, с выходом новых версий ядер 5.1(WinXP) и 5.2(Win.Net)  было добавлено ещё некоторое количество функций, описания которых я, к сожалению не нашёл.

Тут  стоит сказать ещё об одном важном моменте –именах функций, точнее их назначении. Как Вы заметили, вроде бы с виду одинаковые функции имеют префиксы Ntxxx и  Zwxxx. Если заглянуть дизассемблером в модуль NTDLL.DLL, то особой разницы в этих функциях нет, кроме этих самых префиксов – Nt  и сопоставленные им Zw указывают на один и тот же код. Что касается модуля NTOSKRNL.EXE, то в нем дела обстоят несколько иным образом. Функции, имена которых начинаются с префикса Zw, реально экспортируются данным модулем и вызываются напрямую. Функции, содержащие в своем имени префикс Nt – должны пройти ряд проверок на безопасность, прежде чем управление получат соответствующие функции Zw. Впрочем, советую не слишком ломать голову по данному вопросу, если честно, там сам чёрт ногу сломит.

 Как мы видим, непосредственное участие здесь принимают два регистра. В регистр EAX помещается  некоторый условный номер (индекс) функции (об этом расскажем чуть позже). В регистр EDX помещается указатель на стек переданных функции аргументов за вычетом 4 байт, которые являются ни чем иным, как адресом точки возврата. Затем происходит переключение режима процессора командой вызова прерывания INT 0x2E, зарезервированного как точка входа в обработчики системных сервисов. Вот это как раз то, что нам и нужно. Дело в том, что все вызовы системных функций от приложений в пользовательском режиме (User Mode) в конечном итоге будут направлены к ядру и пройдут через этот самый шлюз INT 0x2E, который и осуществит переключение режима процессора с кольца 3 на кольцо 0, т.е. Kernel Mode. Хорошо, что находится по эту сторону шлюза системного сервиса, мы уже разобрались. Теперь, заглянем за зеркало.

KiSystemService & ServiceDescriptorTable

Код (Text):
  1.  
  2. ; _KiSystemService:
  3. .text:00464FCD  push    0   ; блок кода, который M$ именует ENTER_SYSCALL macro,
  4. ; его можно встретить не только в прологе данной
  5. ; функции. Самое интересное то, что, начиная с версии 3.51
  6. ; Windows NT в код данной функции практически не
  7. ; вносилось никаких изменений.
  8. ; сохраняем основные регистры в стеке
  9. .text:00464FCF  push    ebp
  10. .text:00464FD0  push    ebx
  11. .text:00464FD1  push    esi
  12. .text:00464FD2  push    edi
  13. .text:00464FD3  push    fs
  14. ; загружаем в FS указатель на  PCR  
  15. .text:00464FD5  mov ebx, 30h
  16. .text:00464FDA  db  66h
  17. .text:00464FDA  mov fs, bx
  18. ; сохраняем в стеке указатель на предыдущую цепочку обработчиков исключений
  19. .text:00464FDD  push    dword ptr ds:0FFDFF000h
  20.  
  21. ; инициализируем новый список обработчиков исключений, так называемых фреймов SEH
  22. .text:00464FE3  mov dword ptr ds:0FFDFF000h, 0FFFFFFFFh
  23. ; получить адрес структуры текущего потока
  24. .text:00464FED  mov esi, ds:0FFDFF124h
  25. ; сохраняем в стеке адрес предыдущего режима User/Kernel и всё что с ним связано
  26. .text:00464FF3  push    dword ptr [esi+134h]
  27. .text:00464FF9  sub esp, 48h
  28. .text:00464FFC  mov ebx, [esp+6Ch]
  29. .text:00465000  and ebx, 1
  30. .text:00465003  mov [esi+134h], bl
  31. ; корректируем текущий стековый фрейм
  32. .text:00465009  mov ebp, esp   
  33. ; сохраняем текущий стековый фрейм
  34. .text:0046500B  mov ebx, [esi+128h]
  35. ; устанавливаем новый стековый фрейм
  36. .text:00465011  mov [ebp+3Ch], ebx
  37. .text:00465014  mov [esi+128h], ebp
  38. .text:0046501A  cld
  39. ; а вот это уже совсем интересно, проверяется, не отлаживается ли текущий поток, в случае,
  40. ; если  да, можете сами заглянуть по адресу
  41. .text:0046501B  test    byte ptr [esi+2Ch], 0FFh
  42. .text:0046501F  jnz loc_464F49
  43. .text:00465025
  44. .text:00465025 loc_465025:                            
  45. .text:00465025                                        
  46. .text:00465025  sti
  47. .text:00465026
  48. .text:00465026 loc_465026:                            
  49. .text:00465026    
  50. _KiSystemServiceRepeat:                                    
  51. ; копируем номер сервиса в регистр edi для дальнейших манипуляций
  52. .text:00465026  mov edi, eax
  53. .text:00465028  shr edi, 8
  54. .text:0046502B  and edi, 30h
  55. .text:0046502E  mov ecx, edi
  56. ; получаем указатель на дескрипторную таблицу потока (заметьте, что каждый поток может
  57. ; иметь собственную структуру SERVICE_DESCRIPTOR_TABLE, поскольку структура ; ;_KTHREAD хранит в себе указатель на неё)
  58. .text:00465030  add edi, [esi+0DCh]
  59. .text:00465036  mov ebx, eax
  60. ; ещё одна проверочка, а вдруг данный вызов направлен к драйверу win32k.sys или вообще
  61. ; такого сервиса не существует ?
  62. .text:00465038  and eax, 0FFFh
  63. .text:0046503D  cmp eax, [edi+8]
  64. .text:00465040  jnb loc_464E02
  65. .text:00465046  cmp ecx, 10h
  66. .text:00465049  jnz short loc_465065
  67. ; получить адрес текущего TEB
  68. ; далее идёт возня на тему, не GDI ли это вызов?
  69. .text:0046504B  mov ecx, ds:0FFDFF018h
  70. .text:00465051  xor ebx, ebx
  71. .text:00465053  or  ebx, [ecx+0F70h]
  72. .text:00465059  jz  short loc_465065
  73. .text:0046505B  push    edx
  74. .text:0046505C  push    eax
  75. .text:0046505D  call    dword_482220
  76. .text:00465063  pop eax
  77. .text:00465064  pop edx
  78. .text:00465065
  79. .text:00465065 loc_465065:                            
  80. .text:00465065  
  81. ; увеличиваем счётчик системных вызовов, один из счётчиков производительности,
  82. ; используется такими утилитами, как Performance Monitor и всякими разными подобного рода
  83. .text:00465065  inc dword ptr ds:0FFDFF5DCh
  84. ; помещаем в esi указатель на стек пользовательских аргументов. Помните тот самый регистр
  85. ; edx в заглушке функции ZwWriteFile библиотеке ntdll.dll?  Надеюсь, Вы понимаете, почему в
  86. ; данном случае используется механизм передачи параметров через регистровый указатель, а не
  87. ; как обычно - через стек? Потому, что после вызова ловушки 0x2e и переключения режимов
  88. ; процессор загрузит в регистр SS, то значение, которое до этого было
  89. ; заботливо припрятано в сегменте TSS(0x28) и теперь он будет указывать на совершенно
  90. ; другой дескриптор в таблице GDT, нежели в пользовательском режиме. Так как данная
  91. ; структура одна на все потоки (для обработчика двойной ошибки(_KiTrap08) используется, к
  92. ; примеру, свой сегмент TSS, не стоит понимать вышесказанное в буквальном смысле), то после
  93. ; переключения режимов происходит донастройка стека относительно конкретного
  94. ; выполняемого в данный момент потока. Стеки пользовательского режима и режима ядра  
  95. ; потока различны, так как если бы было наоборот, то единственное что мы видели бы на экране
  96. ; - это  BSOD. Кроме того, стеки индивидуальны для конкретного потока. Необходимо, что бы
  97. ; Вы чётко представляли себе это. Продолжим разбор кода функции системного сервиса.
  98. .text:0046506B  mov esi, edx
  99. ; получить указатель на таблицу аргументов сервисов KiArgumentTable.
  100. .text:0046506D  mov ebx, [edi+0Ch]

 Итак, что бы быть полностью в курсе событий, о которых идёт речь, расскажем немножко об этой самой таблице сервисов и её аргументах. Смотрим на рисунок ниже.

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

Код (Text):
  1.  
  2. typedef struct _SERVICE_DESCRIPTOR_TABLE
  3. {
  4. SYSTEM_SERVICE_TABLE NtoskrnlTable; // ntoskrnl.exe (native api)
  5. SYSTEM_SERVICE_TABLE Table2;            // свободна
  6. SYSTEM_SERVICE_TABLE Table3;            // используется Internet Information Services
  7. SYSTEM_SERVICE_TABLE Table4;            // свободна
  8. }
  9.         SERVICE_DESCRIPTOR_TABLE,
  10.      * PSERVICE_DESCRIPTOR_TABLE,
  11.     **PPSERVICE_DESCRIPTOR_TABLE;
  12.  
  13. typedef struct _SYSTEM_SERVICE_TABLE
  14.     {
  15.  PNTPROC ServiceTable;              // указатель на массив точек входа в обработчики
  16.  PDWORD  CounterTable;              // счётчик вызовов сервисов (не используется)
  17.  DWORD   ServiceLimit;              // количество поддерживаемых сервисов
  18.  PBYTE   ArgumentTable;             // указатель на массив аргументов сервисов
  19. }
  20.         SYSTEM_SERVICE_TABLE,
  21.      * PSYSTEM_SERVICE_TABLE,
  22.     **PPSYSTEM_SERVICE_TABLE;

Кроме данной структуры, открыто экспортируемой модулем Ntoskrnl.exe в WinNT имеется ещё одна, именуемая ServiceDescriptorTableShadow, которая не экспортируется и известна только внутри модуля.

Если имена внутренних функций не экспортируются, то это незначит, что нам не дано их узнать. Существует довольно простой способ получить доступ к ним. Всего-навсего необходимо воспользоваться однимиз отладчиков, входящих в комплект DDKKernel Debugger(i386kd.exe),   WinDbg(windbg.exe) либо моим любимым SoftIce, но, предварительно установив  отладочные символы, которые можно слить с сайта мелкомягких. Кроме того,  если Вы все-таки стянули отладочные символы (смотреть здесь

http://www.microsoft.com/whdc/devtools/debugging/symbolpkg.mspx ), а весят они не

мало, для дизассемблирования и анализа бинарников используйте IDA, которая

при загрузке соответствующего плагина (меню Edit->Plugins->Load PDB File или

Ctrl+F12) без труда разберётся с сопоставленным файлом .PDB.

 В Windows 2000 найти её можно непосредственно сразу за структурой ServiceDescriptorTable. Однако не все версии Windows NT следуют данному правилу. Интересна данная структура тем, что, кроме поля NtoskrnlTable, второе поле имеет не нулевые значения, а содержит указатель на структуру SystemServiceTable, ссылающуюся на массив точек входа  функций GDI, предоставляемых нам драйвером Win32k.sys. Как известно, в WinNT, графика ‘сокрыта’ в ядре, для ускорения соответствующих процессов. Но, в большинстве своём случаев, на этот факт мало обращают внимание, поскольку  перехват графических функций опять же мало кому интересен.

Теперь поподробнее расскажем о данных структурах. Как мы видим, структура KeServiceDescriptorTable включает в себя четыре совершенно одинаковых структуры SystemServiceTable. Значение имеет только первая из них, которую я условно именную NtoskrnlTable. Структура SystemServiceTable опять же в свою очередь состоит из 4х полей. Поясним их.

ServiceTable – указатель на массив указателей на точки входа в обработчики системных сервисов. Интересен тот факт, что одни точки входа сервисов указывают на реально существующие и экспортируемые модулем Ntoskrnl.exe функции, а другие – нет.

 Например, функция NtCreateProcess не экспортируется, а точка входа, тем не менее, указывает на некоторый код, который и  реализует последнюю. Поэтому, возникают сложности с реализацией данной функции внутри, к примеру, разрабатываемого Вами драйвера, поскольку мы не имеем возможности ни воспользоваться готовым экспортом, ни каким либо простым универсальным интерфейсом, кроме опять же того, что предоставляется нам функцией KiSystemService. Но, NtCreateProcess – это лишь вершина айсберга, большая часть подготовительных операций, связанных с созданием процесса сокрыто внутри модулей Kernel32.dll и Ntdll.dll. И это не единственная функция подобного рода. Так что, если Вам загорелось реализовать подобную операцию внутри драйвера, придётся сначала сесть и как следует разобраться с тем, как это делает Win NT. Задача данная далеко нетривиальна, и потребует серьёзных знаний и усидчивости, кроме того, всё усугубляется ещё и тем, что код может быть сильно размазан по различным модулям, а не локализован где то внутри определённой третьей функции. Думайте что хотите, но в Windows 9х операция по созданию процесса была чётко определена в режиме ядра и выражена определённой функцией, “экспортируемой” драйвером Shell.vxd – это _ShellExecute. Собственно, на вопрос, почему Майкрософт поступила так в случае с Windows NT, мне в голову приходит только один ответ – в целях обеспечения безопасности системы.

CounterTable указатель на переменную ядра, используемую как счетчик количества обращений к KiSystemService. Имеет значение только в отладочной версии. В свободной версии он всегда равен NULL.

ServiceLimit количество функций-сервисов, реализуемых данной версией Windows NT. К примеру в Windows 2000 build 2195 это число 248(0xF8), а в Windows XP build 2600 уже 284(0x11C). Как видите, их количество растет.

ArgumentTable – указатель на массив байтов, указывающих на количество аргументов, передаваемых функции в байтах. То есть, если функция NtAcceptConnectPort принимает на стек 0x18 байт, получается 6 двойных слов. Индекс двойного слова в массиве ServiceTable сопоставляется элементу с тем же индексом в таблице ArgumentTable.

Кроме вышесказанного, не стоит оставлять без внимания ещё одну немалоинтересную деталь. Как Вы уже смогли заметить, в структуре ServiceDescriptorTable при самом наилучшем раскладе занятыми могут быть только две структуры SystemServiceTable (кроме Ntoskrnl.exe, вторая может быть занята сервисами IIS). Естественно, остаётся ещё две, которые могут быть отданы пользователю для добавления в систему собственных сервисов, путём вызова функции KeAddSystemServiceTable, описанной в DDK.

Так, думаю, здесь всё достаточно ясно. Продолжим разбор кода _KiSystemService.

Код (Text):
  1.  
  2. ; загружаем в CL соответствующий элемент массива ArgumentTable,
  3. ; для того, что бы точно такое же число байт перенести в стек ядра
  4. .text:00465070  xor ecx, ecx
  5. .text:00465072  mov cl, [eax+ebx]
  6. .text:00465075  mov edi, [edi]
  7. ; загружаем в регистр EBX точку входа в функцию соответствующего сервиса
  8. .text:00465077  mov ebx, [edi+eax*4]
  9. .text:0046507A  sub esp, ecx
  10. ; передача двойными словами
  11. .text:0046507C  shr ecx, 2
  12. .text:0046507F  mov edi, esp
  13. ; проверяем, находится ли пользовательский стек в пользовательском
  14. ; адресном пространстве?
  15. .text:00465081  cmp esi, MmUserProbeAddress         ; = 0x7FFFFFFF
  16. .text:00465087  jnb loc_465271
  17. .text:0046508D
  18. .text:0046508D loc_46508D:                            
  19. .text:0046508D        
  20. ; передаём параметры в стек вызываемой функции                                  
  21. .text:0046508D  repe    movsd
  22. ; и, собственно, когда все приготовления и проверки позади, вызываем сам сервис
  23. .text:0046508F  call    ebx
  24. ; Приглядитесь повнимательнее к приведённому коду. Помните, регистр EDX в функции
  25. ; NtDll!ZwWriteFile? Подсказка: его содержимое осталось прежним, и это значит, что
  26. ; мы им ещё воспользуемся.

Что к чему?

Ну, а теперь, о самом главном, о сути нашей статьи.  Надеюсь, многие из нас помнят старый добрый MsDos? Точнее, прерывания и их перехват, путём внедрения в таблицу прерываний указателя на собственную функцию-обработчик с дальнейшей передачей управления оригинальной функции, а может быть и после выполнения последней. Ну, на этот случай я советую Вам обратиться к  Зубкову или Финогенову. Но, это не суть важно. Дос давно умер, а правила остались те же самые. И так, если хорошо приглядеться к вышеприведенному листингу, можно узреть метод «номер один»: поправить соответствующий дескриптор 0x2e в таблице IDT, указав смещение нашего обработчика взамен оригинального. Но, нам он не подойдет, потому как в таком случае придётся брать обработку исключений и проверку корректности переданных аргументов на себя. Лучше воспользуемся методом «номер два». К тому же, ранее бывши уже описанным  Марком Руссиновичем и Брюсом Когсвеллом, а чуть позже о нём написал в своей книге Свен Шрайбер. Некоторые моменты в данной книге мне не понравились, к примеру, слишком сложная реализация драйвера перехватчика, не преследующего никаких конкретных целей и, неоговоренность до конца реализации некоторых интереснейших деталей, играющих ключевую роль в драйвере. Впрочем, это сугубо моё личное мнение, а в целом, книга достаточно информативна и интересна. Тем не менее, в данной статье я решил упростить сложное, сказать о главном и подчеркнуть необходимые и немаловажные детали. Нечто вроде, мини-конспекта, содержимое которого преследует цели чисто практической реализации задачи перехвата функций native api.)

            Так вот, суть сего метода сводится к тому, что бы подменить оригинальные указатели на функции в таблице KiServiceTable указателями на наши функции-перехватчики. Тогда, первыми управление будем получать именно мы, и, выполнив свою задачу, отдадим оригинальные функции системе (или может сначала система сделает свои дела, а потом уже мы свои, в общем, тут уж как заблагорассудится). В таком случае, мы получаем доступ к аргументам перехваченных функций и можем делать с ними всё, на что хватит нашей фантазии. К тому же, подчеркну ещё раз то, что через нас «пройдут» все вызовы почти из всех потоков процессов. (кроме системных ядерных потоков, в том числе Iddle. Последние обычно предпочитают обращаться к функциям напрямую, минуя _KiSystemService.) Но, опять же,  тут необходимо кое-что уточнить. Для того, что бы получить доступ к структуре ServiceDescriptorTable, используем экспортируемую модулем Ntoskrnl.exe переменную KeServiceDescriptorTable, которая является указателем на данную структуру. Однако заметьте, что код функции _KiSystemService поступает иным образом: указатель на данную структуру он выуживает в одном из полей структуры _KTHREAD, но, тем не менее, инициализируется значением той самой экспортируемой переменной. В этом есть свой плюс – можно создать свою структуру SystemServiceTable, инициализировать её соответствующими значениями, загрузить в соответствующее поле структуры потока адрес последней и таким образом уйти от преследования мониторящего драйвера. Но, поскольку действие это имеет смысл только в режиме ядра, то соответственно, должно осуществляться только драйвером режима ядра. Тем не менее, драйвер-монитор может предусмотреть подобный разворот событий со стороны процесса. Тут уж все зависит от разработчиков по обе стороны баррикад.

                                                          

                                                                  Реализация

            Итак, перейдём непосредственно к делу. В нашем драйвере мы реализуем перехват двух достаточно интересных функций: NtOpenFile и NtCreateFile. Они интересны тем, что, переведя всё это на язык объектной абстракции, получается, что это не просто некие функции, а методы, применимые к большинству объектов системы. Таким образом, методы применимы к тем объектам системы, над данными которых они могут выполняться. А это, как Вы понимаете, файлы, устройства, пайпы и т.п. Для того, чтобы сделать заготовку драйвера, я применил генератор шаблонов с компакт-диска к книге Свена Шрайбера (Как им пользоваться, написано в книге). Достаточно удобный и понятный. Далее, генерируем шаблон, указав путь и слово mfilemon в качестве имени проекта. Кстати, не забудьте правильно инициализировать некоторые поля в файле w2k_wiz.ini, иначе, компилятор может объявить бойкот драйверу.  (Впрочем, для более детальной информации, можете прямо сейчас обратиться к исходным текстам драйвера, находящимся в архиве, приложенном к статье.) Исходный текст состоит из нескольких модулей, наиболее интересный, где, собственно, и располагается основной код – fmonitor.c. Я не буду приводить весь код в данной статье, а поясню лишь самые важные и интересные моменты реализации драйвера.

Модуль “mfilemon.c”

Алгоритм работы драйвера следующий: первым делом проверяем версию ядра, сможем ли мы работать?

Код (Text):
  1.  
  2.         if (*NtBuildNumber == 2195);  else  //2k   
  3.         if (*NtBuildNumber == 2600);  else  //XP
  4.         if (*NtBuildNumber == 3790);  else  //2003

при инициализации(DriverInitialize()) вызываем функцию IoRegisterShutdownNotification(gpDeviceObject) для того, что бы зарегистрировать  callback – функцию (DriverShutdown),  которую система вызовет непосредственно перед перезагрузкой.

Код (Text):
  1.  
  2. IoRegisterShutdownNotification(gpDeviceObject);  // регистрируем событие завершения работы
  3.                              //системы
  4. if (!SetFMonHandler())                       // устанавливаем наши перехватчики
  5. {
  6.     IoDeleteDevice (pDeviceObject);
  7.     return ns;
  8. }                  
  9. SystemGetLocalTime();                        // спрашиваем время
  10. }
  11. else IoDeleteDevice (pDeviceObject);
  12.  
  13. if ((ns = IoInitializeTimer(pDeviceObject,&TimerProc,NULL)) == STATUS_SUCCESS) IoStartTimer(pDeviceObject);
  14.   else IoDeleteDevice (pDeviceObject);
  15.  }
  16.    return ns;
  17.  } 

Следом за ней вызовом функции SetFMonHandler() создаём и открываем файл, для записи в него протокола перехвата и заодно устанавливаем перехватчики.

Теперь нам необходимо кое-что вспомнить, точнее – регистр EDX. Как я уже упоминал, через него _KiSystemService получает указатель на пользовательский стек. Поскольку содержимое регистра не меняется в течение всех проверок вплоть до инструкции CALL EBX, то, собственно, им мы и воспользуемся для аналогичных целей. Кроме того, необходимо иметь ввиду ещё один момент – индексы функций в таблице SST. Как Вы уже поняли, они различны в различных версиях ядер. Впрочем, если Вам необходимо получить данные соответствия, то, всего-навсего стоит прибегнуть к помощи отладчика или дизассемблера.

Модуль “fmonitor.c”

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

Код (Text):
  1.  
  2. _asm mov eax,CR0
  3. _asm mov CR0Reg,eax
  4. _asm and eax,0xFFFEFFFF     // сбросить WP bit
  5. _asm mov cr0, eax

Это так называемая защита от модификации ядерных страниц. При сброшенном бите WP, в режиме ядра возможна модификация  пользовательских страниц, имеющих атрибут readonly и системных страниц памяти без какой-либо генерации #GP. Если данный бит поднят, то при записи в системные страницы памяти, имеющие атрибут защиты от записи, будет сгенерированно исключение #GP, и это в свою очередь приведёт к BSOD. Поскольку считается, что была произведена попытка разрушить системные данные.

Объясним. Я не имею представления, как это работало в windows nt4, в windows 2000+, ядро может организовываться на страницах размером как 4 кб, так и 4 Мб. Всё зависит от того, какой объём оперативной памяти непосредственно доступен системе. В случае windows 2000 при наличии оперативной памяти 128 Мб и выше, эти страницы имеют размер уже 4Мб. В Windows XP и Windows 2003 Server барьер поднят уже до 256 Mб. Итак, драйвер(.sys), да и практически все исполняемые модули Windows NT имеют формат PE. А, как известно, секции внутри данного файла выровнены по границе  4кб. В случае, если размер системной страницы памяти составляет 4 Мб, защита отключается (сбрасывается бит WP регистра CR0), поскольку, сами понимаете, в данную страницу необходимо уместить как код, так и данные, к тому же ещё и не одного драйвера. А уж данные в редких случаях не модифицируются. (Кроме того, если принять размер страницы равным 4 Мб, то это будет довольно большим плюсом. Вопрос касается буферов ассоциативной трансляции (TLB). Как известно, TLB хранит отдельно записи для страниц кода, страниц данных, и так же страниц, различных по размеру. В случае 4 Мб-х страниц, пространство буферов TLB расходуется более бережно за счет меньшего количества записей и это в свою очередь ведёт пусть и к незначительному, но всё же увеличению производительности процессора.) Поскольку мы модифицируем данные таблицы SST, то, соответственно, рискуем напороться на защиту и уронить систему. Поэтому, мы и воспользовались несколько ”варварским”, как считают многие (собственно, не понимаю, почему он варварский, некоторые системные функции используют этот метод, да и что может случиться за то время, пока мы поправим табличку и установим защиту на место, кроме того, утилита “FileMon” Марка Руссиновича применяет данный метод и ничего страшного не происходит), способом, что бы снять защиту и модифицировать нужный нам участок памяти ядра. Ну, я думаю, с этим вопросом мы разобрались.

Осталось рассмотреть наиболее, на мой взгляд, важный момент.

Функция WriteToLogFile(PVOID Stack). Итак, функция принимает в качестве параметра указатель на пользовательский стек аргументов, тот самый указатель, что передается нам в регистре EDX. Заметьте, что перехватчик использует один и тот же код для обеих NtCreateFile и NtOpenFile функций, поскольку структура их стеков почти полностью идентична. Код функции заключен в блок __try __except. Надёюсь, Вы понимаете, для чего это было сделано? Поскольку мы имеем дело с функциями (RtlUnicodeStringToAnsiString()), которые в свою очередь работают с памятью, рано или поздно здесь может возникнуть исключение. По крайней мере, когда я отлаживал код под Windows 2000, ничего не происходило и всё работало без проблем. Как только приступил к испытаниям драйвера под Windows XP, то очень скоро был “приятно удивлён” BSODом. В принципе, я не долго соображал что к чему, и в конечном итоге решил снабдить функцию фреймом SEH. Что меня и спасло. Почему ядро WinXP повело себя подобным образом, только одна догадка – код ядра был облегчен, убраны различного рода обработчики фреймов SEH, и задача по реализации последних была переложена на плечи разработчиков драйверов. Следующим рассмотрим стеки функций NtCreateFile и NtOpenFile.

Код (Text):
  1.  
  2. NTSYSAPI
  3. NTSTATUS
  4. NTAPI
  5. NtCreateFile(
  6.   OUT PHANDLE                   FileHandle,
  7.   IN ACCESS_MASK                DesiredAccess,
  8.   IN POBJECT_ATTRIBUTES     ObjectAttributes,
  9.   OUT PIO_STATUS_BLOCK      IoStatusBlock,
  10.   IN PLARGE_INTEGER         AllocationSize OPTIONAL,
  11.   IN ULONG                      FileAttributes,
  12.   IN ULONG                      ShareAccess,
  13.   IN ULONG                      CreateDisposition,
  14.   IN ULONG                      CreateOptions,
  15.   IN PVOID                      EaBuffer    OPTIONAL,
  16.   IN ULONG                      EaLength );
  17.  
  18. NTSYSAPI
  19. NTSTATUS
  20. NTAPI
  21. NtOpenFile(
  22.   OUT PHANDLE             FileHandle,
  23.   IN ACCESS_MASK          DesiredAccess,
  24.   IN POBJECT_ATTRIBUTES   ObjectAttributes,
  25.   OUT PIO_STATUS_BLOCK   IoStatusBlock,
  26.   IN ULONG                  ShareAccess,
  27.   IN ULONG                  OpenOptions );

Как Вы видите, до некоторых пределов, функции одинаковы. Поскольку нас интересует только структура  OBJECT_ATTRIBUTES, то разница в них для нас опциональна. Указатель на данную структуру располагается в стеке третьим по вложенности. Следует код:

Код (Text):
  1.  
  2. (PBYTE)Stack = (PBYTE)Stack+8;
  3. pObjectAttributes = *(PDWORD)Stack;

Таким образом, мы выловили указатель на структуру OBJECT_ATTRIBUTES и считайте, что получили имя объекта, который будет открыт. Теперь нам нужно узнать, какой процесс хочет открыть или создать объект? Желательно так же полный путь к исполняемому файлу, аргументы  командной строки, идентификатор его процесса и текущщего потока. Для решения первой части вопроса, мы обратимся к структуре описателя процесса PEB, которая в nt-системах располагается по адресу 0x7FFDF000 пользовательского адресного пространства. Но опять же, тут нас может подстерегать неприятный сюрприз: если процесс живёт только режиме ядра(например system), то по данному адресу мы ничего не найдём, кроме того, по данному адресу страницы могут быть и не отображены и тогда мы точно получим BSOD, но, от последнего нас защищает фрейм SEH. Всё же, картина может оказаться малоприятной. Поэтому, мы явно проверим, действительна ли память по данному адресу вызовом функции MmGetPhysicalAddress(), передав в качестве аргумента адрес PEB. Если функция возвращает ненулевое значение (конечно, эту задачу можно было решить и более цивилизованным способом, например, проверить поле *Peb структуры EPROCESS, но, думаю, что поступил проще), значит, мы получим командную строку процесса и аргументы. Далее, я думаю, код понятен. Полученную информацию укладываем в буфер, добавляем время для красоты,  перевязываем всё это бантиком и скидываем буфер в файл. Теперь посмотрите на следующий код:

Код (Text):
  1.  
  2. CurrentThreadPriority = KeQueryPriorityThread(KeGetCurrentThread());
  3.         // повышаем
  4. KeSetPriorityThread(KeGetCurrentThread(),HIGH_PRIORITY);
  5.         //приоритет потока

 Для чего это было сделано? Надеюсь, Вы понимаете, что функция _KiSystemService является полностью реентерабельной, как и большинство системных функций ядра. То есть, поток в данном месте может быть в любой момент прерван и переключен контекст. Это опасно для нас тем, что мы используем функции для работы с памятью, и инициализируем некоторые структуры, и всё это должно отработать с начала и до конца, что  бы достичь должного результата. Если мы берём у системы память, то должны её вернуть. В противном случае в конечном итоге мы столкнёмся с её нехваткой или даже отсутствием, и тогда BSOD нам товарищ. Как известно, потоки, имеющие приоритет реального времени, в режиме ядра не прерываются. Поэтому, мы временно поднимаем приоритет потока то уровня реалтайма и тем самым решаем задачу контроля памяти и гарантии того, что выполнение потока не будет прервано по переключении контекста.

В конечном итоге работы драйвера, в корне диска C будет находиться файл с именем KiSystemService.log. Заглянув в него обыкновенным текстовым редактором можно будет увидеть примерно следующее:

Код (Text):
  1.  
  2. 0    16:38:51   3a4   40c   C:\WINNT\Explorer.EXE   \Device\{56A954E7-28ED-471A-B406-2936BB2363B3}
  3. 1    16:38:52   3a4   40c   C:\WINNT\Explorer.EXE   \Device\{56A954E7-28ED-471A-B406-2936BB2363B3}
  4. 2    16:38:53   3a4   2dc   C:\WINNT\Explorer.EXE   \??\C:\
  5. 3    16:38:53   3a4   2dc   C:\WINNT\Explorer.EXE   \??\C:\Program Files\desktop.ini
  6. 4    16:38:53   3a4   2dc   C:\WINNT\Explorer.EXE   \??\C:\Program Files\desktop.ini
  7. 5    16:38:53   3a4   2dc   C:\WINNT\Explorer.EXE   \??\C:\Recycled\desktop.ini
  8. 6    16:38:53   3a4   2dc   C:\WINNT\Explorer.EXE   \??\C:\Recycled\desktop.ini
  9. 7    16:38:53   3a4   40c   C:\WINNT\Explorer.EXE   \Device\{56A954E7-28ED-471A-B406-2936BB2363B3}
  10. 8    16:38:54   3a4   42c   C:\WINNT\Explorer.EXE   \??\C:\
  11. 9    16:38:54   3a4   42c   C:\WINNT\Explorer.EXE   \??\C:\Documents and Settings\
  12. 10   16:38:54   3a4   42c   C:\WINNT\Explorer.EXE   \??\C:\Documents and Settings\Администратор\
  13. 11   16:38:54   3a4   42c   C:\WINNT\Explorer.EXE   \??\C:\Documents and Settings\Администратор\Избранное\desktop.ini
  14. 12   16:38:54   3a4   42c   C:\WINNT\Explorer.EXE   \??\C:\Documents and Settings\Администратор\Избранное\
  15. 13   16:38:54   3a4   42c   C:\WINNT\Explorer.EXE   \??\C:\
  16. 14   16:38:54   3a4   42c   C:\WINNT\Explorer.EXE   \??\PIPE\srvsvc
  17. 15   16:38:54   3a4   42c   C:\WINNT\Explorer.EXE   \??\C:\Documents and Settings\Администратор\Избранное\Ссылки
  18. 16   16:38:54   3a4   42c   C:\WINNT\Explorer.EXE   \??\C:\WINNT\Web\folder.htt
  19. 17   16:38:54   3a4   42c   C:\WINNT\Explorer.EXE   \??\C:\WINNT\Web\
  20. 18   16:38:54   3a4   42c   C:\WINNT\Explorer.EXE   \??\C:\WINNT\Web\folder.htt
  21. 19   16:38:54   3a4   42c   C:\WINNT\Explorer.EXE   \??\C:\WINNT\Web\

Да, кстати, для того, что бы запустить драйвер, используйте утилиту KmdManager из пакета KmdKit от Four-F.

Ну вот, всё, что было важно, уже рассмотрено и объяснено. Теперь,  осталось дело за Вами. Предложения и замечания высылайте на мой адрес troguar@yandex.ru.

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

Исходники к статье прилагаются.

 


0 1.532
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532