Escolar Documentos
Profissional Documentos
Cultura Documentos
Apresentação
Nesta terceira aula, após aprendermos a realizar uma análise básica no artefato, tanto estática quanto dinamicamente,
abordaremos a tão temida linguagem de máquina – Assembly.
Para isso, revisaremos alguns tópicos necessários para o entendimento da arquitetura Intel x86 e o funcionamento padrão
de um programa.
Objetivo
Identificar características da arquitetura Intel x86;
Arquitetura de computadores
Para começar, é importante sabermos que:
compare_arrows
possível identificar as funções que são apresentam algumas deficiências. Por
importadas, mas ainda não é possível meiodelas é possível entender o que um
determinar como elas serão usadas ou se artefato precisa e como age ou responde
serão usadas em algum momento. ao receber um determinado pacote ou
arquivo ao requisitá-lo de um determinado
domínio.
Entretanto, só será possível entender o formato e como toda a engrenagem funciona com aprofundamento no fluxo da
aplicação. É nesse ponto que entraremos nas ferramentas de desmontagem (disassembly).
Comentário
Num primeiro momento, a desmontagem pode parecer algo sem sentido até mesmo para programadores experientes. Mas não
desanime, nesta aula cobriremos algumas bases necessárias ao entendimento dos artefatos, dos comuns aos mais complexos.
Como esse curso não é de engenharia reversa, não conseguiremos exaurir por completo este campo de estudo, mas daremos os
primeiros passos.
Níveis de abstração
Dentro da arquitetura de computadores tradicionais há vários níveis de abstrações, que existem para facilitar os detalhes de
implementação dos programas e sistemas.
Para entender mais facilmente basta imaginarmos que um sistema operacional como o Windows, por exemplo, pode ser
executado em diversos tipos de hardware sem que haja qualquer tipo de ajuste.
Isso só é possível porque os componentes físicos não são levados em consideração, ou abstraídos, pelo sistema operacional.
A Figura 1 mostra três níveis de codificação que serão abarcados em uma análise de um binário.
Então:
O malware será criado em uma linguagem de
alto nível, como a linguagem C, por exemplo.
Partiremos desse ponto em nossos exemplos e, é desse ponto também que os analistas de malware trabalham e analisam os
binários.
A ferramenta de desmontagem (disassembly) transforma o código de máquina em uma linguagem de baixo nível, também
conhecida como Assembly.
Hardware
Nível físico, consiste em circuitos elétricos que implementam combinações complexas de operações lógicas e não são
facilmente manipulados pelo Software.
Micro-código
Também conhecido como firmware, opera apenas no circuito exato para o qual foi projetado. Contém microinstruções
que fornecem uma interface de tradução do código de máquina para o hardware. Não será nosso foco, pois é geralmente
específico para cada hardware.
Código de máquina
Consiste basicamente em opcodes (muito importante), que são dígitos hexadecimais que dizem ao processador qual
instrução deveser executada. Normalmente implementado com várias instruções de microcódigo para que o hardware
subjacente possa executar o código, sendo criado quando uma linguagem de alto nível é compilada.
Linguagens de Baixo Nível
Versão legível por humanos do conjunto de instruções de uma arquitetura de computador. Sua versão mais comum é a
linguagem Assembly. Operaremos nesse nível porque o código de máquina é muito difícil para um ser humano
compreender.
A maioria dos programas é criada em linguagens de alto nível, pois elas fornecem grande abstração no nível da máquina e
facilitam o uso de lógica de programação e dos mecanismos de controle de fluxo. Possuem como exemplos as
linguagem C, C++, dentre outras. Essas linguagens são normalmente transformadas em código de máquina por um
compilador por meio de um processo conhecido como compilação.
Linguagens Interpretadas
Essas linguagens não são compiladas em código de máquina; em vez disso, são traduzidas em bytecode — uma
representação intermediária específica da linguagem de programação. O bytecode é executado dentro de um
interpretador, que é um programa que traduz o bytecode em código de máquina executável durante a execução. São
exemplos de linguagens interpretadas: C#, Perl, .NET e Java.
Unidade de processamento central Memória principal do sistema Sistema de entrada/saída que faz
(CPU) que executa o código. (RAM) que armazena todos os interface com dispositivos como
dados e códigos. discos rígidos, teclados e
monitores.
Unidade de controle
Registradores
1. Dados
2. Código
3. Heap
4. Pilha
Código
Área reservada para as instruções buscadas pela CPU para executar as tarefas do programa. Essa área possui as rotinas
que definem o que o programa faz e como as tarefas do programa serão orquestradas.
Heap
Porção da memória utilizada para alocações dinâmicas durante a execução do programa para criar (alocar) novos valores e
eliminar (liberar) outros de que o programa não precisa mais. O heap é conhecido como memória dinâmica porque seu
conteúdo pode ser alterado com frequência durante a execução do programa.
Pilha
Área utilizada para variáveis locais e parâmetros para funções, além de auxiliar no controle do fluxo do programa.
Fonte: O autor.
No exemplo a seguir, movemos o valor de 42 em hexadecimal para o registrador ecx, ou seja, o valor de eax passará a ser 0x42
(em hexadecimal):
movecx, 0x42
Cada instrução corresponde a opcodes (códigos de operação) que informam à CPU qual operação o programa deseja executar.
Uma ferramenta de disassemble traduz opcodes em instruções legíveis para humanos. No exemplo anterior, a instrução
movecx, 0x42 possui os seguintes opcodes: B9 42 00 00 00.
Isso acontece porque a arquitetura x86 usa o formato little-endian na ordem dos bytes. O endianness dos dados descreve se o
byte mais significativo (big-endian) ou menos significativo (little-endian) é ordenado primeiro (no menor endereço) dentro de um
item de dados maior.
Comentário
Vamos simplificar. Se estivermos em um ambiente little-endian e quisermos mover 0x12345678 para o eax, os opcodes deverão
ser: B9 78 56 34 12; se estivermos em um ambiente big-endian: B9 12 34 56 78.
Mudar entre ordem de bytes é algo que o malware deve fazer durante a comunicação de rede, pois os dados de rede usam big-
endian e um programa x86 usa little-endian.
Portanto, o endereço IP 127.0.0.1, em hexadecimal 127 – 0x7f; 0 – 0x00; 0 – 0x00; e 1 – 0x01, será representado como
0x7F000001 no formato bigendian (pela rede) e 0x0100007F no formato little-endian (localmente na memória).
Atenção
Como analista de malware, você deve estar ciente da ordem de bytes para garantir que não inverta acidentalmente a ordem de
importantes indicadores, como um endereço IP.
Os operandos são usados para identificar os dados usados por uma instrução. Existem três tipos de dados que podem ser
usados:
1 2
Imediatos operandos de valor fixo, como no exemplo (0x42); Registradore operandos são registradores, como no
exemplo (ecx).
Bases numéricas
É importante lembrarmos:
Não entendeu? Quanto é 9 + 1? Não existe uma resposta com o uso de apenas um dígito, então reiniciamos a contagem e
adicionamos uma unidade na frente: 10. Isso é simples. Não é à toa que estudamos esse princípio desde a educação básica.
No entanto, vamos quebrar alguns paradigmas. Em um sistema binário existem apenas 0 e 1. Quanto é 1 + 1? Para responder
precisamos nos lembrar de que a resposta não pode ser representada por um único dígito (no sistema binário só existe 0 e 1).
Então, como no decimal, vamos reiniciar a contagem e adicionar uma unidade na frente: 10. Estranho, não? Em binário: 1+1=10;
em decimal: 1+1=2.
Agora vamos expandir esse conceito para um outro sistema, o hexadecimal. Como você pode imaginar, para representar um
valor de 32 bits, na arquitetura x86, são necessários 32 dígitos (0s e1s); para facilitar a sua representação, o hexadecimal é
utilizado.
Comentário
A diferença é que existem algumas opções a mais de dígitos: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E e F.
0 0 0
1 1 1
10 2 2
11 3 3
100 4 4
101 5 5
110 6 6
111 7 7
1000 8 8
1001 9 9
1010 10 A
1011 11 B
1100 12 C
1101 13 D
1110 14 E
1111 15 F
Fonte: Autor.
Como pode ser visto na Tabela 1, o uso de números em hexadecimais economiza na razão 4:1 da quantidade de números
binários. Ou seja, para cada 4 binários é necessário apenas 1 hexadecimal para representar seu valor. É importante lembrarmos
que um byte são oito bits, portanto, são necessários 2 hexadecimais para representar o mesmo byte.
Registradores
O que é um registrador?
Um registrador nada mais é do que uma pequena quantidade de
armazenamento de dados disponíveis para a CPU, cujo conteúdo pode ser
acessado mais rapidamente do que em outro lugar.
Os processadores x86 têm uma coleção de registros disponíveis para uso como armazenamento temporário, e os mais
comuns se enquadram em quatro categorias (SIKORSKI,2012):
1 2
3 4
EBP (BP) FS
ESP (SP) GS
ESI(SI)
Fonte: Autor.
Numa arquitetura de x86, os registradores gerais possuem 32 bits, mas podem ser referenciados em 32 ou 16 bits. Para
acessar sua parte de 16 bits, basta retirar a letra “E” de seu mnemônico. Existem ainda quatro registradores (EAX, EBX, ECX e
EDX), com os quais é possível acessar 8 bits da parte superior e inferior de seu registrador de 16 bits.
Portanto, para estes registradores é possível acessar os 32 bits, 16 bits, 8 bits superiores e inferiores da porção de 16 bits,
conforme figura a seguir:
Tabela 3: Porções do Registrador EAX
EAX – 32 BITS
AX – 16 BITS
AH – 8 BITS AL – 8 BITS
Fonte: Autor.
Apesar de o nome sugerir o uso geral, por definição, algumas instruções utilizam registros específicos. As instruções de
multiplicação e divisão, por exemplo, sempre usarão os registradores EAX e EDX, mas isso não afetará o uso, nem o
entendimento.
Comentário
Existem algumas convenções usadas pelos compiladores que devem ser de conhecimento de um analista de malware, pois isso
permitirá que examine o código mais rapidamente. Um exemplo dessa convenção é que o EAX geralmente é usado para
armazenar o valor de retorno nas chamadas de função.
Portanto, ao verificar o uso do registrador EAX imediatamente após uma chamada de função, provavelmente verá a
manipulação do valor de retorno da função utilizada.
Flags (EFLAGS)
O registrador EFLAGS é um registrador que armazena os status das operações realizadas durante a execução do programa. Na
arquitetura x86, o tamanho do registrador é de 32 bits e cada bit é uma flag. Durante a execução, cada flag possui o valor (1) ou
(0) para controlar as operações da CPU ou indicar os resultados de alguma operação realizada pela CPU.
A flag zero recebe o valor de 1 quando o resultado de uma operação é igual a 0. Não confunda: Se o resultado de uma
operação for zero, a flag zero terá o valor 1. Para os outros casos, ela será zero.
A flag de carregamento recebe o valor de 1 quando o resultado de uma operação é muito grande ou muito pequeno para o
operando de destino. Por exemplo, um registrador possui o valor de 0xffffffff, ou seja, o valor máximo paraum registrador
de 32 bits. Érealizada uma operação de soma um nesse registrador, o valor do registrador passará a ser 0x00000000 e a
flag será ativada. Nos outros casos, ela será zero.
A flag de sinal recebe o valor de 1 quando o resultado de uma operação é negativo, e o valor de 0 quando o resultado é
positivo. Esse sinalizador também é definido quando o bit mais significativo é definido após uma operação aritmética.
Flag de trap – TF
A flag de trap é usada para depuração. O processador x86 executará apenas uma instrução por vez se esse sinalizador for
definido.
Registrador de instrução
Na arquitetura x86:
O EIP, também conhecido como ponteiro de instrução ou, ainda, contador de programa é um registrador que contém o
endereço de memória da próxima instrução a ser executada por um programa. O único propósito do EIP é dizer ao
01 processador o local da próxima instrução a ser executada.
Apesar de parecer um registrador simples, o EIP é fundamental para uma aplicação pois, quando corrompido, ou seja,
se ele apontar para um endereço de memória que não contém código de programa legítimo, a CPU não será capaz de
02 buscar código legítimo para executar, causando um erro e o fechamento do programa em questão.
Além disso, o controle do EIP significa um controle por parte do fluxo do programa. Por isso, os invasores tentam obter
03 o seu controle por meio de exploração de falhas da aplicação.
Instruções em Assembly
Instrução mov
Anteriormente apresentamos um exemplo simples utilizando mov, o comando mais comum que serve para copiar dados de
um lugar para outro.
Atenção
Apesar de o nome do comando ser mov, de “mover”, o comando não move o valor de um local para outro.
E qual é a diferença?
Vamos utilizar a sintaxe Intel de comandos assembly (a mais utilizada no mundo, a outra sintaxe é da AT&T). Portanto, o
formato do comando será mov destino, origem. Ou seja, “movemos” o valor do operando da direita para o operando da
esquerda.
Vejamos a seguir alguns exemplos para entender e fixar o conhecimento. Vale lembrarmos que, ao colocar qualquer operando
entre colchetes, acessaremos o valor da memória apontada por ele.
Copia o conteúdo de EAX Copia os 4 bytes que estão Copia os 4 bytes que estão localizados na
para o registrador ECX (sem na memória em 0x45678 memória na posição especificada pela equação
modificar EAX). para ebx. ebx+eax para o registrador eax.
Comentário
Este último exemplo só pode ser usado para calcular posições de memória, ou seja, se o colchete não fosse utilizado
receberíamos um erro por ser uma instrução inválida. Uma instrução semelhante ao mov é o lea, que significa carregar endereço
efetivo (loadeffectiveaddress). Seu formato de instrução é o mesmo do mov: lea destino, origem.
Instrução lea
A instrução lea é usada para escrever um endereço de memória no destino. Não confunda a instrução mov com a instrução lea.
Vejamos a diferença entre elas:
compare_arrows
Armazenará em EAX o valor de EBX + 8 em Carregará os dados do endereço de
EAX. memória especificado por EBX + 8 em
EAX.
Instruções aritméticas
Num programa, várias instruções aritméticas são usadas, como soma, subtração e operações lógicas.
Adição e subtração
O formato da adição e da subtração é o mesmo das instruções anteriores: add destino, origem e sub destino, origem. Com um
detalhe: O resultado da soma ou da subtração é armazenada no destino.
Atenção
Devemos lembrar que essas operações podem modificar as flags de zero ou de carregamento, se uma operação tiver como
resultado zero ou se ultrapassar o valor máximo de um registrador, respectivamente.
Outras instruções importantes são o inc e dec, que incrementam ou decrementam um registrador pelo valor de um.
Multiplicação e divisão
A multiplicação e a divisão atuam em um registrador predefinido, de modo que o comando é simplificado a uma instrução mais
o valor pelo qual o registrador será multiplicado ou dividido. Seu formato de instrução é: mulvalor e divvalor.
A atribuição do registrador no qual será armazenado o valor da multiplicação ou da divisão pode ocorrer muitas instruções
antes; portanto, pode ser necessário pesquisar em um programa para encontrá-la.
A instrução de multiplicação sempre utiliza o registrador EAX pelo valor passado como operando. O resultado é armazenado
como um valor de 64 bits em dois registradores: EDX e EAX. O EDX armazena os 32 bits mais significativos e o EAX armazena
os 32 bits menos significativos.
Exemplo
Vamos supor que o resultado de uma multiplicação seja 800.000.000.000 ou 0xBA43B74000. Como o valor é muito grande para
apenas um registrador, edx receberá o valor de 0xBA e eax receberá o valor de 0x43B74000. Já a divisão divide o valor
armazenado em edx e eax pelo operando valor, o resultado é armazenado em eax e o resto armazenado em edx.
Operadores lógicos
Outra operação muito utilizada são os operadores lógicos, como OR, AND e XOR. Sua forma de operação é semelhante à soma
e à subtração. Eles executam a operação especificada entre os operandos de origem e destino e armazenam o resultado no
destino.
Preenche-se com bits 0 os valores deslocados durante a mudança. Parece confuso mas é bem simples. Vejamos um exemplo:
Exemplo
Se tivermos o valor binário 1000 e deslocá-lo para a direita em 1, o resultado será 0100. Se tivermos o valor binário 1000 e
deslocá-lo para a esquerda em 1, o resultado será 10000.
Atividade
1. Seja o valor do registrador EAX: 0x01020304. Qual o valor de AH?
a) 0x0102
b) 0x0304
c) 0x03
d) 0x04
e) 0x02
mov eax,0x01020304
add eax,0x02030405
leaebx,[eax+8]
a) 0x01020304
b) 0x03050709
c) 0x0305070A
d) 0x03050710
e) 0x03050711
Referências
Notas
NLEY, C. The shell coder’s Handbook: discovering and exploring security holes. 2.ed. [S.1.]: WileyPublishing, Inc., 2007.
SIKORSKI, M.; HONIG, A. Practical Malware Analysis: The Hands-On Guide to Dissecting Malicious Software (1.ed.). No Starch
Press,2012.
TANENBAUM, A.S. Organização Estruturada de Computadores. 5.ed. São Paulo: Pearson Prentice Hall, 2010.
Próxima aula
Explore mais