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. Исходники прилагаются
конечно, на самом деле, грита никто не задалбливал вопросами. просто он напился раствориля и ему захотелось пообщаться. методом написания большого кол-ва текста с ядерными названиями и тп ; ) а вообще помог, thx!
Подумал я тут, и вспомнил про LPC. А что если из драйвера открыть LPC порт и из клиентского приложения подключиться к нему? Думаю нужно обдумать тему.
Неправильно сказал. Не из драйвера открыть порт, а из юзермод приложения, а в драйвере можно будет подключиться к этому порту через 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