Слежение за вызовом функций 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):
.text:77F84F40 public ZwWriteFile ; NtDll!ZwWriteFile .text:77F84F40 ZwWriteFile proc near .text:77F84F40 .text:77F84F40 arg_0 = byte ptr 4 .text:77F84F40 .text:77F84F40 mov eax, 0EDh ; NtWriteFile .text:77F84F45 lea edx, [esp+arg_0] .text:77F84F49 int 2Eh .text:77F84F4B retn 24h .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):
; _KiSystemService: .text:00464FCD push 0 ; блок кода, который M$ именует ENTER_SYSCALL macro, ; его можно встретить не только в прологе данной ; функции. Самое интересное то, что, начиная с версии 3.51 ; Windows NT в код данной функции практически не ; вносилось никаких изменений. ; сохраняем основные регистры в стеке .text:00464FCF push ebp .text:00464FD0 push ebx .text:00464FD1 push esi .text:00464FD2 push edi .text:00464FD3 push fs ; загружаем в FS указатель на PCR .text:00464FD5 mov ebx, 30h .text:00464FDA db 66h .text:00464FDA mov fs, bx ; сохраняем в стеке указатель на предыдущую цепочку обработчиков исключений .text:00464FDD push dword ptr ds:0FFDFF000h ; инициализируем новый список обработчиков исключений, так называемых фреймов SEH .text:00464FE3 mov dword ptr ds:0FFDFF000h, 0FFFFFFFFh ; получить адрес структуры текущего потока .text:00464FED mov esi, ds:0FFDFF124h ; сохраняем в стеке адрес предыдущего режима User/Kernel и всё что с ним связано .text:00464FF3 push dword ptr [esi+134h] .text:00464FF9 sub esp, 48h .text:00464FFC mov ebx, [esp+6Ch] .text:00465000 and ebx, 1 .text:00465003 mov [esi+134h], bl ; корректируем текущий стековый фрейм .text:00465009 mov ebp, esp ; сохраняем текущий стековый фрейм .text:0046500B mov ebx, [esi+128h] ; устанавливаем новый стековый фрейм .text:00465011 mov [ebp+3Ch], ebx .text:00465014 mov [esi+128h], ebp .text:0046501A cld ; а вот это уже совсем интересно, проверяется, не отлаживается ли текущий поток, в случае, ; если да, можете сами заглянуть по адресу .text:0046501B test byte ptr [esi+2Ch], 0FFh .text:0046501F jnz loc_464F49 .text:00465025 .text:00465025 loc_465025: .text:00465025 .text:00465025 sti .text:00465026 .text:00465026 loc_465026: .text:00465026 _KiSystemServiceRepeat: ; копируем номер сервиса в регистр edi для дальнейших манипуляций .text:00465026 mov edi, eax .text:00465028 shr edi, 8 .text:0046502B and edi, 30h .text:0046502E mov ecx, edi ; получаем указатель на дескрипторную таблицу потока (заметьте, что каждый поток может ; иметь собственную структуру SERVICE_DESCRIPTOR_TABLE, поскольку структура ; ;_KTHREAD хранит в себе указатель на неё) .text:00465030 add edi, [esi+0DCh] .text:00465036 mov ebx, eax ; ещё одна проверочка, а вдруг данный вызов направлен к драйверу win32k.sys или вообще ; такого сервиса не существует ? .text:00465038 and eax, 0FFFh .text:0046503D cmp eax, [edi+8] .text:00465040 jnb loc_464E02 .text:00465046 cmp ecx, 10h .text:00465049 jnz short loc_465065 ; получить адрес текущего TEB ; далее идёт возня на тему, не GDI ли это вызов? .text:0046504B mov ecx, ds:0FFDFF018h .text:00465051 xor ebx, ebx .text:00465053 or ebx, [ecx+0F70h] .text:00465059 jz short loc_465065 .text:0046505B push edx .text:0046505C push eax .text:0046505D call dword_482220 .text:00465063 pop eax .text:00465064 pop edx .text:00465065 .text:00465065 loc_465065: .text:00465065 ; увеличиваем счётчик системных вызовов, один из счётчиков производительности, ; используется такими утилитами, как Performance Monitor и всякими разными подобного рода .text:00465065 inc dword ptr ds:0FFDFF5DCh ; помещаем в esi указатель на стек пользовательских аргументов. Помните тот самый регистр ; edx в заглушке функции ZwWriteFile библиотеке ntdll.dll? Надеюсь, Вы понимаете, почему в ; данном случае используется механизм передачи параметров через регистровый указатель, а не ; как обычно - через стек? Потому, что после вызова ловушки 0x2e и переключения режимов ; процессор загрузит в регистр SS, то значение, которое до этого было ; заботливо припрятано в сегменте TSS(0x28) и теперь он будет указывать на совершенно ; другой дескриптор в таблице GDT, нежели в пользовательском режиме. Так как данная ; структура одна на все потоки (для обработчика двойной ошибки(_KiTrap08) используется, к ; примеру, свой сегмент TSS, не стоит понимать вышесказанное в буквальном смысле), то после ; переключения режимов происходит донастройка стека относительно конкретного ; выполняемого в данный момент потока. Стеки пользовательского режима и режима ядра ; потока различны, так как если бы было наоборот, то единственное что мы видели бы на экране ; - это BSOD. Кроме того, стеки индивидуальны для конкретного потока. Необходимо, что бы ; Вы чётко представляли себе это. Продолжим разбор кода функции системного сервиса. .text:0046506B mov esi, edx ; получить указатель на таблицу аргументов сервисов KiArgumentTable. .text:0046506D mov ebx, [edi+0Ch]Итак, что бы быть полностью в курсе событий, о которых идёт речь, расскажем немножко об этой самой таблице сервисов и её аргументах. Смотрим на рисунок ниже.
Вот так схематически выглядит наша дескрипторная таблица. Обратите внимание на бледно серые поля в ячейках таблицы. Дело в том, что я выделил жирным ярким шрифтом только те поля, которые представляют для нас действительный интерес. Остальные поля нормально содержат нулевые указатели или нули. Теперь посмотрим, как всё это выглядит на языке С.
Код (Text):
typedef struct _SERVICE_DESCRIPTOR_TABLE { SYSTEM_SERVICE_TABLE NtoskrnlTable; // ntoskrnl.exe (native api) SYSTEM_SERVICE_TABLE Table2; // свободна SYSTEM_SERVICE_TABLE Table3; // используется Internet Information Services SYSTEM_SERVICE_TABLE Table4; // свободна } SERVICE_DESCRIPTOR_TABLE, * PSERVICE_DESCRIPTOR_TABLE, **PPSERVICE_DESCRIPTOR_TABLE; typedef struct _SYSTEM_SERVICE_TABLE { PNTPROC ServiceTable; // указатель на массив точек входа в обработчики PDWORD CounterTable; // счётчик вызовов сервисов (не используется) DWORD ServiceLimit; // количество поддерживаемых сервисов PBYTE ArgumentTable; // указатель на массив аргументов сервисов } SYSTEM_SERVICE_TABLE, * PSYSTEM_SERVICE_TABLE, **PPSYSTEM_SERVICE_TABLE;Кроме данной структуры, открыто экспортируемой модулем Ntoskrnl.exe в WinNT имеется ещё одна, именуемая ServiceDescriptorTableShadow, которая не экспортируется и известна только внутри модуля.
Если имена внутренних функций не экспортируются, то это незначит, что нам не дано их узнать. Существует довольно простой способ получить доступ к ним. Всего-навсего необходимо воспользоваться однимиз отладчиков, входящих в комплект DDK – Kernel 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):
; загружаем в CL соответствующий элемент массива ArgumentTable, ; для того, что бы точно такое же число байт перенести в стек ядра .text:00465070 xor ecx, ecx .text:00465072 mov cl, [eax+ebx] .text:00465075 mov edi, [edi] ; загружаем в регистр EBX точку входа в функцию соответствующего сервиса .text:00465077 mov ebx, [edi+eax*4] .text:0046507A sub esp, ecx ; передача двойными словами .text:0046507C shr ecx, 2 .text:0046507F mov edi, esp ; проверяем, находится ли пользовательский стек в пользовательском ; адресном пространстве? .text:00465081 cmp esi, MmUserProbeAddress ; = 0x7FFFFFFF .text:00465087 jnb loc_465271 .text:0046508D .text:0046508D loc_46508D: .text:0046508D ; передаём параметры в стек вызываемой функции .text:0046508D repe movsd ; и, собственно, когда все приготовления и проверки позади, вызываем сам сервис .text:0046508F call ebx ; Приглядитесь повнимательнее к приведённому коду. Помните, регистр EDX в функции ; NtDll!ZwWriteFile? Подсказка: его содержимое осталось прежним, и это значит, что ; мы им ещё воспользуемся.Что к чему?
Ну, а теперь, о самом главном, о сути нашей статьи. Надеюсь, многие из нас помнят старый добрый 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):
if (*NtBuildNumber == 2195); else //2k if (*NtBuildNumber == 2600); else //XP if (*NtBuildNumber == 3790); else //2003при инициализации(DriverInitialize()) вызываем функцию IoRegisterShutdownNotification(gpDeviceObject) для того, что бы зарегистрировать callback – функцию (DriverShutdown), которую система вызовет непосредственно перед перезагрузкой.
Код (Text):
IoRegisterShutdownNotification(gpDeviceObject); // регистрируем событие завершения работы //системы if (!SetFMonHandler()) // устанавливаем наши перехватчики { IoDeleteDevice (pDeviceObject); return ns; } SystemGetLocalTime(); // спрашиваем время } else IoDeleteDevice (pDeviceObject); if ((ns = IoInitializeTimer(pDeviceObject,&TimerProc,NULL)) == STATUS_SUCCESS) IoStartTimer(pDeviceObject); else IoDeleteDevice (pDeviceObject); } return ns; }Следом за ней вызовом функции SetFMonHandler() создаём и открываем файл, для записи в него протокола перехвата и заодно устанавливаем перехватчики.
Теперь нам необходимо кое-что вспомнить, точнее – регистр EDX. Как я уже упоминал, через него _KiSystemService получает указатель на пользовательский стек. Поскольку содержимое регистра не меняется в течение всех проверок вплоть до инструкции CALL EBX, то, собственно, им мы и воспользуемся для аналогичных целей. Кроме того, необходимо иметь ввиду ещё один момент – индексы функций в таблице SST. Как Вы уже поняли, они различны в различных версиях ядер. Впрочем, если Вам необходимо получить данные соответствия, то, всего-навсего стоит прибегнуть к помощи отладчика или дизассемблера.
Модуль “fmonitor.c”
Функция SetFMonHandler(). Вызовом функции SystemDeleteFile() затираем лог файл на диске, если тот остался от предыдущего запуска драйвера. Затем создаём чистый файл протокола, открываем его и сохраняем описатель. Причем, обратите внимание, что описатель файла должен быть описателем ядра, поскольку в таком случае он не будет привязан к конкретному процессу. Затем, в зависимости от версии ядра правим соответствующие элементы в таблице SDT, путем записи в них адресов наших перехватчиков, предварительно сохранив системные. Ещё один интересный момент:
Код (Text):
_asm mov eax,CR0 _asm mov CR0Reg,eax _asm and eax,0xFFFEFFFF // сбросить WP bit _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):
NTSYSAPI NTSTATUS NTAPI NtCreateFile( OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PLARGE_INTEGER AllocationSize OPTIONAL, IN ULONG FileAttributes, IN ULONG ShareAccess, IN ULONG CreateDisposition, IN ULONG CreateOptions, IN PVOID EaBuffer OPTIONAL, IN ULONG EaLength ); NTSYSAPI NTSTATUS NTAPI NtOpenFile( OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, OUT PIO_STATUS_BLOCK IoStatusBlock, IN ULONG ShareAccess, IN ULONG OpenOptions );Как Вы видите, до некоторых пределов, функции одинаковы. Поскольку нас интересует только структура OBJECT_ATTRIBUTES, то разница в них для нас опциональна. Указатель на данную структуру располагается в стеке третьим по вложенности. Следует код:
Код (Text):
(PBYTE)Stack = (PBYTE)Stack+8; pObjectAttributes = *(PDWORD)Stack;Таким образом, мы выловили указатель на структуру OBJECT_ATTRIBUTES и считайте, что получили имя объекта, который будет открыт. Теперь нам нужно узнать, какой процесс хочет открыть или создать объект? Желательно так же полный путь к исполняемому файлу, аргументы командной строки, идентификатор его процесса и текущщего потока. Для решения первой части вопроса, мы обратимся к структуре описателя процесса PEB, которая в nt-системах располагается по адресу 0x7FFDF000 пользовательского адресного пространства. Но опять же, тут нас может подстерегать неприятный сюрприз: если процесс живёт только режиме ядра(например system), то по данному адресу мы ничего не найдём, кроме того, по данному адресу страницы могут быть и не отображены и тогда мы точно получим BSOD, но, от последнего нас защищает фрейм SEH. Всё же, картина может оказаться малоприятной. Поэтому, мы явно проверим, действительна ли память по данному адресу вызовом функции MmGetPhysicalAddress(), передав в качестве аргумента адрес PEB. Если функция возвращает ненулевое значение (конечно, эту задачу можно было решить и более цивилизованным способом, например, проверить поле *Peb структуры EPROCESS, но, думаю, что поступил проще), значит, мы получим командную строку процесса и аргументы. Далее, я думаю, код понятен. Полученную информацию укладываем в буфер, добавляем время для красоты, перевязываем всё это бантиком и скидываем буфер в файл. Теперь посмотрите на следующий код:
Код (Text):
CurrentThreadPriority = KeQueryPriorityThread(KeGetCurrentThread()); // повышаем KeSetPriorityThread(KeGetCurrentThread(),HIGH_PRIORITY); //приоритет потокаДля чего это было сделано? Надеюсь, Вы понимаете, что функция _KiSystemService является полностью реентерабельной, как и большинство системных функций ядра. То есть, поток в данном месте может быть в любой момент прерван и переключен контекст. Это опасно для нас тем, что мы используем функции для работы с памятью, и инициализируем некоторые структуры, и всё это должно отработать с начала и до конца, что бы достичь должного результата. Если мы берём у системы память, то должны её вернуть. В противном случае в конечном итоге мы столкнёмся с её нехваткой или даже отсутствием, и тогда BSOD нам товарищ. Как известно, потоки, имеющие приоритет реального времени, в режиме ядра не прерываются. Поэтому, мы временно поднимаем приоритет потока то уровня реалтайма и тем самым решаем задачу контроля памяти и гарантии того, что выполнение потока не будет прервано по переключении контекста.
В конечном итоге работы драйвера, в корне диска C будет находиться файл с именем KiSystemService.log. Заглянув в него обыкновенным текстовым редактором можно будет увидеть примерно следующее:
Код (Text):
0 16:38:51 3a4 40c C:\WINNT\Explorer.EXE \Device\{56A954E7-28ED-471A-B406-2936BB2363B3} 1 16:38:52 3a4 40c C:\WINNT\Explorer.EXE \Device\{56A954E7-28ED-471A-B406-2936BB2363B3} 2 16:38:53 3a4 2dc C:\WINNT\Explorer.EXE \??\C:\ 3 16:38:53 3a4 2dc C:\WINNT\Explorer.EXE \??\C:\Program Files\desktop.ini 4 16:38:53 3a4 2dc C:\WINNT\Explorer.EXE \??\C:\Program Files\desktop.ini 5 16:38:53 3a4 2dc C:\WINNT\Explorer.EXE \??\C:\Recycled\desktop.ini 6 16:38:53 3a4 2dc C:\WINNT\Explorer.EXE \??\C:\Recycled\desktop.ini 7 16:38:53 3a4 40c C:\WINNT\Explorer.EXE \Device\{56A954E7-28ED-471A-B406-2936BB2363B3} 8 16:38:54 3a4 42c C:\WINNT\Explorer.EXE \??\C:\ 9 16:38:54 3a4 42c C:\WINNT\Explorer.EXE \??\C:\Documents and Settings\ 10 16:38:54 3a4 42c C:\WINNT\Explorer.EXE \??\C:\Documents and Settings\Администратор\ 11 16:38:54 3a4 42c C:\WINNT\Explorer.EXE \??\C:\Documents and Settings\Администратор\Избранное\desktop.ini 12 16:38:54 3a4 42c C:\WINNT\Explorer.EXE \??\C:\Documents and Settings\Администратор\Избранное\ 13 16:38:54 3a4 42c C:\WINNT\Explorer.EXE \??\C:\ 14 16:38:54 3a4 42c C:\WINNT\Explorer.EXE \??\PIPE\srvsvc 15 16:38:54 3a4 42c C:\WINNT\Explorer.EXE \??\C:\Documents and Settings\Администратор\Избранное\Ссылки 16 16:38:54 3a4 42c C:\WINNT\Explorer.EXE \??\C:\WINNT\Web\folder.htt 17 16:38:54 3a4 42c C:\WINNT\Explorer.EXE \??\C:\WINNT\Web\ 18 16:38:54 3a4 42c C:\WINNT\Explorer.EXE \??\C:\WINNT\Web\folder.htt 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, а так же всем тем людям, благодаря которым существует этой великолепный сайт.
Исходники к статье прилагаются.
Слежение за вызовом функций Native API
Дата публикации 12 ноя 2004