0% acharam este documento útil (0 voto)
6 visualizações7 páginas

Disassembler e Debugger: Conceitos Essenciais

Disassemblers convertem linguagem de máquina em assembly, enquanto debuggers analisam e depuram aplicações, permitindo a identificação de erros e engenharia reversa. Ferramentas como OllyDbg são populares por suas funcionalidades de depuração e disassembly. O conhecimento sobre conceitos como processador, memória e assembly é essencial para entender o funcionamento de debuggers e disassemblers.

Enviado por

marcilio lisboa
Direitos autorais
© All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato DOC, PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
6 visualizações7 páginas

Disassembler e Debugger: Conceitos Essenciais

Disassemblers convertem linguagem de máquina em assembly, enquanto debuggers analisam e depuram aplicações, permitindo a identificação de erros e engenharia reversa. Ferramentas como OllyDbg são populares por suas funcionalidades de depuração e disassembly. O conhecimento sobre conceitos como processador, memória e assembly é essencial para entender o funcionamento de debuggers e disassemblers.

Enviado por

marcilio lisboa
Direitos autorais
© All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato DOC, PDF, TXT ou leia on-line no Scribd

Oque é Disassembler?

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.

Oque é são Debuggers?

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 vários de debuggers e disassemblers na Rede, mas os mais 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

Para Baixar o Programa OllyDbg (OllyDebugger) Acesse o site ou para mais rapidez baixe
a ultima versão no Attachments. OllyDbg v1.10

Mais pra que se ultiliza 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:
 Tratar um Erro: Uma das principais funções é utilizada para corrigir aquele erro que
na hora que você estava fazendo um arquivo/programa, e não viu um o erro em uma
unica linha. Na maioria dos casos, o melhor a fazer é utilizar um Debugger para
abrir o arquivo e procurar em código binário o erro que está ocorrendo o mal
funcionamento ou o não funcionamento. E nesse mesmo item existe também
corrigir erros de arquivos que não utilizam mais ou continuação dos mesmo, e para
nós da MPGH, a abertura de Dll's como [Link], no Combat Arms, para achar os
address.

 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 confundem Cracking com Engenharia Reversa, mas essas duas coisas são
totalmente diferentes entre si, cracking, é o processo de você abrir o programa,
alterá-lo, e fazer com que a licença do programa, não expire, ou alterar incluindo
outra função de uma versão mais recente, entre outros. Engenharia Reversa, por si já
é uma atividade legal, sem a mesma não seria possível a utilização de vários
programas do SO Windows, em Linux, engenharia reversa é o ato de fazer uma
analise de um programa e retirar estruturas de dados dali, para construir um novo
com a mesma finalidade, incluindo outros códigos, ou seja aperfeiçoando, sem
atingir sua licença.

 Aprendizado: Debuggers, são muito utilizados por ser uma das melhores formas de
aprender Assembly, ao criar um arquivo em codigo fonte, por exemplo C++, o
programador pode passar um Debugger nesse arquivo e estuda-lo em codigo
Binário. 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:
 Processador/CPU: Primeiramente devemos saber que CPU não é a mesma coisa que
Gabinete, CPU = Processador, esse erro é cometido por muitas pessoas, ele é 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.
ASSEMBLY

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.

Nesse tutorial, não colocarei como se utiliza, nem aprofundarei no assunto, mas em Breve
estarei adicionando recursos para você dominar essa linguagem aqui na MPGH.

Aqui vamos 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 assembler), esse comando é traduzido para um conjunto de números que
possa ser interpretado pelo processador como:

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

Nesse Quadro os sinais (-), significam espaços.

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 nessa atualização nós procuramos e adicionamos aqui
apenas aquelas que serão mais utilizadas. Para uma lista completa com uma explicação
mais profunda dos opcodes, recomendo ver a lista apresentada nestes endereço:

Você também pode gostar