Вступление
Данная статья относится только к операционным системам семейства NT и рассматривает перехват вызовов т.н. "Native API" в них. Перед тем как остановиться на реализации алгоритма перехвата конкретной функции (в нашем случае это будет ZwCreateProcess), необходимо уточнить некоторые моменты.
1. Native API
ОС Windows предлагает разработчикам ПО множество функций, с помощью которых программы могут взаимодействовать с пользователем, аппаратурой, друг с другом и т.п. Набор этих функций известен как Windows API. Эти функции выполняют абстрактные действия и Microsoft обеспечивает их поддержку для будущих ОС. Но фактически, они используют функции NativeAPI, которая не столь "официальна" и характерна только для WindowsNT. Так, например, если программа хочет запустить приложение, она вызывает WinAPI функцию CreateProcess (или fork, если мы говорим о POSIX). Эта функция находится в DLL файлах операционной системы и поставляется Microsoft. Фактически, все что известно об этой функции пользователю - это ее сигнатура (набор входных параметров и их типы). Однако, ничего сверхсекретного в самой функции нет - в ней происходят некоторые элементарные действия по созданию процесса (поиск/открытие/чтение файла, выделение памяти). Но очевидно, что не все действия могут быть выполнены в режиме пользователя ("ring3"), и когда возникает необходимость в выполнении кода в режиме ядра ("ring0") (например для создания виртуального адресного пространства нового процесса, регистрации его в системе), вызываются NativeAPI функции "Nt..." (например "NtCreateSection"). У них есть аналоги режима ядра - "Zw..." (например "ZwCreateSection"). Различие состоит в том, что Zw функции, будучи вызыванными из режима ядра, не производят security-проверок. Из режима пользователя, Zw-функции не вызываются (их адреса в этом случае совпадают с адресами Nt-функций)
2. Взаимодействие с ядром
При вызове функции режима ядра, упрощенно происходят следующие действия: 1) параметры помещаются в стек 2) в регистр EAX помещается номер сервисной функции, которую нужно выполнить 3) с помощью прерывания (инструкция int) или инструкции sysenter происходит переход в режим ядра.
Как только что неявно было замечено, у сервисных функций есть свои идентификаторы. Это есть ни что иное, как номера в таблице реальных адресов сервисных функций. Отсюда следует замечательная возможность перехвата вызовов этих функций по аналогии с "IAT hooking" (изменением адресов в этой таблице).
3. Драйвер
Логично, что из режима пользователя, эта таблица недоступна. Поэтому возникает необходимость написания драйвера. Сама по себе эта тема большая, и, к счастью, есть материалы, достаточно подробно описывающие этот процесс. Я постараюсь лишь дать поверхностное представление о том, что это такое. По сути, драйвер это PE файл, представляющий собой библиотеку функций, которые по мере надобности вызывает ОС. Реализовывать все эти фунцкии, к счастью, необязательно (и наш случай как раз такой). В чем-то, драйвер схож с DLL файлом. Одно из различий состоит, например, в том, что процедуры из файла драйвера ОС может вызывать по "собственному" желанию (а не только по просьбе пользователя) и код драйвера, вообще говоря, при этом исполнятся в контексте того потока, который был активен на момент вызова.
Загрузка (регистрация) драйвера выполняется так же как и установка системной службы (сервиса) (с помощью набора функций Service Control Manager API). За установкой следует запуск. При запуске драйвера, ОС вызывает функцию "DriverEntry". Эта функция должна выполнять действия по его инициализации. В частности, указываются ссылки на необходимые функции, которые поддерживаются драйвером (DriverUnload, обработчики ввода/вывода, открытия/закрытия устройства, и т.п.). Также указывается имя регистрируемого устройства. Пользовательское приложение, которое хочет взаимодействовать с драйвером создает "файл" с помощью функции CreateFile где в качестве имени "файла" указывается имя нашего устройства. Само взаимодействие традиционно осуществляется двумя способами: 1) отправкой контрольного сообщения с ассоциированными буферами (IOCTL). Выглядеть это может так:
function CTL_CODE(DeviceType, Func, Method, Access: DWORD): DWORD;
begin
// ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method)
result:=0 or (devicetype shl 16) or (access shl 14) or (func shl 2) or (method);
end;
function TestSmth(InBuf: Pointer; InBufSize: DWORD): DWORD;
const//задаются пользователем
FIRST_IOCTL_INDEX = $800;
FILE_DEVICE_myDrv = $00008000;
var
IOCTL_TEST_SMTH: DWORD;
test: pointer; //ответ драйвера
begin
test:=0;
result:=0;
IOCTL_TEST_SMTH:=CTL_CODE(FILE_DEVICE_myDrv, $800 + 101, METHOD_BUFFERED, FILE_ANY_ACCESS);
DeviceIoControl(hDevice, IOCTL_TEST_SMTH, InBuf, InBufSize, @test, 4, Result, nil);
end;
2) функциями Read/WriteFile.
Для создания драйвера Windows класса WDM, используется компилятор Microsoft Visual C (c установленным пакетом DDK).
Другой неприятной новостью является то, что код драйвера исполняется в режиме ядра, что черевато следующим:
1) Любая ошибка приводит к остановке системы с синим экраном. Код ошибки является первым шестнадцатиричным числом на экране. Его описание можно найти в файле ntstatus.h. Осюда, кстати, следует правило: чтобы реже видеть синий экран, используйте обработчики исключений (блок try/except)
2) WinAPI функции недоступны. Вам придется пользоваться другим классом функций - NativeAPI.
3) Нужно внимательно относиться к синхронизации (блокировки в режиме ядра могут стать настоящей головной болью).
4) Отладка сложнее (для отладки на локальном компьютере подойдет SoftICE; для отладки по сети можно воспользоваться предлагаемым Microsoft отладчиком ядра). Вы, однако, можете воспользоваться и схемой DbgPrint (для вывода диагностических сообщений) + бесплатная утилита Dbgview (http://www.sysinternals.com/)
4. KeServiceDescriptorTable
ntoskrnl.exe экспортирует важную функцию - KeServiceDescriptorTable. Она возвращает адрес той самой таблицы сервисных функций. Традиционно, работают с ней так:
#pragma pack(1)
typedef struct ServiceDescriptorEntry {
unsigned int *ServiceTableBase;
unsigned int *ServiceCounterTableBase;
unsigned int NumberOfServices;
unsigned char *ParamTableBase;
} ServiceDescriptorTableEntry_t, *PServiceDescriptorTableEntry_t;
#pragma pack()
__declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable;
#define SYSTEMSERVICE(_function) KeServiceDescriptorTable.ServiceTableBase[*(PULONG)((PUCHAR)_function+1)]
#define SYSTEMSERVICEID(_function) KeServiceDescriptorTable.ServiceTableBase[_function]
Т.е. макрос SYSTEMSERVICEID[142] укажет нам на адрес сервисной функции номер 142.