Archive for June, 2007

Começar de novo

24 de June de 2007

Alguns drivers precisam iniciar logo que o sistema carrega, ou melhor, enquanto o sistema carrega. Quando configuramos nosso driver com Start = 0 (Boot), nosso driver carrega junto com drivers bem básicos, tais como File Systems, Bus Drivers e por aí vai. Certa vez, precisei que um driver de Boot abrisse um arquivo para obter algumas informações do sistema. Infelizmente coisas como partições, volumes e File Systems ainda não estavam trabalhando, assim, tive que adiar um pouco este processo. Neste post vou comentar sobre como dar continuidade à inicialização do seu driver depois que a função DriverEntry terminou.

Para exemplificar o caso, escrevi um driver que ilustra claramente essa situação. O projeto completo está em um link para download no final deste post. Olhando para a implementação da nossa DriverEntry temos:

/****
***     DriverEntry
**
**      Ponto de entrada do driver, que será executado enquanto
**      o sistema Boota (do verbo "acabei de ligar a máquina").
*/
extern "C" 
NTSTATUS DriverEntry(IN PDRIVER_OBJECT  pDriverObj,
                     IN PUNICODE_STRING pusRegistryPath)
{
    NTSTATUS    nts;
 
    //-f--> Ói nóis aqui traveis...
    KdPrint(("Starting DrvReinit...\n"));
 
    //-f--> Registra rotina de descarga do driver
    pDriverObj->DriverUnload = OnDriverUnload;
 
    //-f--> Tenta abrir o arquivo o quanto antes, afinal
    //      eu nasci de sete meses mesmo.
    nts = TryOpenFile();
 
    //-f--> E como vocês podem ver...
    if (!NT_SUCCESS(nts))
    {
        //-f--> O arquivo não foi aberto
        KdPrint(("Scheduling reinitialization.\n"));
 
        //-f--> E eu até imagino qual seja o erro.
        ASSERT(nts == STATUS_OBJECT_PATH_NOT_FOUND);
 
        //-f--> Registra OnReinitialize para ser chamada mais tarde.
        //      Mais tarde significa quando todos os drivers de boot
        //      foram carregados.
        IoRegisterBootDriverReinitialization(pDriverObj,
                                             OnReinitialize,
                                             NULL);
    }
    else
    {
        //-f--> Er... Tem certeza de que você instalou esse driver
        //      direito? Bom, então o sistema não está iniciando
        //      agora e você iniciou este driver na mão.
    }
 
    //-f--> Se a função DriverEntry não retornar STATUS_SUCCESS,
    //      a rotina agendada para reiniciar o driver não será chamada.
    return STATUS_SUCCESS;
}

A função IoRegisterBootDriverReinitialization (Ufa! Algumas destas funções exigem fôlego) registra uma rotina de callback que será chamada ao final da carga de todos os drivers de Boot. Isso vai nos dar uma segunda oportunidade de tentar fazer o que nos foi proposto. Esta rotina é normalmente utilizada por filtros que se atacham sobre dispositivos não Plug-and-Play, e assim, não podem contar com a chamada da função AddDevice para serem notificados de que um novo device foi criado.

Em nosso exemplo, o objetivo é simplesmente ler um arquivo. Nestas condições, o erro que recebemos é o STATUS_OBJECT_PATH_NOT_FOUND. Para garantir que o arquivo que estamos querendo ler realmente existe, vamos utilizar o próprio arquivo onde o driver é implementado. Logo, se nosso código está rodando, o arquivo está lá.

/****
***     TryOpenFile
**
**      Tenta abrir o arquivo onde este driver está implementado.
**      Se este código está rodando, então este arquivo tem que estar lá.
*/
NTSTATUS TryOpenFile(VOID)
{
    NTSTATUS            nts;
    IO_STATUS_BLOCK     IoStatusBlock;
    HANDLE              hFile;
    OBJECT_ATTRIBUTES   ObjAttributes;
    UNICODE_STRING      usFilePath =
        RTL_CONSTANT_STRING(L"\\??\\C:\\Windows\\System32\\drivers\\DrvReinit.sys");
 
    //-f--> Olá depurador...
    KdPrint(("Trying open the file...\n"));
 
    //-f--> Preenchendo a estrutura OBJECT_ATTRIBUTES,
    //      fazê o quê, faz parte.
    InitializeObjectAttributes(&ObjAttributes,
                               &usFilePath,
                               OBJ_CASE_INSENSITIVE,
                               NULL,
                               NULL);
 
    //-f--> Solicita a abertura do arquivo
    nts = ZwCreateFile(&hFile,
                       GENERIC_READ,
                       &ObjAttributes,
                       &IoStatusBlock,
                       NULL,
                       0,
                       FILE_SHARE_READ | FILE_SHARE_WRITE,
                       FILE_OPEN,
                       0,
                       NULL,
                       0);
 
    //-f--> Abriu ou não?
    if (!NT_SUCCESS(nts))
    {
        //-f--> Sinto muito, não foi dessa vez
        KdPrint(("Error 0x%08x opening the file.\n", nts));
    }
    else
    {
        //-f--> Até que não foi tão difícil assim
        KdPrint(("File opened OK. Er... So, let's close it now.\n"));
 
        //-f--> Fecha o handle do arquivo. Não queremos nada com ele mesmo.
        ZwClose(hFile);
    }
    return nts;
}

Inicialmente quando tentamos abrir este arquivo à partir da função DriverEntry e nos é retornado o erro indicando que o mesmo não existe, podemos cair em uma crise existencial. Afinal, se o arquivo não existe, como o driver foi carregado? Será que o driver também não existe? Será que eu também não existo? Outras crises poderiam ser mencionadas como a tão conhecida:

“Deus é amor.
O amor é cego.
Steve Wonder é cego.
Logo, Steve Wonder é Deus.

Disseram-me que eu sou ninguém.
Ninguém é perfeito.
Logo, eu sou perfeito.
Mas só Deus é perfeito.
Portanto, eu sou Deus.

Se Steve Wonder é Deus, eu sou Steve Wonder!
Meu Deus, eu sou cego!”

Mas voltando ao assunto, caso a rotina de reinicialização for executada e o serviço que o driver precisa ainda não estiver disponível, então podemos re-agendar a chamada desta rotina mais uma vez (ou quantas vezes forem necessárias). Um dos parâmetros que a rotina de reinicialização recebe, é o que informa quantas vezes ela foi chamada pelo sistema. Isso poderia ser utilizado para desistir de procurar o recurso que nunca aparece quando a milésima tentativa falhasse. Acompanhe nossa rotina de exemplo.

/****
***     OnReinitialize
**
**      Rotina que é registrada pela funçao 
**      IoRegisterBootDriverReinitialization para ser chamada mais tarde.
*/
VOID OnReinitialize(IN PDRIVER_OBJECT     pDriverObj,
                    IN PVOID              pContext,
                    IN ULONG              ulCount)
{
    NTSTATUS    nts;
 
    //-f--> Esta pode ser reagenda quantas vezes forem necessárias.
    //      Vamos mostrar quantas foram.
    KdPrint(("OnReinitialize was called %d times...\n", ulCount));
 
    //-f--> Se não houvesse este comentário, vocês nunca descobririam
    //      que a função abaixo TentaAbrirArquivo
    nts = TryOpenFile();
 
    //-f--> E aí? Deu certo?
    if (!NT_SUCCESS(nts))
    {
        //-f--> Se o erro for diferente deste abaixo, então
        //      não sei de nada. A culpa não é minha. Eu não te conheço.
        ASSERT(nts == STATUS_OBJECT_PATH_NOT_FOUND);
 
        //-f--> Não pode ser, isso não está acontecendo de verdade.
        //      Vamos tentar um cadim mais tarde então.
        IoRegisterBootDriverReinitialization(pDriverObj,
                                             OnReinitialize,
                                             NULL);
    }
}

Instalando um driver de Boot

Para assegurar que teremos a necessidade de atrasar a inicialização do nosso driver, vamos fazer com que nosso driver de exemplo seja um dos primeiros a ser carregado. Para isso, vamos utilizar o já mencionado DriverLoader para instalá-lo em um grupo específico.

Depois de compilar o código de exemplo, copie o driver para o diretório System32\drivers da máquina vítima. Execute o DriverLoader, preencha os campos como demostrado abaixo e clique em RegisterService.

Para completar a experiência, vamos reiniciar o sistema e conectar o depurador de Kernel para acompanhar. Deveriamos ter a seguinte saída.

Mas não poderiamos utilizar um work Item para isso? Na verdade sim, mas existem sutis diferenças entre essas alternativas. Utilizar um Work Item não garante que a rotina não será executada antes da sua DriverEntry terminar, e obviamente, também não garante que ela seja executada somente depois que todos os drivers de Boot forem carregados.

Até a proxima… 😉

DrvReinit.zip

Nós queremos exemplos

18 de June de 2007

Alguns meses após minha entrada na Open, pediram me que eu fizesse uma participação em uma palestra sobre Código Seguro. Minha parte falava sobre o estouro de pilha e como tirar proveito desse descuido do programador para invadir o programa. O grande ponto a notar foi que além dos programadores, o público era composto por engenheiros e arquitetos de software, o pessoal do comercial, que tinha mais contato com os clientes, também estava lá, havia uma ou duas pessoas do administrativo lá também, ou seja, um público que poucos já ouviram falar em pilha. Teve um que disse: “Eu lembro mais ou menos disso na minha certificação de .Net, que dizia que estruturas são criadas na pilha enquanto objetos são criados no heap, ou vice-versa”. Enfim, as coisas começaram a complicar quando comecei a destrinchar o código de exemplo que forneci com PUSHs, POPs e MOVs. Resultado, alguns só dormiam enquanto outros babavam e falavam naquela língua alienígena que só falamos enquanto dormimos. Conheço bem essa língua porque minha esposa sempre troca a maior idéia comigo enquanto ela dorme e eu estou sentado na cama com o notebook. Apesar dela dizer palavras completamente incompreensíveis, ela sempre responde quando faço alguma pergunta à respeito. Ela introduz o assunto dizendo: “Admivoza bumizav”, então eu pergunto: “Por que você acha isso?”, e ela responde: “Zumirag abmish mua”. Mas voltando ao assunto, ficou claro que a quantidade de detalhes técnicos ficou incompatível com o público. Por isso, há pouco tempo atrás, em uma palestra sobre drivers para Windows, tentei passar uma visão pouco detalhada, apenas dar a idéia do que eles são e como eles contribuem para funcionamemto do sistema. Afinal, todo o departamento de desenvolvimento estava lá, incluindo arquitetos, engenheiros e .Net coders (nada contra). Para a minha surpresa, ao final da palestra, todos ficaram com aquela cara de “Ué, e o resto? Não tem nem um exemplinho?”. Então tá bom… Neste post vou escrever um driver mínimo, mas que já tenha alguma interação com uma aplicação de teste.

E no início…

Aqui vou partir do princípio que você já sabe como criar um projeto de driver do zero e como utilizar o Visual Studio para codificar drivers, indo direto ao ponto onde escrevemos o driver. O exemplo de hoje será um driver de eco bem básico que receberá comandos de escrita e leitura. Então seria assim, você inicialmente escreve buffers, que em nosso exemplo serão strings, utilizando a função WriteFile e tudo será armazenado no driver. As leituras subsequentes a partir da função ReadFile trarão os mesmos dados que foram escritos. Este exemplo vai nos ser muito útil em outros posts, e também vai servir como ponto de partida para os que estão querendo escrever seu primeiro driver.

Sabendo que vamos armazenar os dados enviados em uma lista, então criaremos uma lista de buffers formada pelos nós definidos como mostra abaixo.

//-f--> Definição para o tipo para ser armazenado
//      em nossa lista de buffers.
typedef struct _BUFFER_ENTRY
{
    PVOID       pBuffer;    //-f--> Buffer enviado
    ULONG       ulSize;     //      Tamanho do buffer
    LIST_ENTRY  Entry;      //      Nó para a lista
 
} BUFFER_ENTRY, *PBUFFER_ENTRY;
 
 
//-f--> Ponta da nossa lista e mutex de proteção
LIST_ENTRY      g_BufferList;
KMUTEX          g_Mutex;

Depois de definida a estrutura, criamos uma variável global que será a ponta de nossa lista e também um mutex para proteger nossa lista de possíveis acessos em paralelo. Isso aconteceria se tivéssemos duas aplicações de teste rodando ao mesmo tempo. Se quiser mais detalhes sobre listas ligadas do DDK, existe um post que fala sobre isso também.

Escrevendo a DriverEntry

O primeiro ponto a notar aqui é que como nosso exemplo foi codificado em um arquivo .CPP, por isso precisaremos colocar um sonoro extern “C” à definicão da função DriverEntry. Caso contrário, o linker não encontrará o ponto de entrada do driver. Logo no início da implementação, temos a mensagem que será lançada ao depurador via KdPrint. Em seguida vou inicializando a ponta da nossa lista ligada bem como o mutex que a protege. Agora vamos setar alguns membros na estrutura DriverObject, e o primeiro deles será o membro DriverUnload, que recebe um ponteiro de função de callback que informará ao driver que este está sendo descarregado. Os próximos membros são as rotinas que serão chamadas quando o driver receber as solicitações de Create/Open, Close, Read e Write. Falaremos destas rotinas com mais detalhes um pouco adiante. Depois disso, vamos criar o DeviceObject, que vai ser nosso meio de comunicação com o driver. Conforme eu disse na palestra, todas as solicitações que um driver recebe, são por meio de um device. A função IoCreateDevice faz isso para nós. Criado o device vamos configurá-lo de maneira que este utilize buffers intermediários. Para isso devemos setar o bit DO_BUFFERED_IO no campo Flags do DeviceObject que acabou de ser criado. Buffers intermediários? Falaremos sobre BufferedIo versus DirectIo numa próxima oportunidade. Existem muitos conceitos novos neste post e não vamos nos ater a todos eles, caso contrário, ninguém vai ler isso com medo de nunca acabar.

Ter um DeviceObject é legal, mas não é tudo na vida de um driver, para que uma aplicação User Mode possa se comunicar com um driver, é necessário criar um Symbolic Link. Isso é feito logo em seguida com a função IoCreateSymbolicLink. O restante do código desta função não deve causar grandes surpresas à grande maioria de vocês, mas em caso de dúvidas, é só mandar um e-mail que a gente resolve na porrada.

/****
***     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)
{
    NTSTATUS        nts;
    PDEVICE_OBJECT  pDeviceObj = NULL;
 
    __try
    {
        //-f--> Dizendo olá para o Kernel Debugger
        KdPrint(("Starting KernelEcho driver...\n"));
 
        //-f--> Inicializando a lista de buffers e mutex
        InitializeListHead(&g_BufferList);
        KeInitializeMutex(&g_Mutex, 0);
 
        //-f--> Setando nossa função de descarga do driver
        pDriverObj->DriverUnload = OnDriverUnload;
 
        //-f--> Setando as rotinas que meu driver vai dar
        //      suporte.
        pDriverObj->MajorFunction[IRP_MJ_CREATE] = OnCreate;
        pDriverObj->MajorFunction[IRP_MJ_CLOSE] = OnClose;
        pDriverObj->MajorFunction[IRP_MJ_WRITE] = OnWrite;
        pDriverObj->MajorFunction[IRP_MJ_READ] = OnRead;
 
        //-f--> Criando device de controle
        nts = IoCreateDevice(pDriverObj,
                             0,
                             &g_usDeviceName,
                             FILE_DEVICE_UNKNOWN,
                             0,
                             FALSE,
                             &pDeviceObj);
        if (!NT_SUCCESS(nts))
            ExRaiseStatus(nts);
 
        //-f--> Vamos fazer I/O com buffer intermediário
        pDeviceObj->Flags |= DO_BUFFERED_IO;
 
        //-f--> Criando symbolic link para que aplicações
        //      possam ver este device.
        nts = IoCreateSymbolicLink(&g_usSymbolicLink,
                                   &g_usDeviceName);
        if (!NT_SUCCESS(nts))
            ExRaiseStatus(nts);
 
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        //-f--> Obtendo código da cagada
        nts = GetExceptionCode();
 
        //-f--> Isso vai fazer o depurador parar aqui.
        //      Mas só se compilado em Checked
        ASSERT(FALSE);
        KdPrint(("An exception occurred at " __FUNCTION__ "\n"));
 
        //-f--> Como tivemos problemas na inicialização, vamos
        //      desfazer o que foi feito.
        if (pDeviceObj)
            IoDeleteDevice(pDeviceObj);
    }
 
    return nts;
}

Escrevendo Dispatch Functions

Também vou partir do princípio de que você já tem uma idéia do que é uma IRP. Agora vamos escrever as funções que às manipulam. São as chamadas Dispatch Functions. Estas funções são setadas na inicialização do driver como vocês puderam observar no código acima. Na estrutura DriverObject, o membro MajorFunction é um array de ponteiros de função indexado pelas macros do tipo IRP_MJ_READ. O protótipo da função é o mesmo para todas as funções e será exibido logo abaixo. Uma Dispatch Function tem que manipular IRPs seguindo algumas regras, como tudo no DDK. Uma função mínima poderia ser escrita como segue abaixo.

/****
***     OnDispatch
**
**      Exemplo de uma Dispatch Function mínima
*/
 
NTSTATUS
OnDispatch(IN PDEVICE_OBJECT  pDeviceObj,
           IN PIRP            pIrp)
{
    //-f--> Aqui preencho o status desta IRP para a aplicação.
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;
 
    //-f--> Completo a IRP. Depois disso, é terminantemente
    //      proibido tocar na estrutura pIRP. Isso não te pertence mais.
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
 
    //-f--> Retorna o status para o IoManager.
    return STATUS_SUCCESS;
}

Mas o que aconteceria se chamássemos a função ReadFile com um handle para o nosso device quando não preenchemos a posição IRP_MJ_READ? Queimariamos eternamente no mármore do inferno? Na verdade, se dermos uma olhada na tabela MajorFunction antes de preenchê-la, veremos que existe o mesmo endereço em todas as posições da tabela. Vamos colocar um break-point logo na entrada da DriverEntry, dar uma fuçada na tabela antes de ser preenchida e ver o que tem por lá.


Como vemos, essa tabela é toda inicializada com um ponteiro de função que na sua implementação, considerando que as IRPs de gerenciamento de energia têm um tratamento especial, completa a IRP com o status de STATUS_INVALID_DEVICE_REQUEST.

Uma Dispatch Function basicamente segue uma das três alternativas para tratar uma IRP. Em casos de filtros, nosso driver poderia repassar a IRP para o driver o qual estivesse atachado. Tudo bem, já está anotado aqui… “Fazer um post dando um exemplo de filtro”. A segunda alternativa seria reter a IRP para fazer um tratamento assíncrono, e por último e não menos importante, simplesmente completar a IRP. Notem que em nosso exemplo, tudo o que fazemos é dizer à aplicação que a IRP foi executada com sucesso e completamos a mesma. Para completar a IRP, utilizamos a função IoCompleteRequest, que recebe a IRP a ser completada e o Boost de Prioridade. Ah? Supondo que sua IRP tivesse alguma interação com hardware, isso iria consumir algum tempo da thread em Kernel Mode, esse tempo iria gerar um atraso na thread atual e que seria compensado por este Boost. Como esse não é o nosso caso, vamos utilizar a macro de define nenhum Boost. O DDK tem uma lista de constantes que determina qual o Boost que a thread deveria receber para cada tipo de dispositivo. Veja um deles extraído do meu wdm.h (essa definição pode estar no ntddk.h dependendo da sua versão de DDK).

//
// Priority increment for completing CD-ROM I/O.  This is used by CD-ROM device
// and file system drivers when completing an IRP (IoCompleteRequest)
//
 
#define IO_CD_ROM_INCREMENT             1

Ao final deste post , haverá um link para baixar todos os arquivos necessários para gerar a aplicação e o driver. Reparem nos fontes de exemplo que nossas Dispath Functions OnCreate e OnClose se parecem muito com o exemplo acima. Isso porque não tomamos nenhuma ação quando se abre ou se fecha um handle para o device que criamos.

Obtendo parâmetros da IRP

Já nas outras Dispatch Functions OnRead e OnWrite, temos que obter os dados que o driver precisa para executar a IRP, tais como buffer enviado pelo usuário e tamanho do mesmo, tanto na escrita como na leitura. Estes parâmetros estão em uma Stack Location dentro da IRP. Nossa, quanto mais eu rezo, mais nominho esquisito me aparece… Stack Locations são estruturas de parâmetros que são alocados junto com a IRP. Existe uma Stack Location para cada device na pilha de dispositivos que foi chamada. Essa conversa pode ficar bem divertida, mas temos um post para terminar. Vamos deixar para falar sobre Stack Locations em nosso exemplo de filtro. Lá esse assunto fará mais sentido. Mas se você não se agüenta de curiosidade e quiser saber mais sobre o assunto, veja o que a referência diz a respeito de Stack Locations. Por agora vamos apenas considerar que estes parâmetros estão lá e que para ter acesso a esta estrutura devemos utilizar a macro IoGetCurrentIrpStackLocation. Para se ter uma idéia mais prática de todo esse blablablá, segue todo o código da função OnWrite com comentários a dar com pau.

/****
***     OnWrite
**
**      Esta rotina é chamana quando a API WriteFile é chamada
**      utilizando o handle do nosso device.
*/
 
NTSTATUS
OnWrite(IN PDEVICE_OBJECT  pDeviceObj,
        IN PIRP            pIrp)
{
    PIO_STACK_LOCATION  pStack;
    PVOID               pUserBuffer;
    ULONG               ulSize;
    PBUFFER_ENTRY       pBufferEntry = NULL;
    NTSTATUS            nts;
    BOOLEAN             bMutexAcquired = FALSE;
 
    __try
    {
        //-f--> Um olá para o depurador
        KdPrint(("Writing into EchoDevice...\n"));
 
        //-f--> O buffer é um dos parâmetros que vêm
        //      na própria IRP
        pUserBuffer = (PCHAR)pIrp->AssociatedIrp.SystemBuffer;
        ASSERT(pUserBuffer != NULL);
 
        //-f-->Obtém endereço da Stack Location corrente
        pStack = IoGetCurrentIrpStackLocation(pIrp);
 
        //-f--> Obtém o tamanho do buffer
        ulSize = pStack->Parameters.Write.Length;
 
        //-f--> Aloca o nó junto com o buffer que será ocupado
        //      pelo buffer enviado pelo usuário.
        pBufferEntry = (PBUFFER_ENTRY) ExAllocatePoolWithTag(
            PagedPool,
            sizeof(BUFFER_ENTRY) + ulSize,
            ECHO_TAG);
 
        //-f--> Se não tem memória já viu...
        if (!pBufferEntry)
            ExRaiseStatus(STATUS_NO_MEMORY);
 
        //-f--> Inicializando a estrutura
        pBufferEntry->pBuffer = (pBufferEntry + 1);
        pBufferEntry->ulSize = ulSize;
 
        //-f--> Copia o buffer enviado pelo usuário
        //      para o buffer alocado aqui.
        RtlCopyMemory(pBufferEntry->pBuffer,
                      pUserBuffer,
                      ulSize);
 
        //-f--> Obtém o mutex que protege a lista
        //      de acessos em paralelo.
        nts = KeWaitForMutexObject(&g_Mutex,
                                   UserRequest,
                                   KernelMode,
                                   FALSE,
                                   NULL);
        if (!NT_SUCCESS(nts))
            ExRaiseStatus(nts);
 
        //-f--> Temos que lembrar disso caso algo muito
        //      ruim aconteça
        bMutexAcquired = TRUE;
 
        //-f--> Insere novo elemento no final da lista
        InsertTailList(&g_BufferList,
                       &pBufferEntry->Entry);
 
        //-f--> Informa ao IoManager que todos os dados enviados
        //      ao driver foram lidos com sucesso.
        pIrp->IoStatus.Information = ulSize;
        nts = STATUS_SUCCESS;
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        //-f--> Obtém código da cagada
        nts = GetExceptionCode();
 
        //-f--> Isso vai fazer o depurador parar aqui.
        //      Mas só se compilado em Checked
        ASSERT(FALSE);
        KdPrint(("An exception occurred at " __FUNCTION__ "\n"));
 
        //-f--> Se deu algo errado e já alocamos
        //      este buffer, então vamos desalocar
        if (pBufferEntry)
            ExFreePool(pBufferEntry);
 
        //-f--> Informa ao IoManager que nada foi transferido.
        pIrp->IoStatus.Information = 0;
    }
 
    //-f--> Libera mutex
    if (bMutexAcquired)
        KeReleaseMutex(&g_Mutex,
                       FALSE);
 
    //-f--> Completando a IRP.
    pIrp->IoStatus.Status = nts;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return nts;
}

Outro ponto importante a se notar aqui é o preenchimento do campo Information da estrutura IoStatus que fica na IRP. Nestas funções de transferência de dados, este campo informa ao IoManager a quantidade de dados que foi transferida da aplicação para o driver e vice-versa. Este campo se reflete diretamente sobre o quarto parâmetro da API WriteFile, que tem exatamente a mesma função. Depois de recebidos e validados os parâmetros, alocamos o nó que vai receber o buffer. Reparem que estamos alocando em memória paginada, afinal de contas, todas as nossas funções serão executadas em PASSIVE_LEVEL. Apesar desta função ser uma Dispatch Function, isso não significa que ela seja executada em DISPATCH_LEVEL. Vamos com calma, essas são coisas bem diferentes. Anotando… “Post sobre IRQLs e POOL_TYPEs”. A função OnRead é similar à OnWrite, assim vou poupá-los de colocar todo o código aqui.

Quando meu driver for descarregado

A função OnDriverUnload será chamada quando o driver estiver sendo terminado. Aqui, além de esvaziar a lista de buffers que podem ter ficado esquecidos no driver, vamos apagar o Symbolic Link e o DeviceObject que foi criado na inicialização. Simples assim…

/****
***     OnDriverUnload
**
**      A festa acabou, vai pra casa, um abraço pra muié,
**      e bejo nas criança.
*/
 
VOID OnDriverUnload(IN PDRIVER_OBJECT   pDriverObj)
{
    PLIST_ENTRY     pEntry;
    PBUFFER_ENTRY   pBufferEntry;
 
    //-f--> Diga boa noite
    KdPrint(("Terminating KernelEcho driver...\n"));
 
    //-f--> Aqui removemos todos os nós que ainda não foram lidos
    //      pela aplicação. Isso aconteceria se a aplicação chamasse
    //      WriteFile e não chamasse ReadFile.
    while(!IsListEmpty(&g_BufferList))
    {
        //-f--> Pega o primeiro nó da lista
        pEntry = RemoveHeadList(&g_BufferList);
 
        //-f--> Obtém o endereço a partir do nó
        pBufferEntry = CONTAINING_RECORD(pEntry, BUFFER_ENTRY, Entry);
 
        //-f--> Finalmente libera a memória utilizada por
        //      este nó
        ExFreePool(pBufferEntry);
    }
 
    //-f--> Apagando DeviceObject e SymbolicLink
    //      criados na inicialização.
    IoDeleteSymbolicLink(&g_usSymbolicLink);
    IoDeleteDevice(pDriverObj->DeviceObject);
}

Nossa, que erro horrível! E se o driver for terminado enquanto alguma leitura ou escrita estiver sendo realizada? Será que um futuro negro nos aguarda e nossas almas serão amaldiçoadas pelo resto da eternidade? Será que a Pequena Sereia tem algo a ver com isso?

Bom, melhor deixar nossas crenças de lado e focar no DDK. Um driver não pode ser terminado enquanto houverem referências para algum device deste driver. Note que esta rotina não tem retorno para que possamos informar ao sistema que o driver pode ou não ser descarregado. Se alguma aplicação ainda tiver um handle aberto para algum device enquanto você solicita a parada do mesmo, o sistema responderá que o driver não poderá ser terminado. Nessa condição, a rotina OnDriverUnload nem será chamada. Mas caso contrário, se nada impedir do driver ser descarregado e nossa rotina for chamada, já era… Seu driver já está a caminho do céu dos drivers.

O mundo encantado de User Mode

Não vou colocar todo o código fonte da aplicação aqui no post, mas todos os fontes estão no arquivo disponível para download. Creio que uma coisa que vale a pena mostrar aqui é a sintaxe de como obter o handle para o device que criamos em nosso driver de exemplo.

    //-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("\\\\.\\EchoDevice",
                         GENERIC_ALL,
                         0,
                         NULL,
                         OPEN_EXISTING,
                         0,
                         NULL);

Depois que o handle para o device foi obtido, as operações de Read, Write e Close seguem exatamente como se estivéssemos realizando as mesmas operações com arquivos. Não precisa ser nenhum mestre Jedi para conseguir utilizar estas funções.

    //-f--> Envia a string recebida ao driver via
    //      WriteFile.
    if (!WriteFile(hDevice,
                   szBuffer,
                   dwBytes,
                   &dwBytes,
                   NULL))

Instalando e testando

Já mostrei em um outro post como instalar um driver na mão. Entretanto, existem meios mais civilizados de instalar um driver. Um deles é utilizando o OSR Driver Loader, uma ferramenta que oferecida pela OSR que instala seu driver sem a necessidade de reiniciar a máquina. Na verdade, este é um procedimento bem simples de se fazer, mas não simples o suficiente para comentar sobre isso ainda neste post, então vamos utilizar a ferramenta mesmo.

Depois de compilar o driver, coloque uma cópia dele no System32\drivers da máquina vítima. Em seguida execute o DriverLoader e preencha os campos como exibido na figura abaixo.


Depois é só clicar em Register Service para instalar o novo driver e em seguida clicar em Start Service para iniciar o driver. Pronto, agora você já poderá utilizar a aplicação de teste. A aplicação é muito simples de utilizar. Depois de iniciada, digite as strings que deveriam ser enviadas ao driver. Uma string vazia indica o fim das strings e então começa a leitura das mesmas strings enfiladas no driver.

Ufa! Como vimos, mesmo um driver que faça algo muito simples requer uma quantidade considerável de código e muitos conceitos diferentes. Sei que ficaram algumas lacunas durante o post, mas espero ter ajudado. Caso tenham dúvidas em algum ponto do driver ou mesmo da aplicação de teste, não hesitem em perguntar ou mandar seus comentários. Os contatos ajudam muito a definir os próximos posts.
Have fun!

KernelEcho.zip