Chegamos no capítulo onde a engenharia reversa de fato começa. Aqui vamos estudar a depuração, ou debugging em inglês. O conceito, como o nome em sugere, é buscar identificar erros (bugs) num programa, a fim de corrigi-los. No entanto, os debuggers - como são chamados os softwares que servem a este fim - servem para muito mais que isso.
Neste livro usaremos o x64dbg por ser um debugger de código aberto, gratuito e muito atualizado para Windows.
Na próxima seção apresentaremos como baixar e configurar o x64dbg. Também utilizaremos um binário de exemplo durante o livro, que é um desafio disponível em menteb.in/analyseme00. Com ele estudaremos os conceitos de engenharia reversa que precisamos para criar um fundamento sólido para avançar nesta área.
Um patch é qualquer alteração, seja nos dados ou no código de um programa. O que fizemos na seção anterior é justamente isto. No entanto, não salvamos nossas modificações e neste caso elas serão perdidas caso você feche o x64dbg.
É possível salvar as alterações acessando o menu View -> Patch file..., clicando no botão com um pequeno curativo (tipo um band-aid) na barra de ferramentas ou pressionando Ctrl+P.
Se você veio da seção anterior e tem ainda as modificações no AnalyseMe-00, sua tela de patches aparecerá assim:
A partir desta tela, é possível exportá-los para um arquivo (Export), mas também criar um novo arquivo executável com as alterações salvas (Patch File).
Perceba que você pode fazer qualquer tipo de patch, desde uma simples alteração da lógica de um salto até inserir de fato novas funções num programa. Pode dar trabalho, mas é possível.
E assim, chegamos ao fim desta primeira etapa de estudo dos fundamentos de engenharia reversa de software. Espero que tenha curtido a jornada e aproveitado o conteúdo. Ainda há um longo caminho pela frente, mas tenho certeza que agora será mais fácil entender conceitos mais profundos de Assembly x86, iniciar estudos de outras arquiteturas como x86-64 e ARM, de conceitos mais avançados no Windows ou mesmo de engenharia reversa em outras plataformas como Linux e macOS.
Se este livro te ajudou, considere nos apoiar em https://menteb.in/apoie, pois a Mente Binária depende de doações e venda de cursos para sustentar sua operação e continuar entregando conteúdo de qualidade para milhares de pessoas. Muito obrigado por chegar até aqui! Sucesso!
Agora que já sabemos o básico do debugging e sabemos colocar breakpoints de software, vamos começar a manipular o programa da maneira que queremos.
Em geral, quando falamos de manipulação, falamos de alguma alteração no código do programa, para que este execute o que queremos, da forma como queremos.
Tomemos como exemplo o AnalyseMe-00 mesmo. Supondo que não sabemos o que o mesmo faz. Um bom início para a engenharia reversa é a busca por chamadas intermodulares (o botão com um celular e uma seta azul). Ao clicar nele, encontrará duas chamadas à função DeleteFileA da KERNEL32.DLL. Colocaremos um breakpoint em todas as chamadas à estas funções, bastando para isso dar um clique com o botão direito do mouse em uma delas e escolher a opção "Set breakpoint on all calls to DeleteFileA", como sugere a imagem abaixo:
Ao voltar à aba CPU e rodar o programa (F9), paramos aqui:
Esta função é bem simples. No endereço 00401105 há um PUSH que coloca o endereço 402020 na pilha, depois há a chamada da DeleteFileA em si.
O x64dbg já resolve a referência do endereço e, caso encontre uma string, exibe ao lado (na quarta coluna), como acontece com a string "C:\Windows\System32\cmd.exe". Ora, se este é o argumento passado para a função DeleteFile(), este é o caminho do arquivo que o programa AnalyseMe-00 pretende deletar.
O que a gente vai fazer é mudar esta string, mudando assim o programa que o AnalyseMe-00 tenta deletar. Para isso, clique com botão direito do mouse sobre a instrução PUSH e escolha "Follow in Dump -> 402020".
O endereço em questão é exibido no Dump 1. Outra opção seria ir no Dump 1, teclar Ctrl+G, digitar 402020 e clicar em OK.
Para alterar a string, você vai precisar selecionar todos os bytes desejados, pois o x64dbg não sabe exatamente onde começa e onde termina cada bloco de dados usado pelo programa. Supondo que queiramos alterar "cmd.exe" para "calc.exe", fazendo assim com que o programa tente excluir a calculadora do Windows. Para este caso, selecionamos o trecho e pressionamos Ctrl+E, que é o equivalente ao clicar com o botão direito sobre a seleção e escolher "Binary -> Edit".
Após fazer a alteração e clicar em OK, perceba que o Dump 1 agora destaca os bytes alterados em vermelho:
Ao seguir com a execução da chamada à DeleteFileA (F8), o programa tenta excluir o calc.exe ao invés de o cmd.exe. No entanto, como em versões modernas do Windows o conteúdo deste diretório é protegido, a função retorna zero (perceba o registrador EAX zerado), que no caso desta função, indica que houve falha, e as variáveis LastError
e LastStatus
são modificadas para refletir o que aconteceu.
Espero que com esta seção você entenda que, tendo o programa sob o controle de um debugger, é possível modificar praticamente tudo o que queremos. Podemos impedir que funções sejam chamadas, podemos chamar novas funções, alterar dados, modificar parâmetros, enfim, a lista é quase infinita. Na próxima seção vamos ver como salvar as alterações feitas.
Ao observar a região que chamamos de disassembly, você verá 5 colunas onde a primeira exibe algumas informações e relações entre endereços. A segunda mostra os endereços em si. A terceira mostra os bytes do opcode e operandos das instruções. A quarta mostra os mnemônicos onde podemos ler Assembly e por fim, a quinta mostra alguns comentários, sejam estes gerados automaticamente pelo debugger ou adicionados pelo usuário.
Perceba que por padrão o debugger já destaca vários aspectos das instruções, na janela de disassembly. O nome mais comum para este destaque é sua versão em inglês highlight.
O highlight refere-se principalmente às cores, mas o debugger também dá altas dicas. Na instrução acima, perceba que a operação em si está em azul, enquanto o argumento está em verde.
O endereço para o qual o ponteiro de instrução aponta (EIP, embora o x64dbg chame-o genericamente de CIP, já que em x86-64 seu nome é RIP) é destacado com um fundo preto (na imagem anterior, vemos que é o endereço 401000).
Na coluna de comentários, temos os comentários automáticos em marrom.
É importante lembrar que no arquivo há somente os bytes referentes às instruções (terceira coluna). Todo o resto é interpretação do debugger para que a leitura seja mais intuitiva.
Neste primeiro momento, o debugger está parado e a próxima instrução a ser executada é justamente o que chamamos de OEP (Original EntryPoint).
O primeiro comando que aprenderemos é o Step over, que pode ser ativado por pelo menos três lugares:
Menu Debug -> Step over.
Botão Step over na barra de botões (é o sétimo botão).
Tecla de atalho F8.
Digitando um dos comandos a seguir na barra de comandos: StepOver/step/sto/st
Se você emitir este comando uma vez, verá que o debugger vai executar uma única instrução e parar. Na janela do disassembly, você vai perceber que o cursor (EIP) "pulou uma linha" e a instrução anterior foi executada. No caso de nosso binário de teste, é a instrução PUSH EBP. Após sua execução, perceba que o valor de EBP foi agora colocado no topo da pilha (observe a pilha, abaixo dos registradores).
Você pode seguir teclando F8 até alcançar a primeira instrução CALL em 401007, destacada por um fundo azul claro.
O comando Step over sobre uma CALL faz com que o debugger execute a rotina apontada pela instrução e "volte" para o endereço imediatamente após a CALL. Você não verá essa execução, pois o debbugger não a instrumentará. Caso queira observar o que foi executado "dentro" da CALL, é necessário utilizar o Step into (F7). Vamos fazer dois testes:
Com o EIP apontado para a CALL em 401007, tecle F8. Você verá que a execução simplesmente "passa para a linha abaixo da CALL". Isso quer dizer que ela foi executada, mas você "não viu no debugger".
Agora reinicie o programa no debugger (CTRL + F2), vá teclando F8 até chegar sobre a CALL novamente e tecle F7, que é o Step into. Perceba que o debugger agora "entrou" na CALL. Neste caso, você vai precisar teclar F8 mais três vezes até voltar ao fluxo de execução original, isso porque esta CALL só possui três instruções.
Um outro comando importante é o Run (F9). Ele simplesmente inicia a execução a partir do EIP de todas as instruções do programa. Se você emiti-lo com este binário, vai ver que a execução vai terminar, o que significa que o programa rodou até o final e saiu. Aí basta reiniciar o programa (CTRL + F2) para recomeçar nossos estudos. ;)
Na próxima seção, vamos entender os pontos de paradas, mais conhecidos como breakpoints.
Na sua máquina Windows, baixe o snapshot mais recente do x64dbg. É um arquivo .zip chamado snapshot_YYYY-MM-DD_HH-MM.zip que vai variar dependendo da data e hora do release (quando o software é liberado) pelos autores do projeto.
Ao descompactar o arquivo .zip, execute o arquivo x96dbg.exe dentro do diretório release. Esse nome deve-se ao fato de que o x64dbg tem suporte tanto a 32 quanto a 64-bits, então o autor resolveu somar 32+64 e nomear o binário assim.
O x96dbg.exe é o launcher do x64dbg e tem três botões. Escolha Install e responda "Sim" para todas as perguntas.
À esta altura você já deve ter o atalho x32dbg na área de trabalho. Ao clicar, você verá a tela inicial do debugger. A ideia é que você depure binários (.exe, .dll, etc) portanto, vamos abrir o AnalyseMe-00.exe e seguir.
Vá em Options -> Preferences e desmarque a caixa System breakpoint. Isso vai fazer com que o debugger pare direto no entrypoint de um programa ao abrirmos.
Clique em Save.
Existem muitas outras opções de configuração que você pode experimentar, mas para o momento isso basta.
Descompacte e abra o AnalyseMe-00.exe
no x32dbg clicando em File -> Open. Você deverá ver uma tela como esta:
A aba CPU é sem dúvida a mais utilizada no processo de debugging, por isso, fizemos questão de nomear algumas de suas áreas, que descreveremos agora.
Nesta região são exibidos os endereços (VA's), os opcodes e argumentos em bytes de cada instrução, seu disassembly (ou seja, o que significam em Assembly) e alguns comentários úteis na quarta coluna, como a palavra EntryPoint, na primeira instrução do programa a ser executada (em 401000 no nosso exemplo).
Tomei a liberdade de nomear essa seção de Helper, porque de fato ela ajuda. Por exemplo, quando alguma instrução faz referência a um dado em memória ou em um registrador, ela já mostra que dado é este. Assim você não precisa ir buscar. É basicamente um economizador de tempo. Supondo que o debugger esteja parado na instrução push esi
, no Helper aparecerá o valor do registrador ESI.
O dump é um visualizador que você pode usar para inspecionar bytes em qualquer endereço. Por padrão há cinco abas de dump, mas você pode adicionar mais se precisar.
Como o nome sugere, mostra o valor de cada registrador do processador.
Mostra a pilha de memória, onde o endereço com fundo em preto indica o topo da pilha.
Na próxima seção, iremos depurar o binário de exemplo e devemos nos atentar às informações exibidas em cada uma das regiões da tela do debugger, acima apresentadas.
Um breakpoint nada mais é que um ponto do código onde o debugger vai parar para que você analise o que precisa. É o mesmo conceito dos breakpoints presentes nos IDE's como Visual Studio, NetBeans, CodeBlocks, etc. A diferença é que nestas IDE's colocamos breakpoints em determinadas linhas do código-fonte. Já nos debuggers destinados à engenharia reversa, colocamos breakpoints em endereços (VA's), onde há instruções.
Há várias maneiras de se colocar um breakpoint em um endereço utilizando o x64dbg. Você pode selecionar a instrução e pressionar F2, usar um dos comandos SetBPX/bp/bpx ou simplesmente clicar na pequena bolinha cinza à esquerda do endereço. Ao fazê-lo, este ficará com um fundo vermelho, como sugere a imagem:
Um segundo clique desabilita o breakpoint, mas não o exclui da aba Breakpoints (Alt+B). O terceiro clique de fato o deleta.
Após colocar o breakpoint nesta CALL, rode o programa (F9). O que acontece? O debugger executa todas as instruções anteriores a este breakpoint e pára onde você pediu. Simples assim.
Talvez você tenha notado que ao atingir um breakpoint, o x64dbg denuncia na barra de status: INT3 breakpoint at analyseme-00.00401007 (00401007)!. Este é um tipo de breakpoint de software. Existem ainda os de memória e de hardware, mas não trataremos neste curso básico.
A instrução INT é uma instrução Assembly que gera uma interrupção. A interrupção número 3 é chamada de Breakpoint Exception (#BP) no manual da Intel. Seu opcode (0xcc) tem somente um byte, o que facilita para que os debuggers implementem-na.
De forma resumida, para parar nesta CALL, o que o x64dbg faz é:
Substituir o primeiro byte do opcode da CALL (0xff, neste caso) por 0xcc e salvar o original numa memória.
Executar o programa.
Restaurar o primeiro byte do opcode da CALL, substituindo o 0xcc por 0xff (neste caso).
Isso poderia ser feito manualmente, mas os debuggers facilitam o trabalho, bastando você pressionar F2 ou clicar na bolinha para que todo este trabalho seja executado em segundo plano, sem que o usuário perceba. Incrível, não é?
Você pode adicionar quantos breakpoints de software quiser numa sessão de debugging. Todos ficam salvos na aba Breakpoints, a não ser que você os exclua. Veja a imagem abaixo:
Você também pode assistir a aula do CERO, que trata sobre este assunto.