Вступление
Данная статья относится только к операционным системам семейства 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.

5. ZwCreateProcess
Собственно эту функцию (ZwCreateProcess) мы и будем перехватывать. Но у нас с самого начала возникнут трудности: компоновщик во-первых скажет, что он о ней ни чего не знает (она проэкспортирована в ntdll.dll), а если и скомпилировать драйвер с ntdll.lib, то он просто не запустится (по крайней мере так было у меня). Тем не менее, это несерьезная проблема: что мешает нам передать номер этой функции при загрузке драйвера из пользовательской программы? (это делать крайне желательно, так как номера функций могут меняться и меняются в различных версиях ОС или даже при установке ServicePack'ов)
Далее: взглянем на декларацию этой функции:

typedef NTSTATUS (*ZWCREATEPROCESS)(
OUT PHANDLE phProcess,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN HANDLE hParentProcess,
IN BOOLEAN bInheritParentHandles,
IN HANDLE hSection OPTIONAL,
IN HANDLE hDebugPort OPTIONAL,
IN HANDLE hExceptionPort OPTIONAL
);

Т.е. на входе мы имеем только hParentProcess, ObjectAttributes (которые всегда не заданы) и hSection. Возникает вопрос: а как мы узнаем имя программы, которую хотят запустить? На самом деле, мне не известно "легальных" способов того, как это сделать. Чтобы понять почему, достаточно взглянуть на прототип процедуры CreateProcess (из книги Garry Nebbet'а "Windows NT/2000 Native API Reference"). Все верно: командная строка запускаемого приложения нам не передается. Фактически все, что мы о нем знаем, это его образ, спроецированный в память (hSection). Способы восстановления хотя бы имени образа (не говоря уж о параметрах командной строки) по hSection мне не известны. Как видно даже из указанного прототипа, эти параметры инициализируются после вызова ZwCreateProcess (но, кстати, перед вызовом ZwCreateThread). Проблема может быть решена довольно некрасивым способом: с помощью хардкодинга получим адрес командной строки, сконвертированной в юникод при вызове CreateProcess. Непосредственно в процедуре фильтрации рекомендуется не перегружать стек (желательно иметь максимум одну переменную). Также, как показала практика, недопустимо использование вставок ассемблера ("__asm"). Проблема решается вызовом собственных процедур и динамическим выделением памяти (ExAllocatePoolWithTag) под большие структуры.
Итак, вот процедура, которая выведет нам имя запускаемого приложения (протестировано на XP SP(1/2), XP, 2k):
int DoMyHack()
{
int rc=256;
int j, stack;
__try
{
{
__asm
{
mov stack, esp
push eax
mov eax, stack
mov eax, [eax] //load value from current stack location to eax...
mov stack, eax //and consider it as a previous (before calling DoMyHack) stack location
add eax, 96
mov eax, [eax] //load the address of user-mode stack
mov j, eax
add eax, 16 //16 for XP and 2kSp3, but 12 for 2kSp<3
mov eax, [eax]
mov rc, eax
pop eax
}
DbgPrint("CMDLine: %S", rc);
}
}__except(EXCEPTION_EXECUTE_HANDLER)
{
DbgPrint("Access Violation while hardcoding!!!");
}
return rc;
}


На ум может придти еще один полулегальный вариант: с помощью функции ZwQueryInformationProcess, мы получим указатель на структуру, содержащую крайне полезную информацию о процессе - Process Environment Block (или PEB. см. ntdll.h для описания недостающих структур). Что характерно, в режиме пользователя этот указатель можно получить тривиальным способом:

PPEB MyPeb
__asm{
push eax
mov eax, fs:[0x18]
mov eax, [eax+0x30]
mov MyPEB, eax
pop eax
}


Далее. У этой структуры есть элемент "Ldr". "Ldr.InLoadOrderModuleList.FLink" ссылается на связный список с элементами типа LDR_DATA_TABLE_ENTRY, которые, в свою очередь, содержат замечательный элемент "FullDllName". Первый элемент в этом связном списке соответсвует нашему исполняемому файлу. Однако, на момент вызова ZwCreateProcess эта структура еще не будет заполнена даже этим именем.

Функция фильтрации в итоге может выглядеть так:

NTSTATUS NTAPI NewZwCreateProcess(OUT PHANDLE phProcess, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes,
IN HANDLE hParentProcess, IN BOOLEAN bInheritParentHandles, IN HANDLE hSection OPTIONAL, IN HANDLE hDebugPort OPTIONAL,
IN HANDLE hExceptionPort OPTIONAL)
{
int rc = 0xc0000005;
DoMyHack();
if (условие) rc=((ZWCREATEPROCESS)(OriginalHooks.OldNtCreateProcess)) (phProcess, DesiredAccess, ObjectAttributes, hParentProcess, bInheritParentHandles, hSection, hDebugPort, hExceptionPort);
return rc;
};


6. Замечания
В приведенном выше примере есть на самом деле существенные недостатки.

Во-первых, командной строки может и не быть, либо она может содержать только лишь параметры а не всю строку целиком (т.е. например вместо '"application.exe" /P' мы получим просто '/P'). Поэтому, разумно будет получать не только командную строку, но и имя приложения в чистом виде (т.е. 'application.exe'). Для этого нам нужно будет в DoMyHack использовать сдвиги не 16/12 (для WinXP/2kSP<3), а 12/8

Во-вторых, есть общая проблема с функцией DbgPrint. Если ее вызвать в виде DbgPrint("%S", PSomeUnicodeString);, то она будет честно пытаться интерпретировать память по адресу PSomeUnicodeString как строку в юникоде и остановится как только дойдет до символа с кодом 0 (в юникоде это будут два подряд идущих байта с кодом 0, первый из которых четный по счету). Но трагедия состоит в том, что этот теминатор (0) выставляется далеко не всегда, и поэтому вообще говоря запись DbgPrint("%S", ..) является преступлением. Вы должны обеспечить наличие этого символа-терминатора. В противном случае, функция будет читать и читать до тех пор, пока не выйдет за границы страницы и, тем самым, вызовет PAGE_FAULT с синим экраном.

В-третьих, есть тонкость с ExAllocatePoolWithTag. Она не имеет ни какого отношения к разобранному примеру, но о ней лучше еще раз напомнить. Каждый знает, что адрес выделенного пула (полученный от ExAllocatePoolWithTag) надо сохранять и передавать в ExFreePool. Если вы этот адрес немного измените (скажем увеличите байт на 8), то с очень большой вероятностью все будет работать замечательно (ExFreePool не будет против такой подмены). Однако в долгосрочной перспективе, скорее всего пойдут мистические синие экраны в самых неожиданных местах (при вызове ExAllocatePoolWithTag например).

В-четвертых, настоятельно рекомендуется проверять результаты возвращаемых функций на STATUS_SUCCESS. Т.е. не должно быть кода вроде ZwCreateSection(); ZwMapViewOfSection(); Должно быть что-то вроде IF ZwCreateSection(), THEN ZwMapViewOfSection();. Ситуация, при которой ZwCreateSection() не срабатывает совсем не экзотическая.


7. Ссылки
Пример перехвата NativeAPI из книги Prasad Dabak'а (там же есть и сама книга): http://www.windowsitlibrary.com/Content/356/06/1.html

Руководства по написанию драйверов, примеры а также книги по сопутствующей тематике (на русском): http://club.shelek.com/index.php

Еще материалы (на русском): http://gl00my.chat.ru





Copyright
При перепечатке ссылка на оригинал обязательна.


Top100 Rambler's Top100
Hosted by uCoz