Prevenindo Execução de Processos

17 de May de 2010 - Fernando Roberto

Durante esse longo período em que estive distante de novos posts no blog, algumas coisas aconteceram e que mereceram um lugarzinho aqui na em minha listinha de posts a escrever. Uma delas foi a longa discussão que aconteceu na lista do grupo de C/C++. Ela falava sobre quais as passos a serem seguidos para se escrever um driver que faria um pouco de tudo sobre serviços de segurança. Um dos ítens que foi especialmente discutido foi a idéia de se escrever um driver que pudesse impedir que um determinado processo fosse executado. Já vou logo dizendo que não vou me envolver se isso resolve ou não um problema de sergurança. Não estou aqui pra discutir isso, e para ser bem sincero, eu nem quero. Neste post vou demonstrar de uma maneira bem simples como podemos evitar a execução de um processo.

Rastreando o tempo de vida dos Processos

Antes de sair colocando os dois pés no peito de um processo para que ele caia, vamos primeiro apenas ver como monitorar seu tempo de vida. Isso é facilmente feito chamando a rotina PsSetCreateProcessNotifyRoutine() que está disponível desde quando o arco-iris era preto e branco. Embora a documentação diga que está disponível desde o Windows 2000, já conheço essa rotina de outros carnavais e sei que está por aí pelo menos desde o finado Windows NT 3.51. Nossa, estou ficando velho. Mas voltando ao assunto, essa rotina registra uma função de callback que notifica nosso driver sobre o ínico e o término dos processos no sistema. Isso é especialmente útil se um determinado driver quer manter informações relacionadas aos processos, e assim, saber quando um processo foi encerrado é fundamental para liberar os recursos utilizados tais informações.

NTSTATUS PsSetCreateProcessNotifyRoutine(
  __in  PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
  __in  BOOLEAN Remove
);

A função de callback registrada por essa rotina tem a assinatura como exibido abaixo:

VOID
(*PCREATE_PROCESS_NOTIFY_ROUTINE) (
    IN HANDLE  ParentId,
    IN HANDLE  ProcessId,
    IN BOOLEAN  Create
    );

Bem simples não? O primeiro parâmetro é o ProcessId do processo pai nessa criação. Isso significa que, se por exemplo você iniciar o Notepad a partir do “Executar…” no menu Iniciar do Windows, teremos o Explorer.exe como processo pai do novo processo Notepad.exe. O segundo parâmetro é o ProcessId do processo sendo iniciado ou encerrado no momento da chamada. Por último e não menos importante, temos a flag que vai indicar se esta é uma notificação de início ou de término de um processo.

Uma coisa importante a notar aqui é sobre o ProcessId do processo pai. Esse parâmetro é confiável nas notificações de inicio de processo, mas nem tanto quando se trata do término. Isso ocorre porque quando um processo está sendo iniciado, seu processo pai ainda está lá, firme e forte, mas quando um processo termina, apesar de o ParentId trazer o mesmo valor da notificação de ínicio de processo, esse dado não tem mais validade. Deixa eu dar um exemplo pra ficar mais fácil.

  1. Processo1(32) cria Processo2(57), recebemos chamada: CreateProcessNotifyRoutine(32, 57, TRUE);
  2. Processo1(32) termina.
  3. Processo3(32) é criado e granha Id igual a 32.
  4. Processo2(57) termina e recebemos a chamada: CreateProcessNotifyRoutine(32, 57, FALSE);

Na notificação de término do Processo2 que ocorreu no passo 4,  o processo cujo Id é 32 agora é o Processo3, que por sinal, não é realmente o processo pai do processo2. Então ao reunir informações sobre um processo, o faça durante sua inicialização, mantenha estes dados em uma lista e depois as remova quando o processo terminar.

Registrar a função de callback é muito simples, mas o importante mesmo é remover esse registro quando o driver for descarregado. Consegue imaginar o que aconteceria se uma dessas notificações fosse entregue a um driver que não está mais na memória? Bom, eu consigo.

Obtendo o caminho da imagem de um processo

É provável que você queira obter mais informações sobre os processos envolvidos nessas notificações, uma dessas informações é o caminho do arquivo que está sendo executado. Pode-se obter essa informação utilizando o Id dos processos que recebemos na notificação de novo processo. Para isso, teremos que utilizar a rotina quase documentada ZwQueryInformationProcess(). Essa é uma API nativa que existe desde sempre mas que nunca foi documentada oficialmente. Para utilizá-la basta declarar sua assinatura como mostra abaixo.

NTSTATUS
ZwQueryInformationProcess(IN HANDLE ProcessHandle,
                          IN PROCESSINFOCLASS ProcessInformationClass,
                          OUT PVOID ProcessInformation,
                          IN ULONG ProcessInformationLength,
                          OUT PULONG ReturnLength OPTIONAL);

Se quiser saber mais sobre APIs nativas não documentadas, esse link é uma super mão na roda, mas nada como este livro.

O código abaixo utiliza essa API para obter a imagem de um processo a partir do seu Pid. Reparem que ZwQueryInformationProcess() pede um handle para o processo sobre o qual se quer obter informações. Para se obter esse handle precisaremos primeiro obter a estrutura EPROCESS que representa um processo em Kernel-Mode. Faremos isso utilizando a função PsLookupProcessByProcessId() que nos retornará um ponteiro para essa estrutura.

NTSTATUS PsLookupProcessByProcessId(
  __in   HANDLE ProcessId,
  __out  PEPROCESS *Process
);

Apesar de opaca, essa estrutura vai nos possibilitar obter o handle para processo representado por ela utilizando agora a função ObOpenObjectByPointer() do Object Manager.

NTSTATUS ObOpenObjectByPointer(
  __in      PVOID Object,
  __in      ULONG HandleAttributes,
  __in_opt  PACCESS_STATE PassedAccessState,
  __in      ACCESS_MASK DesiredAccess,
  __in_opt  POBJECT_TYPE ObjectType,
  __in      KPROCESSOR_MODE AccessMode,
  __out     PHANDLE Handle
);

Acho que tudo vai ficar mais fácil de ser entendido com o fonte abaixo. Afinal, uma linha de código vale mais que mil palavras. A função a seguir obtém a estrutura EPROCESS de um processo, em seguida obtém o handle para ele, com esse handle obtém-se as informações que queremos do processo. Tá, tá, tá… Segue o fonte, mas não esqueça de ler os comentários.

/****
***     GetProcessImageName
**
**      Retorna um PUNICODE_STRING contendo o caminho
**      da imagem utilizada pelo processo cujo Pid foi
**      fornecido como parâmetro.
*/
NTSTATUS
GetProcessImageName(HANDLE           hProcessId,
                    PUNICODE_STRING* ppusImageName)
{
    NTSTATUS        nts;
    PUNICODE_STRING pusImageName = NULL;
    ULONG           ulSize;
    HANDLE          hProcess;
    PEPROCESS       pEProcess;
 
    //-f--> Primeiro de tudo, zera a variável de saída.
    *ppusImageName = NULL;
 
    //-f--> Aqui obtemos a estrutura que representa um processo
    //     (EPROCESS) a partir do seu Pid;
    nts = PsLookupProcessByProcessId(hProcessId,
                                     &pEProcess);
    if (!NT_SUCCESS(nts))
        return nts;
 
    //-f--> Agora obtemos um handle para este objeto.
    nts = ObOpenObjectByPointer(pEProcess,
                                OBJ_KERNEL_HANDLE,
                                NULL,
                                0,
                                *PsProcessType,
                                KernelMode,
                                &hProcess);
 
    //-f--> Independente do handle ter sido ou não obtido,
    //      teremos que desfazer a referência que obtivemos
    //      para o EPROCESS.
    ObDereferenceObject(pEProcess);
    if (!NT_SUCCESS(nts))
        return nts;
 
    //-f--> Agora que temos o handle para o processo, podemos
    //      obter informações a respeito dele, nesse caso,
    //      vamos obter o caminho da imagem do processo.
    nts = ZwQueryInformationProcess(hProcess,
                                    ProcessImageFileName,
                                    NULL,
                                    0,
                                    &ulSize);
 
    //-f--> Para obter o tamanho certo, passamos zero na primeira
    //      tentativa, isso vai nos retornar um erro e a quantidade
    //      de bytes necessários para obter essa informação.
    if (nts != STATUS_INFO_LENGTH_MISMATCH)
        return nts;
 
    //-f--> O tamanho retornado inclui o tamanho da estrutura UNICODE_STRING,
    //      então tudo é alocado de uma única vez.
    pusImageName = (PUNICODE_STRING) ExAllocatePoolWithTag(PagedPool,
                                                           ulSize,
                                                           TRACER_TAG);
    //-f--> Oops! Fecha o Photo Shop e tenta de novo.
    if (!pusImageName)
        return STATUS_INSUFFICIENT_RESOURCES;
 
    //-f--> Agora oferecemos o buffer alocado com o tamanho certo.
    //      O que pode sair errado? (TUDO!)
    nts = ZwQueryInformationProcess(hProcess,
                                    ProcessImageFileName,
                                    pusImageName,
                                    ulSize,
                                    &ulSize);
    if (!NT_SUCCESS(nts))
    {
        //-f--> Oops! Algo deu errado.
        ExFreePoolWithTag(pusImageName,
                          TRACER_TAG);
    }
    else
    {
        //-f--> Tudo bem até aqui. A rotina chamadora fica encarregada
        //      de liberar a memória alocado aqui.
        *ppusImageName = pusImageName;
    }
 
    return nts;
}
 

Com essa rotina fica fácil escrever a seguinte função de callback que vai nos mostrar as informações básicas sobre os processos iniciados e terminados.

/****
***     OnCreateProcess
**
**      Função de callback que será registrada caso este
**      driver esteja rodando em Windows Vista ou anterior.
*/
VOID
OnCreateProcess(HANDLE  hParentId,
                HANDLE  hProcessId,
                BOOLEAN bCreate)
{
    //-f--> Aqui verificamos se o evento trata de uma criação
    //      ou término de um processo.
    if (bCreate)
    {
        NTSTATUS        nts;
        PUNICODE_STRING pusImageName = NULL;
 
        //-f--> Obtém o caminho da imagem que foi usada por
        //      este processo.
        nts = GetProcessImageName(hProcessId,
                                  &pusImageName);
 
        if (NT_SUCCESS(nts))
        {
            //-f--> Caso o caminho tenha sido obtido com sucesso,
            //      registra a notificação de início de processo.
            DbgPrint("[Process Tracer] Action = Starting\n"
                     "                 Process Id = 0x%x\n"
                     "                 Parent Id = 0x%x\n"
                     "                 Image name = %wZ\n\n",
                     hProcessId,
                     hParentId,
                     pusImageName);
 
            //-f--> Libera os recursos alocados.
            ExFreePool(pusImageName);
        }
    }
    else
    {
        //-f--> Registra o evento de término de processo.
        DbgPrint("[Process Tracer] Action = Finishing\n"
                 "                 Process Id = 0x%x\n"
                 "                 Parent Id = 0x%x\n\n",
                 hProcessId,
                 hParentId);
    }
}
 

Com estas funções trabalhando em um Windows XP, teremos a seguinte saída no depurador.

[Process Tracer] Action = Starting
                 Process Id = 0x35c
                 Parent Id = 0x694
                 Image name = \Device\HarddiskVolume1\WINDOWS\system32\notepad.exe
 
kd> !process 0x35c 0
Searching for Process with Cid == 35c
Cid handle table at e1003000 with 380 entries in use
 
PROCESS 82100020  SessionId: 0  Cid: 035c    Peb: 7ffd7000  ParentCid: 0694
    DirBase: 08840400  ObjectTable: e10d2400  HandleCount:  41.
    Image: notepad.exe
 
kd> !process 0x694 0
Searching for Process with Cid == 694
Cid handle table at e1003000 with 380 entries in use
 
PROCESS 821cc228  SessionId: 0  Cid: 0694    Peb: 7ffd6000  ParentCid: 0640
    DirBase: 08840200  ObjectTable: e18df2a0  HandleCount: 365.
    Image: explorer.exe
 
kd> g
[Process Tracer] Action = Finishing
                 Process Id = 0x35c
                 Parent Id = 0x694
 

Logo no início eu inicio o processo Notepad a partir do menu “Executar…” como eu comentei logo no início. Depois disso eu utilizo a extensão !process do WinDbg para obter informações mínimas sobre os processos envolvidos nesta notificação. Em seguida eu encerro o Notepad dando origem a última mensagem demonstrada acima.

Uma nova API no Windows Vista SP1

Tudo bem, isso é muito legal, mas estamos aqui para falar sobre como previnir que um certo processo seja executado. Impedir a execução de processo no Windows Vista virou coisa de criança com a nova rotiva PsSetCreateProcessNotifyRoutineEx() que tem sua assinatura listada logo abaixo:

NTSTATUS PsSetCreateProcessNotifyRoutineEx(
  __in  PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
  __in  BOOLEAN Remove
);

Essa rotina registra uma função de calback que também notifica seu driver a sobre o início e término dos processos no sistema. O primeiro parâmetro indica a rotina de callback a ser registrada, enquanto que o segundo parâmetro indica se a rotina deve ser registrada ou removida.  Muito semelhante à sua irmã mais velha PsSetCreateProcessNotifyRoutine(). A rotina de callback deve ter a seguinte assinatura:

VOID CreateProcessNotifyEx(
  __inout   PEPROCESS Process,
  __in      HANDLE ProcessId,
  __in_opt  PPS_CREATE_NOTIFY_INFO CreateInfo
);

Fica fácil perceber que os parâmetros dessa função de callback mudaram bastante. Para saber se o evento se trata da criação ou do término de processo, basta verificar parâmetro CreateInfo. Se esse for não nulo, então se trata de um novo processo sendo executado, caso contrário do seu término. Vamos dar uma olhada nessa estrutura:

typedef struct _PS_CREATE_NOTIFY_INFO {
  SIZE_T              Size;
  union {
    ULONG  Flags;
    struct {
      ULONG FileOpenNameAvailable  :1;
      ULONG Reserved  :31;
    } ;
  } ;
  HANDLE              ParentProcessId;
  CLIENT_ID           CreatingThreadId;
  struct _FILE_OBJECT *FileObject;
  PCUNICODE_STRING    ImageFileName;
  PCUNICODE_STRING    CommandLine;
  NTSTATUS            CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;

Pois é, parace que a vida ficou bem mais fácil para quem quer buscar maiores detalhes sobre os processos envolvidos na notificação. O grande atrativo dessa nova versão é que podemos previnir a criação de um processo apenas modificando o campo CreationStatus. Apenas para exemplificar essa facilidade, eu escrevi a função de callback abaixo. Sempre leia os comentários.

/****
***     OnCreateProcessEx
**
**      Função de callback que será registrada caso este
**      driver esteja rodando em Windows Vista SP1 ou superior.
*/
VOID
OnCreateProcessEx(PEPROCESS                 pEProcess,
                  HANDLE                    hProcessId,
                  PPS_CREATE_NOTIFY_INFO    pCreateInfo)
{
    //-f--> Aqui verificamos se o evento trata de uma criação
    //      ou término de um processo.
    if (pCreateInfo)
    {
        UNICODE_STRING  usBlockingApp;
 
        //-f--> Como se trata apenas de um exemplo, estou colocando o path
        //      do arquivo hard coded aqui, mas lembre-se que referências às
        //      imagens de arquivos podem utilizar HarddiskVolume1 ou outras
        //      variações que mudam dependendo de muitas coisas.
        RtlInitUnicodeString(&usBlockingApp,
                             L"\\??\\C:\\Windows\\System32\\Notepad.exe");
 
        //-f--> Comparando a imagem do processo que acabada de ser criado
        //      com o caminho que usei acima.
        if (RtlEqualUnicodeString(&usBlockingApp,
                                  pCreateInfo->ImageFileName,
                                  TRUE))
        {
            //-f--> Tudo bem, agora é apertar o maluco e chegar apavorando:
            //      "Perdeu praybooy!"
            DbgPrint("[Process Tracer] Action = Blocking\n"
                     "                 Process Id = 0x%x\n"
                     "                 Parent Id = 0x%x\n"
                     "                 Image name = %wZ\n\n",
                     hProcessId,
                     pCreateInfo->ParentProcessId,
                     pCreateInfo->ImageFileName);
 
            //-f--> Muda o status da criação do processo fazendo com que
            //      ela não prossiga.
            pCreateInfo->CreationStatus = STATUS_ACCESS_DENIED;
        }
        else
        {
            //-f--> Não é o nosso "homem", deixa o processo inciar normalmente.
            DbgPrint("[Process Tracer] Action = Starting\n"
                     "                 Process Id = 0x%x\n"
                     "                 Parent Id = 0x%x\n"
                     "                 Image name = %wZ\n\n",
                     hProcessId,
                     pCreateInfo->ParentProcessId,
                     pCreateInfo->ImageFileName);
        }
    }
    else
    {
        //-f--> Aqui apenas registramos a notificação do término de
        //      um processo.
        DbgPrint("[Process Tracer] Action = Finishing\n"
                 "                 Process Id = 0x%x\n\n",
                 hProcessId);
    }
}
 

Repare que o caminho para o arquivo que estou impedindo sua criação está hard-coded no fonte de exemplo. Esse caminho pode ter sua sintaxe diferente para o mesmo arquivo dependendo de como o processo é criado ou em qual versão do Windows estamos rodando. Por essa razão, caso queria rodar esse teste na sua casa, verifique se a sintaxe está como utilizei aqui, caso contrário acerte e recompile o exemplo.

Um driver, duas opções

Esta nova API está disponível apenas para Windows Vista SP1 e posteriores, mas é provável que você queria ter um driver que seja capaz de ainda rodar em versões anteriores do Windows mesmo que o sistema não tenha suporte a essa rotina. Como você já deve saber, simplesmente chamar a rotina PsSetCreateProcessNotifyRoutineEx() no seu driver vai criar uma dependência estática e seu driver não será capaz de ser carregado em versões mais antigas do Windows.

Para previnir essa dependência estática tendo um único binário que possa ser carregado tanto em versões mais antigas como nas mais novas, utilizando a versão mais nova dessa rotina, usaremos a função MmGetSystemRoutineAddress(), que é a irmã Kernel-Mode da bem conhecida GetProcAddress() em User-Mode. O driver de exemplo disponível para download no final deste post possui essas caracteristicas justamente para demonstrar como isso pode der feito. Obviamente que rodando o driver em Windows XP não teremos o suporte do sistema operacional para interromper um processo, e teremos que recorrer a técnicas alternativas para obter o mesmo resultado.

A função DriverEntry para este driver fica da seguinte maneira:

/****
***     DriverEntry
**
**      Ponto de entrada do driver. Se ainda estiver pensando
**      é fácil, não se preocupe, você acabará mudando de opinião.
*/
NTSTATUS
DriverEntry(PDRIVER_OBJECT     pDriverObj,
            PUNICODE_STRING    pusRegistryPath)
{
    UNICODE_STRING  usSystemRoutine;
    NTSTATUS        nts;
 
    //-f--> Registra nossa função de finalização para que
    //      nosso driver possa ser descarregado.
    pDriverObj->DriverUnload = OnDriverUnload;
 
    //-f--> Inicia o nome da rotina que tentaremos buscar dinamicamente.
    RtlInitUnicodeString(&usSystemRoutine,
                         L"PsSetCreateProcessNotifyRoutineEx");
 
    //-f--> Aqui verificamos de o sistema já tem suporte a PsSetCreateProcessNotifyRoutineEx
    //      Exatamente como a tia ensinou GetProcessAddress() lá no prézinho.
    *(PVOID*)&pfPsSetCreateProcessNotifyRoutineEx = MmGetSystemRoutineAddress(&usSystemRoutine);
 
    if (pfPsSetCreateProcessNotifyRoutineEx)
    {
        //-f--> Se o estivermos em Windows Vista SP1 ou superior, teremos o endereço
        //      desta rotina, e portanto, vamos nos registrar com ela.
        nts = pfPsSetCreateProcessNotifyRoutineEx(OnCreateProcessEx,
                                                  FALSE);
    }
    else
    {
        //-f--> Puuts! Estamos rodando em algum 386. Vamos nos registrar com
        //      essa rotina da década de 90.
        nts = PsSetCreateProcessNotifyRoutine(OnCreateProcess,
                                              FALSE);
    }
 
    ASSERT(NT_SUCCESS(nts));
    return nts;
}

Testando a PsSetCreateProcessNotifyRoutineEx()

Até aqui você deve estar dando pulos de alegria imaginando que aquele seu driver de dominar o mundo finalmente vai funcionar com grande facilidade utilzando essa nova API, mas o caso é que essa rotina não é para qualquer um. Isso porque apenas driver digitalmente assinados podem chamar essa rotina nova sem receber o retorno STATUS_ACCESS_DENIED.

Mas Fernando, como vou poder testar o seu driver de exemplo? Não tenho certificado nem nada!

Esta rotina inicialmente verifica de o módulo onde seu driver está definido tem o bit de verificação de integridade setado. Para setar esse bit só de brincadeirinha, adicione a opção /INTEGRITYCHECK nas opções do linker do seu projeto. O arquivo sources do projeto de exemplo está da seguinte forma:

TARGETNAME=ProcessTracer
TARGETTYPE=DRIVER
 
SOURCES=ProcessTracer.cpp
 
LINKER_FLAGS=/INTEGRITYCHECK
 

Isso fará com que o sistema verifique a assinatura do seu driver, mas como seu driver não está assinado, você ainda receberá o mesmo erro. Para finalmente ver isso funcionar sem ter mesmo um certificado, você terá que desabilitar e verificacão de integridade de código para drivers no Windows Vista.

Tudo bem, sem pânico. Inicie o Windows Vista e pressione F8 logo que o Boot iniciar, em seguida selecione a opção abaixo no menú que aparecerá como mostra abaixo:

Com estas duas modificações é possível testar o driver de exemplo e ter uma saída no depurador como a ilustrada abaixo:

[Process Tracer] Action = Blocking
                 Process Id = 0x3a4
                 Parent Id = 0xd98
                 Image name = \??\C:\Windows\system32\notepad.exe

Neste caso, mais uma vez, tentei iniciar o Notepad pelo menú “Executar…”, mas desta vez a saída que obtive foi a exibida abaixo:

Ufa! Mais um post gigante para a coleção. Espero ter ajudado e até mais!

ProcessTracer.zip

4 Responses to “Prevenindo Execução de Processos”

  1. Ismael Rocha says:

    Olá Fernando, show de bola essa rotina do Vista hein?
    Então, implementei a funcionalidade de bloqueio de processos no Windows XP/2003.
    A abordagem utilizada é a checagem de flags de execução na stack location das IRP_MJ_CREATE.
    Abraços

    • Olá Ismael,

      Legal, você só precisa tomar cuidado com os falsos positivos, onde alguns arquivos podem ser abertos para execução (com esse flag), mas na verdade não são para execução. Depuradores e anti-virus são os mais comuns nesse tipo de atividade.

      Um abraço,
      Fernando.

  2. dheeraj says:

    hi Fernando,
    very good tutorial.I am new in the driver’s field and i learn a lot from your site.
    i get the image name of a process as your tut successfully but i want the dos based name of the image name…
    as “/Device/HarddiskVolume1/Windows/….” to “C:/Windows/” etc.
    how can i get it.please help me.
    thank you.

Deixe um comentário