Cursos de Informática Grátis www.megainforcursos.com

20 de maio de 2012

UM POUCO DE ASSEMBLY

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:

Código:
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:

Código:
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:

Código:
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):

Código:
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:

Código:
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/informatic...ncias/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!

0 comentários:

Postar um comentário

 
Design by Wordpress Theme | Bloggerized by Free Blogger Templates | coupon codes