Lesson 3


A ARTE DA ENGENHARIA REVERSA





Você sabe o que acontece quando um programa é instalado ou simplesmente executado? Como um programa verifica códigos de validação? Como alguém poderia passar por cima destas verificações? Como alguém poderia entender os detalhes mais obscuros de uma vulnerabilidade, de forma a conseguir explorá-la ou corrigi-la. Qual é o comportamento de um malware no sistema? As respostas para estas e muitas outras questões, você encontrará através da Engenharia Reversa.

A arte da Engenharia Reversa de Software, mais conhecida como Reversing, trata-se de descobrir o que está acontecendo com um programa sem ter em mãos o código fonte. Isto pode ser alcançado através de de uma análise dinâmica ou estática.

Para dominar o tópico, é imprescindível o conhecimento da programação de baixo nível arquitetura de computadores, sistemas operacionais, compiladores e linguagens assembly. Você também deve se familiarizar com ferramentas como debuggers, decompiladores, de monitoramento de sistema, e disassemblers.

Nesta lição nós apresentaremos apenas o básico da engenharia reversa, mas suficiente para você começar a resolver algumas questões.

Os comandos FILE e STRINGS

Um ótimo começo, antes até mesmo de executar a aplicação, é tentar criar algumas hipóteses de como o programa funciona.

Lembre-se:

Quando estiver analisando um malware ou algum exploit, faça em um ambiente controlado, como uma máquina virtual ou um computador que não seja crítico para você. Desta forma, se algo sair fora de controle, será mais fácil de recuperar!

Existem dois comandos que devem sempre ser usados: file e strings.

file é um comando que determina o tipo do arquivo. file executa uma sequência de de testes para tentar classficar o arquivo. O processo de engenharia reversa depende do tipo de arquivo e de qual arquitetura (32 bits ou 64 bits) ele foi construído. Entretanto, você deve saber que este comando nem sempre dará o resultado correto. file executa 3 tipos de testes na seguinte ordem: teste de filesystem, teste do magic number, e testes de linguagem. O primeiro teste que for feito com sucesso será a saída impressa pelo comando, e estas informações podem ser falsificadas, principalmente o magic number.

Não confie em tudo cegamente.

strings imprime as strings de caracteres imprimíveis contidos no binário do arquivo, e algumas vezes, os binários contém informações sensíveis, ou algo que lhe ajudará no processo de engenharia reversa, como nomes de bibliotecas sendo usadas e textos do programa. Vale a pena utilizar este comando e analisar com atenção os resultados.

Antes de seguirmos em frente, teste executar os comandos que acabamos de mencionar no programa disponibilizado no desafio a seguir. O que você consegue dizer sobre o arquivo sem executá-lo? Ele é 32 bits ou 64? Existe alguma string sensível?



Após executar o comando file, podemos ver que se trata de um executável do tipo ELF, e compilado para 64 bits.


Você pode ver também muito mais informações que serão úteis durante a engenharia reversa mas nós não entraremos em detalhe agora.


Como mencionamos,
strings vai mostrar todo os caracteres que podem ser exibidos no binário. Ao ver esses textos, deve te dar uma ideia do que o binário foi feito. Você pode ver mensagens como "Usuário Correto", "Senha Correta" e também "Tente Novamente".

Então, quando você digita o usuário ou senha, o programa provavelmente vai verificar o que você digitou e então retornar uma dessas mensagens. O usuário e a senha estão embutidos no código, o que significa que o usuário e senha corretos já se encontram o binário. Você pode encontrá-los apenas olhando para o resultado do comando strings? Caso não, não se preocupe, daqui a pouco nós iremos ajudar você a encontrar.

Linguagem Assembly

Uma vez que você sabe algo sobre o arquivo, é hora de mergulhar no executável e descobrir o que realmente faz. Disassemblers e debuggers vão ser seus melhores amigos nessa etapa. Mas para entender o que essas ferramentas estão dizendo, você primeiro deve aprender a linguagem deles.

Nós costumamos dizer que a Linguagem Assembly é a linguagem da Engenharia Reversa e o motivo é porque quando se está trabalhando em projetos de engenharia reversa, a maioria das vezes será a única coisa que você terá que trabalhar. Primeiro, você deve lembrar que Assembly não é o nome de uma única linguagem mas o nome de um conjunto de linguagens. Cada arquitetura diferente possui uma linguagem Assembly ligeiramente diferente.

Quando você escreve um software, o processo de compilar irá transformar o código fonte que você escreveu nessa linguagem de baixo nível que pode ser entendida pelos computadores. Linguagens como C/C++ são diretamente traduzidas para o Assembly, enquanto linguagens como Java e C# são traduzidas para outras linguagens: Java Bytecode no caso do Java e Microsoft Intermediate Language (MSIL) no caso do C#. O que vai ser interpretado por suas respectivas máquinas virtuais.

A ideia por trás da máquina virtual é fazer com que o código seja compatível com qualquer plataforma, já que o código será compilado para o código da máquina virtual e apenas interpretado pela máquina virtual para o computador que estiver.

A Engenharia Reversa nessas duas últimas linguagens são completamente diferentes do C/C++. Porque estas linguagens que usam máquina virtual contém muitos detalhes do programa, podem ser decompiladas em um alto nível de precisão, tornando mais fácil de ler o código fonte original.

Nesta lição nós iremos focar em uma linguagem assembly chamada MIPS e IA-64. Essa linguagem é comumente utilizada em sistemas embarcados como roteadores, videogames como Nintendo 64, Sony Playstation, entre outros.

Para entender até o mais básico da linguagem assembly, você precisa primeiro entender o que são registradores. O conjunto de registradores de cada linguagem assembly pode ser diferente, mas os conceitos são essencialmente os mesmos. Os registradores são usados em conjunto com as instruções que nós iremos ver mais tarde.

Alguns dos Registradores de CPU comuns são descritos abaixo:

  • EIP: Instruction Pointer - Guarda o endereço de memória da próxima instrução a ser executada. O CPU olha para ele quando precisa saber o que fazer a seguir. Você irá ver bastante esse registrador.

  • ESP: Stack Pointer - Responsável por guardar a referência para o topo da pilha. Em outras palavras, o último item na pilha.

  • EBP: Base Pointer - É utilizado para "caminhar" pela pilha.

  • EAX: Também chamado de "Acumulador", é normalmente utilizado para operações aritiméticas e para guardar resultados.

  • EBX: É o registrador "Base", normalmente utilizado para armazenar dados em geral e endereços de memória.

  • ECX: É o registrador utilizado para "Contagem". O nome fala por si só. É utilizado como um contador, principalmente para controlar loops/iterações.

  • EDX: Este é o registrador de dados e é utilizado para armazenar o endereço de uma variável na memória.

  • ESI/EDI: Respectivamente "Índice de Origem" and "Índice de Destino" são menos utilizados do que os registradores anteriores. Normalmente são usados para transferir dados, com o ESI armazenando o endereço de origem de uma variável e o EDI armazenando o endereço de destino. Eles não podem ser acessados a nível de byte.

Agora vamos ver um exemplod e uso destes registradores no MIPS.

O add é uma instrução aritmética responsável por adicionar dois números e salvá-los no registrador. Por exemplo:

Código C:

A = B + C;

Código MIPS:

add $s0, $s1, $s2

Adiciona conteúdo de $s1 e $s2, armazenando o resultado em $s0.

Também existem instruções de comparação como slt e instruções de salto/jump como j.

Instruções de comparação, comparam dois valores e colocam o resultado em um registrador. slt representa é menor que. Por exemplo:

slt $t0, $t1, $t2

O primeiro registrador $t0 é o destino, em outras palavras, é o registrador no qual o resultado será armazenado. O segundo e terceiro registradores são comparados com menor que:

$t1 < $t2

O resultado vai ser 1 se a comparação for verdade e 0 se a comparação for falsa.

A maioria das linguagens de programação modernas são capazes de lidar com declarações condicionais como a estrutura se-então-senão e ações repetidas como loops/iterações. As instruções de máquina do processador não possuem essas estruturas, e nem a linguagem assembly. Ao invés disso, elas possuem suporte para saltos condicionais and inconditionais, que são basicamente declarações goto para controlar o fluxo de um programa.

Lembre-se:

Pulos incondicionais sempre ocorrem. Não existem condições a serem verificadas.

No MIPS, um pulo condicional é chamado de branch. Nós temos diversas instruções jump no MIPS, como um exemplo, nós temos j.

j significa simplesmente: pule para um endereço.

mas nós também temos jr, que significa: pule para um endereço contido em um registrador.

j alvo # pulo incodicional para o alvo/rótulo do programa
jr $t3 # pule para o endereço contido em $t3

Siga estes links se você quiser mergulhar na linguagem MIPS e aprender mais instruções:
Após você aprender um pouco mais, veja se você é capaz de entender o que está acontecendo nesse programa:



Ferramentas

Disassemblers pegam os executaveis como entrada e devolvem o código assembly equivalente para você. Aqui você irá testar o seu conhecimento em linguagens assembly. Com o código em suas mãos, você pode aprender sobre o funcionamento interno da aplicação mas algumas partes podem não ser analisadas apenas por usar um disassembler ou o código estático que retorna. Estas são as partes dinâmicas do código, aquelas que realmente existem durante o tempo de execução. É aí que outra ferramenta toma lugar, o: debugger.

Ao executar a aplicação em um debugger, você será capaz de capturar o seu fluxo de execução, identificar areas interessantes e criar alguns pontos de pausa (breakpoints) com o objetivo de parar a execução da aplicação e analisar o estado no momento daquela pausa. Muitos debuggers dão a capacidade de mudar os dados utilizados pela aplicação. Muitos debuggers também vêm juntos com disassemblers. Dependendo do propósito no qual você está realizando a engenharia reversa, você pode inclusive mudar o seu código em tempo real.
  • GDB: GDB, o GNU Project debugger, permite que você veja o que está "dentro" do programa enquanto o executa -- ou o que o programa está fazendo no momento que deu erro.
    https://www.gnu.org/software/gdb/

  • OllyDBG: O OllyDbg é um debugger 32-bit de nível assembler para Microsoft® Windows®. Com ênfase na análise do código binário, torna-se particulamente útil quando o código fonte está indisponível.
    http://ollydbg.de/

  • Immunity Debugger: O Immunity Debugger é uma nova forma poderosa para escrever exploits, analisar malwares, e realizar a engenharia reversa em arquivos binários. É construído em uma sólida interface gráfica, a primeira ferramenta de análise heap da industria construída especificamente para criaçao de heap, e uma API Python grande e bem suportada para fácil extensibilidade.
    https://www.immunityinc.com/products/debugger/

  • IDA: O IDA Disassembler e Debugger é um disassembler interativo, programável, extensível, multiprocessador, capaz de rodar em Windows, Linux ou Mac OS X. O IDA se tornou o padrão para análise de códigos maliciosos, pesquisa de vulnerabilidade e validação de softwares comerciais.
    https://www.hex-rays.com/products/ida/

  • ObjDump: Do manual “objdump exibe informações sobre um ou mais arquivos. As opções controlam qual informação em particular deve ser mostrada. Essa informação é em grande parte útil para programadores que estão trabalhando nas ferramentas de compilação, opondo-se a programadores que apenas querem que seu programa compile e funcione.”

  • Radare2: O projeto Radare iniciou como uma ferramenta forense, um editor hexadecimal capaz de abrir arquivos de disco. Mais tarde, passou a suportar análise de binários, disassemble de código, debug de programas e etc..
    https://github.com/radare/radare2

Praticando com o GDB

Vamos tentar executar nosso programa com o GDB e ver o que nós temos.

root@kali:~# gdb ./rev001.bin

Primeiramente, você pode tentar apenas executar o binário seguindo o fluxo normal sem nenhuma interação. Para fazer isso, apenas digite r ou run.


Ok. Agora o que nós estamos tentando fazer é descobrir o que seria o nome de usuário correto.
Um bom ponto de partida seria ler o código assembly desse binário. Para fazer isso, dentro do GDB digite disassemble main.


Ao olhar para o código, nós conseguimos identificar em (1) o primeiro retorno do programa, que deve ser a mensagem de boas vindas. Então, nós temos outro retorno em (2), que é a mensagem de usuário. Em (3) você identifica o método ‘scanf’, que irá ler o que você digitar. Agora chegamos na parte interessante. Você já digitou o usuário e o programa deve decidir se você digitou corretamente. No endereço 0x000000000040069c é chamado o método strcmp. E antes de chamar o método, deve ser definido os 2 parâmetros. Esses parâmetros são o texto que você digitou e o usuário a ser comparado com esse texto. Um é copiado para o registrador %rdx, e o outro para o registrador %rax. Vamos ver se nós conseguimos ler o que são estes registradores durante a execução.

Primeiramente, eu irei definir um ponto de pausa no endereço 0x000000000040069c, isso é logo antes de chamar o método para comparar os dois textos e eles já estão definidos para os seus respectivos registradores. Um ponto de pausa (breakpoint) significa que a execução alcançou esse endereço de memória e irá parar e aguardar pelo meu próximo comando.

Para definir o ponto de pausa in GDB, use o comando b *addr. Defina o ponto de pausa e execute o programa.


Quando você atingir o ponto de pausa, uma mensagem será exibida e o GDB irá aguardar pela sua próxima interação. Tudo o que nós precisamos fazer agora é ler os valores nesses registradores.


Agora que nós sabemos o conteúdo do $rax, “user”, é o que nós digitamos quando fomos perguntados pelo usuário. Então, o conteúdo do $rdx, é elliot, que deve ser o usuário que o programa estava esperando.

Vamos tentar. Digite r para executar o programa novamente, digite o usuário que você encontrou no $rdx e quando você atingir o ponto de pausa, apenas digite c ou continue.


Bom trabalho! Você pegou o nome correto do usuário. Você pode prosseguir e realizar o mesmo para encontrar a senha? Facá esse exercício mesmo se você já tenha encontrado através do comando strings anteriormente.

Desafio:


Leitura Adicional:

Share: