🇧🇷 API do Windows e carregamento paralelo de DLL

Como carregar uma DLL paralelamente a qualquer aplicativo

Conteúdo

  1. Introdução
  2. Funcionamento da API
  3. Carregamento de aplicativos
  4. Conclusão
  5. Referências

1. Introdução

Você já se perguntou alguma vez sobre o porquê do Windows ser suscetível a falhas e execução de códigos mal intencionados? Neste artigo eu vou tentar explicar melhor como funciona a relação de um aplicativo com o sistema operacional e porque esse método de interação permite que códigos perigosos sejam executados. Começaremos com uma breve introdução.

Os aplicativos não possuem acesso direto ao processador e a memória, de modo que o usuário fique restrito a alterar somente aquilo que o sistema permitir. A comunicação com esses componentes é feita através das APIs (Application Programming Interface). As APIs correspondem a uma série de funções do sistema - mais especificamente do Kernel - que tem como objetivo fazer um intermédio de comunicação dos aplicativos com o hardware. Através das APIs que os executáveis interagem com o sistema, lendo dados do disco, exibindo caixas de mensagem, entre muitas outras funções.

O nível de acesso ao hardware é dividido nos chamados Rings, sendo Ring0 e Ring3 os principais. Aplicações em Ring0 são aquelas com os maiores privilégios de acesso, podendo se comunicar diretamente com os componentes do computador. Nesta classe ficam o Kernel do Windows, assim como alguns antivírus e debuggers. Em Ring3 ficam todas as outras aplicações, que rodam com muito menos privilégios e dependem das APIs para se comunicar com hardware do computador. Veja a imagem abaixo exemplificando os níveis de acesso:

Teoricamente as APIs deveriam proteger o sistema, já que qualquer acesso ao processador e à memória dependem da mesma. Infelizmente ela não é perfeita, pois quando um aplicativo incorpora uma API no código, eles estabelecem uma relação de confiança, subentendendo que não haverá interceptação nos valores enviados e recebidos pela API/programa.

2. Funcionamento da API

Como mencionado anteriormente, toda aplicação que opera em Ring3 faz o uso das APIs. Por exemplo: quando um programa precisa exibir uma mensagem de texto, ele faz uma chamada para a função MessageBoxA pertencente a API do Windows (User32.dll), indicando o texto e o título da janela. A API por sua vez recebe esses argumentos, confiando que não houve alteração deles por código externo, os interpreta e envia as instruções relativas a exibição da mensagem de texto ao processador. Vejamos um exemplo de como é a função Sleep do Windows:

VOID Sleep(
    DWORD dwMilliseconds // tempo de espera, em milisegundos
);

A função Sleep recebe como argumento o tempo que o programa deve “adormecer”, em milisegundos. Veja como a chamada da função ocorre em nível de máquina, na linguagem assembly:

PUSH 1000
CALL Kernel32.Sleep

Nesse caso, o PUSH 1000 – correspondente ao argumento da função Sleep - está sendo colocado no que chamamos de pilha, uma estrutura de memória com funcionamento semelhante a uma pilha de livros. Em seguida é feita a chamada para a função Sleep. Esta, quando iniciar a execução, vai primeiramente buscar o valor colocado na pilha (1000) e em seguida enviar para o processador os códigos necessários para fazer o aplicativo dormir por 1 segundo.

Como eu mencionei, a API confia que o valor retirado da pilha foi o mesmo valor que colocado pelo programa. Infelizmente existem formas de interceptar essa chamada da função, desviar o código e executar qualquer outra seqüência de instruções, sendo elas danosas ou não.

Existem diversas formas de desviar o código, executar alguns procedimentos e retornar ao código original. Uma delas é alterando as instruções diretamente no executável através de um assembler/debugger. Em alguns casos, o executável vem compactado e encriptado, dificultando essa alteração. Nessa situação, o Windows apresenta uma grande falha, que é uma chave de registro capaz de forçar o carregamento de uma DLL durante a inicialização do mesmo.

3. Carregamento de aplicativos

Toda vez que você executa um aplicativo, o Windows coloca as instruções na memória, juntamente com as funções do Kernel que vão ser utilizadas. A lista com todas essas funções fica armazenada em uma região especial dos executáveis, chamada “Import Data Section”.

No Windows 95/98/ME, as DLLs do sistema ficavam carregadas em um local “público” da memória, o qual podia ser acessado por qualquer programa. Se por ventura algum aplicativo corrompia essa memória, todos os outros programas seriam afetados, já que usavam as DLLs de forma compartilhada. Isso explica o porquê do Win9x ser tão instável ao se comparar com o Windows 2000 e seus sucessores.

A partir do Windows 2000, cada aplicação passou a ter as DLLs carregadas em um local restrito, podendo ser utilizado única e exclusivamente pelo executável que as carregou. Se algum trecho for corrompido, somente o aplicativo responsável por elas será afetado. De forma resumida, cada programa possui seu próprio Kernel32.dll, seu próprio User32.dll e assim por diante, tornando o SO muito mais estável. Apesar do benefício, essa forma de gerenciar as bibliotecas do sistema utiliza muito mais memória, uma vez que você tem basicamente diversas cópias idênticas das mesmas DLLs, uma para cada aplicação rodando.

O Windows 2000/XP protege a memória de cada aplicativo, sendo que a única forma de escrever algo nessa região é forçando o carregamento do código “externo” no mesmo espaço de memória paginada destinada ao programa. Caso você tente acessar locais fora da região designada para o seu aplicativo, o Windows responde através de uma “Operação Ilegal”. Dito isso, como que os malwares conseguem alterar e monitorar dados dos aplicativos, sendo que eles não estão dentro do espaço alocado? Uma das maneiras é alterando o código do próprio executável, mas nem sempre isso é possível, pois ele pode estar comprimido e/ou criptografado. Aí que entra a chave de registro mencionada previamente.

No registro do Windows existe uma chave chamada AppInit_DLLs, que força o carregamento de qualquer DLL/EXE quando um aplicativo é iniciado. Ela pode ser encontrada através do Regedit (“Iniciar->Executar->regedit”) no seguinte local:

HKLM\Software\Microsoft\Windows NT\CurrentVersion\Windows

Ela não existe por padrão, mas pode ser criada manualmente (Botão direito->Novo->Valor de seqüência). O valor da chave corresponde ao local da DLL a ser carregada, como mostra a imagem abaixo:

Creio que agora tenha ficado mais claro. Todo aplicativo que for iniciado vai carregar junto com ele a DLL apontada por essa chave de registro. Além disso, ela vai ser carregada dentro do trecho de memória destinada ao executável, dando à DLL o controle total sobre essa região de memória.

Certo, mas como que isso é capaz de alterar o comportamento de um programa? Lá no início eu comentei que todo aplicativo e suas dependências (DLLs, etc.) são alocadas na memória quando executados, sendo que o executável tem o controle da memória destinada à ele. Além disso, sempre que uma DLL é carregada, o código contido na sua rotina “main” (a rotina principal) é executado.

Uma vez que o código do executável é carregado, você pode alterar as instruções do aplicativo diretamente da memória. Ironicamente a API do Windows possui funções que facilitam esse processo. Imagine um programa no qual ao clicar em um botão, é exibida uma mensagem texto (User32.MessageBoxA). A DLL poderia descobrir o endereço onde a MessageBox é chamada através da função GetProcAddress, pertencente à API do Windows. Com essa informação é possível desviar o código da MessageBox para um outro qualquer, danoso ou não. O processo pode ser descrito basicamente dessa forma:

  1. Arquivo é executado
  2. O aplicativo e as APIs utilizadas são carregadas na memória.
  3. A DLL indicada pelo registro é carregada, caso exista.
  4. Todas as DLLs executam sua função principal
  5. A função principal da DLL externa desvia/altera as instruções do executável na memória, pois tem acesso livre à região de memória alocada pelo Windows.
  6. O executável passa a executar um código diferente do original, que foi desviado diretamente na memória pela DLL.

Sabendo dessas características, é possível ter noção de uma das formas das quais softwares mal intencionados podem adquirir o controle sobre uma aplicação. Saber como tudo funciona já é um grande passo para melhorar a segurança do sistema.

A melhor iniciativa é sempre desconfiar das DLLs. Elas não vêm “soltas” por aí e, se vierem, deve-se redobrar a atenção e ter certeza do que elas realmente fazem (Google é a melhor ferramenta para descobrir informações sobre as DLLs). Também é interessante verificar se existe ou não a chave de registro mencionada anteriormente. Se existir, veja se ela pertence a algum programa em particular. Caso não ache nenhuma informação, remova-a, pois é muito raro um aplicativo fazer o uso dessa chave de registro.

Na segunda parte deste guia irei explicar passo a passo o funcionamento do que foi dito neste artigo, utilizando um exemplo real e um aplicativo criado especialmente para servir como base de estudo.

4. Conclusão

O intuito deste tutorial era tentar explicar como funciona o sistema de comunicação dos aplicativos com o Windows, assim como exemplificar algumas falhas que são aproveitadas pelos malwares para executar códigos mal intencionados sem que o Windows ou o usuário perceba. Espero ter atingido meu objetivo, mostrando mais a fundo, em nível do sistema, como os vírus e os aplicativos funcionam quando executados. Até a próxima!

5. Referências:

  • Microsoft API Guide:

    • http://msdn.microsoft.com/librarv/default.asp7urh/librarv/en-us/winprog/winprog/windows api start page.asp
  • Weakness of Windows API, Gabriel