Engenharia Reversa: Primeiro Contato - Parte 1
!Ryan Gozlyngg
Posted on March 17, 2024
Você vai praticar engenharia reversa pela primeira vez.
"Engenharia reversa", na área de T.I. refere-se à engenharia reversa de software, que, a grosso modo, é a prática de entender o funcionamento de um software alheio, "nos mínimos detalhes".
Esse tutorial é uma breve introdução ao uso do debugger x64dbg, que é pré-requisito para o próximo tutorial.
Esse tutorial foi escrito para os iniciantes, o intuito é lhe preparar para o próximo tutorial: Debugando um programa crackme; observe que um é complemento do outro.
Programas "crackme" são softwares com algum tipo de desafio, como, por exemplo, descobrir uma senha de acesso ao próprio crackme.
O maior objetivo disso tudo, é mostrar um pouco desse mundo para pessoas que têm interesse em "baixo nível", mas não sabem se "isso é para elas".
Quero lhe ajudar a ter o "primeiro gostinho" desse mundo...
Lista de Conteúdo
O que é um Debugger
Debugger é um software que serve para "debugar" e testar programas.
"Debugar" é o processo de procurar bugs.
Bugs são erros ou problemas em um software.
E "testar", aqui, se define como "fazer oque quisermos com o software".
O Debugger lhe fornece acesso ao programa compilado.
Em um debugger, você vai ter acesso aos seguintes recursos:
- Valores das suas variáveis em memória - Mapa de memória
- Suas linhas de código transformadas em instruções Assembly
- Módulos (dll's e lib's) usados
- Threads e Handles
Sobre a Nomenclatura "x64dbg"
Não se confunda: o nome do programa como um todo é x64dbg.
Ele possui dois APLICATIVOS que são chamados x64dbg e x32dbg.
Você sempre deve saber qual dos dois deve usar para debugar um programa em específico, dependendo da plataforma para a qual o programa foi compilado:
- o aplicativo chamado x64dbg serve para debugar programas de 64 bits.
- o aplicativo chamado x32dbg serve para debugar programas de 32 bits.
Caso você não saiba se o seu programa é de 32-bits ou se é de 64-bits, abra o link:
Como saber se um programa é 32 ou 64 bits no windows.
No tutorial a seguir, quando eu disser "x64dbg", eu estarei me referindo ao programa como um todo, e não ao aplicativo específico.
Antes de começar
-
Vou trabalhar apenas com Assembly de x86_x64:
Assembly não é uma linguagem de programação comum, como C, que você aprende e sai criando programas para "qualquer coisa": há diversos processadores, e cada um possui uma arquitetura, que vai dizer como as instruções são nomeadas e como vão funcionar, que dita o nome e funcionamento dos registradores, etc.
Mais sobre: ISA-Instruction Set Architecture Não é esperado nenhum conhecimento prévio sobre debuggers ou Assembly.
É esperado conhecimento básico em programação, e processo de compilação.
Conhecimento sobre fundamentos da computação lhe ajudarão a entender melhor o que se passa aqui. (Nota: Você pode usar um debugger para reforçar os estudos dos fundamentos da computação, já que, dessa forma, irá ver as coisas acontecendo na prática).
O básico sobre sistemas numéricos é esperado.
Espero lhe dar um overview de como é usar um debugger, mas tenha em mente que tópicos extremamente importantes estão sendo deixados de lado para não lhe "inundar" de informações no início. Entenda isso como um "Quick Overview".
Se estiver procurando obter os fundamentos da computação, confira os links deixados na parte final.
Baixando o x64dbg
- Vá ao site oficial: x64dbg.com
- Clique em Download
- Você será redirecionado ao site da Sourceforge. Clique em Download Latest Version
"Instalando" o x64dbg
- Extraia o arquivo zip. Você pode criar uma pasta em qualquer lugar para armazená-lo.
- Abra a pasta extraída
- Vá em: release/x32, ache o aplicativo x32dbg e crie um atalho na área de trabalho
- Vá em: release/x64, ache o aplicativo x64dbg e crie um atalho na área de trabalho
Abrindo o x64dbg
Para abrir qualquer programa no x64dbg:
- Abra o x64dbg
- Clique em Arquivo -> Abrir (ou clique em F3)
- Alternativa: Arraste o programa para um dos atalhos do x64dbg
- Para abrir um programa que está rodando, abra o debuger, clique em Arquivo -> Anexar/Attach e escolha o programa.
Mapeando as partes mais usadas da Interface Gráfica
Botões de controle do Debugger
Área com os botões de controle do Debugger. Clique em Debug (ao lado de Exibir) para ver todos os nomes e atalhos dos botões nessa seção.
- Ícones, da esquerda para a direita:
- Abrir Arquivo
- Recarregar programa
- Fechar programa que está carregado
- Rodar programa carregado
- Pausar programa carregado
- Step Into: anda, de instrução em instrução, e entra nas instruções
call
: instruçõescall
chamam outras funções. "Entrar nelas" quer dizer ir até a função. - Step Over: anda, de instrução em instrução, e NÃO entra nas instruções
call
Não vou comentar sobre os outros botões, para saber sobre eles:
https://help.x64dbg.com/en/latest/gui/views/Trace.html
https://help.x64dbg.com/en/latest/introduction/ConditionalTracing.html
Atalhos para Funcionalidades
Atalhos para algumas funcionalidades do x64dbg (não entrarei em detalhes).
Vou mencionar apenas o quinto, da esquerda para a direita, que é o ícone de favoritos.
Quando você quiser salvar alguma localização de instrução, use CTRL+D, que a instrução selecionada será salva nos favoritos. Clique no atalho mencionado para ser direcionado aos favoritos.
Janelas mais usadas
Nessa parte do x64dbg, você tem todas as janelas de funcionalidades do programa.
Só marquei na caixa amarela as que eu vou estar comentando. Para você começar a usar o programa, acredito que só precisa dessas janelas. Mas não deixe de aprender todas, caso queira prosseguir nos estudos.
Clicando em exibir (ao lado de Arquivo, canto superior esquerdo) você pode habilitar as janelas que não estiverem aparecendo, e também pode ver os atalhos para cada uma delas.
Na imagem acima, se encontra a janela CPU, que será comentada no final desta parte sobre janelas.
- Janela Breakpoints: breakpoints são pontos de parada, locais em que o programa vai parar obrigatoriamente. Logo falaremos mais sobre breakpoints. E nessa janela você tem acesso aos breakpoints que estão setados no programa todo. Aqui você pode manipular breakpoints, habilitando-os, desabilitando-os, ou mesmo removendo-os, e é até possível editá-los. Clicando em um deles, você será direcionado até o local em que ele está setado, na janela CPU.
- Janela de Símbolos (Modules): essa é uma janela de extrema importância para algumas atividades, já que ela nos mostra os módulos importados pelo programa, inclusive o módulo do próprio programa.
- Janela Referências: Aqui temos as referências, que procuramos da seguinte maneira: Dentro da janela CPU, clique com o botão direito do mouse, bem no final você vai encontrar "Pesquisar por" e "Encontrar referência aos": Os resultados das pesquisas feitas por essas opções serão mostrados na janela **Referências**.
- Janela Mapa de Memória: Como o nome diz, é um mapa da memória do programa todo. Você pode ver onde um determinado endereço se encontra da seguinte maneira:
- Dentro da janela CPU, selecione um endereço desejado (sobre endereços, comentarei mais a frente)
- Dentro da janela CPU, clique com o botão direito do mouse em cima do endereço desejado
- Clique em "Seguir no Mapa da Memória":
Você verá as informações a respeito do endereço selecionado no mapa de memória.
- Janela CPU: Nos mostra tudo o que estamos vendo na imagem "mapeada" acima, e por essa ser a janela principal, todos os comentários às marcações com cores a seguir, serão sobre ela.
Mapeando a janela CPU
Vamos passar a maior parte do tempo nessa janela, ela é a janela principal do x64dbg.
Coluna de Endereços
Na primeira coluna da grande área, nós temos os endereços de memória (endereços de memória virtual, Virtual Address - VA).
A marcação verde nos mostra qual a próxima instrução a ser executada. O destaque em dourado é uma instrução marcada como "favoritos" usando CTRL+D.
Para saber mais sobre endereços de memória: https://learn.microsoft.com/pt-br/windows/win32/memory/virtual-address-space
Coluna de Opcodes
Na segunda coluna da grande área, nós temos os Opcodes, que são códigos de operação, eles ditam uma operação a ser executada pelo processador (por isso do nome OPeration Code);
Aqui estão em hexadecimal. Cada opcode é usado para formar uma instrução Assembly. Os opcodes também são chamados de Código de Máquina, e isso é oque chamamos de Assembly.
Por exemplo, temos a instrução chamada add
, que soma dois operandos.
Em Opcode ela é equivalente a um dos seguintes, dependendo do contexto (na imagem, coluna Opcode, lado esquerdo):
Isso é a tabela que nos mostra como a instrução add
é montada em arquitetura Intel x86_x64.
Repare as colunas 64-bit Mode, para programas de 64 bits e Compat/Leg Mode para programas de 32 bits. Para saber mais, clique no link:
Manual da Intel para desenvolvedores.
Vamos ver qual opcode obtemos compilando um programa para um sistema de 64 bits:
// Programa em C
int main(){
int primeiro_numero = 3;
int segundo_numero = 2;
int resultado_da_soma;
resultado_da_soma = primeiro_numero + segundo_numero;
return 0;
}
Repare na linha selecionada: o programa está com o opcode 03 /r
.
Coluna de Instruções Assembly
Na terceira coluna da grande área, nós vemos as instruções Assembly. Falamos sobre elas anteriormente, na coluna dos Opcodes.
Coluna de Comentários
Na quarta coluna da grande área, temos a seção de comentários.
Podemos ter comentários automáticos, por exemplo, no caso de instruções que estão carregando uma string, vamos ter a própria string como comentário. Também podemos adicionar nossos comentários, basta clicar na tecla ;
e escrever algo, ao terminar é só dar enter
.
Coluna de Registradores
Aqui, no lado extremo direito, temos os registradores e seus conteúdos.
Os registradores são espaços de memória dentro do processador, e funcionam como variáveis temporárias.
Há registradores de propósito geral, que sempre vão ser usados para coisas diversas:
Registradores de Propósito Geral x64:
- RAX
- RBX
- RCX
- RDX
- RSI
- RDI
- R8, R9, R10, R11, R12, R13, R14 e R15. Em x64 temos esses oito registradores a mais.
Registradores de Propósito Geral x86:
- EAX
- EBX
- ECX
- EDX
- ESI
- EDI
Em teoria esses registradores podem ser usados para qualquer coisa, mas há situações em que RDI/EDI e RSI/ESI são usados de forma exclusiva por certas instruções.
Também há registradores que servem para armazenar coisas específicas.
Vou mencionar apenas os que considero mais importantes para o momento.
Para saber mais, não deixe de ler:
How many registers does an x86-64 cpu have
Alguns Registradores de uso específico (na ordem: x64/x86):
- RSP/ESP - Ponteiro para o topo da stack/pilha
- RBP/EBP - Ponteiro para a base da stack/pilha
- RIP - Ponteiro para a próxima instrução a ser executada. IP - Instruction Pointer (vai ser mostrado como RIP em x64, e EIP em x86). É por esse registrador que nós sabemos onde estamos dentro do programa, durante o fluxo de execução. Como ele tem o valor da próxima instrução a ser executada, estamos exatamente "um passo" atrás dele.
*Também deixei de mencionar os registradores que lidam com float's. Mais sobre: https://my.eng.utah.edu/~cs4400/sse-fp.pdf
Stack
Abaixo dos registradores, nós temos a Stack, também chamada de pilha (em pt-br).
A stack/pilha é um segmento de memória que opera com uma estrutura de dados de pilha (stack), por isso do nome.
Aqui nós temos os valores de nossas variáveis locais. Enquanto você estiver dentro de uma função, seus dados locais vão estar ali.
A coluna da esquerda representa o endereço da stack/pilha, e a coluna da direita nos mostra o valor armazenado naquele endereço.
A stack/pilha é uma estrutura de dados do tipo LIFO - Last In First Out, ou último a entrar, primeiro a sair. Todo mundo costuma usar o exemplo de uma pilha de pratos para explicar a stack: em uma pilha de pratos, qual prato você tira PRIMEIRO? O último empilhado, ou o primeiro, que deu início a pilha?
É lógico que, primeiro, você tira o que está no topo, LIFO...
Lembra dos registradores RSP e RBP? Então, é aqui que eles entram: RSP aponta para o topo da pilha, e RBP aponta para a base da pilha.
É assim que o layout de memória de uma função é delimitado e manipulado, através desses registradores.
E isso é que é chamado "stackframe", é o espaço de memória reservado para uma função.
Veremos um pouco mais sobre isso em "Noções básicas sobre funções em Assembly".
Dump de Memória
Ao lado esquerdo dos registradores, nós encontramos um dump da memória em hexadecimal.
Você pode clicar com o botão direito do mouse em qualquer instrução, e seguir tanto o endereço dela, quanto algum endereço que ela esteja operando. Também é possível fazer o mesmo nos registradores.
Voltando a imagem do Dump, a coluna mais a esquerda mostra o endereço respectivo aos dados hexadecimais nas quatro colunas seguintes (quatro colunas na configuração padrão).
Na última coluna, mais a direita, temos a representação ASCII dos dados em memória.
Também podemos alterar a visualização dos dados do dump: além de hexadecimal, ele nos mostra os dados como Inteiros e Floats. Basta clicar com o botão direito do mouse na área de dump e clicar em "Integers" ou em "Floats".
Na parte de cima nós temos vários dumps, para os quais você pode direcionar a visualização de algo que lhe interessar. Também temos outras funções, mas não vou mencioná-las aqui. Para saber mais, leia: Watch Control
Status do programa debugado
Aqui, no canto inferior esquerdo, nos é mostrado em que situação o programa debugado se encontra, como, por exemplo, se ele está em execução, ou pausado, e se estiver pausado, o motivo.
Rodando primeiro programa no x64dbg
Bom, agora que você já sabe como abrir o seu programa no x64dbg, vamos aprender mais algumas coisas antes de seguirmos para o próximo tutorial.
Abra qualquer programa com o x64dbg, de preferência um feito por você mesmo.
Em seguida, abra a janela (em vermelho) da seguinte maneira: Clique em Options -> Preferências
E aí desmarque a caixinha apontada pela seta, System Breakpoint. Assim você desativa alguns breakpoints "desnecessários" para nós agora (para saber mais sobre System Breakpoints: https://www.youtube.com/watch?v=vdyyg72tc2w).
Ao abrir o programa, ele vai ser mostrado na barra de tarefas somente quando for carregado. Eu estou abrindo um programa simples, ele já carrega a janela de primeira.
Se for um programa mais complexo, ele vai carregar outras coisas antes de poder desenhar a janela. Basta clicar em RUN (F9) até parar em um breakpoint: entry breakpoint, que você vê ao lado do status do programa:
Sempre acompanhe o status do programa na barra inferior, ao lado do PAUSE.
Vamos tentar encontrar nossas funções. Se o programa não foi "cuidadosamente estripado", achar a função principal, main, não é tão difícil.
Vá até a janela Símbolos, que possui nossos módulos, encontre o módulo principal (que possui o nome do próprio programa rodando), clique nela e seja direcionado para o início desse módulo:
Assim , se o programa não for extremamente complexo, você vai encontrar a main.
Dica: os programas compilados para x86, geralmente mantêm a função main no topo, então assim que o programa parar no entry breakpoint, vá ao topo das instruções que você encontrará a main.
Encontrando Strings
Outro modo de encontrar a main, é quando ela chama alguma string.
Para encontrar as strings dentro do programa é fácil:
na janela CPU, clique com o botão direito do mouse no meio da janela, vá até
Pesquisar Por -> All User Modules -> Referências String.
Carregando as strings na janela de referências, procure pela que você sabe que é usada na main, e aí clique nela. Você será redirecionado para o local em que ela aparece no programa, na janela CPU. O início da função, é o início da main, a nossa função principal, que o programador escreveu.
Noções básicas sobre funções em Assembly
Toda função em Assembly possui um prólogo e um epílogo.
Normalmente as funções possuem as seguintes instruções no início (o prólogo):
push ebp
mov ebp, esp
E a seguinte instrução no final (o epílogo):
ret
Lembre-se que as instruções em x64 utilizam os registradores rsp
e rbp
No início, o programa salva o valor que está no registrador EBP/RBP com push ebp
. Esse valor é o endereço da base da stack/pilha da função anterior; lembre-se que a stack/pilha possui os valores das variáveis locais, e que as variáveis locais são variáveis declaradas e/ou definidas dentro das funções (somente as variáveis definidas, isto é, variáveis com valores, vão ser colocadas na stack/pilha).
Após salvar o valor de EBP, um novo stackframe é iniciado; em outras palavras, um novo local ("local" se refere a um bloco de memória na stack) para guardar as variáveis dessa nova função é "delimitado". Isso é feito com mov ebp, esp
.
Tenha em mente que, para cada nova função, um novo stackframe é mapeado e alocado para ela, assim, cada função lida somente com os valores das suas próprias variáveis.
Você vai precisar saber sobre calling conventions para entender como os argumentos são passados para as funções, eu não irei explicar isso aqui, então se quiser saber mais, não deixe de ler:
https://en.wikipedia.org/wiki/X86_calling_conventions
Calling Convention Stack Frame
Atalhos úteis
Clicando com o botão direito do mouse no meio da janela CPU, vá até "Ir Para", lá você terá acesso as seguintes funcionalidades (decore os atalhos):
Os atalho mais úteis de todos são o primeiro e o segundo:
- RIP
*
: ele te direciona para onde o seu programa está no momento, seguindo o RIP. - Anterior
-
: Volta para a instrução executada anteriormente.
Final: Considerações e Recomendações
Não deixe de fazer o tutorial prático com o x64dbg [COLOCAR LINK QUANDO PRONTO].
Os debuggers são poderosos e úteis para as mais diversas atividades na área da computação. O x64dbg faz muitas coisas, várias delas eu deixei de mencionar aqui, como, por exemplo, alterar as instruções de um programa, para modificar sua funcionalidade.
Quanto mais você for estudando sobre tópicos de engenharia reversa, mais os debuggers se mostrarão úteis. Você irá se aprofundar nas diversas funcionalidades conforme a sua necessidade.
Um site muito bom para continuar praticando a leitura de Assembly é o seguinte: godbolt.org Selecione uma linguagem, como C ou C++, escolha um compilador, e comece os testes.
Caso esteja procurando obter os fundamentos da computação, eu sugiro que você confira os links desta área:
Guia de estudos: https://www.mentebinaria.com.br/studying-materials/basico-para-computacao/
Tutorial Gratuito de Assembly pt-br: https://github.com/andreluispy/assembly4noobs
Livro Gratuito de Assembly em pt-br: https://mentebinaria.gitbook.io/assembly/
Fundamentos da Engenharia Reversa pt-br: https://mentebinaria.gitbook.io/engenharia-reversa/
Curso de Engenharia Reversa Online (vídeo pt-br): https://www.youtube.com/playlist?list=PLIfZMtpPYFP6zLKlnyAeWY1I85VpyshAA
Livro Em Inglês para iniciantes em Engenharia Reversa en: https://beginners.re/
Posted on March 17, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.