|
[18/07]
:. Wii passa do Xbox 360 e Playstation 3 nos EUA [18/07] :. SwitchProxy: troque de servidor proxy facilmente no Firefox [18/07] :. KOffice 2.0 apresenta seu alpha 9 [18/07] :. Malware infecta arquivos em redes P2P disfarçado de codec [18/07] :. Digital Wall, enorme touchscreen da Panasonic [18/07] :. Google compra empresa russa de anúncios online [17/07] :. Testes de desempenho: kernel 2.6.26 versus 2.6.25 [17/07] :. Balada gera energia a partir da dança das pessoas na pista [17/07] :. Resumo do dia [17/07] :. Tutorial: instalação do OpenVZ no Fedora 9 [17/07] :. O primeiro passo para um mundo maior [17/07] :. BIC além de canetas, agora também faz celular [17/07] :. Ericsson e operadora testam 3G com upload a 5,8 Mbps [17/07] :. Asus Eee PC 1000H finalmente entra no mercado [16/07] :. Japoneses desenvolvem memória flash de longa durabilidade :. Mais noticias » |
IntroduçãoNo artigo anterior, vimos um pouco sobre o funcionamento das APIs do Windows e as falhas de segurança na relação de um aplicativo com o kernel do sistema. Talvez ainda não tenha ficado bem claro como todo aquele esquema funciona, então nesta segunda parte vamos colocar aqueles conhecimentos em prática. Pretendo mostrar na prática e passo a passo como interceptar uma chamada de API e alterar o comportamento de um aplicativo. No caso, vamos apenas habilitar um botão em um pequeno aplicativo que eu mesmo programei para este fim, da forma mais simples possível. A princípio, pode ser um pouco complicado, já que vamos trabalhar com ferramentas de “baixo nível” como debbugers e programação em assembly. Recomendo ter um conhecimento básico nas duas, mas caso não tenha, não se preocupe, pois vou tentar explicar o melhor possível como trabalhar nesses programas. Antes de indicar quais programas vamos usar, gostaria de deixar claro que este artigo e o alvo sobre o qual vamos trabalhar foram feitos para fins estudantis - programados justamente para exemplificar o funcionamento das APIs - e tem o intuito de alertar os usuários sobre o perigo de executar aplicativos e DLLs desconhecidos. FerramentasPara iniciar nosso estudo, precisamos de algumas ferramentas básicas para entender como nosso alvo funciona e como alterar o funcionamento do mesmo. Segue a lista dos aplicativos que vamos usar. Todos são gratuitos: A instalação desses aplicativos é bastante simples. Na maioria dos casos basta descompactar o conteúdo em uma pasta qualquer. O único aplicativo que vale ressaltar algumas configurações é o WinAsm, de modo que aponte para os diretórios do MASM32 (libs, includes e bin). Os diretórios podem variar dependendo do local de instalação, então configure o WinAsm (menu “Tools->Options->Files & Paths”) da seguinte maneira, apenas alterando os diretórios conforme necessário (o item marcado em vermelho é opcional): ![]() Um pouco de Assembly e seu funcionamentoAntes de iniciar a parte prática, precisamos nos familiarizar um pouco com a linguagem que vamos usar: assembly. Muitos têm medo desse nome, mas eu diria que não é tão complicado assim, pois em certos casos ela é mais clara e direta que qualquer outra linguagem estruturada. Conhecendo um pouco dos comandos básicos você já é capaz de entender e analisar aplicativos simples, como o nosso alvo. Não vou explicar cada instrução existente no assembly, mas vou deixar um link com uma lista de cada uma e a explicação do seu funcionamento: http://www.numaboa.com.br/informatica/oiciliS/assembler/referencias/opcodes/
Este site contém uma das melhores referências nacionais sobre a linguagem assembly (ou asm, como é abreviada). Muito completa e bem explicada. Além da lista de opcodes - como são chamadas as instruções - vou colocar mais algumas referências, que valem a pena serem lidas: Arquitetura Intel: http://www.numaboa.com.br/informatica/oiciliS/assembler/referencias/arquitetura.php Funcionamento da Stack (pilha): http://www.numaboa.com.br/informatica/oiciliS/assembler/textos/stack1.php Outras referências: http://www.numaboa.com.br/informatica/oiciliS/assembler/ Eu recomendo muito a leitura desses textos, pois explicam alguns conceitos sobre a arquitetura dos computadores, que são fundamentais para o entendimento deste artigo. Agora que temos todas as ferramentas em mãos, creio que possamos dar início ao nosso estudo. Vamos começar analisando o funcionamento do alvo através do OllyDbg. Análise do alvoO primeiro passo é dar uma olhada geral no alvo. Execute o arquivo “ative-me.exe”. Nada muito impressionante, apenas um pequeno texto e um botão desabilitado. O nosso objetivo vai ser habilitar esse botão sem alterar sequer um byte do binário. Para que um controle, como botões e caixas de texto, sejam desabilitados, é usada uma função da API do Windows (User32) chamada EnableWindow. BOOL EnableWindow(
HWND hWnd, BOOL bEnable ; Ela recebe dois argumentos: hWnd e bEnable. O primeiro é o que chamamos de “handle”, um valor de referência a determinado item, sobre o qual queremos ter o controle para alterar suas propriedades. O segundo é o bEnable. Quando seu valor é 0 (FALSE), o controle indicado pelo Handle vai ter seu estado alterado para “Desativado”. Caso contrário (TRUE), ele fica “Ativado”. Inicie o OllyDbg. Vá em “File->Open” e selecione o nosso alvo. Assim que ele carregar, você verá uma tela semelhante a essa: ![]() No canto superior esquerdo, encontramos o disassembly do executável. Este seria o código de máquina do nosso aplicativo. Repare que existem 4 colunas no disassembly. Caso não exista, clique com o botão direito sobre ele, vá em “Appearence->Show Bar”. A mais da esquerda é o Offset, endereço de referência de cada instrução. Entenda como se fossem as “linhas de código” do alvo, generalizando. À direita dela tem o Hex Dump de cada instrução. São esses valores em Hexadecimal que o processador usa para realizar os procedimentos. Cada operação que você realiza possui um determinado código de instrução, chamado opcode. Em seguida vêm a coluna do Disassembly, que nada mais é do que os opcodes traduzidos para uma linguagem mais agradável ao ser humano. Por último vem a coluna de comentários, onde o Olly analisa as instruções e informa dados úteis, como por exemplo as chamadas de APIs e os argumentos que ela recebe. No canto superior direito está a janela de registradores (EAX, EBX, ECX, EDX, etc.). Registradores são pequenas porções memória localizadas dentro do processador, das quais boa parte das instruções dependem para realizar as operações. A janela inferior direita contém a Stack (Pilha). Não vou explicar profundamente o que ela é e como funciona, já que indiquei um link explicando sobre isso. Somente para constar, ela também é uma estrutura de memória, mas se comporta de uma forma chamada de LIFO: Last In First Out. Imagine uma pilha de livros, na qual é necessário tirar todos os livros do topo para poder remover algum livro no meio. Os dados da pilha são colocados ou retirados por dois comandos: PUSH e POP. O primeiro é usado para colocar um dado na pilha, e o segundo para removê-lo. Por último o canto inferior esquerdo, correspondente ao Memory Map. Como o próprio nome já diz, este local mostra os dados armazenados no espaço de memória alocado para a execução do nosso aplicativo. Agora que estamos um pouco mais familiarizados, podemos realmente começar o nosso estudo. Com o alvo aberto no Olly, vamos olhar as funções da API que o programa utiliza. Aperte CTRL+N para abrir a janela Names. Esta janela mostra uma lista de todas as APIs utilizadas pelo alvo. Um dos itens nessa lista nos chama atenção: user32.EnableWindow. Clique duas vezes sobre este item e o Olly nos leva ao local onde essa API é chamada pelo aplicativo, como na imagem abaixo. Repare que o Olly identificou a chamada da API, assim como os dois argumentos recebidos - hWnd e Enable - que são colocados na pilha através do comando PUSH. Note também que ele coloca na pilha o valor 0 para a opção Enable, representando o estado do nosso controle, onde 0 = desativado. Em seguida é posto na pilha o EAX, que logicamente contém o Handle do controle. Depois ele simplesmente faz a chamada para a função que vai desabilitar o botão. Bom, e se eu alterasse o PUSH 0 para PUSH 1? Assim eu alteraria o argumento da API, fazendo com que eu ative o botão ao invés de desativá-lo. Correto, isso funciona. Basta dar um duplo clique sobre o PUSH 0 e alterar para PUSH 1, clicando posteriormente em Assemble. Feche a janela do Assemble at... e clique no botão Play. Voilá! Nosso botão está ativado. ![]() Mas o intuito deste artigo não é esse, pois queremos fazer isso sem mexer no código do executável, mas sim alterar diretamente na memória. Volte ao Olly e reinicie o alvo através do CTRL+F2 ou pelo File->Open. Vamos adicionar um breakpoint no local onde ele coloca o primeiro valor na pilha (PUSH 0). Clique sobre a linha que contém o PUSH 0 e aperte F2. O endereço do PUSH deve ter ficado vermelho. O que o breakpoint faz? Ele congela a execução do programa assim que a execução atingir o determinado endereço, para que possamos avaliar o estado dos registradores/memória/stack. Com o breakpoint ativado, clique em “Play”. Instantaneamente o programa congela e retorna para o Olly, indicando que atingimos o nosso breakpoint. O comando PUSH 0 ainda não foi executado. Vamos rodar o programa instrução por instrução agora e ver o que acontece. Aperte F7 uma vez e preste atenção na pilha (canto inferior direito). Assim que você pressionar a tecla, o PUSH 0 vai ser executado e vai adicionar o valor 00000000 na pilha. Aperte F7 novamente, para executar a instrução do PUSH EAX. Repare novamente que o valor de EAX foi adicionado ao topo da stack: ![]() A coluna da esquerda indica o endereço da pilha. A coluna do meio é o valor armazenado naquele endereço. Na terceira coluna, ficam os comentários, como na janela do disasm. Estamos prestes a realizar a chamada para a API. Veja que o “cursor” (offset colorido de preto) indicando a posição atual do programa está sobre o CALL <JMP.&user32.EnableWindow>. Aperte F7 mais uma vez. Fomos levados até a chamada “Jump Table”. Sempre que o programa chamar a função EnableWindow, o código é redirecionado para essa Jump Table, que por sua vez vai em si chamar a tal função. ![]() Repare na pilha, ela está completa agora, já possui os dois argumentos, assim como o endereço de retorno. Quando a API terminar de executar seu código, ela vai utilizar esse endereço para descobrir a instrução para a qual o fluxo deve ser redirecionado. ![]() Podemos esquematizar a chamada da API da seguinte forma: ![]() Certo, mas como vamos alterar aquele valor 00000000 da stack para 00000001, sem mexer o executável? Assim que o alvo for iniciado, vamos carregar uma DLL junto a ele, como explicado na primeira parte deste artigo. A DLL, quando for iniciada, vai buscar pela chamada da função EnableWindow e desviar a sua execução para uma rotina que vamos programar na DLL. Essa rotina, por sua vez, vai alterar o valor do bEnable diretamente na stack e em seguida repassar o controle para o executável novamente, de modo que ele execute a chamada para a EnableWindow como se nada tivesse acontecido. É o que chamamos de “API Intercept”. Voltando ao Olly, você deve estar na JumpTable para o EnableWindow. Aperte F7 mais uma vez e entraremos na rotina dessa API. As suas primeiras instruções são: É aqui que ocorrerá o desvio. Vamos ter que substituir essas primeiras instruções por um salto - JMP - para a nossa própria rotina, codificada na DLL. Não é possível inserir instruções no executável, pois isso comprometeria toda a integridade e alinhamento de endereços. A única forma é substituir as instruções existentes pelo desejado. Claro que também não podemos simplesmente substituir e esquecer dos dados que estavam naquela posição anteriormente. Vamos utilizar os seguintes procedimentos: Agora começa a parte mais complexa, que é escrever a DLL. Infelizmente não poderei explicar a fundo o que cada comando vai realizar, pois tornaria o artigo muito longo (seria mais uma aula de assembly do que um artigo sobre segurança). O código escrito aqui não possui comentários, pois eles quebrariam o layout da página, no entanto o código disponível junto com os arquivos de estudo está devidamente comentado. Caso sinta dificuldade, recomendo ler alguns daqueles links indicados logo no início do artigo. Programação da DLLAbra o WinAsm, com os caminhos devidamente configurados, indicados lá no começo do artigo. Com ele aberto, vá em “File->New Project”. Marque a opção “Create a new empty project” e clique em Next. Selecione “Standard DLL” e conclua o assistente. Se for perguntado para salvar, escolha um nome qualquer. Cole o código abaixo no projeto recém criado. Cuidado ao copiar. Copie em partes e verifique se alguma parte do texto não foi corrompida durante o processo. .586
.model flat, stdcall include windows.inc include kernel32.inc includelib kernel32.lib substituir PROTO .data nDll db "user32.dll",0 nProc db "EnableWindow",0 nRedirect db "redir.dll",0 nFunc db "substituir",0 nSize dd 060000h nIncrement dd 0Ch .data? nAddress dd ? nLoc dd ? nOldProt dd ? nRedirectAddress dd ? nRedirectLoc dd ? nBackup1 dd ? nBackup2 dd ? nTopOfStack dd ? .code LibMain proc h:DWORD, r:DWORD, u:DWORD INVOKE LoadLibrary, ADDR nDll mov nAddress, eax INVOKE GetProcAddress,nAddress,ADDR nProc mov nLoc, eax INVOKE VirtualProtect,nAddress,nSize,PAGE_EXECUTE_READWRITE,OFFSET nOldProt INVOKE LoadLibrary, ADDR nRedirect mov nRedirectAddress, eax INVOKE GetProcAddress,nRedirectAddress,ADDR nFunc mov nRedirectLoc, eax MOV EAX, DWORD PTR DS:[nLoc] PUSH ECX mov ECX, DWORD PTR DS:[EAX] MOV nBackup1, ECX ADD EAX, 4 mov ECX, DWORD PTR DS:[EAX] MOV nBackup2, ECX SUB EAX, 4 mov BYTE PTR DS:[EAX], 0E9h MOV ECX, nRedirectLoc ADD EAX, 5 Sub ECX, EAX SUB EAX, 4 mov DWORD PTR DS:[EAX], ECX POP ECX mov eax, 1 ret LibMain Endp substituir proc PUSH ECX MOV nTopOfStack,ESP MOV EAX, nIncrement ADD EAX, nTopOfStack MOV DWORD PTR DS:[EAX], 01h MOV EAX, DWORD PTR DS:[nLoc] MOV ECX, nBackup1 MOV Dword PTR DS:[EAX],ECX ADD EAX,4 MOV ECX, nBackup2 MOV Dword PTR DS:[EAX],ECX Sub EAX,4 POP ECX JMP EAX ret substituir endp End LibMain
Como mencionei alguns parágrafos acima, esse código não está comentado, dificultando um pouco o entendimento. Para visualizar os comentários você pode abrir o código fonte disponibilizado junto com o nosso alvo, dentro do arquivo fontes_api.rar. O nome do arquivo é main.asm e está localizado na pasta “fontes\dll”. Lá tem cada linha comentada. O código fica da seguinte maneira dentro do WinAsm: ![]() Antes de compilar a DLL precisamos criar um arquivo de definições da mesma, contendo o nome da DLL e as funções que ela possui (no caso, apenas uma). No lado direito, em “Explorer”, clique com o botão direito em qualquer lugar e selecione “Add New Other”. No arquivo que ele adicionou a lista, clique com o botão direito e vá em “Rename”. Na janela para salvar, dê um nome qualquer e, no tipo de arquivo, selecione “Definition Files”. Após salvar, adicione o seguinte código ao arquivo recém criado: LIBRARY redir
EXPORTS substituir
Salve novamente o projeto, pois podemos finalmente compilar a nossa DLL. Verifique novamente se está tudo certo, se você tem os arquivos mais ou menos desta forma: ![]() O arquivo main.asm deve conter o código da nossa DLL. O arquivo definições.def , aquelas 2 linhas que eu mencionei logo acima. Se estiver tudo certo, vá ao menu “Make->Go All”. Caso os caminhos para o MASM32 estejam configurados corretamente, sua DLL deve ter sido compilada com sucesso. Uma mensagem em verde, no rodapé do WinAsm, deve indicar que não houve nenhum erro: ![]() Se essa mensagem não apareceu é porque algo errado aconteceu. Verifique novamente se os arquivos .asm e .def existem na pasta do seu projeto e que o conteúdo de ambos está corretamente escrito. Verifique também nas configurações de “Files & Paths” do WinAsm se o endereço para todas aquelas pastas existem de fato. Caso tudo tenha ocorrido normalmente, você vai ter uma .dll na pasta do seu projeto, cujo nove varia de acordo com o nome dado ao mesmo. Você deve renomear essa DLL para “redir.dll”, sem aspas, pois foi esse o nome que indicamos no arquivo .def. Execução da DLLTemos a DLL, agora só nos resta testá-la. Para facilitar o processo, copie a redir.dll para a raiz de uma partição (C:\ por exemplo). Feito isso, vamos agora forçar a execução dela quando o nosso alvo for executado. Como mencionado na primeira parte deste artigo, utilizaremos uma chave do registro do Windows, que força o carregamento de uma DLL sempre que qualquer aplicativo for iniciado. Isso significa que, caso essa chave de registro exista e aponte para uma dll, qualquer programa iniciado - não somente o nosso alvo - vai iniciá-la junto, sendo que os efeitos da DLL serão notados em todos os aplicativos. Vá até o menu “Iniciar->Executar” e digite “regedit” (sem aspas), seguido do Enter. O Editor de registro foi aberto. Navegue até o caminho:
![]() Agora é “a hora da verdade”. Pode-se fechar o regedit e iniciar o nosso alvo. Se tudo foi feito corretamente, o botão do “Ative-me” não está mais desativado: ![]() Se quisermos ter certeza de que a DLL foi carregada, podemos utilizar o LordPE. Selecione o alvo na lista de aplicativos carregados, e no espaço abaixo estarão listadas as DLLs carregadas pelo aplicativo selecionado. A redir.dll vai ser uma delas: ![]() Para voltar ao estado normal, basta fechar o alvo e remover aquela entrada de registro. Recomendo não deixar ela lá, pois alguns programas podem parar de responder devido ao carregamento desta DLL. Utilize-a apenas durante o período de estudo. Vou aproveitar o final deste capítulo para esclarecer algumas dúvidas que podem surgir durante a análise do código. ConclusãoÉ isso aí. Aqui se encerra este artigo. Antes de finalizá-lo, vou colocar algumas dicas de como se proteger desse tipo de falha, já que é a maior razão para esse artigo ter sido escrito. Acho que as principais dicas que eu posso apontar são essas. Não deixe de fazer valer aquelas outras tradicionais: Espero que tenham gostado e aproveitado bastante. Pode ter ficado um pouco confuso, mas fiz o melhor que eu pude para tentar deixar o mais simples possível. Abraços e até a próxima! Por Fernando Birck aka Fergo - www.fergonez.net Agradecimentos especiais: » Gostou do texto? Veja nossos livros impressos
|