1120 Alameda Orquidea, Atibaia, SP,  BRA

fernando@driverentry.com.br

Criando e usando IOCTLs

Essa semana recebi a seguinte pergunta de um leitor:

“É possível meu aplicativo passar um IOCTL customizável (feito por mim) para o driver, e este reconhecer sem problemas?”

A resposta à curto prazo é: “Sim, e boa sorte!”, mas que graça tem ter um blog se não podemos falar um pouco mais a respeito e até dar um exemplo assim de lambuja? Este post assume que você já sabe alguns conceitos básicos, tais como compilar, instalar e testar drivers. Mas se você ainda não sabe fazer isso, não se preocupe, a vida ainda vale a pena. Basta ler este post.

O driver que calculava

Vamos criar um driver de exemplo que usa a mesma idéia que meu amigo Heldai já usava para ilustrar o uso de IOCTLs quando eu ainda estava aprendendo a fazer telas azuis. No exemplo de hoje vamos fazer um driver que some dois números contidos em uma estrutura que será recebida via um IOCTL.

Sem mais blablablas acho que podemos começar pela definição da estrutura. Como a tia do prezinho já nos ensinou, vamos criar apenas um arquivo de header que seja incluído tanto pelo projeto da aplicação como pelo projeto do driver. Este arquivo de header não deve incluir nenhum outro arquivo de header específico de User Mode e nem de Kernel Mode, ou seja, nada de Windows.h nem de ntddk.h. Tudo isso já está prontinho, testadinho, e como meu amigo Rafael costuma dizer, “compilandinho” num projeto disponível para download ao final do post.

typedef struct _KERNEL_MATH_REQUEST
{
    //-f--> Eu posso definir minha estrutura da maneira
    //      que eu achar o maior dos melhor de bão.
    //      Estes serão os dois números a serem somados.
    ULONG   x;
    ULONG   y;
 
} KERNEL_MATH_REQUEST, *PKERNEL_MATH_REQUEST;
 
 
typedef struct _KERNEL_MATH_RESPONSE
{
    //-f--> Aos mais mocinhos: Eu sei que dá para fazer
    //      tudo em uma só estrutura. Estou fazendo desta
    //      forma para melhor ilustrar.
    ULONG   r;
 
} KERNEL_MATH_RESPONSE, *PKERNEL_MATH_RESPONSE;

Criando o IOCTL

O IOCTL é mais do que simplesmente um número para identificar a operação desejada. Ele é composto por uma máscara de bits que são interpretados pelo Windows. Esta máscara é definida como mostra a figura abaixo. Você pode obter maiores detalhes sobre esta macro neste link.


Para definir o IOCTL nós utilizamos a macro CTL_CODE que tem seus parâmentros como mostra abaixo.

#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)

Vamos definir nosso IOCTL como mostra abaixo.

//-f--> Aqui definimos o IOCTL para nosso driver.
#define IOCTL_SOMA_QUE_EU_TO_MANDANDO \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
 
//-f--> Apesar deste driver ter "Soma" como parte do nome, resolvi
//      colocar um IOCTL de subtração para exemplificar.
#define IOCTL_SUBTRAI_QUE_EU_TO_MANDANDO \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)

Agora vamos dar uma olhada em cada parâmetro que foi escolhido aqui e dar uma pincelada a respeito.

FILE_DEVICE_UNKNOWN – Dando uma olhada em nossa DriverEntry podemos verificar que este foi o mesmo tipo utilizado na chamada à rotina IoCreateDevice.

/****
***     DriverEntry
**
**      Ponto de entrada do nosso driver.
**      Faca nos dentes e sangue nos olhos.
*/
extern "C" NTSTATUS
DriverEntry(__in PDRIVER_OBJECT     pDriverObj,
            __in PUNICODE_STRING    pusRegistryPath)
{
    UNICODE_STRING  usDeviceName = RTL_CONSTANT_STRING(L"\\Device\\KernelSum");
    UNICODE_STRING  usSymbolicLink = RTL_CONSTANT_STRING(L"\\DosDevices\\KernelSum");
    NTSTATUS        nts;
    PDEVICE_OBJECT  pDeviceObj;
 
    //-f--> Setando a rotina de unload e permitir que o driver
    //      possa ser descarregado dinamicamente
    pDriverObj->DriverUnload = OnDriverUnload;
 
    //-f--> As rotinas de Open, Cleanup e Close são idênticas.
    //      Então, pela lei do mínimo esforço, serão uma única.
    //      Além dessa, temos que tratar a DeviceControl, que é
    //      onde os IOCTLs são recebidos
    pDriverObj->MajorFunction[IRP_MJ_CREATE] =
    pDriverObj->MajorFunction[IRP_MJ_CLEANUP] =
    pDriverObj->MajorFunction[IRP_MJ_CLOSE] = OnCreateCleanupClose;
    pDriverObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = OnDeviceIoControl;
 
    //-f--> Criamos o device de controle do driver. Repare que utilizamos
    //      FILE_DEVICE_UNKNOWN como device type. Este mesmo device type
    //      é utilizado na macro CTL_CODE. Veja: IoCtl.h
    nts = IoCreateDevice(pDriverObj,
                         0,
                         &usDeviceName,
                         FILE_DEVICE_UNKNOWN,
                         0,
                         FALSE,
                         &pDeviceObj);
 
    //-f--> Tudo bem até aqui? Então beleza...
    if (!NT_SUCCESS(nts))
    {
        ASSERT(FALSE);
        return nts;
    }
 
    //-f--> Cria symbolic link para que a aplicação consiga
    //      obter um handle para o device de controle
    nts = IoCreateSymbolicLink(&usSymbolicLink,
                               &usDeviceName);
 
    //-f--> Tá tudo bem?
    if (!NT_SUCCESS(nts))
    {
        //-f--> Ops... Acho que eu pisei em alguma coisa mole...
        IoDeleteDevice(pDeviceObj);
 
        ASSERT(FALSE);
        return nts;
    }
 
    //-f--> Beleza, fechou, é nóis na fita!
    return STATUS_SUCCESS;
}

O segundo parâmetro, o Function, recebe o número 0x800, já que os números abaixo disso são reservados à Microsoft.

METHOD_BUFFERED – Indica ao IoManager que o driver receberá uma cópia do buffer de entrada passado à função DeviceIoControl. O IoManager aloca um buffer de sistema, ou seja, em Kernel Space, suficientemente grande para que caibam tanto os dados de entrada como os dados de saída. Quando a função DeviceIoControl é chamada, o IoManager aloca o buffer de sistema e copia o buffer de entrada para dentro dele. O driver recebe a IRP, obtém os parâmetros de entrada, processa-os e escreve os dados de saída no mesmo buffer de sistema. Quando a IRP é completada, o IoManager copia os dados de saída do buffer de sistema para o buffer de saída oferecido pela aplicação.

Além do método Buffered, ainda existem os métodos de Direct I/O e Neither. Para o nosso exemplo de hoje, o méthodo Buffered está ótimo. Fico devendo um post que fala sobre como usar cada um dos outros métodos.

FILE_ANY_ACCESS – Indica que o handle não precisa ser aberto com um tipo de acesso especial para poder executar este IOCTL.

Da aplicação para o driver

Para enviar um IOCTL para o driver, você precisa utilizar a função DeviceIoControl como mostra o exemplo abaixo. Este é um programa bem simples que mostra este uso.

/****
***     main
**
**      Espero que todos saibam que este é o ponto
**      de entrada de uma aplicação. Caso contrário,
**      você pode estar se preciptando em ler um blog
**      de driver para Windows.
*/
int __cdecl main(int argc, CHAR* argv[])
{
    HANDLE                  hDevice;
    DWORD                   dwError = ERROR_SUCCESS,
                            dwBytes;
    KERNEL_MATH_REQUEST     Request;
    KERNEL_MATH_RESPONSE    Response;
 
    printf("Opening \\\\.\\KernelSum device...\n");
 
    //-f--> Aqui abrimos um handle para o nosso device que
    //      foi criado pelo driver. Vale lembrar que nosso
    //      driver de exemplo tem que ser instalado e iniciado
    //      para que a chamada abaixo funcione corretamente.
    hDevice = CreateFile("\\\\.\\KernelSum",
                         GENERIC_ALL,
                         0,
                         NULL,
                         OPEN_EXISTING,
                         0,
                         NULL);
 
    //-f--> Verifica se o handle foi aberto.
    if (hDevice == INVALID_HANDLE_VALUE)
    {
        printf("Error #%d opening device...\n",
               (dwError = GetLastError()));
        return dwError;
    }
 
    //-f--> Fiquei com preguiça e coloquei os valores hard-coded mesmo.
    Request.x = 3;
    Request.y = 2;
 
    printf("Calling DeviceIoControl...\n");
 
    //-f--> Envia o IOCTL
    if (!DeviceIoControl(hDevice,
                         IOCTL_SOMA_QUE_EU_TO_MANDANDO,
                         &Request,
                         sizeof(KERNEL_MATH_REQUEST),
                         &Response,
                         sizeof(KERNEL_MATH_RESPONSE),
                         &dwBytes,
                         NULL))
    {
        //-f--> Ops...
        printf("Error #%d calling DeviceIoControl...\n",
               (dwError = GetLastError()));
 
        CloseHandle(hDevice);
        return dwError;
    }
 
    //-f--> Mostrando resultatdos
    printf("%d + %d = %d\n",
           Request.x,
           Request.y,
           Response.r);
 
    printf("Closing device...\n");
    //-f--> Fim de conversa
    CloseHandle(hDevice);
    return 0;
}

Note que não existe nenhuma amarração forte entre o IOCTL e os bufferes de entrada ou de saída utilizados, mas é preciso ficar atento aos tamanhos dos bufferes quando o driver fizer uso deles. Ninguém vai querer corromper o heap de alocações de Kernel ou sair disparando excessões em Kernel Mode, vai?

Vejam como os dados são tratados pelo driver ao receber a IRP_MJ_DEVICE_CONTROL. Leiam os comentários, eles fazem parte do texto explicativo. Nossa, até que essa frase que representa a minha preguiça ficou legal. No fundo mesma frase quer dizer: “Ah meu! Fala sério que além de preparar esse exemplo, você ainda quer que eu duplique toda a informação dos comentários. E na bunada não vai dinha?”

/****
***     OnDeviceIoControl
**
**      Esta rotina é chamada quando uma aplicação
**      envia um IOCTL via DeviceIoControl para nosso device.
*/
NTSTATUS
OnDeviceIoControl(__in PDEVICE_OBJECT   pDeviceObj,
                  __in PIRP             pIrp)
{
    PIO_STACK_LOCATION      pStack;
    NTSTATUS                nts;
    PKERNEL_MATH_REQUEST    pRequest;
    PKERNEL_MATH_RESPONSE   pResponse;
 
    //-f--> Obtemos os parâmetros para nosso driver
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    switch(pStack->Parameters.DeviceIoControl.IoControlCode)
    {
    case IOCTL_SOMA_QUE_EU_TO_MANDANDO:
    case IOCTL_SUBTRAI_QUE_EU_TO_MANDANDO:
 
        //-f--> Vamos fazer as verificações de tamanho dos
        //      buffers de entrada e saída.
        if (pStack->Parameters.DeviceIoControl.InputBufferLength !=
            sizeof(KERNEL_MATH_REQUEST) ||
            pStack->Parameters.DeviceIoControl.OutputBufferLength !=
            sizeof(KERNEL_MATH_RESPONSE))
        {
            nts = STATUS_INVALID_BUFFER_SIZE;
            pIrp->IoStatus.Information = 0;
            break;
        }
 
        //-f--> O seguro morreu de velho.
        ASSERT(pIrp->AssociatedIrp.SystemBuffer != NULL);
 
        //-f--> Utilizando METHOD_BUFFERED, o sistema aloca um buffer único
        //      para transportar os dados de entrada e de saída. O tamanho
        //      este buffer é mesmo do maior deles. Assim, tome cuidado
        //      para não escrever nada na saída antes de ler toda a entrada.
        pRequest = (PKERNEL_MATH_REQUEST)pIrp->AssociatedIrp.SystemBuffer;
        pResponse = (PKERNEL_MATH_RESPONSE)pIrp->AssociatedIrp.SystemBuffer;
 
        //-f--> Faz a operação e sinaliza sucesso
        if (pStack->Parameters.DeviceIoControl.IoControlCode == IOCTL_SOMA_QUE_EU_TO_MANDANDO)
            pResponse->r = pRequest->x + pRequest->y;
        else
            pResponse->r = pRequest->x - pRequest->y;
 
        nts = STATUS_SUCCESS;
 
        //-f--> Informa ao IoManager quantos bytes devem ser tranferidos de
        //      volta para a aplicação
        pIrp->IoStatus.Information = sizeof(KERNEL_MATH_RESPONSE);
        break;
 
    default:
        //-f--> Ops... Recebemos um IOCTL não previsto.
        nts = STATUS_INVALID_DEVICE_REQUEST;
        pIrp->IoStatus.Information = 0;
    }
 
    //-f--> Copia o status final da IRP e a completa.
    pIrp->IoStatus.Status = nts;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
 
    //-f--> NOTA: Lembre-se de que não podemos tocar na IRP depois de completa-la,
    //      então não seja espertão modificando a linha abaixo para a obter o Status
    //      de dentro da IRP, e assim usa-lo como retorno de função.
    return nts;
}

Compilando assim ou assado

O projeto de exemplo que está disponível para download ao final deste post pode ser compilado de duas maneiras. Uma delas é utilizando a maneira padrão de compilar drivers oferecida pelo WDK. Resumidamente você vai em Start->All Programs->Windows Drivers Kits->WDK 6000->Build Environments->Windows XP->Windows XP x86 Checked Build Environment. Isso deve abrir uma jabela de prompt como mostra abaixo. Depois disso é só ir ao diretório onde você baixa essas tranqueiras da internet e entrar no diretório raiz do projeto. Lá você chama o Build.exe e um abraço.


A outra maneira de compilar todo o projeto permite que você compile de dentro do Visual Studio 2008, que foi o ambiente que utilizei para compor este projeto. Porém, para compilar o projeto pela IDE, você precisará usar o DDKBUILD como mostra este outro post que fala a respeito.

Enquanto estive fora, recebi uma outra dúvida de um leitor que queria saber como ler/escrever no registro usando um driver. No próximo post falarei sobre isso além de outros detalhes referentes ao registro do ponto de vista de um driver.

Até lá!

KernelSum.zip

2 Responses

  1. Obrigado pelo ótimo mini-tut, muito boa a explicação.

    Eu estou tentando fazer a comunicação com um filter driver Plug And Play, mais especificamente com um keyboard filter, e esse tipo de driver exige que você crie um segundo deviceobject somente pra receber as IOCTLs. Como sou iniciante ainda, to apanhando feio, tentando converter o exemplo disponível no site da MS (http://support.microsoft.com/kb/262305/en-us).

    Um dia eu chego lá!

    Abraços

    Thiago

  2. Olá Thiago, a um ano e meio atrás estava como vc :), e eu cheguei lah, essa semana completei exatamente o q vc tah tentando, um keyboard filter e um mouse filter, com um pouco de criatividade e com a grande ajuda PROFI nas horas de desespero do nosso querido Fernando. Se quiser umas dicas no assunto basta responder a esse comment. O filtros em especifico repassam tudo pra qualquer programa user mode q quiser se attachar a um device, depois disso o programa pode fazer o q quiser como remapear td pra outras teclas ou sequencias.

Leave a Reply

Your email address will not be published.