Author: Fernando

  • Try, Except, Finally e IoWriteErrorLogEntry (Parte 1)

    A maioria de vocês já deve conhecer o tratamento de exceção da linguagem C. Sabendo que a Run Time C em Kernel não oferece suporte ao tratamento de exceções do C++, o tratamento de exceções do C nos cai como uma luva. Exceções nem sempre significam que um erro crítico ocorreu, mas independente disso, tratá-las é essencial. Uma exceção não manipulada em Kernel significa tela azul. Em se tratando de uma exceção inesperada, tal como um Access Violation, conseguimos prevenir o sistema de terminar em azul utilizando manipuladores de exceção. Isso é ótimo, parabéns, mais uma vida salva, mas não podemos nos esquecer que houve uma exceção inesperada. Esse post não vai falar como utilizar manipuladores de exceção em detalhes, mas vai falar sobre como podemos reportar eventos como esses ao administrador do sistema.

    Uma típica manipulação de exceção

    Para os desavisados de plantão, vou apenas dar uma breve descrição sobre manipulação de exceção no fonte abaixo. Detalhes estão na referência. O fonte a seguir foi retirado de um dos exemplos disponíveis neste blog. Esta função consegue nos salvar de uma impiedosa tela azul no caso de o mané que chamar esta função passar uma string inválida como fonte desta duplicação.

    /****
    ***     DupString
    **
    **      Recebe uma Unicode String e a duplica.
    **      A string resultante deve ser liberada
    **      utilizando a função RtlFreeUnicodeString
    */
     
    NTSTATUS
    DupString(IN PUNICODE_STRING   pusSource,
              OUT PUNICODE_STRING  pusTarget)
    {
        NTSTATUS    nts = STATUS_SUCCESS;
     
        __try
        {
            __try
            {
                //-f--> Inicializa destino
                pusTarget->Buffer = NULL;
     
                //-f--> Aloca buffer de destino
                if (!(pusTarget->Buffer = (PWSTR)ExAllocatePool(PagedPool,
                                                                pusSource->Length)))
                    ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
     
                //-f--> Copia buffer
                RtlCopyMemory(pusTarget->Buffer,
                              pusSource->Buffer,
                              pusSource->Length);
     
                //-f--> Copia tamanhos
                pusTarget->MaximumLength = pusTarget->Length = pusSource->Length;
            }
            __finally
            {
                //-f--> Se algo deu errado, desalocamos buffer de destino
                if (AbnormalTermination())
                    if (pusTarget->Buffer)
                        RtlFreeUnicodeString(pusTarget);
            }
        }
        __except(EXCEPTION_EXECUTE_HANDLER)
        {
            nts = GetExceptionCode();
     
            DbgPrint("DupString >> An exception occourred at "__FUNCTION__
                     " with status 0x%08x.\n", nts);
     
            ASSERT(FALSE);
        }
        return nts;
    }

    Repare que existem dois blocos de execução. O mais externo, que vai lidar com exceções não manipuladas, e o mais interno, que vai lidar com finalizações. O manipulador do bloco de exceções vai executar o bloco __except quando alguma exceção for lançada de dentro do bloco __try. Isso nos dará a oportunidade de tentar descobrir o que ocorreu e em alguns casos até solicitar que a instrução que causou a exceção seja re-executada. O bloco de finalização __finally é executado quando a linha de execução sair do bloco principal, seja pelo simples fim do bloco, por um return, por um goto para fora do bloco, ou mesmo uma exceção que foi lançada. O bloco __finally é sempre executado. Isso nos dá a oportunidade de liberar qualquer recurso que tenha ficado pendente durante a execução do bloco. Juntando tudo, podemos imaginar a seguinte situação: Vamos supor que a função RtlCopyMemory lance uma exceção, neste caso, o bloco __except será executado, mas para que isto ocorra, o fluxo de execução vai sair do bloco mais interno também, causando assim a execução do bloco __finally.

    Dentro de cada manipulador existe uma chamada de função especial. Dentro do __except temos a GetExceptionCode, que nos retorna o código da exceção que foi lançada. Esta função só pode ser chamada dentro deste contexto. Note também que dento do bloco __finally existe a chamada à função AbnormalTermination que indica que o bloco não foi executado até o seu final. Em nosso exemplo, utilizamos esta função para determinar que algo errado aconteceu e que é necessário liberar o buffer que seria retornado à função chamadora. Enfim, dê uma olhada na referência para uma completa descrição destes recursos.

    O principal ponto a notar aqui, é o ASSERT ao final do bloco __except. Caso este fonte seja compilado em Checked, o ASSERT causará um prompt no depurador. Caso não haja um depurador conectado ao sistema, então uma tela azul será exibida. Isso porque esta macro lançará a exceção de break para o depurador, mas como o depurador não estará lá para manipular esta exceção, então já viu. Quando compilado em Free, a macro ASSERT é traduzida para nada, nossa função vai manipular a exceção e graciosamente retornar o código da exceção à função chamadora. Que lindo, mas quando o administrador vai ficar sabendo?

    Um sinal de vida

    A maneira padrão de deixar este sinal é escrevendo no registro de eventos do sistema. O que é isso? Este link pode lhe dar informações detalhadas sobre o logs de eventos do sistema. Para escrever um registro nesta lista a partir do Kernel, temos inicialmente que alocar o buffer que vai carregar toda as informações do registro utilizando a função IoAllocateErrorLogEntry.

    PVOID 
      IoAllocateErrorLogEntry(
        IN PVOID  IoObject,
        IN UCHAR  EntrySize
        );

    O ponteiro retornado por esta função, apesar de ser do tipo PVOID, aponta para uma estrutura do tipo IO_ERROR_LOG_PACKET, que possui tamanho variável dependendo das informações que a compõe. Essa estrutura é utilizada para empacotar todos os dados do evento que será logado.

    typedef struct _IO_ERROR_LOG_PACKET
    {
        UCHAR MajorFunctionCode;
        UCHAR RetryCount;
        USHORT DumpDataSize;
        USHORT NumberOfStrings;
        USHORT StringOffset;
        USHORT EventCategory;
        NTSTATUS ErrorCode;
        ULONG UniqueErrorValue;
        NTSTATUS FinalStatus;
        ULONG SequenceNumber;
        ULONG IoControlCode;
        LARGE_INTEGER DeviceOffset;
        ULONG DumpData[1];
     
    } IO_ERROR_LOG_PACKET, *PIO_ERROR_LOG_PACKET;

    Embora esta estrutura tenha três quilos de membros, você não vai precisar preencher a maioria deles. Feito isso, o passo seguinte é passar a estrutura para a função IoWriteErrorLogEntry e pronto. Como disse um amigo meu: “É mel na sopa”. Provavelmente ele misturou a frase “É mel na chupeta” com algo que leve sopa. Ainda não consegui descobrir o que seria. Alguma dica?

    VOID 
      IoWriteErrorLogEntry(
        IN PVOID  ElEntry
        );

    Vai pensando que é fácil

    Então é só alocar a estrutura, inicializá-la, chamar a função de escrita e correr para o abraço? Engraçada esta pergunta. Há pouco tempo atrás estive em Porto Alegre, e lá me disseram que quando uma explicação é iniciada por “É só…”, o trabalho real é cinco vezes maior que o explicado. Vamos começar escrevendo a tão famosa frase “Hello World” para ver o tamanho da encrenca.

    Muito bem… Onde é que passo a mensagem mesmo? Ah tá, isso mesmo, no arquivo de mensagens. Diferente dos arquivos de log que normalmente fazemos em aplicações, as mensagens não são passadas diretamente para a função de escrita. Ao invés disso, o membro ErrorCode da estrutura IO_ERROR_LOG_PACKET recebe o identificador da mensagem a ser exibida. Quando a função IoWriteErrorLogEntry é chamada, esta coloca o pacote em uma lista em memória. Mais tarde, este pacote é gravado no arquivo de log de eventos do sistema. O EventViewer, software utilizado para visualizar tais eventos, obtém o identificador da mensagem contido no pacote. Com esse identificador, ele localiza a string que vai estar armazenada em um módulo em separado. Este módulo pode ser o próprio driver ou uma DLL. Assim, a string é lida desse módulo e exibida ao usuário. Ufa!

    Hã? Quem? Quando? Onde? Vamos nos basear no driver de exemplo mais simples que temos para adicionar estes recursos. Vamos começar compondo o módulo de mensagens escrevendo um arquivo texto de extensão .mc. Este arquivo texto vai ser compilado pelo Message Compiler. Esta compilação irá gerar o arquivo de recurso que conterá as strings junto com o arquivo de header que fará referência a alguns símbolos criados. Veja o modelo de arquivo de mensagem abaixo:

    MessageIdTypedef = NTSTATUS
     
    SeverityNames =
    (
        Success         = 0x0:STATUS_SEVERITY_SUCCESS
        Informational   = 0x1:STATUS_SEVERITY_INFORMATIONAL
        Warning         = 0x2:STATUS_SEVERITY_WARNING
        Error           = 0x3:STATUS_SEVERITY_ERROR
    )
     
    FacilityNames =
    (
        System          = 0x0
        DriverEntryLogs = 0x2A:DRIVERENTRY_FACILITY_CODE
    )
     
    LanguageNames =
    (
        Portuguese  = 0x0416:msg00001
        English     = 0x0409:msg00002
    )
     
    MessageId = 0x0001
    Facility = DriverEntryLogs
    Severity = Informational
    SymbolicName = EVT_HELLO_MESSAGE
     
    Language = Portuguese
    "Ola mundo!"
    .
    Language = English
    "Hello world!"
    .

    Repare que podemos ter a mesma mensagem em várias línguas. Nossa que chique hein? Salve este arquivo com o nome de LogMsgs.mc. Pode ser o nome de sua preferência, mas lembre-se que este nome vai se repetir nos arquivos gerados. Para compilar este arquivo, basta colocá-lo na lista de fontes a serem compilados que fica no arquivo sources do seu projeto como mostra abaixo. Se você decidir que as mensagens deverão ser contidas dentro do próprio driver, então é necessário adicionar o arquivo LogMsgs.rc que vai ser gerado pelo Message Compiler.

    TARGETNAME=Useless
    TARGETPATH=obj
    TARGETTYPE=DRIVER
     
    SOURCES=LogMsgs.mc\
            Useless.c \
            LogMsgs.rc

    Depois de compilado, os arquivos LogMsgs.rc e LogMsgs.h serão gerados. O arquivo de header define os códigos de erros definidos no arquivo de mensagens e deve ser incluído nos fontes que farão as chamadas para a criação do log. Observe um trecho deste arquivo que foi gerado automagicamente.

    //
    // MessageId: EVT_HELLO_MESSAGE
    //
    // MessageText:
    //
    //  "Ola mundo!"
    //
    #define EVT_HELLO_MESSAGE                ((NTSTATUS)0x402A0001L)

    Finalmente já podemos escrever as linhas de código que utilizarão toda essa parafernália que criamos.

    #include <ntddk.h>
    #include "LogMsgs.h"
     
    /****
    ***     DriverEntry 
    **
    **      Ponto de entrada do nosso driver, tudo começa aqui,
    **      depois vai enrolando, enrolando, ...
    */
     
    NTSTATUS DriverEntry(IN PDRIVER_OBJECT  pDriverObject,
                         IN PUNICODE_STRING pusRegistryPath)
    {
        PIO_ERROR_LOG_PACKET    pLogPacket;
     
        //-f--> Aloca a entrada para o evento
        pLogPacket = IoAllocateErrorLogEntry(pDriverObject,
                                             sizeof(IO_ERROR_LOG_PACKET));
     
        //-f--> Inicializa toda a estrutura
        RtlZeroMemory(pLogPacket, sizeof(IO_ERROR_LOG_PACKET));
     
        //-f--> Coloca a mensagem desejada
        pLogPacket->ErrorCode = EVT_HELLO_MESSAGE;
     
        //-f--> Envia a entrada para a lista de eventos
        IoWriteErrorLogEntry(pLogPacket);
     
        //-f--> Ufa, conseguimos chegar até aqui, isso merece
        //      um retorno de sucesso para o sistema.
        return STATUS_SUCCESS;
    }

    Tudo pronto agora?

    Do ponto de vista do driver já está tudo pronto. O driver já pode ser compilado, carregado e assim criar a entrada no log carregando o número da mensagem a ser exibida pelo EventViewer. Depois de pôr o driver para rodar, vamos abrir o EventViewer e localizar a entrada gerada pelo nosso driver, e assim, ver a mensagem.


    Ops! Ainda temos que informar ao EventViewer o módulo responsável por armazenar as strings. Em nosso caso, as mensagens estão no próprio driver. Para isso temos que adicionar uma chave com nome do nosso driver dentro da chave de registro do EventLog. Dentro desta chave, ainda termos que criar dois valores informando o path do módulo que conterá as mensagens e quais tipos de eventos nosso driver pode gerar. Observe com atenção o caminho do registo na figura abaixo.


    Assim que entramos com os valores acima descritos no registro, é só reabrir o mesmo evento para que a mensagem seja exibida como se deve. Lembre-se que o driver não precisa enviar o evento novamente para que a mensagem seja exibida corretamente. O driver apenas manda uma entrada com o identificador da mensagem. Isso já foi feito independente do módulo de mensagem ter sido configurado. Na figura abaixo podemos observar as mensagens em português e em inglês, dependendo da língua do sistema operacional. Olha que bunitim!

    Como este post está ficando maior que eu esperava (pra variar), vou dividi-lo em partes. Ainda pretendo comentar sobre como inserir parâmetros nas entradas e esclarecer a dúvida que alguns de vocês podem estar se fazendo neste instante: “Se a entrada do log fica inicialmente em uma lista ligada, e só depois de um tempo é que efetivamente vai para disco, as entradas podem ser perdidas se a máquina cair neste intervalo?”.

    Será que o culpado é mesmo o mordomo? Será que a Matrix está dentro de outra Matrix? Tostines vende mais porque é fresquinho, ou é fresquinho porque vende mais? Não percam!
    Até mais… 🙂

  • Como assim eu não gosto de tela azul?

    Não que eu morra de alegria toda vez que vejo uma. Tela azul é um sinal de que algo errado aconteceu, mas melhor você tê-la visto que um cliente seu ligar dizendo que viu uma. Por isso, precisamos fazer a maior força para que elas aconteçam. Já vi programadores fugirem da tela azul tentando se esconder atrás de um manipulador de exceções. Acho que não preciso dizer que isso é tapar o sol com a peneira. Desta forma você apenas deixa de encontrar agora os erros que você ou alguém vai encontrar daqui a algum tempo, até lá, os micros que estão utilizando seus drivers podem ter uma reinicialização repentina aqui ou alí. Mas telas azuis acontecem mesmo, é tudo uma questão de sorte, só que seus clientes podem perceber que usar os seus drivers dá o maior azar. Neste post, vou dar algumas dicas de como podemos ver mais telas azuis em nossos testes de drivers.

    Uma passada de F8

    Não entenda isso como “Teste você mesmo”, mas entenda por testar todos os códigos de retorno que você puder. Lembre-se que se algo der errado, não será apenas mais um MessageBox que vai aparecer, mas normalmente quando as coisas tomam esse rumo, tudo termina em azul, mais cedo ou mais tarde. Vamos torcer pelo mais cedo. Costumo dizer que todo código merece pelo menos uma passada de F8, que é a minha tecla de Step into. É, eu sei que é diferente do normal, mas quando comecei a utilizar o Visual C/C++ 1.52, o layout do teclado era esse. Mas voltando ao ponto, certa vez eu fiz uma função que deveria simplesmente ler um arquivo, e na minha passada de F8 recebi um código de retorno que não era STATUS_SUCCESS, mas passou na macro NT_SUCCESS(). A pressa me fez ignorar aquilo, afinal não era um erro, era apenas um aviso. Semanas depois, a equipe de teste me disse que por algum motivo o driver estava retornando lixo na leitura do arquivo. Um pouco de debug me voltou a mostrar aquele mesmo código de retorno. Foi só depois de olhar a sua definição no arquivo ntstatus.h que entendi tudo.

    //
    // MessageId: STATUS_PENDING
    //
    // MessageText:
    //
    //  The operation that was requested is pending completion.
    //
    #define STATUS_PENDING                   ((NTSTATUS)0x00000103L)    // winnt

    Enquanto depurando, o sistema tem tempo suficiente para fazer o I/O que ficou para depois, mas quando botamos o bixo pra correr a história é outra.

    Um código que passou no seu teste de F8 rodará nos mais diversos ambientes existentes e imagináveis. Logo, essa primeira fase serve apenas para retirar os erros mais grosseiros. Depois disso, é a equipe de testes que deveria surrar a vítima. Existem pessoas que realmente tem o dom de testar software. Trabalhei em uma empresa em que a pessoa que testava os produtos devia ter algum problema pessoal com o software que produzíamos (ou com nós, vai saber). Eu podia testar por dias, mas quando eu entregava o software para teste, não demorava meia hora para ele me ligar de volta dizendo aquela frase que já virou sua marca registrada: “Muito ruim!!!”. Não tinha explicação. Costumávamos dizer que o micro dele tinha sido formatado em cima de um cemitério indígena. Não é à toa que teste de desenvolvedor é tão mal visto.

    Acerte no ASSERT

    Imagino que a maioria de vocês já tenha ouvido falar na macro ASSERT. Esta macro tem a finalidade de assegurar que determinada condição seja verdadeira. Caso a condição seja falsa, uma tela azul nos salta aos olhos. Nossa, que macro incrível, ajudou muito mesmo. É, eu sei, não é bem assim. Na verdade este é o comportamente que teríamos se o depurador não estivesse atachado ao sistema. Então seria perfeito utilizar esta macro somente em Checked. Devo lembrá-los que Checked significa Debug e Free significa Release? Bom, já foi. Se dermos uma olhada em sua definição, veremos que o óbvio já foi pensado.

    #if DBG
     
    #define ASSERT( exp ) \
        ((!(exp)) ? \
            (RtlAssert( #exp, __FILE__, __LINE__, NULL ),FALSE) : \
            TRUE)
    ...
     
    #else
     
    #define ASSERT( exp )         ((void) 0)
     
    ...
     
    #endif // DBG

    Mas o que acontece se estivermos com o depurador atachado? Boa pergunta, essa é realmente uma pergunta interessante, fico feliz que você a tenha feito. Alguém já lhe disse que você leva jeito para programar? Enfim, eu alterei um dos nossos exemplos para forçar esta condição.

    extern "C"
    NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObj,
                         IN PUNICODE_STRING pusRegistryPath)
    {
        //-f--> Eu acho que tenho certeza de que esta conta está certa.
        ASSERT(1 + 1 == 3);

    Caso a condição falhe e estivermos com um debugger de Kernel atachado ao sistema, este exibirá a condição que falhou na janela de saída e nos solitará que entremos com uma das quatro alternativas como mostra abaixo.


    Como vocês podem ver, o ASSERT é prático, fácil e não engorda. Ele só requer um pouquinho assim de cérebro. Digo isso porque todos nós já tivemos dias difíceis, e depois das 23h nenhum programador deveria responder por qualquer código produzido. Certa vez custou-me descobrir por que o código abaixo não funcionava adequadamente. Apesar de tudo funcionar perfeitamente bem quando compilado em Checked, parecia simplesmente que uma certa função não estava sendo chamada em Free. Se derem mais uma olhada na definição desta macro, verão que a condição desaparece quando compilada em Free, e neste caso, a chamada também.

        //-f--> Não basta querer programar, tem que pensar.
        //      << Não copie isso >>
        ASSERT(DoSomeThing() == STATUS_SUCCESS);

    Um sistema em Checked

    Não seria bom ter um sistema operacional inteiro repleto de ASSERTs e testes para detectar o menor sinal de problemas, e ao encontrá-lo, nos presentearia com uma bela tela azul? Bem, você provavelmente já deve ter ouvido falar das versões Checked Build. Elas são exatamente isso que acabei de escrever. Um sistema operacional inteiro compilado em Checked. Isso significa que todos os ASSERTs que estão nos fontes foram incluidos nos binários finais e estão verificando o sistema para você. Pode parecer besteira ter que instalar seu driver em um desses, mas acredite, vale a pena. Já tive drivers que funcionaram muito bem durante meses, até que meu gerente sugeriu que fossem testados em versões Checked. No topo da minha arrogância, eu pensei comigo mesmo: “Até parece que vai pegar alguma coisa”. É, vivendo e aprendendo. A máquina nem iniciava com meus drivers, era uma tela azul atrás da outra. As versões de Checked Build têm verificações até de deadlock. Você seria capaz de adivinhar o que aconteceria se algum spinlock fosse mantido por mais de um determinado tempo? Aposto que sim.

    Já que as versões Ckecked Build são à prova de falhas, posso usá-las como meu sistema padrão? Poder pode, mas tudo é muito mais lento, milhares de verificações estão sendo feitas todo o tempo e o código não tem qualquer otimização aplicada. Em uma instalação do zero, sem instalar qualquer software adicional, você pode encontrar ASSERTs do Internet Explorer ou de outros programas. Isso mesmo, não é só o Kernel que está em Checked, mas todo o sistema. Você teria sempre que deixar um depurador de Kernel atachado ao seu sistema, pois o menor sinal de fumaça seria motivo mais que suficiente para mais uma tela azul.

    Se passou pelo Checked Build então o driver está perfeito? Sinto desapontá-lo. Lembre-se que o impacto de performance causado por tantos testes pode ocultar problemas como race conditions. O ideal é testar seus drivers em ambas as versões. Uma configuração permite que tenhamos apenas a imagem do sistema e a HAL em Checked enquanto que o restante do sistema fique em Free. Isso permitiria ter testes adicionais no Kernel enquanto o restante do sistema roda a versão mais leve. Este link explica como fazer isso.

    Agora sim meu driver tá lindio?

    Na verdade, isso ainda é só o mínimo que você deveria fazer. Uma outra excelente ferramenta geradora de telas azuis é o Driver Verifier. Este aplicativo trabalha em conjunto com o sistema para monitorar ações de uma lista de drivers composta por você. A partir do Windows 2000, o Driver Verifier já vem instalado. Experimente agora mesmo! Não requer prática e tão pouco habilidade. Na janela de “Run…” do Windows, digite “Verifier” e um simples Wizard será iniciado. Como pretendo terminar este post ainda nesta vida, não vou descrever passo a passo como utilizar esta ferramenta, mas existem vários links na página do produto que podem te ajudar nisso.

    Vai gostar de tela azul assim no inferno!

    Para se ter uma idéia de como a busca por telas azuis é importante para uma companhia de software, a Microsoft promove o IFS Plug Fest com o intuito de testar a interoperabilidade de diversos produtos que implementam File Systems ou filtros para eles. O evento é gratuito, mas cada compania deve arcar com as despesas de viagem e estadia de seus representantes. Nestes encontros, os profissionais do mundo todo reunem-se para fazer os testes entre si além de poder ter o contato e tirar possíveis dúvidas a respeito deste complexo modelo de desenvolvimento. São também promovidos seminários que discutem os problemas comuns enfrentados por esta comunidade. Ainda não estive em um desses, mas um dia quem sabe.

    Calma, ainda tem mais

    Além das ferramentas aqui citadas, existem outras que eu não poderia deixar de citar. Mas não vou me prolongar em explicar como cada uma delas funciona.

    • Prefast – Ferramenta que faz análize dos fontes a serem compilados pelo DDK em busca de erros comuns de programação para drivers de Kernel.
    • Static Driver Verifier – Esta ferramenta faz uma análise estática do binário já compilado. O teste é bem mais demorado, mas obtém mais resultados que os obtidos no Prefast.
    • Hardware Compatibility Test – Conjunto de softwares que aplicam o testes para a obtenção da certificação de drivers da Microsoft. Descontinuada há pouco tempo.
    • Driver Test Manager – Este é o software sucessor do HCT para testes de certificação de drivers.

    Depois de tanta tela azul, só café mesmo pra ajudar.
    Até mais!

  • Usando FileObject e FsContext

    Muito bem, com alguns posts e um pouco de paciência para agüentar minhas piadinhas, podemos construir um simples driver que responda às chamadas de aplicações. A chamada à função CreateFile cria uma conexão entre a aplicação e o device e nos retorna um handle, que mais tarde será utilizado para encaminhar solicitaçoes de leitura e/ou escrita ao device. Se houverem mais chamadas ao CreateFile, outros handles serão retornados. Como com arquivos, cada handle possui seu próprio contexto. Suponha que você abra duas vezes o mesmo arquivo, obtendo assim dois handles com contextos diferentes. Uma leitura de 100 bytes a partir do primeiro handle faz com que a posição atual deste arquivo agora seja diferente da posição inicial. Uma nova leitura utilizando este mesmo handle resultaria nos próximos 100 bytes adiante dos bytes já lidos, mas se você utilizar o segundo handle, você obteria novamente os 100 primeiros bytes do arquivo. Neste post vou falar como manter este contexto entre os vários handles abertos para o seu device.

    O problema na prática

    Há pouco tempo atrás, em um dos meus posts, dei um exemplo básico de driver que implementa uma lista ligada de buffers que foram escritos no device. Os mesmos buffers são obtidos em leituras posteriores ao mesmo device. Este post vai se basear nesse driver de exemplo para fazer os testes e modificações sugeridas. A lista desse driver foi implementada como uma única variável global. Se existe uma única lista, os diversos handles retornados às aplicações manipularão a mesma lista.

    Lembre-se que o programa de teste escreve as strings digitadas no console no device até que uma string vazia seja fornecida. Quando isso ocorre, a aplicação passa a realizar leituras e exibir o resultado na tela. Assim, a mesma seqüência de strings fornecidas deveria ser exibida. Para reproduzir o problema, siga os passos abaixo com o driver já instalado.

    1. Abra uma janela de Prompt e execute uma instância do programa de teste.
    2. Entre com a seqüência de strings de “111”, “222”, até “555”, mas ainda não entre com a string vazia.
    3. Abra uma nova janela de Prompt e execute uma outra instância do programa de teste.
    4. Nesta, entre com uma seqüência de strings de “666” até “999” e em seguida uma string vazia.
    5. Volte ao Prompt anterior e entre com a string vazia na primeira instância do programa de teste.

    Devemos obter uma saída como mostra a figura abaixo. Note que a primeira instância do programa de teste não leu nenhuma string apesar de ter entrado com várias. As strings faltantes foram parar na segunda instância do programa.

    Separando as listas

    Criar um algoritmo que separe as listas por processo até funcionaria, mas tente imaginar que uma mesma aplicação utilize duas bibliotecas distintas, que por sua vez utilizam as listas do driver. Desta forma, a lista de uma biblioteca se misturaria com a lista de outra, já que ambas as bibliotecas estão no mesmo processo. Para separar o contexto das várias aberturas do device utilizamos o membro FileObject da stack location atual.

    Quando uma aplicação chama a função CreateFile, esta requisição vai para o ObjectManager que verifica a existência do objeto desejado, em seguida, verifica se a aplicação tem direitos para obter um handle para este objeto, e se tudo estiver de acordo, a solicitação chega até seu driver em forma de uma IRP com o MajorFunction igual a IRP_MJ_CREATE. Para obter FileObject referente a esta conexão que está sendo criada, você precisa fazer como mostra o código abaixo.

        //-f--> Recupera o FileObject referente a esta lista
        pStack = IoGetCurrentIrpStackLocation(pIrp);
        pFileObject = pStack->FileObject;

    Essa ou aquela lista?

    Todas as operações que vierem da mesma instância do objeto, virão com o mesmo FileObject. Isso ajuda a rastrear o contexto de uma entre as várias solicitações de aberturas que chegarão ao seu driver.

    Então é só criar uma lista de FileObjects e vincular cada um a uma lista? A idéia de vincular o FileObject a uma estrutura que carregue este contexto é tão óbvia que o sistema já reservou um espaço na estrutura FILE_OBJECT para que você coloque os dados referentes a este contexto.

    typedef struct _FILE_OBJECT
    {
        CSHORT  Type;
        CSHORT  Size;
        PDEVICE_OBJECT  DeviceObject;
        PVPB  Vpb;
        PVOID  FsContext;
        PVOID  FsContext2;
        PSECTION_OBJECT_POINTERS  SectionObjectPointer;
        PVOID  PrivateCacheMap;
        NTSTATUS  FinalStatus;
        struct _FILE_OBJECT  *RelatedFileObject;
        BOOLEAN  LockOperation;
        BOOLEAN  DeletePending;
        BOOLEAN  ReadAccess;
        BOOLEAN  WriteAccess;
        BOOLEAN  DeleteAccess;
        BOOLEAN  SharedRead;
        BOOLEAN  SharedWrite;
        BOOLEAN  SharedDelete;
        ULONG  Flags;
        UNICODE_STRING  FileName;
        LARGE_INTEGER  CurrentByteOffset;
        ULONG  Waiters;
        ULONG  Busy;
        PVOID  LastLock;
        KEVENT  Lock;
        KEVENT  Event;
        PIO_COMPLETION_CONTEXT  CompletionContext;
        KSPIN_LOCK  IrpListLock;
        LIST_ENTRY  IrpList;
        PVOID  FileObjectExtension;
     
    } FILE_OBJECT, *PFILE_OBJECT;

    Os campos FsContext e FsContext2 são utilizados para este fim, a menos que você implemente um filtro, você pode utilizá-los à vontade. Para fazer com que cada abertura ao device tenha sua própria lista, vamos armazenar o endereço da ponta de nossa lista em um destes campos como é mostrado abaixo na nova implementação do OnCreate. Todo o código fonte alterado está disponível para download ao final deste post.

    /****
    ***     OnCreate
    **
    **      Esta rotina é chamada quando uma aplicação ou driver
    **      tenta obter um handle para o device que criamos.
    */
     
    NTSTATUS
    OnCreate(IN PDEVICE_OBJECT  pDeviceObj,
             IN PIRP            pIrp)
    {
        NTSTATUS            nts = STATUS_SUCCESS;
        PBUFFER_LIST_HEAD   pListHead;
        PIO_STACK_LOCATION  pStack;
     
        //-f--> Um Olá para o depurador...
        KdPrint(("Opening EchoDevice...\n"));
     
        //-f--> Aloca a ponta da lista para este IRP_MJ_CREATE
        pListHead = (PBUFFER_LIST_HEAD) ExAllocatePoolWithTag(
            PagedPool,
            sizeof(BUFFER_LIST_HEAD),
            ECHO_TAG);
     
        if (pListHead)
        {
            //-f--> Inicializando a lista de buffers e mutex
            InitializeListHead(&pListHead->BufferList);
            KeInitializeMutex(&pListHead->Mutex, 0);
     
            //-f--> Armazenamos nosso contexto no FileObject.
            pStack = IoGetCurrentIrpStackLocation(pIrp);
            pStack->FileObject->FsContext = pListHead;
        }
        else
            nts = STATUS_INSUFFICIENT_RESOURCES;
     
        //-f--> Completando a IRP com sucesso.
        pIrp->IoStatus.Status = nts;
        pIrp->IoStatus.Information = 0;
        IoCompleteRequest(pIrp, IO_NO_INCREMENT);
        return nts;
    }

    FileObject é equivalente a um Handle?

    Não. Inicialmente temos um handle para cada FileObject, mas a quantidade de handles para um FileObject aumenta à medida que duplicamos os handles. Quando a função CreateFile é chamada, seu driver recebe uma IRP_MJ_CREATE, mas seu driver não é avisado quando alguém chama a função DuplicateHandle. Assim, cada handle aponta para um objeto, mas um objeto pode ser apontado por vários handles.

    Quem vai limpar esta bagunça?

    Quando todas as referências a um determinado FileObject forem liberadas, seu device receberá um IRP_MJ_CLOSE. Vamos utilizar este evento para efetuar a limpeza de qualquer string que ainda estiver na lista.

    /****
    ***     OnClose
    **
    **      Vou simplificar dizendo que esta rotina é chamada quando
    **      a aplicação ou driver fecha o handle que foi previamente
    **      aberto. Mas no fundo não é isso (fica pra um outro post).
    */
     
    NTSTATUS
    OnClose(IN PDEVICE_OBJECT  pDeviceObj,
            IN PIRP            pIrp)
    {
        PBUFFER_LIST_HEAD   pListHead;
        PIO_STACK_LOCATION  pStack;
        PLIST_ENTRY         pEntry;
        PBUFFER_ENTRY       pBufferEntry;
     
        //-f--> Um olá para o depurador
        KdPrint(("Closing EchoDevice...\n"));
     
        //-f--> Recupera a lista referente a este FileObject
        pStack = IoGetCurrentIrpStackLocation(pIrp);
        pListHead = (PBUFFER_LIST_HEAD)pStack->FileObject->FsContext;
     
        //-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(&pListHead->BufferList))
        {
            //-f--> Pega o primeiro nó da lista
     
            pEntry = RemoveHeadList(&pListHead->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--> Libera a memória utilizada pela ponta da lista
     
        ExFreePool(pListHead);
     
        //-f--> Completando a IRP com sucesso.
        pIrp->IoStatus.Status = STATUS_SUCCESS;
        pIrp->IoStatus.Information = 0;
        IoCompleteRequest(pIrp, IO_NO_INCREMENT);
        return STATUS_SUCCESS;
    }

    Quando uma aplicação é terminada ou mesmo quando ela cai sem sua vontade, todos os handles desta aplicação são fechados pelo sistema. Desta forma, é garantido que seu driver sempre receberá o IRP_MJ_CLOSE reference ao objeto que foi liberado. Na verdade existe uma novelinha a respeito do IRP_MJ_CLEANUP e IRP_MJ_CLOSE que vai ficar para uma próxima vez.

    Depois de implementada a alteração, cada handle aberto terá sua própria lista, a menos que o handle seja duplicado. Se repetirmos a mesma seqüência de passos enumerados para forçar o erro, teremos a seguinte saída.


    Em um post futuro, mostrarei como dar nomes às listas de forma a permitir que processos distintos possam abrir a mesma lista a partir de um nome conhecido.

    Até mais! 🙂

    FileObjEcho.zip

  • Debug ou falha de segurança?

    A um tempo atrás, escrevi um post que fala sobre como substituir drivers defeituosos utilizando o WinDbg. Uma técnica excelente para substituir drivers, que por algum problema, impedem a máquina de iniciar. Mas para conseguir desfrutar de toda essa facilidade, é necessário que haja uma conexão do sistema com o Kernel Debugging. Isso nos leva ao meu último post, que fala justamente sobre como habilitar este recurso bastando apenas estar em frente à máquina, e no pior caso, ter uma porta serial. Tudo isso é muito cômodo, afinal, nunca sabemos onde um problema desconhecido pode acontecer, e muito menos, em qual máquina vai acontecer. Mas essa praticidade toda tem um preço. O efeito colateral disso é que qualquer um pode depurar o Kernel de uma máquina sem ao menos ter qualquer conta válida nela. Na verdade, um notebook, um cabo e uma porta serial já são armas mais que suficientes para desmontar a sengurança do sistema. Sem senha, sem cabo de rede, sem brute-force. Apenas Kernel Debugging.

    Instalando um driver pela serial

    Drivers são um tipo de software que precisa ser confiável e muito bem escrito. Afinal de contas, eles rodam no nível mais baixo do sistema. Com drivers fica realmente muito fácil enganar o sistema, já que boa parte do sistema roda sobre serviço oferecidos pelo Kernel. Os posts acima me motivaram a escrever uma pequena prova de conceito que tem por finalidade injetar um driver que se instala no sistema, este driver, por sua vez, extrai e instala um programa no Windows que será executado a cada logon no sistema. Para tanto, precisaremos apenas de uma máquina que possa ser depurada. Meu último post mostra que essa condição não é tão difícil de ser encontrada. Quer testar? Qual a percentagem de computadores à sua volta que possuem portas seriais?

    Passo a passo

    Este é mais um daqueles posts que vêm com código disponível para download. Todo o procedimento demonstrado aqui pode ser reproduzido e modificado pelos leitores deste blog. Este não é um blog especializado em ataques ao Windows e meu objetivo aqui é puramente didático. Não vou explicar cada linha do código de exemplo, caso contrário, esse post ficaria muito extenso. Entretanto, me disponho a responder quaisquer dúvidas a respeito. A figura abaixo enumera os passos a serem tomados para atingir nosso objetivo.

    1. O primeiro passo acontece durante a compilação do driver de exemplo. O solution disponível possui três projetos. O primeiro projeto é o da aplicação a ser injetada. Neste caso, uma aplicação Win32 que apenas mostra uma mensagem e termina. Vamos nos referir a esta aplicação como MyFile.exe. A Run Time do C/C++ foi removida para obtermos a menor aplicação possível. Isso vai contar bastante quando você tiver que transferir tudo por um cabo serial a 19200. O segundo projeto é apenas uma ferramenta. A partir do arquivo MyFile.exe já compilado, esta ferramenta gera um arquivo de header que cria uma variável do tipo array de caracteres com o corpo do arquivo passado como linha de comando. Este não é o método mais avançado de se fazer um Self-Extractor, mas é suficiente para nosso objetivo aqui. Vamos chamar esta ferramenta de HeaderFile.exe. Quando o driver é compilado, este inclui o arquivo de header e ganha uma variável com a aplicação a ser injetada. Vamos chamar nosso driver de MapInject.sys. O driver neste exemplo é compilado utilizando o ddkbuild.cmd com Visual Studio 2005. Mais detalhes a respeito neste post.

      /****
      ***     WinMain
      **
      **      Não faz quase nada e não usa a Run Time do C/C++
      **      para evitar código longo.
      */
       
      int WINAPI WinMain(HINSTANCE   hInstance,
                         HINSTANCE   hPrevInstance,
                         LPSTR       lpCmdLine,
                         int         nShowCmd)
      {
          //-f--> É só isso mesmo...
          MessageBox(NULL,
                     L"A serial port was found",
                     L"DriverEntry.com.br",
                     MB_OK | MB_ICONEXCLAMATION);
       
          return 0;
      }
    2. Utilizando as técnicas de mapeamento de drivers demostradas neste post, podemos fazer com que nosso driver possa ser carregado no lugar de qualquer driver pré-existente no sistema. Obviamente nosso driver não vai realizar todos os passos que o driver sobrescrito deveria realizar, deste modo, não vamos escolher um driver que realize tarefas essenciais à carga do sistema, tais como drivers de disco, de vídeo ou algo assim. Particularmente escolhi utilizar o driver da porta paralela Partport.sys, que é um driver que existirá em qualquer instalação básica de Windows e não vai nos fazer tanta falta enquanto brincamos por aqui. Este também não é nenhum driver do tipo Boot, que no caso do Windows XP, seria necessário ter um Loader especial para poder ser mapeado pelo Debugger.

      map 
      \Windows\System32\drivers\parport.sys 
      Z:\MapInject\Driver\objfre_wnet_x86\i386\MapInject.sys
    3. Com o driver mapeado e carregado pelo sistema, basta um pouco de boa vontade para poder pintar e bordar com o sistema. Quando nosso driver é carregado pela primeira vez, este é carregado no lugar do Parport.sys. Nesta condição, nos instalamos no sistema. Certas coisas nunca mudam. Para instalar um driver no Windows plataforma NT basta criar uma chave e poucos valores no registro do sistema como descrito neste post. Isso ainda funciona no Windows Vista. Nosso driver faz isso neste passo.

    4. Feitas as modificações no registro, agora temos que renomear nosso driver, que pelo fato de ter sido mapeado, ganhou o nome do driver nativo do sistema. Assim, uma função troca o nome do driver de Parport.sys para MapInject.sys. Poderiamos permanecer instalados no sistema da maneira que fomos mapeados se quizéssemos, mas perderiamos nosso lugar caso o verdadeiro driver fosse restaurado.

    5. Como sabem, nosso driver possui uma variável que contém todo o corpo de nossa aplicação. Assim, fica muito simples extrair a aplicação e salvá-la em disco. A pasta System32 vem bem a calhar como destino para nossa aplicação. Como estamos rodando em conta de sistema neste momento, não teremos problemas em fazer isso.

    6. Agora que nossa aplicação foi extraída, temos que fazer com que ela seja executada automagicamente. Alguns de vocês devem saber que não existem meios documentados ou mesmo suficientemente confiáveis de se criar um processo a partir do Kernel, mas no registro existem vários lugares onde poderíamos escrever e fazer com que um processo já existente no Windows execute a nossa aplicação. Uma das mais clássicas chaves do registro que permite isso é a Run do Windows Explorer, mas como mostra este aplicativo, ainda existem outras tantas chaves a serem escolhidas. Quando o Logon é realizando no sistema, o Shell enumera esta chave e executa cada linha na conta do usuário que fez o logon. Isso vai fazer nosso programa ser chamado.

    Colocando para funcionar

    Mais uma vez, uma máquina virtual nos cai como uma luva para este tipo de brincadeira. Se você não sabe como fazer Kernel Debugging utilizando uma máquina vitual, este post poderá te ajudar. Tudo é muito simples depois que o driver já está codificado. A única coisa necessária para realizar o teste é configurar um mapeamento que substitua o driver Parport.sys pelo MapInject.sys como mostra o arquivo de map acima. Depois disso, é só esperar o sistema subir e pronto. Qualquer usuário que efetuar o logon no sistema verá a mensagem do nosso programa. Um ponto a ser notado é que mesmo se o usuário for esperto o suficiente para encontrar o arquivo MyFile.exe, sua chamada no registro e apague ambos, os mesmos serão recriados quando o driver MapInject for iniciado a cada Boot do sistema. Para interromper esse processo, basta remover o driver MapInject do sistema.

    Senha? Pra quê?

    O Windows protege seus mais importantes recursos da maioria dos usuários, permitindo apenas que administradores da máquina tenham acesso às configurações que podem comprometer a estabilidade do sistema. Essa frase até que é “bunitinha” e chega até a gerar um certo conforto aos adminitradores de muitas e muitas máquinas. Mas a vida é uma caixinha de surpresas, e numa bela manhã de sol, um post é publicado em um site de RootKits dando dicas de como logar em um sistema em Debug sem qualquer senha. Recentemente, um outro post surgiu a respeito e não vou repetir tudo aqui. Mas posso afirmar que esta mesma técnica ainda funciona com o Windows Vista. De fato, se você puder logar no sistema sem utilizar qualquer senha, não será necessário mapear um driver para fazê-lo rodar nesta máquina. Basta logar como administrador, instalá-lo e pronto. Mas confesso que foi no mínimo divertido escrever esta prova de conceito, e além do mais, os fontes ainda poderão servir de exemplo para algumas tarefas que você queira fazer em Kernel Mode.

    Uma possível solução? Talvez deixar sua porta serial desabilidata na BIOS, que deveria estar protegida com senha, seja alguma coisa a ser feita. Mas não há muito que se possa fazer quando se tem acesso físico à maquina.

    Chega, já escrevi demais.
    []s.

    MapInject.zip

  • Debug de Emergência

    De repente tela azul. Isso mesmo, aquele seu driver que você já não via o fonte a muito tempo, que era o seu modelo de driver estável, quando foi instalado na máquina do presidente da empresa que você trabalha, resolveu se vingar e dar uma tela azul logo na inicialização do sistema. Ou seja, a máquina liga e reseta e reseta e reseta em um loop infinito que roda em paralelo com a impressão da sua carta de demissão, que por sinal, está sendo impressa em outro micro. Para depurar essa máquina e ver o que está acontecendo, e assim salvar seu projeto, seu emprego, sua dignidade, seu casamento, e por que não dizer, sua vida, você deve configurar seu sistema para entrar em Modo Debug para que o nosso amigo inseparável WinDbg possa entrar em ação. Mas como configurar para Debug uma máquina que não está nem iniciando? Hoje vou falar sobre o suporte que o Windows oferece para evitar esse tipo de situação constrangedora. Como conectar um Debugger em um micro que nunca teve suas configurações de Kernel Debugging habilitadas, seja pelo BOOT.INI ou pelo BdcEdit.

    Este post admite que você já sabe como fazer Kernel Debugging e utiliza alguns termos próprios desta prática. A partir do Windows 2000, se você pressionar a tecla F8 enquanto máquina está rodando o NT Loader, mais precisamente, 2 segundos antes de o sistema começar a ser carregado, o sistema paraliza e exibe o menu como mostra abaixo.

    Selecione o ítem “Debugging Mode”, isso nos vai levar à lista de sistemas operacionais instalados. Selecione o sistema e o resto fica por conta do WinDbg. Nessas condições, a configuração da conexão serial aplicada seria um Baud rate de 19200 na porta serial mais alta existente em sua máquina. Isto é, se seu micro tem duas portas seriais, então a conexão seria feita a 19200 na COM2. Pelo menos isso é o que a referência nos diz.

    Mas na vida real…

    Se você fizer alguns testes, também vai concluir que não podemos acreditar em tudo que lemos. Tanto o Windows 2000 como o Windows XP, utilizam sempre a porta COM2 a 19200.

    Meu computador é moderno e só tem uma porta serial. Posso me matar? Pois é, mesmo nesses casos, o sistema tenta utilizar a porta COM2, mesmo que ela não exista. Mas não se preocupe, a vida é bela e ainda há uma esperança. Algumas BIOS permitem que você configure o endereço base e a interrupção da única porta serial existente. Isso vai nos ajudar muito, afinal de contas, o NT Loader não é uma aplicação Win32 que solicita um handle para o device COM1 ou COM2 através do Object Manager. Ele simplesmente segue os endereços adotados como padrões.

    • COM1: Address 3F8h, IRQ 4
    • COM2: Address 2F8h, IRQ 3

    Desta forma, você poderá configurar a única porta (a COM1) para responder pelos endereços que normalmente são utilizados pela COM2. A BIOS normalmente tem uma lista dos recursos normalmente utilizados pelas portas seriais. Assim, qualquer chinpanzé autista poderia fazer essa troca.

    Mas meu computador é mais moderno ainda e não tem nenhuma porta serial. Posso me matar agora? A esperança é a última que morre. Você pode ainda instalar uma placa expansora PCI (ou PCMCIA em caso de notebooks) que nos forneça as portas seriais de que precisamos. É importante notar que esta placa expansora deve criar as portas COM1 e COM2 nos endereços padrões acima descritos.

    Você não está entendendo, sou um lazarento que comprou um MacBook 13″, que além de não ter portas seriais, também não oferece nenhum slot PCMCIA ou ExpressCard. Posso me matar agora? Como já comentei em outro post, portas seriais criadas a partir de portas USB não são uma opção para serem utilizadas em uma máquina TARGET durante o Kernel Debugging. Esta conexão é feita pelo NT Loader e as portas seriais que ele conhece são apenas aquelas que respondem pelos endereços acima citados. Agora sim, pode se matar.

    Alguma melhoria em Vista?

    Já o Windows Vista, este sim ignora a documentação sem dó. A configuração padrão para Kernel Debugging quando pressionamos F8 durante o Boot do Windows Vista é utilizar a porta COM1 a 115200. Mas se você utilizou o aplicativo BcdEdit com o parâmetro /DbgSettings para modificar as configurações de conexão para Kernel Debugging, então serão estas mesmas configurações a serem utilizadas quando F8 for pressionado. Ou seja, se você é um lazarento que, como eu, configurou a porta IEEE 1397 para fazer Debug em seu MacBook, então estas serão as configurações utilizadas quando você pressionar F8.

    Pode parecer brincadeira, mas é realmente frustrante ter que ir até à máquina do cliente onde seu driver está dando trabalho, armado dos seus vários aparatos tecnológicos, tais como notebook, porta FireWire, cabo serial e assim por diante, e não poder fazer nada simplesmente porque você não consegue conectar o depurador.

    Mais uma vez, espero ter ajudado.
    Até mais!

  • Personal Gina Tabajara

    Em conseqüência à volta às aulas na faculdade, meu tempo para escrever posts já diminuiu, e mais uma vez, vocês vão ter que tolerar um post que não fala nada sobre drivers. Na verdade, foi enquanto eu escrevia um post sobre drivers que escrevi esta Gina de exemplo. Mas depois de ouvir meus amigos, Lesma e Thiago, falarem que eu deveria deixar esse tal post de forma pudesse ser aplicado ao Windows Vista. Sabendo que ginas não são mais suportadas pelo Windows Vista, então a gina acabou ficando de lado. Tadinha… Enfim, como achei que o resultado ficou no mínimo divertido, vou deixar aqui esta Gina Stub (incluindo os fontes) que permite que mudemos o título dos diálogos apresentados.

    O que é uma Gina Stub?

    Seria melhor ainda dizer o que é Gina. Já escrevi algumas poucas coisas a respeito de Ginas no post que fala como utilizar o SoftIce, mas de forma resumida, Gina é o compomente do sistema que implementa a interface gráfica para a autenticação dos usuários em uma estação. Quer mais resumido ainda? É a telinha de logon do sistema. A gina é responsável por receber os dados que identificam o usuário e os repassam para os componentes que podem validar a senha e gerar o token com as credenciais deste usuário. É a partir deste token que é criada a sessão onde o usuário está se logando e onde será criado seu desktop. A gina também implementa a interface que faz a troca de senha, bloqueio e o shutdown da estação. Não vou detalhar todos estes passos aqui, tudo está explicado na documentação do Platform SDK.

    A gina original do Windows está implementada em na DLL de nome MsGina.dll que está no diretório System32 do Windows. Para implementar uma nova gina, você precisa criar uma nova DLL e informar ao sistema que esta DLL será a nova gina através de uma chave no registro. Mas criar uma gina não é algo tão simples assim. Já desenvolvi algumas e digamos que a documentação deixou um pouco a desejar. Pelo menos foi assim naquela época. A gina tem muitas responsabilidades e se você quer apenas complementar ou alterar algum comportamento da gina original, você poderia simplesmente criar uma gina stub. Gina Stub é uma DLL que exporta todas funções que uma gina deveria exportar, mas esta repassa as chamadas para a gina original do sistema, dando assim a opção que alterar apenas as funcionalidades desejadas.

    Isso não é um tutorial

    Se você quer saber como desenvolver uma gina stub e precisa de um ponto de partida, então vá até à pasta de exemplos do Platform SDK e utilize o exemplo da pasta C:\MSSDK\Samples\Security\GINA\GinaStub. O projeto que estou deixando aqui realiza alguns malabarismos para evitar código muito repetitivo e também não utiliza a C/C++ Run Time para poder ser compilada em Visual Studio 2005 e ainda assim poder rodar em Windows NT 4.0.

    Instalando a Gina

    Para cadastrar uma gina, você deve criar um valor chamado GinaDLL na chave Winlogon do registro, conforme mostra a figura abaixo. Esse valor é consultado pelo Winlogon.exe e caso este valor não exista, a gina padrão é carregada. O valor GinaTitle deve conter a mensagem que será exibida no título dos diálogos. Este valor não tem nada a ver com o Windows, é a nossa gina stub quem lê este valor. Junto dos arquivos fontes disponíveis para download no final deste post, existe um arquivo de script do registro que configura estas chaves para facilitar a sua vida.


    Copie o arquivo Gina.dll para o diretório System32 do Windows. Certifique-se que de tudo está certo antes de reiniciar a máquina e fazer com que estas alterações tenham efeito. Caso algo esteja errado e o Winlogon.exe não consiga carregar a gina, a janela abaixo é exibida antes de qualquer coisa.


    Até que o design deste MessageBox melhorou bastante a partir do Windows 2000. Se o mesmo problema acontecesse com o Windows NT 4.0, a seguinte mensagem seria exibida.

    Duas dicas úteis para gina coders

    Escrever ginas é a oportunidade que programadores User-Mode têm de gerar suas próprias telas azuis. Sua DLL é carregada pelo Winlogon.exe, e assim, é executada no address space deste processo. Isso significa que se você tiver uma excessão não tratada, isso vai derrubar este processo. O Winlogon é um processo crítico e não pode ser derrubado. Resumindo, tela azul.

    A próxima dica é meio café com leite, mas vale a pena ser comentada. Durante o processo de desenvolvimento da gina, é natural ter vários builds e a necessidade se substituir a gina que está sendo utilizada no momento sempre aparece. Você já deve ter tentado sobrescrevê-la, mas como o Winlogon.exe está sempre com ela carregada, você não consegue apagar a gina atual. Como qualquer DLL nestas condições, você pode renomeá-la mesmo enquanto está sendo utilizada pelo Winlogon. Isso permite colocar uma nova versão da gina no diretório System32 sem ter que apagar a versão que está sendo executada no momento. Quando o sistema reiniciar, o Winlogon vai pegar a gina nova e largar a velha.

    Espero que tenham gostado do brinquedinho. Agora preciso continuar aquele post.
    Have fun!

    TitleGina.zip

  • ExAllocatePool with Tagging

    Meu amigo Lesma sempre me foi uma excelente referência sobre uma maneira diferente de ver as coisas. Ele normalmente tem um ponto de vista diferente do meu, e sempre aprendo com estes outros pontos de vista. Este post será diferente do que estão acostumados a ver por aqui. Wanderley introduziu o assunto “Como ser um melhor programador nos próximos seis meses” e nos trouxe este novo ponto de vista (pelo menos para mim) de ver os blogs, conhecido como Tagging. Aqui segue a minha resposta.

    Não posso dizer que nos próximos seis meses tudo vai ser diferente, que eu realmente vou me esforçar ao máximo para me tornar um programador melhor. Creio que minha resposta seja parecida com a resposta que o Strauss nos deu, mas não igual.

    Como alguns de vocês sabem, além jornada de trabalho na Open, ainda enfrento a Universidade para concluir minha formação em Engenharia da Computação. Este curso tem me trazido assuntos novos com relação a micro processamento, sistemas embutidos, arquitetura de computadores e outros assuntos diferentes de programação Windows em C/C++. Posso dizer que isso tem aberto um pouco meu leque de oportunidades para aprender coisas novas. Desenvolver drivers é, além de programar, estabelecer a comunicação entre dois universos próximos, mas praticamente desconhecidos entre si. Normalmente que faz muito bem um lado, não consegue fazer o outro tão bem. Isso é natural. Já conheço um pouco de software, e a cada seis meses que se passam, vou conhecendo algumas coisas novas sobre hardware. Conhecendo melhor o hardware, creio poder entender e programar melhor o sistema operacional que interage com ele.

    Por falar em coisas novas, este mês me peguei comprando um livro sobre device drivers para Linux. Tenho sido Microsoft desde sempre. Obviamente estou me referindo ao aspecto profissional. Meus tempos de MSX e CP200 exercem a mesma influência que meu jardim de infância excerce sobre meu cirrículo. O livro ainda não chegou, mas com certeza vai alimentar minha base de dados sobre interação Software/Hardware e me permitir traçar um paralelo entre as duas plataformas de desenvolvimento Windows/Linux, e assim, poder entender melhor o que eles tem em comum, tanto com relação à interação Software/Hardware quanto aos conceitos de sistemas operacionais.

    Um amigo uma vez me disse que uma das melhores maneiras de aprender é ensinando. Já escrevo este blog a quase 1 ano e tenho aprendido bastante com ele. Escrever sobre algumas coisas que eu considerava simples me fez dar mais uma olhada e ver que ainda existem mais e mais detalhes sobre o assunto. Desta forma, acabo aprendendo os detalhes que foram perdidos à primeira vista. Algumas palestras que tenho dado na empresa e o curso de device drivers são um Boost para esta maneira de aprender. Confesso que estou gostando disso.

    Concluindo e resumindo, vou continuar estudando para terminar minha faculdade, ler sobre Linux e continuar blogando e sendo questionado sobre os assuntos. Esse assunto é muito divertido para programadores, mas não podemos nos esquecer que também somos maridos, filhos, irmãos, amigos e assim por diante. Este post pode lhe oferecer aquele ânimo para sair estudando tudo e dedicar todo o seu tempo disponível em prol de ser um programador exemplar. Andar de bicicleta e ir à praia também fazem parte da formação de programadores excelentes.

    Aprecie com moderação. 😉
    Até mais…

  • Bug em meu driver de Boot. Já posso formatar?

    Escrever drivers é uma tarefa que deve ser feita com um pouco de cuidado. Afinal, qualquer situação mal resolvida entre o seu código e o sistema operacional resultará em uma bela tela azul. Mas para tudo tem remédio nessa vida e felizmente Deus criou o depurador para enfrentar essas situações. Encontrando o problema, é só trocar o arquivo .sys e pronto. Só trocar o arquivo significa esperar que a máquina fique em um estado estável, substituir ao arquivo no sistema e reiniciar a máquina. Mas a vida é uma caixinha de surpresas e por encreça que parível, a maioria dos drivers que escrevemos é iniciado automagicamente. Bom, nesse caso é só torcer então para que o erro não aconteça até que o driver seja substituído. Caso contrário teríamos que utilizar recursos como colocar a HD da máquina vítima em outro sistema e substituir o driver doente, ou na falta de outro sistema, utilizar o console de recuperação do XP para evitar do seu driver subir. Então é só torcer para que ele não seja um filtro para algo importante como disco, vídeo ou File System. Sabe como é, se o filtro não sobe, o driver principal também não. Resumindo, existe uma série de malabarismos que você pode inventar para substituir um driver de carga automática que esteja com bug. Mas será que não existe algo que não dependa tanto da sorte ou mesmo da criatividade de cada um? Hoje vou falar um pouco sobre o sistema de substituição de drivers com WinDbg.

    Mapeando arquivos de drivers

    Você pode mapear os arquivos de drivers para que o WinDbg substitua o driver no momento em que este for carregado. Isso não é lindo? Para que isso ocorra, você precisa utilizar o comando .kdfiles. Com este comando, você faz um vínculo entre o driver que roda na máquina Target e o driver que foi corrigido. Uma das maneiras de fazer isso é inicialmente criando um arquivo que relacione estes dois drivers. Este arquivo deve ser um arquivo texto simples onde a sintaxe é a mostrada abaixo. Este arquivo pode ter qualquer nome e extensão.

    map 
    \??\C:\Windows\System32\drivers\MeuDriver.sys 
    C:\Pasta_Do_Driver_Novo\MeuDriver.sys

    A palavra map marca o início de um mapeamento, isso não muda. A linha seguinte é o caminho do arquivo do driver que deve ser substituído. Esta linha deve ter o mesmo formato do utilizado no valor ImagePath na chave do driver que fica no registro. A última linha é o caminho do driver novo. Este caminho pode ser na própria máquina Host ou mesmo na rede.

    Este mapeamento funciona apenas no Windows XP ou superior, na máquina Target obviamente. Se você ainda não está acostumado com termos Host e Target, dê uma olhada neste post.

    Depois de criado o arquivo, você utiliza o WinDbg para lançar o seguinte comando:

    kd>.kdfiles C:\Pasta\MeuMap.txt

    A partir desse momento, toda vez que o driver for carregado pelo sistema, o Kernel verifica se este arquivo está mapeado no depurador, e se estiver, o WinDbg envia o novo driver pelo meio de conexão entre eles, seja serial, firewire ou USB. Para arquivos grandes, recomenda-se utilizar firewire. Entenda que o arquivo é substituído no disco da máquina Target pela versão nova. Isso significa que nas próximas inicializações nosso driver novo ainda será carregado, mesmo que não tenhamos o debugger atachado ao sistema.

    Será que funciona mesmo?

    Para tornar as coisas um pouco mais claras, vamos ver um exemplo prático. Para isso vamos precisar de um fonte de driver bem simples, para não dizer inútil, que pode ser encontrado aqui. Vamos alterar a função DriverEntry de forma que fique como abaixo:

    NTSTATUS DriverEntry(IN PDRIVER_OBJECT  pDriverObject,
                         IN PUNICODE_STRING pusRegistryPath)
    {
        //-f--> Vamos utilizar __DATE__ e __TIME__ para que tenhamos
        //      uma versão nova de driver a cada rebuild
        DbgPrint("Este driver foi compilado em "__DATE__" "__TIME__"\n");
     
    ...

    Compilamos uma versão inicial e o instalamos na maquina Target da maneira que você achar melhor, mas vamos admitir que depois de instalado teremos o registro como na figura abaixo.

    Se iniciarmos o driver, teremos algo parecido com a string abaixo na saída do depurador:

    Este driver foi compilado em Jul 16 2007 00:04:03

    Agora vamos criar nosso arquivo de mapeamento. Aqui vou salvá-lo como Z:\Sources\DriverEntry\Useless\map.txt. Seguindo o formado de como o driver foi registrado no registro, nosso arquivo de mapeamento deveria ter o seguinte conteúdo:

    map
    \??\C:\Windows\System32\drivers\Useless.sys
    Z:\Sources\DriverEntry\Useless\objchk_wxp_x86\i386\Useless.sys

    Notem que a pasta onde salvei o meu arquivo map.txt é o mesmo onde os fontes do driver estão. Isso é apenas por uma questão de organização. O arquivo de mapeamento poderia estar em qualquer pasta. Em seguida utilizamos o comando .kdfiles como mostra abaixo. Notem que listamos os mapeamentos existentes simplesmente utilizando o mesmo comando sem qualquer parâmetro.

    kd> .kdfiles Z:\Sources\DriverEntry\Useless\map.txt
    KD file assocations loaded from 'Z:\Sources\DriverEntry\Useless\map.txt'
     
    kd> .kdfiles
    KD file assocations loaded from 'Z:\Sources\DriverEntry\Useless\map.txt'
    \??\C:\Windows\System32\drivers\Useless.sys ->
    Z:\Sources\DriverEntry\Useless\objchk_wxp_x86\i386\Useless.sys

    Na seqüência, vamos dar um Rebuild no driver (isso deveria mudar aquele time stamp) e vamos reiniciar o driver. Se tudo estiver certo aí do seu lado, você deveria ter uma saída com um time stamp diferente do que tínhamos antes.

    kd> g
    Mas já? Eu nem fiz nada...
    KD: Accessing 'Z:\Sources\DriverEntry\Useless\objchk_wxp_x86\i386\Useless.sys'
     (\??\C:\Windows\system32\drivers\Useless.sys)
      File size 2K.
    MmLoadSystemImage: Pulled \??\C:\Windows\system32\drivers\Useless.sys from kd
    Este driver foi compilado em Jul 16 2007 00:17:04

    Bom, se isso funciona em um driver com start manual, então com o automático também deveria funcionar. Para vermos isso, mude o start do seu driver para System ou Automatic, dê um Rebuild mais uma vêz no driver, e por fim vamos reiniciar a máquina. Quando o driver for carregado, deveremos ter a substituição automática de sua imagem na sua carga. No WinDbg teremos o seguinte:

    Connected to Windows XP 2600 x86 compatible target, ptr64 FALSE
    Kernel Debugger connection established.
    Symbol search path is: SRV*c:\websymbols*http://msdl.microsoft.com/download/symbols
    Executable search path is: 
    Windows XP Kernel Version 2600 UP Free x86 compatible
    Built by: 2600.xpsp_sp2_gdr.070227-2254
    Kernel base = 0x804d7000 PsLoadedModuleList = 0x805533a0
    System Uptime: not available
    KD: Accessing 'Z:\Sources\DriverEntry\Useless\objchk_wxp_x86\i386\Useless.sys'
     (\??\C:\Windows\system32\drivers\Useless.sys)
      File size 2K.
    MmLoadSystemImage: Pulled \??\C:\Windows\system32\drivers\Useless.sys from kd
    Este driver foi compilado em Jul 16 2007 00:25:38

    E não é que funciona mesmo! Mas e se meu driver for um driver com start do tipo Boot? Isso significa que a imagem do driver vai ser carregada antes mesmo da conexão com o WinDbg ser estabelecida. Vocês já ouviram falar que o que não tem remédio, remediado está? Felizmente isso não se aplica aqui ainda. Existe uma maneira de fazer com que mesmo os drivers de Boot sejam mapeados e substituídos.

    Mapeando drivers de Boot

    Para estabelecer uma conexão com o WinDbg, precisaremos substituir o loader do sistema por uma versão especial. Esta versão faz esta conexão com o Kernel Debugger antes mesmo que arquivo Boot.ini seja lido. Por esta razão, os parâmetros de conexão são fixos com porta COM1 e baudrate de 115200. Esta versão do loader é encontrada no diretório C:\WinDDk\3790\debug do DDK com o nome de ntldr_dbg. Este arquivo deve substituir a versão original do loader do sistema que fica no raiz do drive de boot com o nome ntldr. A versão debug deve ficar com o mesmo nome do loader original.

    Antes de reiniciar o sistema, devemos mudar o start do driver para Boot e remover o valor ImagePath do registro. Como vocês devem saber, drivers de boot não têm esse luxo de utilizar caminhos que tenham letras de unidades e outras frescuras. Ao final das alterações, deveríamos ter o registro como mostra abaixo.


    Como falei anteriormente, o formato do caminho do arquivo do driver deve ser o mesmo de como está no registro, mas sabendo que agora não há mais caminho no registro, devemos adotar o mesmo formado adotado pelo sistema. Ah tá! O mesmo formato. E qual seria? Para ver este formato basta reiniciar o sistema com o loader debug, o que deve nos dar a seguinte tela no boot do sistema.

    Este é o momento de conectar com o Windbg utilizando os parâmetros pré-determinados de conexão. Isso deveria nos resultar a seguinte saída no depurador:

    Microsoft (R) Windows Debugger  Version 6.7.0005.1
    Copyright (c) Microsoft Corporation. All rights reserved.
     
    Opened \\.\pipe\com_1
    Waiting to reconnect...
    BD: Boot Debugger Initialized
    BD: osloader.exe base address 00400000
    Connected to Windows Boot Debugger 3790 x86 compatible target, ptr64 FALSE
    Kernel Debugger connection established.
    Symbol search path is: SRV*c:\websymbols*http://msdl.microsoft.com/download/symbols
    Executable search path is: 
    Windows Boot Debugger Kernel Version 3790 UP Checked x86 compatible
    Primary image base = 0x00000000 Loaded module list = 0x00000000
    System Uptime: not available
    The call to LoadLibrary(bootext) failed, Win32 error 0n2
        "The system cannot find the file specified."
    Please check your debugger configuration and/or network access.

    Agora é apresentado o menu de seleção do Boot.ini. Selecione sua opção e continue carregando o sistema. Deve ser apresentada a lista de drivers de Boot, que são os drivers que são carregados antes do primeiro breakpoint que o debugger pode parar.

    BD: \WINDOWS\system32\ntoskrnl.exe base address 804EA000
    BD: \WINDOWS\system32\hal.dll base address 806FF000
    BD: \WINDOWS\system32\KDCOM.DLL base address 80720000
    BD: \WINDOWS\system32\BOOTVID.dll base address 80010000
    BD: \WINDOWS\system32\DRIVERS\ACPI.sys base address 80124000
    BD: \WINDOWS\system32\DRIVERS\WMILIB.SYS base address 80001000
    BD: \WINDOWS\system32\DRIVERS\pci.sys base address 80062000
    BD: \WINDOWS\system32\DRIVERS\isapnp.sys base address 80003000
    BD: \WINDOWS\system32\DRIVERS\compbatt.sys base address 8000C000
    BD: \WINDOWS\system32\DRIVERS\BATTC.SYS base address 80013000
    BD: \WINDOWS\system32\DRIVERS\intelide.sys base address 80017000
    BD: \WINDOWS\system32\DRIVERS\PCIIDEX.SYS base address 80019000
    BD: \WINDOWS\System32\Drivers\MountMgr.sys base address 80152000
    BD: \WINDOWS\system32\DRIVERS\ftdisk.sys base address 8015D000
    BD: \WINDOWS\System32\drivers\dmload.sys base address 80073000
    BD: \WINDOWS\System32\drivers\dmio.sys base address 8017C000
    BD: \WINDOWS\System32\Drivers\PartMgr.sys base address 801A2000
    BD: \WINDOWS\System32\Drivers\VolSnap.sys base address 801A7000
    BD: \WINDOWS\system32\DRIVERS\atapi.sys base address 801B4000
    BD: \WINDOWS\system32\DRIVERS\vmscsi.sys base address 801CC000
    BD: \WINDOWS\system32\DRIVERS\SCSIPORT.SYS base address 801CF000
    BD: \WINDOWS\system32\DRIVERS\disk.sys base address 801E7000
    BD: \WINDOWS\system32\DRIVERS\CLASSPNP.SYS base address 801F0000
    BD: \WINDOWS\system32\DRIVERS\fltMgr.sys base address 801FD000
    BD: \WINDOWS\system32\DRIVERS\sr.sys base address 8021D000
    BD: \WINDOWS\System32\Drivers\KSecDD.sys base address 8022F000
    BD: \WINDOWS\System32\Drivers\Ntfs.sys base address 80246000
    BD: \WINDOWS\System32\Drivers\NDIS.sys base address 802D3000
    BD: \WINDOWS\System32\Drivers\Useless.sys base address 8000F000
    BD: \WINDOWS\System32\Drivers\Mup.sys base address 80300000
    BD: \WINDOWS\system32\DRIVERS\agp440.sys base address 8031B000
    Shutdown occurred...unloading all symbol tables.
    Waiting to reconnect...

    Aqui a conexão com o loader é encerrada. Uma nova conexão seria estabelecida caso você tivesse selecionado a entrada de Debug do Boot.ini, mas o que temos que notar aqui é o formato utilizado para a carga dos drivers de boot pelo sistema. Notem que entre os drivers da lista acima está o nosso driver de teste. Vamos adotar este mesmo formato em nosso arquivo de mapeamento.

    map
    \WINDOWS\System32\Drivers\Useless.sys
    Z:\Sources\DriverEntry\Useless\objchk_wxp_x86\i386\Useless.sys

    Depois de modificar o arquivo de mapeamento e salvar seu conteúdo em disco, devemos atualizar o WinDbg para que ele pegue esta mudança. Em seguida vamos reiniciar o sistema.

    kd> .kdfiles Z:\Sources\DriverEntry\Useless\map.txt
    KD file assocations loaded from 'Z:\Sources\DriverEntry\Useless\map.txt'
     
    kd> .reboot
    Shutdown occurred...unloading all symbol tables.
    Waiting to reconnect...
    BD: Boot Debugger Initialized
    Connected to Windows Boot Debugger 3790 x86 compatible target, ptr64 FALSE
    Kernel Debugger connection established.  (Initial Breakpoint requested)
    Symbol search path is: SRV*c:\websymbols*http://msdl.microsoft.com/download/symbols
    Executable search path is: 
    Module List address is NULL - debugger not initialized properly.
    WARNING: .reload failed, module list may be incomplete
    KdDebuggerData.KernBase < SystemRangeStart
    Windows Boot Debugger Kernel Version 3790 UP Checked x86 compatible
    Primary image base = 0x00000000 Loaded module list = 0x00000000
    System Uptime: not available
    The call to LoadLibrary(bootext) failed, Win32 error 0n2
        "The system cannot find the file specified."
    Please check your debugger configuration and/or network access.

    A mesma seqüência de mensagens acontece, mas desta vez, o mapeamento está feito como se deve e teremos a seguinte saída quando os drivers de boot forem carregados.

    BD: osloader.exe base address 00400000
    BD: \WINDOWS\system32\ntoskrnl.exe base address 804EA000
    BD: \WINDOWS\system32\hal.dll base address 806FF000
    BD: \WINDOWS\system32\KDCOM.DLL base address 80720000
    BD: \WINDOWS\system32\BOOTVID.dll base address 80010000
    BD: \WINDOWS\system32\DRIVERS\ACPI.sys base address 80124000
    BD: \WINDOWS\system32\DRIVERS\WMILIB.SYS base address 80001000
    BD: \WINDOWS\system32\DRIVERS\pci.sys base address 80062000
    BD: \WINDOWS\system32\DRIVERS\isapnp.sys base address 80003000
    BD: \WINDOWS\system32\DRIVERS\compbatt.sys base address 8000C000
    BD: \WINDOWS\system32\DRIVERS\BATTC.SYS base address 80013000
    BD: \WINDOWS\system32\DRIVERS\intelide.sys base address 80017000
    BD: \WINDOWS\system32\DRIVERS\PCIIDEX.SYS base address 80019000
    BD: \WINDOWS\System32\Drivers\MountMgr.sys base address 80152000
    BD: \WINDOWS\system32\DRIVERS\ftdisk.sys base address 8015D000
    BD: \WINDOWS\System32\drivers\dmload.sys base address 80073000
    BD: \WINDOWS\System32\drivers\dmio.sys base address 8017C000
    BD: \WINDOWS\System32\Drivers\PartMgr.sys base address 801A2000
    BD: \WINDOWS\System32\Drivers\VolSnap.sys base address 801A7000
    BD: \WINDOWS\system32\DRIVERS\atapi.sys base address 801B4000
    BD: \WINDOWS\system32\DRIVERS\vmscsi.sys base address 801CC000
    BD: \WINDOWS\system32\DRIVERS\SCSIPORT.SYS base address 801CF000
    BD: \WINDOWS\system32\DRIVERS\disk.sys base address 801E7000
    BD: \WINDOWS\system32\DRIVERS\CLASSPNP.SYS base address 801F0000
    BD: \WINDOWS\system32\DRIVERS\fltMgr.sys base address 801FD000
    BD: \WINDOWS\system32\DRIVERS\sr.sys base address 8021D000
    BD: \WINDOWS\System32\Drivers\KSecDD.sys base address 8022F000
    BD: \WINDOWS\System32\Drivers\Ntfs.sys base address 80246000
    BD: \WINDOWS\System32\Drivers\NDIS.sys base address 802D3000
    KD: Accessing 'Z:\Sources\DriverEntry\Useless\objchk_wxp_x86\i386\Useless.sys'
     (\WINDOWS\System32\Drivers\Useless.sys)
      File size 2K.BD: Loaded remote file \WINDOWS\System32\Drivers\Useless.sys
     
    BlLoadImageEx: Pulled \WINDOWS\System32\Drivers\Useless.sys from Kernel Debugger
    BD: \WINDOWS\System32\Drivers\Useless.sys base address 8000F000
    BD: \WINDOWS\System32\Drivers\Mup.sys base address 80300000
    BD: \WINDOWS\system32\DRIVERS\agp440.sys base address 8031B000
    Shutdown occurred...unloading all symbol tables.
    Waiting to reconnect...

    E mais tarde, nossa prova de que o arquivo foi substituído com sucesso.

    Connected to Windows XP 2600 x86 compatible target, ptr64 FALSE
    Kernel Debugger connection established.  (Initial Breakpoint requested)
    Symbol search path is: SRV*c:\websymbols*http://msdl.microsoft.com/download/symbols
    Executable search path is: 
    Windows XP Kernel Version 2600 UP Free x86 compatible
    Built by: 2600.xpsp_sp2_gdr.070227-2254
    Kernel base = 0x804ea000 PsLoadedModuleList = 0x8056d620
    System Uptime: not available
    Este driver foi compilado em Jul 16 2007 01:18:53

    Eu lhe garanto que isso ainda poderia salvar sua vida se você tivesse um filtro de File System com um bug na DriverEntry em uma máquina de cliente. A expressão de pânico do cliente quando vê a máquina dele reiniciando em um loop infinito é interessante, mas manter seu emprego é um pouco mais interessante.

    Até mais,
    []s.

  • Quem não tem cão caça com DSF

    Vocês já devem estar cansados de ler que a OSR tem kits de hardware para treinamento de construção de drivers para USB e PCI. É realmente muito frustrante querer aprender a desenvolver drivers que controlam placas sem ao menos ter uma por perto. Mas sou pobre pobre pobre de marré marré marré e não tenho grana pra ficar comprando estes brinquedinhos importados. Na verdade eu também engoli meio quadrado quando tive que ir até a agência dos correios buscar os dois kits que comprei e pagar uma tributação simbólica de R$ 319,46 sem contar com o custo dos kits. Bem, este é um ponto.

    Admitindo que você já é um programador experiente e que já sabe fazer drivers para dispositivos USB com um olho amarrado nas costas, imagine que você tenha que escrever um driver para um dispositivo USB que ainda não está pronto. Ou seja, você tem a especificação, conhece as características dele, mas de fato o dispositivo ainda não está pronto para você fazer os testes que você precisa. Neste caso, o kit de treinamento também não ajudaria muito. Este é outro ponto.

    Unindo estes pontos vemos que estamos perdidos mesmo e que o negócio é vender água de coco na praia.

    Uma alternativa considerável

    Pesando nestes mesmos pontos, a Microsoft desenvolveu o Device Simulation Framework for USB Devices. Este framework é composto de, entre outros componentes, um driver que é instalado em sua máquina de teste e que passa a simular uma controladora USB. Este driver é um Lower Filter dos drivers de controladores USB do Windows. Ele intercepta as interações que o Windows faz com o verdadeiro hardware e simula interrupções de hardware. Do ponto de vista do driver, não há nenhuma diferença entre o DSF e dispositivos reais.

    Mas como o framework simularia um dispositivo USB que ele não conhece? Na verdade, o framework redireciona estas interações para componentes em User Mode que podem ser escritos por você em qualquer linguagem que possa usar COM. Desta forma, você pode controlar o comportameto do dispositivo que está sendo simulado. Observem a figura que peguei emprestado da página do MSDN para que vocês tenham uma visão mais clara de como estes componentes estão organizados.

    Instalando o DSF

    O DSF vem no ISO do WDK que você baixa da Microsoft e só pode ser instalado em Windows XP SP2 ou superior (incluindo plataformas x64). Quando você instala o WDK em sua máquina, o DSF não é instalado. Você precisa instalá-lo a parte. Os arquivos dsfx86runtime.msi e dsfx64runtime.msi, responsáveis pela instalação, são encontrados na pasta \dsf do ISO de instalação. A instalação do framework é extremamente simples, mas você só consegue ver alguma coisa quando, depois de instalado, você roda a linha de comando softehcicfg /install na pasta \Program Files\dsf\softehci, como mostra abaixo.


    Esta linha de comando cria a controladora USB virtual na sua arvore de dispositivos do Device Manager. Seu sistema pode pedir os drivers do novo hardware que foi adicionado. Caso isso aconteça, indique o diretório do sistema (C:\Windows) para que seja feita a busca pelos drivers. Instalados os drivers, você deveria ganhar dois novos dispositivos, sendo eles, Microsoft USB 2.0 EHCI Host Controler Simulator e um USB Root Hub, como mostra abaixo.

    Escrevendo disposivos em VBScript

    Para simular os dispositivos, o DSF instala um grupo de componentes COM que implementam objetos tais como: Devices, Configurations, Interfaces, EndPoints, Descriptors e por aí vai. Quem já desenvolveu para USB sabe bem do que estou falando. Com estes objetos, você pode escrever componentes que podem simular qualquer dispositivo USB. Junto do framework, são instalados três exemplos de dispositivos que podem ser simulados. Os fontes destes dispositivos estão juntos dos exemplos que vêm no WDK na pasta \WinDDK\6000\src\Test\DSF\USB. Para utilizar um destes exemplos, precisamos apenas de uma liguagem de script, ou qualquer linguagem que faça interface COM.

    Vamos dar uma olhada no exemplo de teclado e dar uma fuçada nas IRPs do teclado criado. Para colocar o teclado para funcionar, vá até a pasta \Program Files\dsf\usbhid e rode a seguinte linha de comando cscript Create1.1Kbd.wsf e siga os passos indicados. Se for a primeira vez que você estiver fazendo isso, o Windows pode pedir os drivers dos dispositivos novos que serão criados. O script criará um Generic USB Hub e um Microsoft USB Natural Keyboard conforme mostra abaixo.


    Antes de remover estes dispositivos, vamos dar uma olhada com o IRP Tracker, software que já comentei em um outro post, e observar as IRPs deste teclado que está sendo simulado. Inicie o IRP Tracker e selecione todo o driver KbdClass. O KbdClass é o driver responsável por centralizar e implementar as interfaces para os dispositivos a classe Teclado. As IRPs de todos os teclados passam por aqui.

    Enquanto o primeiro prompt de comando está parado, inicie mais um prompt de comando e, no mesmo diretório do exemplo do teclado, execute a seguinte linha de comando: cscript Use1.1Kbd.wsf. Este script simula o pressionar das teclas do teclado simulado escrevendo a frase “Hello World!”. Para cada tecla que o sistema obtém, são gerados os registros das IRPs monitoradas. Desta forma, o IRP Tracker deve ficar como mostra abaixo.


    Esta é uma excelente ferramenta para utilizar com máquinas virtuais e pode lhe adiantar um bom montante de testes até que o hardware real esteja pronto. Nos casos onde o hardware é novo e o driver também, esse pode ser um divisor de águas para saber quem é que está gerando a tela azul. Afinal, hardware também tem bug. Ou mesmo no caso de você ser um estudante e não ter nenhum hardware para treinar seu aprendizado. Pelo menos você não vai queimar nada.

    Até mais,
    []s.

  • Começar de novo

    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