Форум АНТИЧАТ

Форум АНТИЧАТ (https://forum.antichat.xyz/index.php)
-   С/С++, C#, Delphi, .NET, Asm (https://forum.antichat.xyz/forumdisplay.php?f=24)
-   -   Коммуникация с драйвером через прерывания (https://forum.antichat.xyz/showthread.php?t=47285)

_Great_ 21.08.2007 22:59

Коммуникация с драйвером через прерывания
 
Вложений: 1
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

Цитата:

Сообщение от Ni0x
Подумал я тут, и вспомнил про 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


Время: 11:14