Kernel-Mode & User-Mode Сommunication, или KeUserModeCallback Must Die! — Архив WASM.RU

Все статьи

Kernel-Mode & User-Mode Сommunication, или KeUserModeCallback Must Die! — Архив WASM.RU

Как мне уже удалось убедиться, проблема, когда нужно достаточно быстро вызвать User Mode функцию из драйвера, который, как известно работает в более привилегированном Kernel Mode, возникает довольно часто. Например, может возникнуть необходимость давать указания драйверу по управлению внешним устройством на основании текущего состояния устройства. При этом драйверу может требоваться директива из приложения пользователя всякий раз когда он обрабатывает прерывание своего устройства. Разумеется, большинство таких задач можно решить "традиционными" методами, например, в самом грубом случае, передавая в драйвер матрицу 2 на N(где N - количество значений, пробегаемое аргументом), которая будет полностью задавать функцию, которую нужно вызвать из кода пользователя. Однако встречаютя ситуации, когда таким "прямым" способом ограничиться не получается… Пытаясь найти возможность "обрабатывать прерывания User-Mode кодом"(как кто-то выразился на Форуме wasm.ru), я многое перепробовал, и, в один прекрасный момент, уже совсем отчяившийся, я решил просто посмотреть, какой(с каким кодом) синий экран я увижу если попробую выполнить одну простую вещь… В результате я таки синего экрана в тот раз не увидел, а код, который по моему тогдашнему мнению должен был вызвать какую-нибудь ошибку защиты спокойно сработал, как-будто нет вообще разницы между нулевым и третьим кольцом.

Перед изложением решения своей проблемы(на самом деле, там не сильно много рассказывать) я бы хотел немного рассказать о том, как я пытался найти это решение. Мне достаточно быстро попалась статья года этак 1997 как раз по этой теме. Вы ее сами легко найдете, если еще не видели. В ней рассказывалось про два пути решения: один основан на использовании APC(Asynchronous Procedure Call), другой - на использовании недокументированной функции KeUserModeCallback из модуля ntoskrnl. Также там было написано, что второй метод значительно более быстрый, и, поскольку мне важна была скорость вызова, я решил во что бы то ни стало разобраться с этой функцией. Я сразу написал письмо автору статьи с просьбой рассказать что-нибудь дельное о KeUserModeCallback, и сразу же получил ответ… Ответ был в таком духе: "Это было уже почти 10 лет назад! Я вообще тогда этим занимался так, для души, и сейчас уже ничего не помню, Сорри." Кстати, ответ был прислан на русском: Несколько времени позже, перебирая подряд в Google все страницы по слову KeUserModeCallback, я наткнулся на статью, уже года 2003, того же автора, и уже на его родном русском языке. Он в ней писал, как его преследуют, даже до сих пор, читатели той статьи 97 года: Говорил, что и сейчас(2003 год) по 2 письма в месяц(или одно в два месяца - не важно) стабильно получает, даже от русских программистов(интересно чего здесь есть удивительного:? )… В общем мои разбирательства с работой KeUserModeCallback остановились на никак не объяснимой ошибке ACCESS_VIOLATION при вызове функции. Однако, можно утверждать, что способ, который я теперь использую, должен работать быстрее, поскольку он не требует вызова каких-либо других функций. Например, как мне удалось выяснить, User-Mode функция, вызываемая с помощью KeUserModeCallback должна завершаться вызовом NtCallbackReturn, а это все - тоже время…

А теперь, собственно, решение. То, что тогда, как мне казалось, должно было вызвать сбой защиты, как минимум - при возврате в код драйвера, заключалось в следующем. Я просто-напросто понизил IRQL до PASSIVE_LEVEL(если бы вызов производился из потока, уровень прерывания которого PASSIVE_LEVEL, то этого делать было бы не нужно), далее - перешел в контекст пользовательского процесса с помощью функции KeStackAttachProcess, и, наконец, просто-напросто вызвал с помощью команды call eax нужную функцию, адрес которой(вместе с контекстом процесса) предварительно получил через IOCTL. Потом, естественно нужно выполнить вызов KeUnstackDetachProcess и повысить IRQL до исходного значения. Вызывать пользовательские функции таким образом можно практически из любого места драйвера: и из DispatchIoControl Routine, и из потока, выполняющегося с REAL_TIME_PRIORITY, и, даже прямо из обработчика прерывания. Правда, в последнем случае, если прерывание происходит достаточно часто, то возникает такая ситуация, когда при обработке данного прерывания оно не успевает восстановить IRQL до DIRQL, и прерывается прерывается следующим. В этом случае получаем синий экран с ошибкой INVALID_PROCESS_ATTACH_ATTEMPT.

Тут нужно отметить, что во всех руководствах по написанию драйверов можно увидеть правило, говорящее о том, что категорически нельзя понижать IRQL ниже исходного уровня, с которым работает данная Routine. Это может привести к рассинхронизации Системы, и, в конце-концов - к Синему экрану. Ситуация, про которую я написал выше(про понижение с DIRQL до PASSIVE_LEVEL) с точки зрения авторов книг по драйверам вообще совершенно "дикая". Но, даже такой, совершенно "дикий" код может стабильно работать при некоторых определенных условиях. Тут можно вспомнить аналогию из жизни: если Вы сильно спешите, то Вы можете перебежать дорогу на красный свет, хотя это вроде бы как "запрещено", Вы выигрываете так необходимое Вам время, причем в некоторых случаях риск поплатиться за такое действие пренебрежимо мал… В рассмативаемом здесь примере User-Mode функция вызывается из Routines, работающих только с PASSIVE_LEVEL IRQL, поэтому явно документально запрещенных действий здесь не производится.

В заключение я напишу кое-какие пояснения к исходным кодам, которые прилагаются к статье. Рабочий пример состоит из драйвера и консольного приложения, которое его устанавливает и запускает. В приложении находится функция, которая вызывается драйвером. Драйвер вызывает эту функцию из DispatchIoControl Routine, DispatchClose Routine, и из DriverUnload Routine. Вызывая функцию из каждого места, драйвер посылает соответствующий аргумент, чтобы пометить откуда был произведен вызов. Функция, выдает значение "аргумент+1" в знак того, что она корректно получила аргумент и может с ним работать. Возвращенные значения печатаются драйвером с помощью DbgPrint. Итак, в приложении имеем переменные, которые будут меняться при вызове функции драйвером:

typedef struct _CALLBACK_INFORMATION{
	BOOL DicpatchIoControl;
	BOOL DispatchClose;
	BOOL DriverUnload;
} CALLBACK_INFORMATION, *PCALLBACK_INFORMATION;

//Глобальная переменная
CALLBACK_INFORMATION CallbackInfo;

Функция main:

int main()
{
	DRIVER_INFORMATION DrvInfo;
	ULONG num;
	UCHAR buff[128];

	DrvInfo.pDrvDir = DrvDir;
	DrvInfo.pDrvName = DrvName;
	DrvInfo.pServiceName = ServiceName;
	DrvInfo.hDEV = NULL;
	DrvInfo.pDevFileName = DrvFileName;

	//Инициализируем переменные, изменение которых будет означать вызов из 
	//соответствующей рутины драйвера
	CallbackInfo.DicpatchIoControl = FALSE;
	CallbackInfo.DispatchClose = FALSE;
	CallbackInfo.DriverUnload = FALSE;

	LoadDriver(&DrvInfo);

	//Посылаем адрес функции из процесса пользователя
	// IOCTL_KCBEX_SENDCBADDR имеет свойство METHOD_NEITHER - сделано для интереса
	//При этом вызове ожидается что нашу функцию вызовут из DispatchIoControl Routine
	((ULONG*) buff)[0] = (ULONG) CallbackFunction;
	DeviceIoControl(DrvInfo.hDEV,IOCTL_LPTDRV_SENDCBADDR,buff,sizeof(ULONG),buff,0,&num,NULL);

	//Во время этого вызова происходит закрытие handle устройства и функцию, 
	//стало быть, должны будут вызвать из
	//DispatchClose Routine; Далее, вызовется DriverUnload Routine, и мы из нее тоже 
	//попробуем вызвать нашу функцию UnloadDriver(&DrvInfo);

	//А тут, вплоть до ретёрна, проверяется, изменились ли заданные переменные или, 
	//что то же самое, проверяется вызывалась ли наша функция
if(CallbackInfo.DicpatchIoControl)
	{
		cout << "We have been called from DicpatchIoControl routine\n";
	}	else	{
					cout << "We haven't been called from DicpatchIoControl routine\n";
	}
	if(CallbackInfo.DispatchClose)
	{
		cout << "We have called from DispatchClose routine\n";
	}	else	{
					cout << "We haven't been called from DispatchClose routine\n";
	}
	if(CallbackInfo.DriverUnload)
	{
		cout << "We have been called from DriverUnload routine\n";
	}	else	{
					cout << "We haven't been called from DriverUnload routine\n";
	}

	return 0;
}

Вот как выглядит наша User-Mode функция:

//О целесеобразности наличия здесь спецификатора "CALLBACK" я напишу в заключении статьи.
//Он, как оказалось, играет ключевую роль.

ULONG CALLBACK CallbackFunction(ULONG Arg)
{
	switch( Arg )
	{
	case 1:
		CallbackInfo.DicpatchIoControl = TRUE;
		cout << "This call was initiated by a Kernel Mode code of the DicpatchIoControl routine\n";
		break;
	case 2:
		CallbackInfo.DispatchClose = TRUE;
		cout << "This call was initiated by a Kernel Mode code of the DispatchClose routine\n";
		break;
	case 3:
		CallbackInfo.DriverUnload = TRUE;
		cout << "This call was initiated by a Kernel Mode code of the DriverUnload routine\n";
		break;
	default:
		break;
	}
	return ++Arg;
}

Теперь перейдем к драйверу

Вот некоторые объявления:

#define IOCTL_KCBEX_SENDCBADDR CTL_CODE(						\
								FILE_DEVICE_UNKNOWN,	\
								0x801,					\
								METHOD_NEITHER,		\
								FILE_ANY_ACCESS)

typedef struct _DEVICE_EXTENSION {
	PDEVICE_OBJECT pDevice;
	UNICODE_STRING DeviceName;
	UNICODE_STRING SymLinkName;
	
	ULONG CallbackAddress;
	PEPROCESS pEPR;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

typedef ULONG (*PCALLBACK_ROUTINE)(ULONG);

Вся нужная информация о функции и контексте процесса, в котором она находится посылается в драйвер вместе с ранее указанным котрол-кодом:

PDEVICE_EXTENSION	pDE;
	PCALLBACK_ROUTINE KernelCallback;

	case IOCTL_KCBEX_SENDCBADDR:
		//С помощью этой функции получаем контекст вызывающего процесса - то есть нашего.
		//Далее он будет использоваться функцией KeStackAttachProcess при необходимости
pDE->pEPR = IoGetRequestorProcess( pIrp );
		if(pDE->pEPR == NULL)
		{
			DbgPrint("Failed to obtain user context\n");
			break;
		}
		DbgPrint("EPROCESS structure was successfully obtained\n");

		//Адрес функции который прислали из режима пользователя
pDE->CallbackAddress = ((ULONG*) pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer)[0];

		KernelCallback = (PCALLBACK_ROUTINE) pDE->CallbackAddress;

		//Аргумент 1 будет означать, что мы вызываем функцию из DispatchIoControl Routine

		RetVal = 777;

		//Здесь демонстрируется, что при обработке контрол-кодов обладающих 
		//свойством METHOD_NEITHER драйвер переносится в контекст вызывающего 
		// процесса и нет в данном случае надобности в вызове KeStackAttachProcess

		RetVal = KernelCallback(1);

		DbgPrint("User Mode function returned %i\n",RetVal);

После этого пользовательская программа вызовет UnloadDriver, которая, в свою очередь, сначала закроет handle устройства, а потом заставит драйвер выгрузиться. При закрытии хендла вызывается DispatchClose Routine, код, которым вызываем из нее User-Mode функцию - следующий:

	KIRQL CurIrql;
	PCALLBACK_ROUTINE KernelCallback;
	KAPC_STATE  ApcState;
	ULONG RetVal;

	PDEVICE_EXTENSION pDE = (PDEVICE_EXTENSION)
		pDevObj->DeviceExtension;

	if(pDE->pEPR == NULL) goto l;

	KernelCallback = (PCALLBACK_ROUTINE) pDE->CallbackAddress;

	RetVal = 777;

//Посылаемое значение 2 означает что вызов был из DispatchClose Routine

	KeStackAttachProcess(pDE->pEPR,&ApcState);

	RetVal = KernelCallback(2);

	KeUnstackDetachProcess(&ApcState);
	
	DbgPrint("User Mode function returned %i\n",RetVal);

И, наконец, в DriverUnload Routine мы выполним следующий код:

	ULONG RetVal = 777;
	PCALLBACK_ROUTINE KernelCallback;
	KAPC_STATE  ApcState;

	PDEVICE_EXTENSION pDE = (PDEVICE_EXTENSION) pDrvObj->DeviceObject->DeviceExtension;

	if(pDE->pEPR == NULL) goto l;

	KernelCallback = (PCALLBACK_ROUTINE) pDE->CallbackAddress;

	KeStackAttachProcess(pDE->pEPR,&ApcState);
//Аргумент 3 - вызов из DriverUnload Routine
	RetVal = KernelCallback(3);

	KeUnstackDetachProcess(&ApcState);

	DbgPrint("User Mode function returned %i\n",RetVal);
l:

Вот и все☺. При вызове конслольной программы должно выдаться следующее:

The full file name is: D:\Visual Studio projects\kcb_test\kcbex.SYS
This call was initiated by a Kernel Mode code of the DicpatchIoControl routine
This call was initiated by a Kernel Mode code of the DispatchClose routine
This call was initiated by a Kernel Mode code of the DriverUnload routine
We have been called from the DicpatchIoControl routine
We have been called from the DispatchClose routine
We have been called from the DriverUnload routine
Press any key to continue

Теперь заметка по поводу спецификатора CALLBACK в объявлении нашей CallBack-функции. Сначала, я все время собирал и использовал Debug версию драйвера, а спецификатор в объявлении не стоял. При этом все прекрасно работало, я даже не подозревал о том, что могут возникнуть какие-либо проблемы при использовании Release-версии драйвера… Я, уже почти дописав до конца статью, вдруг решил запустить программу с Release-версией драйвера, естественно не сохранив предварительно свеженабранный текст... В общем, запустил - и получил так синий экран, по которому, кстати, за пару дней уже успел соскучиться:. Вы можете себе представить, как я себя почувствовал в этот момент. Кому вообще нужна статья про метод, который неизвестно почему не работает в Release-версии??? К счастью, такое мое состояние продлилось недолго. Я быстро вспомнил про этот спецификатор и сразу все опять стабильно заработало. Правда, с некоторыми ограничениями(такими же как и до этого, в принципе). Например, не стоит в Callback-функции производить вызовы других функций из стандартных библиотек. Так, например, если применение оператора "<<" из "iostream", не вызывает никаких неприятностей, то вызов printf приведет к зависанию программы(но не системы!), а вызов MessageBox обернется BSOD-ом с кодом 7f. Было бы здорово здорово,если бы кто-нибудь на низком уровне разобрался в деталях этого метода - почему не все вызовы можно производить из Callback-функции, откуда берется разница в Debug и Release -версиях, что реально меняет в устройстве программы добавление в определение функции спецификатора CALLBACK, и, наконец, почему при выполнении команды ret при выходе из Callback-функции не происходит ошибки, связанной с "нехваткой"привилегий. Но, и в таком, слегка непонятном, виде этот метод, думаю, многие найдут полезным. А самое главное - теперь можно успокоиться насчет этой, всеми замученной функции KeUserModeCallback. Врядли ее использование окажется эффективнее изложенного метода.

Прилагающиеся исходники:

Прилагающиеся исходники:

  • kcb_test.zip - VC++ проект приложения с Callback-функцией
  • kcbex.zip - VC++ проект драйвера, вызываемного приложением, специальная установка не требуется.*)

    *Если в результате экспериментов(т.е. при некоторых изменениях драйвера/приложения) у Вас возникнет Синий Экран, то при новой загрузке драйвер не запустится, в консоли будет кроме всего прочего написано ERROR_SERVICE_EXISTS. В этом случае надо найти в реестре в разделе Services папку KCBEX, и удалить ее. После этого, при новой загрузке системы можно запускать драйвер опять, немного поправив, разумеется ☺.

    2002-2013 (c) wasm.ru