🇧🇷 Utilizando um debugger - OllyDbg

Introdução a utilização de um debugger de baixo nível - OllyDbg

Conteúdo

  1. Introdução
  2. Um pouco de assembly
  3. Interface do OllyDbg
  4. Formas de aproximação
  5. Re-assembly
  6. Plugins

1. Introdução

Seguindo a minha linha de tutoriais voltados a programação, vou tratar sobre um assunto que me interessa muito e talvez seja interessante para os programadores em geral: disassembler e debuggers.

Primeiramente seria interessante esclarecer um pouco sobre o que é um debugger e o que é um disassembler, pois apesar de andarem quase sempre juntos, possuem finalidades diferentes.

Disassembler é algo que consegue transformar linguagem de máquina para a linguagem assembly, transcrevendo as instruções enviadas ao processador para os seus mnemônicos em assembly (asm). Não deve ser confundido com um descompilador, que procura converter o código nativo em uma linguagem de mais alto nível como C, C++ ou Basic.

Debuggers são programas capazes de analisar, depurar e testar aplicações. Atualmente a maioria das IDEs de programação contam com um debugger embutido (Visual Studio, por exemplo). A principal utilidade deles é para a identificação e tratamento de erro, sendo que é possível rodar o código linha por linha (ou instrução por instrução) e analisar a mudança das variáveis e do comportamento do código. Os debuggers de binários já compilados - como os executáveis do Windows (EXE) - seguem o mesmo conceito dos depuradores normais, mas devido ao fato de o código já ter sido compilado, ele precisa ter um disassembler embutido no debugger para decodificar as instruções.

Atualmente existem dezenas de debuggers e disassemblers por aí, dentre os quais os mais famosos são: W32DASM, IDA, WinDbg, SoftICE e Ollydbg. Neste tutorial será utilizado o OllyDbg, pois é um dos melhores e mais poderosos debuggers (incluindo um disassembler) disponíveis no mercado. É também pequeno e gratuito

Site oficial do OllyDbg, com o link para download: http://www.ollydbg.de

1.1 Qual a utilidade de um debugger?

Muita gente se pergunta do porquê de usar um debugger, sendo que na maioria dos casos você tem a acesso ao código fonte original (caso você tenha programado o aplicativo). Vou citar abaixo algumas das maiores utilidades de um debugger:

  • Tratamento de erro. Certamente uma das principais. Às vezes durante a programação de um aplicativo um pequeno erro passou despercebido, ocasionando mal funcionamento ou gerando uma operação ilegal. Em muitos casos é mais fácil você analisar o binário já compilado dentro de um debugger do que tentar encontrar o erro no código original. Dentro desse mesmo item podemos citar a correção de bugs de aplicações já descontinuadas (desde que com a autorização da empresa dona dos direitos).

  • Engenharia reversa. O processo de engenharia reversa de software não poderia ser feito de forma eficiente sem a utilização de um debugger/disassembler. Muitas pessoas tendem a confundir cracking com engenharia reversa, sendo que são conceitos diferentes. A engenharia reversa por si só é uma atividade completamente legal, pois muito do que vemos hoje só foi possível devido à engenharia reversa. A criação de drivers para Linux de periféricos que antes só funcionavam com o Windows (WinModems) é um bom exemplo de como a engenharia reversa traz coisas boas para nós.

  • Aprendizado. O uso de debuggers e engenharia reversa é uma das melhores formas de se aprender a linguagem assembly. Você programa algo em uma linguagem de médio ou alto nível e posteriormente analisa o resultado do binário compilado dentro de um debugger. Com esse conhecimento é possível dominar melhor a linguagem e criar algoritmos mais otimizados e eficientes.

1.2 Conceitos necessários

Para entender o funcionamento de um debugger é preciso saber um pouco sobre alguns conceitos ligados a informática, como o funcionamento da memória, processador, pilhas e endereços. O conhecimento básico de assembly também é necessário, já que essa é a linguagem que teremos de analisar. Caso não tenha experiência em assembly, fique tranqüilo, pois nos capítulos seguintes darei uma visão geral sobre ela, o suficiente para entender o nosso mini-aplicativo de estudo. Abaixo segue uma breve lista de conceitos:

  • Processador/CPU: É o cérebro de todo computador. É ele que decodifica as instruções e executa os códigos operacionais. É composto basicamente por uma unidade lógico-aritmética (ALU), unidade de ponto flutuante (FPU), registradores, cachê, barramento e gerador de clock.
  • Memória RAM: Local de armazenamento temporário de dados (são apagados ao desligar o computador). Todo aplicativo se utiliza da memória para armazenar seus dados e estes são buscados e gerenciados pelo processador.
  • Endereçamento de memória: É uma faixa de valores que apontam para uma determinada posição de memória. Toda vez que você escreve ou lê algum dado da memória é necessário indicar o endereço de onde está aquele valor, para que o processador possa buscá-lo.
  • Pilha (Stack): É uma estrutura de dados. Sua principal característica é a forma de funcionamento, onde você apenas coloca ou retira os valores, sem indicar um endereço (LIFO – Last in, First Out – Último a entrar, primeiro a sair). Ela funciona de forma semelhante a uma pilha de livros em que você vai os empilhando. Quando precisar remover um deles, é necessário tirar todos os livros de cima.​

  • Registradores: Pequenas partes de memória presentes dentro dos processadores (não confundir com memória RAM). Extremamente rápidas, sendo que a CPU as utiliza como forma temporária de armazenamento de dados e realização de operações. A quantidade de dados que podem ser armazenados vai depender do tipo de processador. Os processadores de 32 bits conseguem armazenar números de até 32 bits em cada registrador, sem precisar de rotinas de conversão.​

2. Um pouco de Assembly

Para fazer o debug de binários compilados é necessário ter um conhecimento (ao menos básico) da linguagem assembly, já que é para ela que a linguagem de máquina é traduzida.

Assembly (ou asm, com é abreviada) é uma linguagem de baixo nível que basicamente interpreta os códigos operacionais (opcodes, veja abaixo) e os transcreve para seus mnemônicos. É literalmente uma tradução da linguagem de máquina. O uso da linguagem assembly pode ser bem variado, podendo fazer de tudo um pouco, mas é amplamente utilizada na programação básica de Kernels e em algoritmos que precisam ser altamente otimizados, onde asm é a linguagem ideal, já que é puramente linguagem de máquina traduzida.

Não pretendo agora explicar todo o funcionamento, estrutura e comandos da linguagem. Vou dar apenas um apanhado geral sobre alguns termos e uma breve descrição sobre os comandos mais básicos e corriqueiros que se encontra. Precisamos primeiramente definir o que são mnemônicos e o que são os opcodes.

Opcodes (traduzido em operational code, ou código de operação) é a instrução que é enviada e interpretada pelo processador. Cada opcode, ao ser interpretado pelo processador, vai realizar uma operação. Mnemônicos são as palavras ou combinação de letras utilizadas para representar um opcode, tornando a linguagem de máquina mais legível. Veja abaixo um exemplo de um mnemônico do comando MOV:

MOV EAX,1

Esse comando em assembly apenas move o valor 1 para o registrador EAX (veremos isso logo adiante na explicação dos comandos). Na hora de transformar isso em linguagem de máquina (por um asssembler), esse comando é traduzido para um conjunto de números que possa ser interpretado pelo processador:

B801000000

A teoria por trás de tradução de mnemônicos em opcode (e vice-versa) é um tanto complexa, principalmente para a plataforma Intel na arquitetura IA32. É um processo que deve ser realizado bit a bit e fugiria um pouco do contexto deste tutorial.

A principal dificuldade na linguagem assembly é certamente a sua estrutura, que foge do padrão de linguagens de mais alto nível como C ou Pascal. Nada de Ifs com múltiplas comparações, Switches, For ou While. Tudo é feito com comparações simples e saltos, perdendo a sua linearidade (semelhante aos GoTo do BASIC).
Felizmente hoje temos debuggers muito inteligentes que conseguem estruturar e identificar rotinas e repetições, facilitando muito o trabalho de interpretação. Mesmo com essas melhorias, ainda acho importante ter um papel e uma caneta ao lado, onde você pode fazer anotações e ir estruturando/convertendo o código na medida em que você os interpreta.

Para o assembly, a localização dos valores e variáveis é sempre baseada nos endereços que elas ocupam na memória. O nome que você define para uma variável durante a programação é substituído pelo endereço de memória que ela ocupa. Cada instrução também possui um endereço, que é utilizado para controlar o fluxo e a estrutura do código. Sempre que você faz um salto, é necessário indicar o endereço que o código deve ser direcionado, semelhante ao que ocorria nas numerações de linhas dos BASICs mais antigos. Veja um exemplo abaixo de como ficaria um código em C e o seu resultado compilado para assembly, utilizando apenas registradores comuns:

void main() {
    int a = 4;
    int b = 6;
    int c;

    if((a == 4) && (b == 6)) {
        c = 5;     
    }
}

O código acima quando compilado pode se transformar em algo semelhante a isso (boa parte do código acima é inútil, estou utilizando somente para exemplificar):

00000000 MOV EAX,4h     ;move o valor 4 para EAX
00000005 MOV EBX,6h     ;move o valor 6 para EBX
0000000A CMP EAX,4h     ;compara EAX com 4, se for verdadeiro: ZF = 1
0000000D JNE 00000019h  ;se ZF != 1, pule para endereço 00000019h
0000000F CMP EBX,6h     ;compara EBX com 6, se for verdadeiro: ZF = 1
00000012 JNE 00000019h  ;se ZF != 1, pule para endereço 00000019h
00000014 MOV ECX,5h     ;move o valor 5 para ECX
00000019 RETN           ;finaliza execução e retorna 

Pra entender o código acima é necessário entender sobre aquilo que compõe a linguagem assembly. Ela é basicamente composta por registradores, endereços e instruções (mnemônicos).

Os registradores foram explicados no capítulo anterior, mas vamos agora saber quem são eles. Os processadores da arquitetura Intel de 32 bits possuem basicamente nove registradores de 32 bits comuns: EAX, EBX, ECX, EDX, ESP, EBP, ESI, EDI e EIP. Teoricamente cada um desses registradores possui uma determinada “função padrão”, mas devido a sua escassez, muitas vezes eles são utilizados como registradores para qualquer propósito. Você pode criar um código usando livremente os oito primeiros registradores que não haverá muitos problemas (desde que saiba o que está fazendo/modificando). O último registrador, EIP, é quase sempre mantido intacto, pois ele é o responsável por contar as instruções e informar o endereço da próxima instrução. Alterar o seu valor pode desviar completamente o fluxo do aplicativo e provavelmente vai gerar uma falha de segmentação ou uma operação ilegal.

Esses registradores apresentados são todos de 32 bits. No entanto também é possível utilizar apenas 8 ou 16 bits, como mostra a tabela abaixo utilizando o EAX como exemplo (a teoria vale para os outros registradores também):

Para o caso da porção de 8 bits, o registrador terminado em L são os 8 bits menos significantes de AX e o terminado em H são os 8 bits mais significantes de AX. Para a porção de 16 bits, são utilizados os 16 bits menos significantes da porção de 32 bits.

Além dos registradores, também existem as Flags, que são bits utilizados como resultado de operações (verdadeiro ou falso, por exemplo). Elas são usadas principalmente para análise condicional em instruções como CMP e TEST. Dentre as diversas flags, as mais corriqueiras são: ZF (Zero Flag), CF (Carry Flag) e SF (Signal Flag). A ZF é setada sempre que uma operação resulta em zero (uma comparação entre dois números através do comando CMP subtrai seus operandos sem alterar valores e seta a ZF caso o resultado da subtração seja zero, indicando valores iguais). A flag CF é setada quando o resultado de uma operação estoura o valor máximo comportado pelo registrador/local sem considerar o sinal (overflow). Por último, tempos a SF, que é ativada sempre que o bit mais significativo de um operando for 1, indicando um valor negativo (pesquise sobre complemento de dois).

Os endereços na linguagem assembly são a base para o fluxo do aplicativo e para o armazenamento de dados. As variáveis que você usa durante a programação são substituídas por endereços que apontam para uma área da memória paginada com acesso a leitura e a escrita. Os destinos dos saltos (Jumps) também dependem dos endereços das instruções, pois é através deles que você informa o destino do salto.
O código abaixo demonstra apenas uma linha de assembly onde é possível ver o endereço da instrução (00401000) e o endereço para um byte de memória (00403000) para o qual o número nove está sendo movido:

00401000 MOV BYTE PTR DS:[00403000], 09h

Por último temos as instruções, que nada mais são do que os opcodes traduzidos em um mnemônico, como demonstrado e exemplificado alguns parágrafos acima.
Abaixo eu vou por uma pequena lista mostrando algumas das instruções mais utilizadas, pois seria inviável colocar todas elas (são aproximadamente 130 instruções bases para a arquitetura Intel).

  • MOV destino, origem Move o valor do campo origem para o destino. Essa instrução possui diversas variações, por isso ela pode aparecer de diversas formas diferentes (pode-se trabalhar com constantes, memória, pilha, etc). Alguns exemplos:

    MOV EAX, 10h  
    MOV AX, WORD PTR DS:[00403000]  
    MOV BYTE PTR DS:[00403002], 1Ch
    
  • CMP arg1, arg2 Realiza uma comparação entre os dois operandos. A comparação é feita simplesmente subtraindo os dois operandos e caso o resultado for zero (valores iguais), ele seta a ZF para 1. Vale lembrar que essa operação não altera os valores dos operandos, apenas as flags.

    CMP EAX, 04h
    
  • JMP endereço Faz um salto incondicional e obrigatório para o endereço indicado.

    JMP 00401008h
    
  • JZ endereço / JE endereço Faz um salto condicional. Caso o valor da zero flag seja 1, ele realiza o salto. Normalmente utilizado junto com um CMP para realizar um desvio caso a comparação seja verdadeira.

    JE 0040101Ah
    
  • JNZ endereço / JNE endereço Semelhante ao item acima, mas realiza o salto somente quando a zero flag não foi setada (ZF = 0).

    JNZ 0040102Ch
    
  • ADD destino, arg1 Adiciona o valor de arg1 ao destino. Também possui diversas variações, pelas mesmas razões do comando MOV. Se o resultado estourar o limite do destino, a CF é setada.

    ADD EBX, 04h  
    ADD EBX, DWORD PTR DS:[00403032]  
    
  • SUB destino, arg1 Realiza uma subtração dos operandos. As variações e características são as mesmas do comando ADD.

    SUB ECX, 2Ah
    
  • PUSH valor Coloca o valor no topo da pilha (Stack). O comando PUSH é amplamente utilizado nas chamadas de funções (CALL), pois é através da pilha que a função busca seus argumentos.

    PUSH 08h
    
  • POP destino Remove o valor do topo da pilha e o armazena no destino.

    POP EAX
    
  • CALL local Faz chamada a uma função. É possível passar o local de diversas formas para o comando CALL, desde uma constante, registrador ou até mesmo uma função externa dentro de uma DLL. O comando CALL usa a pilha para indicar o endereço para o qual a função deve retornar depois de finalizada a sua execução.

    CALL User32!GetDlgItemTextA  
    CALL 0040115Fh
    

Essas são as instruções mais comuns dentro de um binário compilado. Claro que existe mais de uma centena delas, mas eu procurei colocar aqui apenas aquelas que serão utilizadas no aplicativo de aprendizado. Para uma lista completa com uma explicação mais profunda dos opcodes, recomendo ver a lista apresentada neste endereço:
http://www.numaboa.com.br/informatica/oiciliS/assembler/referencias/opcodes/

O próximo capítulo cobrirá a parte da apresentação da interface do OllyDbg para depois podermos realmente colocar a mão na massa e analisar um binário.

3. Interface do OllyDbg

Após uma boa parte teórica, chegou a hora de por em prática aquilo que acabamos de estudar. Neste capítulo vou apresentar um pouco da interface do OllyDbg, que apesar de intuitiva, merece esclarecimentos.

Para o nosso estudo, eu criei um simples aplicativo que iremos depurar mais a frente. Ele é necessário neste já neste capítulo (não para depuração, mas para a apresentação dos itens do Olly). Você pode baixar o arquivo executável juntamente com o seu código fonte (programado em assembly na sintaxe MASM32 utilizando o WinAsm Studio como IDE) no link abaixo:

http://www.fergonez.net/files/adivinhe.rar

O OllyDbg pode ser baixado gratuitamente através do site http://www.ollydbg.de. É bem pequeno e não necessita instalação, basta extrair o conteúdo para uma pasta qualquer.

Após extraído, abra o Olly, vá em “File->Open” e abra o nosso arquivo de estudo (adivinhe.exe). Rapidamente o Olly vai interpretar o arquivo e mostrar o disassembly na janela principal. Vamos deixar essa parte mais frente, já que o objetivo deste capítulo é apenas mostrar a interface, sem depurar o aplicativo por enquanto. A tela deve ser semelhante a essa:

A interface do aplicativo é composta por poucos botões. O segredo do Olly é o botão direito do mouse. A maioria das funções existentes no aplicativo pode ser acessada através do botão direito do mouse, sendo que os itens exibido no menu de popup variam de acordo com o local onde foi dado o clique (dependendo da coluna e da região).

Eu numerei as principais regiões da tela de 1 a 4.

Região 1


Esta é a tela principal do programa, onde é apresentado o disassembly do aplicativo. Ela é dividida em quatro colunas:

  • Coluna 1 – Address. Ela nos mostra o endereço virtual das instruções (para saber mais sobre esse endereçamento, veja meu artigo sobre o funcionamento dos executáveis). Você pode reparar que os endereços não são em intervalos iguais para cada instrução. Isso ocorre devido ao fato de que o tamanho das instruções ser variável, como podemos observar na segunda coluna.
  • Coluna 2 – Hex Dump. Aqui temos o código da instrução no seu formato hexadecimal (a cada 2 caracteres, temos 1 byte). São esses valores que ficam armazenados dentro do arquivo executável e que são passados para o processador. Como mencionado no parágrafo anterior, as instruções variam de tamanho, sendo que o endereço da próxima instrução é dado pelo endereço da instrução atual mais a soma dos bytes da instrução. Veja o exemplo do nosso aplicativo de exemplo. Ele começa no endereço 00401000 (padrão do Windows) e a sua primeira instrução é composta por 2 bytes (6A 00). O endereço da próxima instrução (na linha de baixo) vai ser o endereço atual somado com o tamanho da instrução (00401000+2) = 00401002.
  • Coluna 3 – Disassembly. Essa coluna nada mais é do que a interpretação e a tradução para assembly das instruções presentes na segunda coluna. A análise do aplicativo é feita quase que inteiramente nela.
  • Coluna 4 – Comments. Essa coluna não influencia no aplicativo, ela é utilizada apenas para comentários e informações. O Olly a utiliza para identificar as chamadas de função juntamente com os seus argumentos (você pode ver que ele identifica as chamadas da API do Windows em vermelho e lhe mostra os argumentos, facilitando e muito a interpretação).

Região 2


Essa área mostra todos os registradores e flags que nós vimos anteriormente (juntamente com diversos outros valores). A cada instrução essa tela é atualizada, mostrando o estado atual de cada um dos itens. Caso algum desses itens tenha sido modificado de uma instrução para outra, o Olly as colore com outra cor (nesse caso é o vermelho). As flags são mostradas logo abaixo dos registradores, abreviadas com a letra C (Carry Flag), Z (Zero Flag) e S (Signal Flag)

Região 3


Essa região nos mostra a memória física (RAM) destinada ao aplicativo. É possível observar o valor de cada byte de memória dentro do espaço reservado ao aplicativo. É composta por três colunas:

  • Coluna 1 – Address. Mostra os endereços virtuais de memória.
  • Coluna 2 – Hex Dump. Este espaço contém o valor de cada byte da memória. Por padrão o Olly coloca 8 bytes por linha e por essa razão a coluna de endereços cresce de 8 em 8 bytes.
  • Coluna 3 – ASCII. Essa coluna pode ser utilizada para exibir de formas diferentes os valores contidos na memória. Por padrão o Olly opta por exibir a representação ASCII desses valores. O modo de representação pode ser alterado utilizando o botão direito do mouse.

Região 4


Mostra o estado atual da pilha (stack). Como visto anteriormente, a pilha é amplamente utilizada durante as chamadas de função. O VisualBasic é uma linguagem que faz um uso muito grande da pilha, principalmente pela quantidade de funções que são utilizadas pelo aplicativo. Também é dividida em 3 colunas :

  • Coluna 1 – Address. Cumpre o mesmo papel das outras colunas de endereço. Nota-se que o endereço cresce de quatro em quatro bytes, pois cada posição da pilha é ocupada por um tipo DWORD (4 bytes)
  • Coluna 2 – Value. Valor armazenado naquele endereço da pilha
  • Coluna 3 – Comment. Utilizado para comentários e mostrar informações relevantes sobre aquele endereço. O Olly identifica diversos itens da pilha (como endereços de retorno) e adiciona essas informações na coluna de comentários.

Além das regiões nós temos a barra de ferramentas:

Abaixo uma descrição de cada botão, da esquerda para a direita.

  • Open - Abre o executável para depuração
  • Restart – Recarrega o aplicativo atual
  • Close – Fecha a aplicação carregada
  • Play – Inicia a execução e depuração do aplicativo. Caso nenhum breakpoint tenha sido posicionado (veremos adiante), o programa será executado normalmente.
  • Pause – Pausa o aplicativo em andamento
  • Step Into – Caso um breakpoint tenha sido colocado em uma chamada de função, esse botão lhe permite fazer a depuração do conteúdo dessa função.
  • Step Over – O contrário do item anterior. Ele simplesmente não “entra” dentro da chamada (mas ainda assim a executa), continuando a depuração na próxima instrução.
  • Trace Into – Utilizado apenas quando está se faz backtracing. Ele registra as ações e endereços em um log, registrando também o conteúdo das funções chamadas.
  • Trace Over – Semelhante ao item acima, mas não faz o registro do conteúdo das chamadas.
  • Execute Till Return – Executa as instruções até encontrar o primeiro comando de retorno (RETN).
  • Go to Address – Permite ao usuário especificar um endereço do código para visualizar.

Após os comandos básicos de depuração, temos os botões das janelas:

  • L – Show Log Window: exibe um log, no qual o Olly registra algumas ações como carregamento de plugins, etc.
  • E – Show Modules Window: exibe todos os módulos e funções externas utilizados pelo programa (DLLs). Com o menu direito é possível acessar uma gama de opções dentro dessa janela (o mesmo vale para todas as outras janelas que forem mencionadas). Essa janela de módulos é muito importante para configurar breakpoints nas APIs do Windows, facilitando a aproximação em determinada região do código.
  • M – Show Memory Window: mostra o estado da memória que está sendo utilizado pelo aplicativo, incluindo as seções do executável e tabelas de importação/exportação. Para um detalhamento byte a byte da memória, deve se utilizar a região de memória física mostrada na janela principal do aplicativo.
  • T – Show Threads: exibe o estado de cada thread contida no aplicativo. Em aplicações multi-threading é possível, através dessa janela, ter um controle sobre cada uma das threads.
  • W – Show Windows: mostra a estrutura e configuração das janelas carregadas pelo aplicativo (definida pelo WinProc). Os dados só são mostrados com o programa em execução e precisa ser atualizada manualmente pelo usuário (através do botão direito do mouse).
  • H – Show Handles: exibe uma informação detalhada sobre os handles (referência a um objeto) que estão sendo utilizados pelo aplicativo. Quando o aplicativo abre um arquivo, é retornado um handle, que é utilizado para fazer a leitura e escrita, por exemplo.
  • C – Show CPU: janela padrão do aplicativo, que é aberta automaticamente na hora de carregar o alvo. O seu conteúdo já foi explicado nos itens anteriores (onde as regiões foram numeradas de 1 a 4).
  • / - Show Patches: as modificações feitas no executável ficam registradas nessa janela, facilitando a modificação ou o retorno à instrução original.
  • K – Show Call Stack: mostra uma pilha de todas as chamadas de função até então feitas pelo aplicativo.
  • B – Show Breakpoints Window: exibe todos os breakpoints setados no programa alvo.
  • R – Show references: exibe todas as referências encontradas durante uma busca (seja ela uma constante, instrução, string). Veremos mais sobre elas adiante.
  • ... – Run Trace: nessa janela é mostrado o resultado da operação de tracing (mencionada quando falamos de Trace Into/Over). Tracing é um processo um pouco complicado, por isso a sua explicação detalhada será apresentada mais a frente (é possível encontrar uma boa explicação na própria ajuda do Olly).
  • S – Show Source: quando o aplicativo alvo é compilado com as informações de debug, normalmente o código assembly resultante também é armazenado. Nesse caso essa janela exibe esse código e mostra em tempo real o local no código fonte original que está sendo executado no momento. Muito útil para comparar o código assembly escrito e o compilado.

Por último temos os botões de configuração e ajuda.

  • Debugging Options – Exibe a janela de configuração do Olly. A princípio não é necessário alterar nada para fazer a depuração, a não ser que você tenha noção de onde está mexendo.
  • Appearence – Permite configurar o esquema de cores. Recomendo alterar essas configurações para algo que lhe agrade, fazendo um “syntax highlighting” do código, facilitando a leitura.
  • Help – Mostra a janela de ajuda do Olly. A ajuda está em Inglês.

Estas foram as opções contidas na barra de ferramentas. O Olly também conta com um menu tradicional, que contém basicamente as mesmas funções da barra de ferramentas. Um dos itens do menu que vale a pena mencionar é o de plugins. O Olly suporta a criação de plugins, sendo alguns deles muito úteis. Por padrão ele vêm apenas com dois plugins, um de bookmarks e outro de comandos, que adiciona uma caixa de texto no rodapé do programa onde você pode entrar com ações e comandos de forma semelhante ao SoftICE.

No rodapé do Olly fica uma pequena barra de status, que além de mostrar o estado atual do alvo (finalizado, pausado ou em execução), serve como um informativo.

4. Formas de aproximação

Uma das maiores dificuldades em debugging de baixo nível (em assembly) é identificar o local onde se encontra aquele trecho de código de deseja analisar. Existem diversas manhas para convergir ao local correto, sendo que vou citar duas das mais utilizadas.

A primeira delas consiste em buscar em buscar por strings. Na maioria dos casos todo o texto presente em um aplicativo fica armazenado em uma string table (tabela de strings), cada uma com seu número identificador. Normalmente quem decide o que vai para a tabela de strings ou o que é referenciado diretamente no código é o compilador, por isso esse método nem sempre é totalmente funcional, mas costuma ter bons resultados.

Certo, mas que strings devemos procurar? Nós queremos é encontrar o local onde é feita a comparação do número digitado com o número correto. Se o número não for aquele que você digitou, ele vai exibir uma mensagem, que contém um título e um texto. Isso é bastante interessante, pois a provável lógica do programa é verificar pelo número digitado e caso ele seja incorreto, nos mostrar a MessageBox. Se nós encontrarmos o local onde o texto é utilizado pela MsgBox, sabemos que estamos pertos e um pouco adiante de onde foi feita a verificação.

Há uma maneira bem direta de descobrir o local onde está a chamada para a MsgBox, mas vou focar mais no sistema de busca por string. Vamos lá. Entre com um valor qualquer (maior que 0 e menor que 21) e mande verificar. Provavelmente você recebeu uma mensagem semelhante a essa:

Repare que ela é composta por um título e um texto. Que tal verificar se é possível buscar esses textos dentro do OllyDbg. Para tal, na janela principal, sobre o disassembly do código, clique com o botão direito e vá em “Search For -> All referenced text strings”. Isso fará com que o Olly mostre uma janela contendo todas as strings que são referenciadas por algum comando dentro do código. Note que no conteúdo da janela apareceram três itens:

Podemos observar que temos três referências ao texto mensagem, ocorrendo em diferentes endereços. “Curiosamente” a string “Mensagem.” é o título da mensagem de texto que recebemos ao entrar com um valor errado. Isso significa que encontramos 3 possíveis locais onde a caixa de texto é exibida. Uma maneira fácil de descobrir qual das três é a verdadeira (mais pra frente veremos que na realidade nenhuma delas é “falsa”, são apenas mensagens de texto diferentes) é setando um breakpoint sempre a mensagem de texto for referenciada. Para tal, clique com o botão direito em qualquer uma das linhas e selecione “Set Breakpoint on every command”. Sempre que a mensagem for utilizada, o Olly vai pausar a execução e lhe mostrar onde a execução foi congelada.

Com o breakpoint configurado, apenas digite novamente o número no aplicativo de teste (sem fechar ou reiniciar o Olly). Assim que você clicar no botão, ao invés de exibir a mensagem de número incorreto, o Olly vai pausar a execução e lhe mostrar o local onde a referência de texto foi utilizada. Você deve ter parado aqui (linha marcada em cinza):

Foi bem como queríamos. Paramos bem no local onde o endereço da mensagem é colocado na pilha para ser utilizada pela função MessageBoxA:

Por curiosidade, note que temos 3 chamadas para a função MessageBox. Pelo texto de cada uma é possível identificar que a primeira é referente ao texto de quando você acerta o número, a segunda (que nós estamos) é de quando você erra e a última é para quando você entra com um valor fora do intervalo especificado. Isso explica também o fato de termos três referências a string “Mensagem.”, pois ela é utilizada pelas três chamadas.

Como mencionado anteriormente, esse método nos faz convergir para um local além de onde foi feita a comparação (pois a mensagem de texto é exibida somente depois que o valor é verificado). Para encontrar a comparação a partir do local atual podem-se utilizar diversos métodos. Alguns preferem simplesmente ir analisado o código acima da MsgBox “na mão” ou fazer um backtrace, que consiste em analisar o código asm inversamente. Como esse aplicativo é bem pequeno, fica fácil achar o local na marra, mas vou dar uma visão sobre o backtracing. O Olly felizmente possui várias funções que ajudam na interpretação do código, sendo que vamos utilizar as referências de salto para essa situação. Para chegar até a mensagem de texto, muito provavelmente foi feito um salto, já que a provável lógica seria:

1.  Adquire os dados digitados pelo usuário
2.  Compara com o valor real
3.  É igual?
4.  Caso não seja igual, pule para ...
5.  Caso seja igual continue/pule para outro local

Sabendo onde foi realizado o salto nos deixa mais próximo ainda do local da comparação. Como o pulo foi realizado para mostrar a mensagem de texto, destino mais provável para o salto é quando os dados da mensagem de texto começam a ser empilhados. Selecione a linha logo acima da atual, onde tem o comando PUSH 0 (primeiro valor colocado na pilha). Note que o Olly identifica esse local como sendo o alvo de um salto (veja na parte de baixo da região do disassembly):

Basicamente ele está te dizendo que para chegar ao local atual, foi feito um salto no endereço 00401061. Podemos ir até esse local e verificar se esse salto realmente existe. Clique com o botão direito sobre essa linha (no endereço 00401079) e vá em “GoTo -> JNZ from 00401061”. Isso nos levará diretamente para o local do salto:

Fomos levados até o endereço 00401061 onde realmente existe um salto (JNZ SHORT adivinhe.00401079) e provavelmente estamos bem próximo do local da comparação. Realmente estamos. Analise as linhas que antecedem o salto. Temos uma chamada a função GetDlgItemInt (busca um inteiro contido dentro de um item da janela, que nesse caso é uma caixa de texto) e o armazena em EAX (isso é padrão, todo retorno de função é em EAX). Em sequida temos:

1.  Compare EAX com 1
2.  Se for menor, pule para 0040108F
3.  Compare EAX com 14 (os números são em hex, logo 14h = 20 decimal)
4.  Se for maior, pule para 0040108F
5.  Compare EAX com 4
6.  Se forem diferentes (JNZ/JNE), pule para 00401079

Creio que você já tenha sacado o que está ocorrendo. Ele está primeiramente verificando se o número digitado está dentro do intervalo (20 >= X >= 1). Se eles estão no intervalo, nenhum salto foi realizado, logo ele continua a execução. Logo após o valor digitado é comparado com o número 4, e se eles forem diferentes, o programa pula para aquela mensagem de texto que estávamos anteriormente. Que tal experimentar colocar o número 4 na caixa de texto do programinha de estudo e ver o resultado? Bingo, encontramos o local da comparação e por conseqüência, o número com o qual ele compara o valor digitado.

Esse código asm seria gerado basicamente por uma estrutura semelhante a esta, em um pseudocódigo:

Declara variável inteira X;  
X = Número contido na caixa de texto;  

Se X < 1 ou X > 20 Então  
    Exibe mensagem de texto “Número Inválido”;  
Fim Se  

Se X = 4 Então  
    Exibe mensagem de texto “Parabéns”;
Caso Contrário  
    Exibe mensagem de texto “Você Errou”;
Fim Se

Essa é uma das maneiras para localizar trechos de código em um debugger. É usado por muita gente, sendo que esse exemplo que apresentei é um “clássico”. Outra forma, muito mais direta, mas que exige um conhecimento da API do Windows é buscar pelas chamadas das funções das APIs do Windows.

Supondo que o usuário tenha certa experiência em programação (seja em asm ou em C), ele provavelmente conhece algumas funções do Windows, já que elas são necessárias para qualquer aplicativo visual. Como a lógica desse programa se baseia em buscar e comparar um dado digitado em uma caixa de texto, um usuário que já conheça um pouco da API sabe que é necessário usar uma função do Windows para realizar esse processo. As duas funções mais famosas que pegam dados de controles são: GetDlgItemText e GetDlgItemInt.

O Olly possui uma janela que mostra todas as funções utilizadas pelo programa, então podemos verificar se existe uma dessas duas funções no aplicativo alvo. Para isso, clique no botão E (ou use o atalho ALT+E) para abrir a janela de módulos. Vai ter uma breve lista, contendo na primeira linha o próprio programa e nas outras as DLLs dependentes. Clique sobre a linha que contém o nosso aplicativo (adivinhe) e vá em “View Names”. Isso exibirá uma lista com todas as funções utilizadas pelo aplicativo.

Eis a lista de funções utilizadas:

Note que a função GetDlgItemInt foi utilizada, como buscávamos. Para descobrir o local onde ela é usada pode-se utilizar o mesmo método de antes, clicando com o botão direito e selecionando “Set breakpoint on every referecence”. Daí basta continuar a execução do programa, digitar um número e clicar no botão. Quando o alvo for chamar a função, o Olly congela e exibe o local onde será feita a chamada, que é logo acima de onde é feita a verificação, como vimos anteriormente.

Eu particularmente prefiro este método sobre o das referências, por algumas razões:

  • Ele normalmente nos leva para uma região bem mais próxima da verificação e antes dela. Utilizando as referências, você pode ser levado para um local muito além, necessitando de muito backtracing.
  • Algumas referências de texto não aparecem na lista da string table, o que torna esse método mais prático.
  • Não depende de mensagem de texto ou MessageBox, que nem sempre estão presentes em todos os aplicativos.

5. Re-assembly

O Olly além de ser um ótimo debugger, é um ótimo assembler. Com ele também é possível editar o código em tempo real e observar as mudanças de comportamento. Todas as mudanças que você realiza no código ficam salvas já janela de “Show Patches” (/). Vamos agora ver como podemos modificar o código e re-salvar o nosso executável (muito útil para correção de bugs).

Reabra o nosso arquivo no Olly, caso ele esteja fechado. Nos capítulos anteriores discutimos um pouco sobre o funcionamento do nosso aplicativo de teste, então vamos agora fazer uma pequena modificação no mesmo para que ele sempre exiba a mensagem de “Você acertou”.

Nós tínhamos uma seqüência de comparações seguidas pelos seus respectivos saltos. A primeira verificava se o número era menor que 1, a segunda se ele era maior que 20 e a terceira se ele era em si o número escondido (4). Existem diversas formas de fazer com que o número seja sempre o correto. Vou listar algumas:

  • Alterar o código forçando que o valor 4 seja movido para EAX antes da comparação.
  • Anular o último salto (após o CMP EAX, 4), fazendo com que o fluxo do aplicativo siga direto para a mensagem correta.
  • Desviando e forçando um salto para a mensagem correta logo na primeira comparação realizada.

Existem outras formas ainda, mas vamos ficar com essas três, pois são as mais óbvias. Eu vou utilizar o último método neste tutorial para exemplificar o processo.

O que basicamente nós vamos fazer é forçar um salto logo após a função GetDlgItemInt diretamente para a região que chama a nossa mensagem de texto “Você acertou...”, como mostra a imagem abaixo:

Para modificar o disassembly, basta clicar sobre a linha que deseja alterar e apertar a tecla “Espaço”. Isso fará com que uma janela se abra com um local onde você possa indicar a instrução que deseja colocar no local.

O nosso objetivo aqui é substituir o CMP EAX, 1 por um salto não-condicional até o local onde os argumentos da nossa mensagem de texto começam a ser selecionados (no PUSH 0, localizado no endereço 0x00401063).

Clique sobre a linha que contém o CMP EAX, 1 (no endereço 0x00401054) e pressione “Espaço”. A seguinte janela se abrirá:

Na caixa de texto é onde você entra com a instrução em assembly que deseja substituir. Caso a instrução que nós inserirmos seja menor (em bytes) que a instrução anterior, o Olly substitui esses bytes restantes pela instrução NOP, que não realiza nenhuma operação, evitando assim que fiquem resquícios e “lixos” do comando anterior (caso a caixa de seleção “Fill with NOP’s” esteja selecionada, claro).

Altere o CMP EAX, 1 por JMP 00401063, como mostra a figura:

Em seguida, basta clicar em Assemble para confirmar a modificação. Você deve ter notado que o Olly coloriu com vermelho aquilo que foi modificado. Note também que ele inseriu um comando NOP após o salto, indicando que o nosso opcode do salto era 1 byte menor que o comando anterior.

Você pode rodar o nosso aplicativo dentro do Olly e observar a modificação. Agora, com qualquer valor que você entre (mesmo aqueles fora do intervalo), o programa vai exibir a mensagem que desejávamos.

Como mencionado anteriormente, todas as modificações ficam armazenadas na janela de patches, que pode ser acessada clicando no botão \ ou através o atalho CTRL+P. Para alternar entre a instrução modificada e a original, basta selecionar a modificação desejada na janela de patches e apertar “Espaço” (ou através do botão direito -> “Restore original code”).

Para salvar o novo executável é bem simples. Na janela de dissassembly, clique com o botão direito e vá para “Copy to Executable -> All modifications”. Uma pequena janela se abrirá perguntando se você deseja copiar o código modificado. Selecione “Copy All”. Uma nova janela, contendo todo o código modificado, será exibida. Clique com o botão direito sobre ela e selecione “Save File”. Basta escolher o local e você terá um novo executável, contendo a modificação realizada.

6. Plugins

IDAFicator

Autor: AT4RE
Versão: 1.2.12
Download: http://www.at4re.com/tools/Releases/Zool@nder/IDAFicator/IDAFicator_1.2.12.zip

Um dos melhores plugins já criados para o Olly. Ele traz para o OllyDbg muitas das facilidades e funcionalidades disponíveis no IDA (Interactive Disassembler)

OllyDump

Autor: Gigapede
Versão: 2.21
Download: http://www.openrce.org/downloads/download_file/108

Plugin extremamente útil quando está se trabalhando com descompressão de executáveis, em que é necessário realizar um dump do processo na memória (buscar os dados da memória e jogar para o disco, simplificadamente). Funciona muito bem juntamente com o ImpRec (Import Reconstructor), que re-alinha todo o executável novamente.

OllyScript

Autor: SHaG
Versão: 0.94
Download: http://www.openrce.org/downloads/download_file/106

Indispensável. Com esse plugin você pode rodar scripts dentro do debugger, automatizando diversos processos e lhe salvando tempo. É fácil encontrar sites contendo centenas de scripts, mas recomendo este aqui: http://www.tuts4you.com/download.php?list.53

OllyPerl

Autor: Joe Stewart
Versão: 0.1
Download: http://www.openrce.org/downloads/download_file/220

Outra forma de automatizar o Olly, escrevendo scripts na linguagem Perl. Para utilizar o plugin é preciso ter o ActivePerl instalado.

CommandBar

Autor: Gigapede
Versão: 3.00.108
Download: http://www.openrce.org/downloads/download_file/105

Quem já utilizou o bom e velho (e infelizmente extinto =() SoftICE certamente vai gostar desse plugin. Trata-se de uma caixa de texto na qual você pode praticamente controlar o Olly através de comandos que seguem a mesma sintaxe utilizada no SoftICE (bpx, bd, cpu, etc).

Olly ToolBar Manager

Autor: arjuns
Versão: 0.3
Download: http://tuts4you.com/request.php?84

Com esse plugin você pode adicionar itens na barra de ferramentas do OllyDbg. Muito bom para colocar atalhos para Notepad, Calculadora, editor Hexadecimal, editor de recursos. Dessa forma você não precisa ficar perdendo tempo e navegar até a pasta (ou menu iniciar) onde estão os aplicativos. Recomendo muito.

OllyFlow

Autor: henryouly
Versão: 0.1
Download: http://www.openrce.org/downloads/download_file/178

Talvez um dos plugins mais interessantes. O OllyFlow gera gráficos e fluxogramas do disassembly para facilitar a identificação e a interpretação do fluxo do aplicativo (funcionalidade presente no IDA).

Por enquanto são esses. Mais para frente eu adiciono eventuais plugins e deixo um aviso aqui neste tópico, notificando da atualização.

Vou aproveitar esse tópico para colocar o meu scheme de cores que criei e utilizo no Olly. Para utilizar, modifique o arquivo ollydbg.ini adicionando as seguintes entradas (verifique pela seção correta e altere o asterisco para o índice desejado):

[Colours]
Scheme
[*]=0,12,8,18,7,8,7,13
Scheme name
[*]=Fergo

[Syntax]
Commands
[*]=15,4,10,10,9,10,112,13,111,8,12,0,0,0
Operands
[*]=1,11,11,11,2,2,4,4,0,0,0,0,0,0
Scheme name
[*]=Fergo