Você está na página 1de 20

Análise de Malwares

Aula 3: Análise Estática Avançada e Codificação – Parte I

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;

Examinar as diferenças entre as bases numéricas;

Identificar comandos na linguagem Assembly.

Arquitetura de computadores
Para começar, é importante sabermos que:

Os métodos básicos de análise estática e dinâmica de artefatos são


satisfatórios para a triagem inicial, mas não fornecem informações
suficientes para uma análise completa. Porém, estaremos apenas
arranhando a superfície se quisermos entender toda a dinâmica e extensão
dos danos.

Análise básica Análise dinâmica


Por meio de uma análise estática básica é As técnicas dinâmicas básicas também

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.

O que isso significa?

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.

Figura 1 - Exemplo de níveis de código

 Fonte: Sikorski (2012).

Então:
O malware será criado em uma linguagem de

alto nível, como a linguagem C, por exemplo.

Depois, utiliza-se um compilador que irá gerar



o código de máquina correspondente às
instruções criadas pelo autor e que será
executado pela CPU.

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.

Nesse sentido, o gerenciamento de vulnerabilidde envolve:

Clique nos botões para ver as informações.

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.

Linguagens de Alto Nível 

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.

Arquitetura Intel x86


Os componentes internos da maioria das arquiteturas de computador modernas (incluindo x86) seguem a arquitetura de Von
Neumann, conforme a Figura 2, e possuem três componentes de hardware:

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.

Figura 2 - Arquitetura Von Neumann

 Fonte: TANENBAUM (2010).


Conforme a Figura 2, é possível verificar que a CPU contém vários componentes que serão muito importantes para nosso
entendimento:

Unidade de controle

Obtém instruções a serem executadas da memória RAM usando um


registrador (chamado de ponteiro de instrução – EIP), que armazena o
endereço da instrução a ser executada.

Registradores

Unidades básicas de armazenamento de dados da CPU e que são usados para


economizar tempo, pois a CPU não precisará acessar a RAM.

Unidade Lógica Aritmética (ULA)

Responsável por executar a instrução obtida da RAM; armazena os resultados


em registradores ou na memória.
 Fonte: Adaptado de Freepik.

Figura 3 - Layout básico de um programa na memória

A memória principal (RAM) para um único programa pode


ser dividida em quatro seções principais:

1. Dados

2. Código

3. Heap

4. Pilha

 Fonte: SIKORSKI (2012).


Dados
Área da memória reservada para receber variáveis com valores estáticos que serão usados no decorrer do programa ou
com valores globais que estarão disponíveis para qualquer parte do programa.

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.

Linguagem de baixo nível – Assembly


Agora que revisamos a arquitetura e a forma como funciona um computador, passaremos a abordar a linguagem de baixo nível
assembly. Como em qualquer linguagem, o assembly possui diversas instruções, cada uma com seu propósito;cada instrução
possui um mnemônico e zero ou mais operandos.

Vejamos como isso funciona:

O mnemônico é uma palavra que identifica a instrução a


ser executada e o operando identifica as informações
usadas pela instrução, seja um registrador ou um dado.

 Fonte: O autor.

Parece confuso, mas vamos explicar melhor.

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.

O valor B9 corresponde a movecx; e

0x42000000 corresponde ao valor 0x42.

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

Endereço de memória operandos apontam para uma área


da memória que contenha algum valor de interesse. Pode
ser um valor ou registrador entre colchetes ([ecx] ou
[0x11223344]).

Atenção! Aqui existe uma videoaula, acesso pelo conteúdo online

Bases numéricas
É importante lembrarmos:

Um bit possui dois valores: 0 ou 1, ligado ou desligado. Os


computadores trabalham em modo binário, pois são circuitos
eletrônicos (complexos). Nossa matemática é baseada em números
decimais: 0, 1, 2, 3, 4, 5, 6, 7, 8 e 9. Ao chegar ao limite, adiciona-se
uma unidade na frente do número e reinicia a contagem.
 Fonte: Adi Goldstein / Unsplash.

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.

Tabela 1: Correlações entre bases

Binário Decimal Hexadecimal

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

Registradores gerais Registradores


Usados pela CPU durante a execução. Usados para rastrear seções de memória.

3 4

Flags de status Ponteiros de instrução


Usados para armazenar dados para tomada de decisões. Usados para rastrear a próxima instrução a ser executada.

A tabela a seguir demonstra os registradores separados por categoria:

Tabela 2: Relação de registradores

Gerais Segmento Status Ponteiros

EAX (AX, AH, AL) CS EFLAGS EIP

EBX (BX, BH, BL) SS

ECX (CX, CH, CL) DS

EDX (DX, DH, DL) ES

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.

Registradores de uso geral


Os registradores gerais normalmente armazenam dados ou endereços de memória e costumam ser usados de forma dinâmica
para realizar as tarefas no programa.

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.

Para nossa análise, vamos focar nas seguintes flags:

Clique nos botões para ver as informações.

Flag zero (zero flag – ZF) 

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.

Flag de carregamento (Carryflag – CF) 

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.

Flag de sinal (signflag – SF) 

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.

Atenção! Aqui existe uma videoaula, acesso pelo conteúdo online

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?

O ato de mover é retirar algo de um lugar e colocar em outro, ou seja, você


limpa a origem e move-a para o destino. Você verá que um programa ou o
sistema operacional não tem o costume de limpar áreas da memória após
sua utilização;basta torná-la um local usável e ignorar o seu conteúdo.

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.

movecx, eax movebx, [0x45678] moveax,[ebx+eax]

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:

leaeax, [ebx + 8] moveax, [ebx + 8]

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.

Ou seja, leaeax, [ebx + 8] equivale a moveax, ebx + 8 (se essa


instrução fosse válida).

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.

Destacamos que as operações lógicas são realizadas na forma bit a bit.

Instruções shre e shl


As instruções shre shl, usadas para deslocar registradores, também são muito utilizadas. O formato da instrução, tanto do shl
quanto do shr, é shl destino, valor. Basicamente, as instruções shr e shl deslocam os bits no destino para a direita ou para a
esquerda, respectivamente, pelo número de bits especificado no segundo operando.

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.

Atenção! Aqui existe uma videoaula, acesso pelo conteúdo online

Atividade
1. Seja o valor do registrador EAX: 0x01020304. Qual o valor de AH?

a) 0x0102
b) 0x0304
c) 0x03
d) 0x04
e) 0x02

2. Qual o valor das operações abaixo, respectivamente:

I. (binário) 101001 + 1110100 – resposta em hexa

II. (hexadecimal) 0xA0F21 – 0x9F134 – resposta em decimal

III. (decimal) 12+10 – resposta em binário

a) 157, 7723, 11010


b) 9D, 7661, 10110
c) A3, 6941, 11001
d) 157, 7661, 10110
e) 4F, 7523, 101010

3. Analise as instruções abaixo e responda ao que se segue:

mov eax,0x01020304
add eax,0x02030405
leaebx,[eax+8]

Qual o valor de ebx?

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

Ferramentas para análise de código assembly;

Instruções em C e seu código em assembly.

Explore mais

Você também pode gostar