Коммуникация с драйвером через прерывания

Discussion in 'С/С++, C#, Rust, Swift, Go, Java, Perl, Ruby' started by _Great_, 21 Aug 2007.

  1. _Great_

    _Great_ Elder - Старейшина

    Joined:
    27 Dec 2005
    Messages:
    2,032
    Likes Received:
    1,119
    Reputations:
    1,139
    Article: Коммуникация с драйвером через прерывания
    Author: Great
    Date: 21.08.2007
    Lang: C++ / ASM

    Некоторые личности сильно задолбали вопросами как можно вызвать код драйвера из юзермодного приложения, причем не используя DeviceIoControl и вообще не создавая девайсов.
    Ответ простой - зарегистрировать в системе свое прерывание. Про палевность этого метода промолчу - в конце концов, спалить из ринг0 можно все, что угодно.
    Итак, мы собрались установить новый обработчик в таблице дескрипторов прерываний. Подробно о прерываниях и о том, как это сделать, я описал в своей статье "Прерывания в защищенном режиме процессора IA-32", поэтому подробно останавливаться на этом не буду.
    Пусть наш желаемый вектор равен F3. Тогда в юзермоде можно будет выполнить что-то типа
    Code:
    mov eax, 2  ; номер функции
    call sys_stub
    для вызова функции 2 нашего прерывания (пусть оно имеет много функций), где sys_stub состоит из
    Code:
    sys_stub:
    int 0xF3
    ret
    Удобно, не правда ли, чем создавать девайс, а потом его открывать и делать DeviceIoControl?
    Установку прерывания мы осуществим функцией ConnectSoftwareInterrupt(), которая получит регистр IDTR и создаст запись о дескрипторе прерывания.
    Выглядит эта функция следующим образом:
    Code:
    PVOID
    ConnectSoftwareInterrupt(
      IN BYTE Interrupt,
      IN PVOID Handler
      )
    /*
    Arguments:
    
      Interrupt - Number of interrupt to connect to
    
      Handler   - Address of handler routine
    
    Return Value:
    
      Address of old handler
    
    --*/
    {
        DWORD OldCr0;
        DWORD OldHandler;
    	IDTR Idtr;
        
        //
        // Disable WP and hardware interrupts; get IDTR
        //
    
        OldCr0 = DisableWP();
    	__asm pushfd;
        __asm cli;
    	__asm sidt [Idtr];
    
        //
        // Fill IDT entry
        //
    
        OldHandler = Idtr.Table[Interrupt].OffsetLow  | ( Idtr.Table[Interrupt].OffsetHigh << 16 );
        
        Idtr.Table[Interrupt].OffsetLow  = (WORD) ( (DWORD)Handler )       & 0xFFFF;
        Idtr.Table[Interrupt].OffsetHigh = (WORD) ( (DWORD)Handler >> 16 ) & 0xFFFF;
        Idtr.Table[Interrupt].Present    = 1;
        Idtr.Table[Interrupt].Default    = 1;
        Idtr.Table[Interrupt].DPL        = 3;
        Idtr.Table[Interrupt].Selector   = 0x0008;
        
        //
        // Restore hardware interrupts and CR0 value
        //
    
        __asm popfd;
        RestoreWP(OldCr0);
    
        return (PVOID) OldHandler;
    }
    Эта функция сперва отключает бит WP в регистре CR0, чтобы разрешить запись на системные страницы, на коих распологается IDT - мы ведь собираемся ее модифицировать.
    Далее запрещаются прерывания - установка вектора должна быть атомарной операцией. Потом мы получаем регистр IDTR и модифицируем запись в IDT, чтобы она указывала на новый обработчик.
    После этих нехитрых манипуляций мы восстаналиваем запрещенные прерывания, восстанавливаем старое значение CR0 и возвращаем адрес старого обработчика.

    После этого нетрудно предположить вероятный код DriverEntry и DriverUnload:

    Code:
    #define OUR_INT_NO 0xF3
    PVOID OldHandler;
    
    void DriverUnload(IN PDRIVER_OBJECT DriverObject)
    {
    	DPRINT ("[~] DriverUnload()\n");
    
    	IntConnectSoftwareInterrupt( OUR_INT_NO, OldHandler );
    }
    
    NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
    {
    	DriverObject->DriverUnload = DriverUnload;
    	DPRINT("[~] DriverEntry()\n");
    
    	OldHandler = IntConnectSoftwareInterrupt( OUR_INT_NO, NewHandler );
    
    	DPRINT("[+] Driver initialization successful\n");
    	return STATUS_SUCCESS;
    }
    Далее мы определим две функции, которые будут мапировать и размапировать пользовательский буфер - нам не пойдет прямая работа с юзермодными адресами, т.к. код может содержать повышения и понижения IRQL, что немедленно скажется в виде бсода, если соответствующие адреса были выгружены в своп.
    Рассмотрим функцию MapUserBuffer подробнее. Чтобы осуществить задуманное, нам придется заблокировать пользовательский буфер в физической памяти - это делает API MmProbeAndLockPages() и отмапировать его в системное адресное пространство (>2 гб) с помощью API MmMapLockedPagesSpecifyCache.
    Можно было, конечно, ограничиться блокированием буфера в физической памяти - все равно ошибка страницы не возникнет, т.к. диспетчер памяти не посмеет выгрузить заблокированный буффер. Но некоторые API ядра не любят, когда адреса аргументов лежат ниже 2 гб, поэтому для пущей безопасности, наглядности и важности отмапируем буфер в системное адресное пространство.
    Важно понимать, что в данном случае создается вторая проекция того же буфера - если мы изменим значение по полученному отмапированному системному адресу, изменится и пользовательский буфер - поддержка проекций буферов в ОС Windows реализована совершенно прозрачно для нас.
    Обе эти API MmProbeAndLockPages и MmMapLockedPagesSpecifyCache требуют, чтобы буфер описывала структура MDL (Memory Descriptor List - Описатель Участка Памяти, перевод не дословный). Эту структуру можно создать функцией IoAllocateMdl, передав ей в качестве первых двух аргументов начало буфера и его длину. Освобождается эта структура вызовом IoFreeMdl после размапирования.
    Api MmProbeAndLockPages в случае успеха блокирует страницы буфера в физической памяти (ОЗУ). Но в случае неудачи она выбрасывает исключение, которое нужно поймать в блоке __try/__except и обработать - мы просто уничтожим MDL и вернем NULL, идентифицируя таким образом ошибку.
    Ну и последующий вызов MmMapLockedPagesSpecifyCache создает проекцию буфера на системные адреса, возвращая адрес проекции для последуюзего использования.
    Функция UnmapUserBuffer() проделывает противоположные операции - уничтожается проекция через MmUnmapLockedPages, снимается блокировка страниц через MmUnlockPages и уничтожается MDL через IoFreeMdl.

    Теперь напишем обработчик нашего прерывания. Вот мы попали в начало обработчика.. сначала не помешало бы перезагрузить селекторы ds,es,fs их соответствующими значениями для ring0:
    Code:
    DWORD FunctionNumber, Arguments, ArgumentsLength;
    
    __declspec(naked) void NewHandler( void )
    {
    	// Мы в обработчике прерывания. Инициализируем селекторы значениями селекторов ринг0 сегментов
    	__asm {
    		push fs
    		push es
    		push ds
    
    		push 0x30
    		pop fs
    
    		push 0x23
    		pop ds
    
    		push 0x23
    		pop es
    
    После этого можно получить параметры прерывания, которые юзермодный код должен был передать в трех регистрах eax, ecx, edx:

    Code:
    		//
    		// Получаем параметры
    		// 
    		// При вызове прерывания юзермодный код должен поместить в регистры:
    		//  EAX = номер функции
    		//  ECX = размер аргументов
    		//  EDX = указатель на аргументы
    		//  
    
    		mov FunctionNumber, eax
    		mov ArgumentsLength, ecx
    		mov Arguments, edx
    	}
    Теперь все готово, чтобы обработать прерывание. Вынесем весь код обработки (с логической точки зрения) в функцию ProcessInterrupt(), а здесь лишь вызовем ее и выполним возврат из прерывания:

    Code:
    	// Весь код обработки будет ТАМ
    	ProcessInterrupt( );
    
    	// Восстанавливаем старые селекторы и выходим из прерывания
    	__asm {
    		pop ds
    		pop es
    		pop fs
    		iretd
    	}
    }
    
    Что ж. С технической частью покончено. Осталось включить фантазию и написать код для обработки прерывания. Мы поступим следующим образом: юзермодный код должен передавать в регистрах eax,ecx,edx параметры - в комментариях выше написано, что должно содержаться в каждом регистре.
    Мы реализуем три функции с номерами 0, 1 и 2 для пользовательского кода. Функция 0 будет просто показывать сообщение и всё, функция 1 попытается прочитать аргументы, а функция 2 попробует записать число 12345678h по адресу пользовательского буфера, заданного в первом аргументе.
    Естественно, аргументы и буфера нужно отмапировать по обозначенным выше причинам нашей функцией MapUserBuffer().
    Приступим:

    Code:
    // Тут код обработчика нашего прерывания
    ULONG ProcessInterrupt( )
    {
    	ULONG Status = STATUS_UNSUCCESSFUL;
    
    	// debug break;
    	//debugbreak();
    
    	DPRINT("INT F3 call, FunctionNumber=0x%08x, Arguments=0x%08x, ArgumentsLength=0x%08x\n", FunctionNumber, Arguments, ArgumentsLength);
    
    	switch( FunctionNumber )
    	{
    	case 0: // Функция 0 - покажем сообщение. Аргументов нет
    		DPRINT("Function 0 invoked!\n");
    		Status = STATUS_SUCCESS;
    		break;
    
    	case 1: // Функция 1 - прочитаем пользовательские аргументы
    		{
    			PMDL Mdl;
    			PVOID MappedArgs = MapUserBuffer( (PVOID)Arguments, ArgumentsLength, FALSE /*read*/, &Mdl );
    
    			DPRINT("Function 1 invoked, arguments mapped at address 0x%08x\n", MappedArgs);
    			
    			if( MappedArgs )
    			{
    				if( ArgumentsLength >= 4 )
    				{
    					for( ULONG i=0; i<ArgumentsLength; i+=4 ) {
    						DPRINT("Argument[%d]: 0x%08x\n", i/4, ((ULONG*)MappedArgs)[i/4]);
    					}
    
    					Status = STATUS_SUCCESS;
    				}
    				else
    				{
    					DPRINT("Too small args\n");
    					Status = STATUS_INFO_LENGTH_MISMATCH;
    				}
    
    				UnmapUserBuffer( MappedArgs, Mdl );
    			}
    			else
    			{
    				DPRINT("Arguments mapping failed\n");
    				Status = STATUS_ACCESS_VIOLATION;
    			}
    
    			break;
    		}
    
    	case 2: // Функция 2 - запишем чтонибудь в пользовательский буфер, его адрес задается первым аргументом, а длина - вторым
    		{
    			if( ArgumentsLength != sizeof(ULONG)*2 ) // не 2 аргумента?
    			{
    				DPRINT("Arguments length mismatch: 0x%08x\n", ArgumentsLength);
    				Status = STATUS_INFO_LENGTH_MISMATCH;
    				break;
    			}
    
    			PMDL ArgMdl;
    			PVOID MappedArgs = MapUserBuffer( (PVOID)Arguments, ArgumentsLength, FALSE /*read*/, &ArgMdl );
    
    			if( MappedArgs )
    			{
    				DPRINT("Arguments mapped at address 0x%08x\n", MappedArgs);
    
    				PMDL BufMdl;
    				PVOID MappedBuffer = MapUserBuffer( ((PVOID*)MappedArgs)[0], ((ULONG*)MappedArgs)[1], TRUE /*write*/, &BufMdl );
    
    				if( MappedBuffer )
    				{
    					DPRINT("Buffer mapped at address 0x%08x\n", MappedBuffer);
    
    					if( ((ULONG*)MappedArgs)[1] >= 4 ) // длина буфера больше 4? да - пишем дворд
    					{
    						*(ULONG*)(((ULONG*)MappedArgs)[0]) = 0x12345678;
    
    						DPRINT("Data written\n");
    
    						Status = STATUS_SUCCESS;
    					}
    					else
    					{
    						DPRINT("Too small args\n");
    						Status = STATUS_INFO_LENGTH_MISMATCH;
    					}
    
    					UnmapUserBuffer( MappedBuffer, BufMdl );
    				}
    				else
    				{
    					DPRINT("Buffer mapping failed\n");
    					Status = STATUS_ACCESS_VIOLATION;
    				}
    
    				UnmapUserBuffer( MappedArgs, ArgMdl );
    			}
    
    			break;
    		}
    
    	default:
    		DPRINT("Unknown function number: 0x%08x\n", FunctionNumber);
    		Status = STATUS_INVALID_PARAMETER;
    	}
    
    	return Status;
    }
    Поскольку этот код содержит в основном логику, то в технических пояснениях он, я думаю, не нуждается.

    Рассмотрим теперь пример пользовательского приложения:

    Code:
    include 'win32ax.inc'
    
    .data
    buffer rb 10
    
    .code
    
    callstub:
       int 0xF3
       ret
    
    start:
    
       ; Function 0 test
       xor eax, eax
       xor ecx, ecx
       xor edx, edx
       call callstub
    
       ; Function 1 test
       mov eax, 1
       mov ecx, 8
       push 0xabcdef01  ; arg2
       push 0x12345678  ; arg1
       mov edx, esp
       call callstub
       add esp, 8
    
       ; Function 2 test [illegal]
       mov eax, 2
       mov ecx, 0 ; too short args
       push 0
       mov edx, esp
       call callstub
       add esp, 4
    
       ; Function 2 test [legal]
       mov eax, 2
       mov ecx, 8
       push 10
       push buffer
       mov edx, esp
       call callstub
       add esp, 8
    
       ret
    
    .end start
    Функция callstub осуществляет вызов прерывания - я вынес это в отдельную функцию в связи с тем, что OllyDbg, которым мы соберемся отлаживать эту программу, плохо дружит с инструкцией INT, не устанавливая бряка на следующую за ней команду, поэтому придется делать step over через инструкцию call callstub.

    В данном коде осуществляется:
    1) вызов функции 0, которая только покажет сообщение и все
    2) вызов функции 1, которая покажет переданные аргументы. Мы передаем 0x12345678 и 0xabcdef01
    3) заведомо неправильный вызов функции 2 - ей нужно передать два параметра (адрес и длина буфера, куда записать число 12345678h), а мы передаем только один. Поэтому она вернет нам в регистре EAX статус STATUS_INFO_LENGTH_MISMATCH, сообщая о том, что аргументов слишком мало для нее.
    4) корректный вызов функции 2 с передачей ей двух аргументов - адреса буфера и его длины. Обработчик записывает в первые 4 байта буфера дворд 12345678, что можно непосредственно наблюдать после выполнения этой функции в окне OllyDbg:
    На этом придется завершить сий рассказ и попрощаться. Удачного компилирования и чтобы без BSoD'ов! ;)

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

    Attached Files:

    8 people like this.
  2. KEZ

    KEZ Ненасытный школьник

    Joined:
    18 May 2005
    Messages:
    1,604
    Likes Received:
    754
    Reputations:
    397
    конечно, на самом деле, грита никто не задалбливал вопросами.
    просто он напился раствориля и ему захотелось пообщаться. методом написания большого кол-ва текста с ядерными названиями и тп ; )
    а вообще помог, thx!
     
    1 person likes this.
  3. _Great_

    _Great_ Elder - Старейшина

    Joined:
    27 Dec 2005
    Messages:
    2,032
    Likes Received:
    1,119
    Reputations:
    1,139
    Kez, просто ты не первый, кто спрашивал =\
     
  4. GoreMaster

    GoreMaster Elder - Старейшина

    Joined:
    28 May 2007
    Messages:
    119
    Likes Received:
    32
    Reputations:
    4
    А че в статьи не перенесете? О_о

    Great: Да имхо там затеряется, а тут к самый раз
     
    #4 GoreMaster, 21 Aug 2007
    Last edited by a moderator: 22 Aug 2007
  5. Ni0x

    Ni0x Elder - Старейшина

    Joined:
    27 Aug 2006
    Messages:
    338
    Likes Received:
    157
    Reputations:
    37
    Подумал я тут, и вспомнил про LPC.
    А что если из драйвера открыть LPC порт и из клиентского приложения подключиться к нему? Думаю нужно обдумать тему.
     
  6. _Great_

    _Great_ Elder - Старейшина

    Joined:
    27 Dec 2005
    Messages:
    2,032
    Likes Received:
    1,119
    Reputations:
    1,139
    Тоже вариант. Надо будет посмореть
     
  7. Ni0x

    Ni0x Elder - Старейшина

    Joined:
    27 Aug 2006
    Messages:
    338
    Likes Received:
    157
    Reputations:
    37
    Неправильно сказал. Не из драйвера открыть порт, а из юзермод приложения, а в драйвере можно будет подключиться к этому порту через NtConnectPort.
    Code:
    NtConnectPort(
    OUT PHANDLE     ClientPortHandle,
    IN PUNICODE_STRING    ServerPortName,
    IN PSECURITY_QUALITY_OF_SERVICE  SecurityQos,
    IN OUT PLPCSECTIONINFO    ClientSharedMemory OPTIONAL,
    OUT PLPCSECTIONMAPINFO    ServerSharedMemory OPTIONAL,
    OUT PULONG     MaximumMessageLength OPTIONAL,
    IN OUT PVOID     ConnectionInfo OPTIONAL,
    IN OUT PULONG     ConnectionInfoLength OPTIONAL );
    
    Вообще тема интересна сама по себе в первую очередь своей необкатанностью. У многих стандартных процессов виндовс есть свои LPC порты, при детальном рассмотрении темы можно хоть малварь писать с новой технологией инфекта и тд. С помощью тогоже rpc можно делать опосредованный вызов функций winapi, т.е вызов функций на лету через посредник. Здесь открываются огромные просторы и новые техники. Собственно, небольшая статья по LPC: http://shellcode.ru/index.php?name=News&file=article&sid=17 и исходники по теме: http://www.argeniss.com/research/hackwininter.zip
     
    1 person likes this.