Aprenda Engenharia Reversa

  • Página Inicial
  • Contato!
  • Tudo sobre Engenharia Reversa Parte 1!
  • Tudo sobre Engenharia Reversa Parte 2!
  • Tudo sobre Engenharia Reversa Parte 3!
  • Tudo sobre Engenharia Reversa Parte 4!
  • Tudo sobre Engenharia Reversa Parte 3

    Executáveis ELF - Símbolos, PLT e GOT

    Copie o repositório Git digitando git clone https://github.com/mentebinaria/engrev.git. Depois entre no diretório engrev e crc32, e compile o programa para 32 bits digitando gcc -m32 *c -no-pie -o keygenme.

    Digite no terminal hte keygenme, onde podemos ver os bytes e dados em geral do programa. Falamos dos headers anteriormente (no Windows costumamos chamar de segmentos, no Linux como sessões). Digite F6 para vermos as sessões do programa.

    O segmento do tipo phdr é onde está o program header (entry 0). Os mais importantes no momento é os segmentos do tipo load, que serão carregados (mapeados) na memória, e dentro deles.

    Podemos ver as sessões do Assembly do programa digitando F6 e section header.

    A sessão PLT é como se fosse o import table do PE, que é onde ficam as chamadas para funções externas (de bibliotecas).

    Vá na sessão .symtab, e depois na .dynsym (no F6, lá embaixo aparecerão), lá está as chamadas externas de funções, além de objetos e coisas de outros tipos. Algumas funções, como o printf e o puts, reconhecemos do C.

    Podemos olhar os códigos-fonte do programa em C. São os mesmos na sessão .symtab.

    Muitos das funções na .symtab são usados apenas para fins de debug, e são locais (mas tem alguns externos), enquanto na .dynsym só teremos as funções externas. Podemos digitar no terminal gdb -q ./keygenme e digitar break main para colocar um breakpoint na função main do programa.

    Se dermos no terminal o comando strip -s keygenme, ele suprimirá a função main, e não irá aparecer mais a sessão .symtab no programa. Nesse caso, podemos usar o comando do gdb de novo e tentar dar um break no main, que dará erro por não estar mais no programa.

    Compile o programa novamente, pois corrompemos o mesmo.

    Volte aos sections headers e vá no .plt, que é onde estão os endereços das funções externas. Uma biblioteca dinâmica é carregada no sistema ao executar o programa, o printf por exemplo tá na libc. Digite o comando nm keygenme | less para vermos os símbolos do binário, onde veremos todas as bibliotecas das quais pertencem cada função.

    Digite gdb -q ./keygenme e dentro do programa, digite disassemble main, onde vemos o código Assembly do main. Faça o mesmo com printf e com o endereço do mesmo.

    Entre no hte novamente, clique F6 e vá em elf/image, onde está o programa desassemblado. Digitando F8 podemos ver as funções em Assembly, incluindo o main. A sessão .got.plt é onde estará o endereço real da função printf.

    Linux Syscalls

    Syscalls no Linux são os equivalente as API do Windows, mas funcionam de forma parecida.

    Vamos usar o programa ltrace para nossas análises. Vamos supor esse programa em C:

    
    #include <stdio.h>
    
    int main(int argc, char *argv[]) {
        printf("O número é: %d\n", 255);
    
        return 0;
    }
    
    

    Dê um make no programa pra compilar ele.

    Quando um programa em C é executado, ele chama a função printf, que é nativa do C, presente em uma determinada biblioteca (podemos dar um ldd no programa pra ver elas), mas quem exibirá a mensagem mesmo é o kernel. Dê um ltrace ./hello para ver quais funções foram executadas.

    PS: O número 17 que aparecerá é o número de caracteres que o printf retorna. Para ver os retornos digite man 3 printf.

    Dê o comando ltrace ls para vermos as funções que o programa ls chama. Dê o comando ltrace -S ./hello para ver as syscalls que as funções em C chamam. Um exemplo é a SYS_write(), syscall chamada pelo printf.

    Resumindo, as syscalls são funções nativas do kernel.

    Vamos refazer o programa em C, chamando a função write ao invés do printf:

    
    #include <unistd.h>
    
    int main(int argc, char *argv[]) {
        char* msg = "Oi\n";
    
        write(STDOUT_FILENO, msg, 3);
    
        return 0;
    }
    
    

    Mesmo usando outra função (write()), ele chama a mesma syscall SYS_write(), até porque a linguagem permanece sendo a mesma (C).

    PS: Da mesma forma que acontece no Windows, ao usar uma linguagem como a Java (em qualquer sistema, Windows, Linux, Mac, BSD, etc.), por exemplo, a System.out.println() chama a printf do C que consequentemente chama a mesma syscall SYS_write.