PDA

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


_Great_
21.08.2007, 22:59
Article: Коммуникация с драйвером через прерывания
Author: Great
Date: 21.08.2007
Lang: C++ / ASM

Некоторые личности сильно задолбали вопросами как можно вызвать код драйвера из юзермодного приложения, причем не используя DeviceIoControl и вообще не создавая девайсов.
Ответ простой - зарегистрировать в системе свое прерывание. Про палевность этого метода промолчу - в конце концов, спалить из ринг0 можно все, что угодно.
Итак, мы собрались установить новый обработчик в таблице дескрипторов прерываний. Подробно о прерываниях и о том, как это сделать, я описал в своей статье "Прерывания в защищенном режиме процессора IA-32", поэтому подробно останавливаться на этом не буду.
Пусть наш желаемый вектор равен F3. Тогда в юзермоде можно будет выполнить что-то типа
mov eax, 2 ; номер функции
call sys_stub
для вызова функции 2 нашего прерывания (пусть оно имеет много функций), где sys_stub состоит из
sys_stub:
int 0xF3
ret
Удобно, не правда ли, чем создавать девайс, а потом его открывать и делать DeviceIoControl?
Установку прерывания мы осуществим функцией ConnectSoftwareInterrupt(), которая получит регистр IDTR и создаст запись о дескрипторе прерывания.
Выглядит эта функция следующим образом:
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:

#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:
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:

//
// Получаем параметры
//
// При вызове прерывания юзермодный код должен поместить в регистры:
// EAX = номер функции
// ECX = размер аргументов
// EDX = указатель на аргументы
//

mov FunctionNumber, eax
mov ArgumentsLength, ecx
mov Arguments, edx
}

Теперь все готово, чтобы обработать прерывание. Вынесем весь код обработки (с логической точки зрения) в функцию ProcessInterrupt(), а здесь лишь вызовем ее и выполним возврат из прерывания:

// Весь код обработки будет ТАМ
ProcessInterrupt( );

// Восстанавливаем старые селекторы и выходим из прерывания
__asm {
pop ds
pop es
pop fs
iretd
}
}


Что ж. С технической частью покончено. Осталось включить фантазию и написать код для обработки прерывания. Мы поступим следующим образом: юзермодный код должен передавать в регистрах eax,ecx,edx параметры - в комментариях выше написано, что должно содержаться в каждом регистре.
Мы реализуем три функции с номерами 0, 1 и 2 для пользовательского кода. Функция 0 будет просто показывать сообщение и всё, функция 1 попытается прочитать аргументы, а функция 2 попробует записать число 12345678h по адресу пользовательского буфера, заданного в первом аргументе.
Естественно, аргументы и буфера нужно отмапировать по обозначенным выше причинам нашей функцией MapUserBuffer().
Приступим:

// Тут код обработчика нашего прерывания
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;
}

Поскольку этот код содержит в основном логику, то в технических пояснениях он, я думаю, не нуждается.

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

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:
00401000 78 56 34 12 xV4

На этом придется завершить сий рассказ и попрощаться. Удачного компилирования и чтобы без BSoD'ов! ;)

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

KEZ
21.08.2007, 23:05
конечно, на самом деле, грита никто не задалбливал вопросами.
просто он напился раствориля и ему захотелось пообщаться. методом написания большого кол-ва текста с ядерными названиями и тп ; )
а вообще помог, thx!

_Great_
21.08.2007, 23:08
Kez, просто ты не первый, кто спрашивал =\

GoreMaster
21.08.2007, 23:50
А че в статьи не перенесете? О_о

Great: Да имхо там затеряется, а тут к самый раз

Ni0x
25.08.2007, 23:52
Подумал я тут, и вспомнил про LPC.
Windows LPC (Local/Lightweight Procedure Call) – механизм межпроцессорного взаимодействия, используемый RPC для локальной связи. LPC позволяет процессам, используя LPC порты, взаимодействовать между собой посредством сообщений.
А что если из драйвера открыть LPC порт и из клиентского приложения подключиться к нему? Думаю нужно обдумать тему.

_Great_
26.08.2007, 08:41
Подумал я тут, и вспомнил про LPC.

А что если из драйвера открыть LPC порт и из клиентского приложения подключиться к нему? Думаю нужно обдумать тему.
Тоже вариант. Надо будет посмореть

Ni0x
26.08.2007, 11:17
Неправильно сказал. Не из драйвера открыть порт, а из юзермод приложения, а в драйвере можно будет подключиться к этому порту через NtConnectPort.

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