Baixe o app para aproveitar ainda mais
Prévia do material em texto
Análise de Malwares Aula 3: Análise Estática Avançada e Codi�caçã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 Identi�car características da arquitetura Intel x86; Examinar as diferenças entre as bases numéricas; Identi�car 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 su�cientes 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 Por meio de uma análise estática básica é possível identi�car as funções que são importadas, mas ainda não é possível determinar como elas serão usadas ou se serão usadas em algum momento. Análise dinâmica As técnicas dinâmicas básicas também apresentam algumas de�ciências. Por meiodelas é possível entender o que um artefato precisa e como age ou responde 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 �uxo 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 signi�ca? 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 codi�cação que serão abarcados em uma análise de um binário. Fonte: Sikorski (2012). Figura 1 - Exemplo de níveis de código 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. 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. Hardware Também conhecido como �rmware, 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í�co para cada hardware. Micro-código 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. Código de máquina 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 Baixo 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 �uxo. 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 de Alto Nível 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í�ca 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. Linguagens Interpretadas 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 (CPU) que executa o código. Memória principal do sistema (RAM) que armazena todos os dados e códigos. Sistema de entrada/saída que faz interface com dispositivos como discos rígidos, teclados e monitores. Fonte: TANENBAUM (2010). Figura 2 - Arquitetura Von Neumann Conforme a Figura 2, é possível veri�car que a CPU contém vários componentes que serão muito importantes para nosso entendimento: Fonte: Adaptado de Freepik. 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. 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 de�nem 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 �uxo do programa. 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). Figura 3 - Layout básico de um programa na memória 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: Fonte: O autor. O mnemônico é uma palavra que identi�ca a instrução a ser executada e o operando identi�ca as informações usadas pela instrução, seja um registrador ou um dado. Parece confuso, mas vamos explicar melhor. No exemplo a seguir, movemos ovalor 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 signi�cativo (big-endian) ou menos signi�cativo (little-endian) é ordenado primeiro (no menor endereço) dentro de um item de dados maior. Comentário Vamos simpli�car. 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 identi�car os dados usados por uma instrução. Existem três tipos de dados que podem ser usados: 1 Imediatos operandos de valor �xo, como no exemplo (0x42); 2 Registradore operandos são registradores, como no exemplo (ecx). 3 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 Registradores gerais Usados pela CPU durante a execução. 2 Registradores Usados para rastrear seções de memória. 3 Flags de status Usados para armazenar dados para tomada de decisões. 4 Ponteiros de instrução 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 �gura 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 de�nição, algumas instruções utilizam registros especí�cos. 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 veri�car 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 �ag. Durante a execução, cada �ag 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 �ags: Clique nos botões para ver as informações. A �ag 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 �ag zero terá o valor 1. Para os outros casos, ela será zero. Flag zero (zero �ag – ZF) A �ag 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 �ag será ativada. Nos outros casos, ela será zero. Flag de carregamento (Carry�ag – CF) A �ag 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 é de�nido quando o bit mais signi�cativo é de�nido após uma operação aritmética. Flag de sinal (sign�ag – SF) A �ag de trap é usada para depuração. O processador x86 executará apenas uma instrução por vez se essesinalizador for de�nido. Flag de trap – TF Atenção! Aqui existe uma videoaula, acesso pelo conteúdo online Registrador de instrução Na arquitetura x86: 01 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 processador o local da próxima instrução a ser executada. 02 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 buscar código legítimo para executar, causando um erro e o fechamento do programa em questão. 03 Além disso, o controle do EIP signi�ca um controle por parte do �uxo do programa. Por isso, os invasores tentam obter 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 �xar 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 para o registrador ECX (sem modificar EAX). Copia os 4 bytes que estão na memória em 0x45678 para ebx. Copia os 4 bytes que estão localizados na memória na posição especificada pela equação 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 signi�ca 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] Armazenará em EAX o valor de EBX + 8 em EAX. moveax, [ebx + 8] Carregará os dados do endereço de memória especi�cado 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 modi�car as �ags 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 prede�nido, de modo que o comando é simpli�cado 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 signi�cativos e o EAX armazena os 32 bits menos signi�cativos. 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 especi�cada 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 especi�cado 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 NotasReferências 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. E l i Explore mais
Compartilhar