Logo Hardware.com.br
Fergo
Fergo Highlander Registrado
9.3K Mensagens 1.1K Curtidas

Utilizando um debugger - OllyDbg

#1 Por Fergo 23/09/2007 - 02:03
Boa noite pessoal!
Ultimamente tenho visto uma certa quantidade de posts relativos a programação em linguagem assembly e ao uso de debuggers (seja para depuração ou para fazer adaptações). Resolvi criar uma pequena série de tutoriais (comecei hoje) cobrindo alguns conceitos interessantes sobre a utilização de debuggers e um pouco de engenharia reversa. A medida que eu vou escrevendo eu vou postando aqui, em forma de post mesmo.

Por enquanto escrevi apenas uma introdução e o esclarecimento de alguns conceitos. Estou pensando ainda no aplicativo que vou programar para usar como exemplo de análise, mas provavelmente eu faça apenas aquele programa clássico de "adivinhe o número" (escolha um número de 1 a 10...). Pretendo utilizar o OllyDbg nos tutoriais, pois é o meu debugger predileto e o que estou mais familiarizado (gratuito e pequeno).

Sumário:[LIST=1]
Introdução e conceitos
Um pouco de assembly
Interface do Olly Debugger
Formas de aproximação
Re-assembly
Plugins[/LIST]Mais capítulos em breve!

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

QUAL A UTILIDADE 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.
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.

Parte 2 em breve. Provavelmente com imagens já.


Abraços,
Fergo
Responder
Fergo
Fergo Highlander Registrado
9.3K Mensagens 1.1K Curtidas
#2 Por Fergo
23/09/2007 - 21:41
Continuação...

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:

[code=rich]MOV EAX,1[/code] 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:

[code=rich]B801000000[/code] 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:

[code=rich]
void main() {
int a = 4;
int b = 6;
int c;

if((a == 4) && (b == 6)) {
c = 5;
}
}[/code]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):

[code=rich]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
[/code] 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):

Imagem


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.

Imagem


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:

[code=rich]00401000 MOV BYTE PTR DS:[00403000], 09h[/code] 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.

Comentários/críticas/sugestões são bem vindas, claro (não me importo que as partes não fiquem em sequência).

Abraços!
Fergo
Site pessoal www.fergonez.net
Portfolio
www.fbirck.com
Artigos
Informática
Fergo
Fergo Highlander Registrado
9.3K Mensagens 1.1K Curtidas
#5 Por Fergo
25/09/2007 - 20:57
Valeu pelo suporte pessoal! Aí vai mais um pedaço smile.png

INTERFACE DO OLLY DEBUGGER


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:

Imagem


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:

Imagem


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.

No próximo capítulo pretendo colocar alguns conceitos importantes de depuração.

Abraços,
Fergo
Site pessoal www.fergonez.net
Portfolio
www.fbirck.com
Artigos
Informática
Fergo
Fergo Highlander Registrado
9.3K Mensagens 1.1K Curtidas
#13 Por Fergo
28/09/2007 - 18:57
Valeu galera!
Quarta parte saindo do forninho. Desta vez fala de como encontrar aquilo que você quer no meio de tanta instrução.

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:

Imagem



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:

Imagem


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):

Imagem


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:

Imagem


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:
[LIST=1]
Adquire os dados digitados pelo usuário
Compara com o valor real
É igual?
Caso não seja igual, pule para ...
Caso seja igual continue/pule para outro local[/LIST]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):

Imagem


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:

Imagem


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:[LIST=1]
Compare EAX com 1
Se for menor, pule para 0040108F
Compare EAX com 14 (os números são em hex, logo 14h = 20 decimal)
Se for maior, pule para 0040108F
Compare EAX com 4
Se forem diferentes (JNZ/JNE), pule para 00401079[/LIST]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.

Imagem


Eis a lista de funções utilizadas:

Imagem


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.
Em breve a continuação!
Abraços,
Fergo
Site pessoal www.fergonez.net
Portfolio
www.fbirck.com
Artigos
Informática
Responder Tópico
© 1999-2024 Hardware.com.br. Todos os direitos reservados.
Imagem do Modal