ENCONTRANDO A MENSAGEM SECRETA
Esse é meu primeiro tutorial sobre engenharia reversa. Se você sabe tanto quanto eu ( ou seja, quase nada ), essa página é um bom começo. Vou explicar algumas coisas básicas e fazer um "debug" manual de um programa feito em assembly ( seu objetivo será encontrar a frase escondida ).
O programa que eu vou utilizar nesse tutorial é extremamente simples, feito em assembly e compilado no MASM32. O código está incluso junto com o executável ( mas não olhe o código antes de finalizar este tutorial ).
-Download fergo_ex1.zip
Antes de tudo, precisamos de algo que transforme o nosso executável em uma linguagem que o ser humano possa entender ( ou ao menos tentar ). Para isso, voce precisa de um Debugger ou Disassembler. Neste tutorial eu vou utilizar um dos Debuggers mais completos atualmente ( um dos mais famosos também ), por ter uma interface mais visual e ser freeware: OllyDbg
Ao iniciar o Olly, você terá uma tela semelhante a essa ( as cores podem variar, dependendo da configuração do usuário )
Vamos abrir então o nosso executável para analisar o seu código ( em linguagem de máquina, assembly ( asm )). Vá em "File->Open" e selecione "fergo ex1.exe". Por se tratar de um executável pequeno, ele abre instantaneamente e tem poucas linhas de código efetivas. Você terá algo mais ou menos assim:
Quanta coisa né? Mas não esquente, com o tempo voce fica mais familiarizado. Na janela principal ( superior esquerda ), você tem 4 colunas: Adress, Hex Dump, Disassembly, Comment. Adress contem o endereço de cada instrução, é por esse endereço que você vai determinar saltos ( jumps ), chamadas ( calls ), etc...
Hex Dump é a instrução no formato hexadecimal ( não interessa agora ). Disassembly é o mesmo que o Hex Dump, mas "traduzido" em letras, digamos assim. Comments não tem relação com o código, apenas ajuda a identificar algumas coisas ( chamadas de função por exemplo ).
Como o código é pequeno, eu vou numerar as linhas para começarmos o nosso "debug"
1 00401000 2BC0 SUB EAX,EAX
2 00401002 83F8 00 CMP EAX,0
3 00401005 74 0E JE SHORT fergo_ex.00401015
4 00401007 8D05 25304000 LEA EAX,DWORD PTR DS:[403025]
5 0040100D 8D1D 25304000 LEA EBX,DWORD PTR DS:[403025]
6 00401013 EB 0C JMP SHORT fergo_ex.00401021
7 00401015 8D05 00304000 LEA EAX,DWORD PTR DS:[403000]
8 0040101B 8D1D 09304000 LEA EBX,DWORD PTR DS:[403009]
9 00401021 6A 00 PUSH 0 ; Style = MB_OK|MB_APPLMODAL
10 00401023 50 PUSH EAX ; Title
11 00401024 53 PUSH EBX ; Text
12 00401025 6A 00 PUSH 0 ; hOwner = NULL
13 00401027 E8 14000000 CALL <JMP.&user32.MessageBoxA> ; MessageBoxA
14 0040102C 6A 00 PUSH 0 ; ExitCode = 0
15 0040102E E8 01000000 CALL <JMP.&kernel32.ExitProcess> ; ExitProcess
16 00401033 CC INT3
17 00401034 -FF25 00204000 JMP DWORD PTR DS:[<&kernel32.ExitProcess>; kernel32.ExitProcess
18 0040103A -FF25 0C204000 JMP DWORD PTR DS:[<&user32.wsprintfA>] ; user32.wsprintfA
19 00401040 $-FF25 08204000 JMP DWORD PTR DS:[<&user32.MessageBoxA>] ; user32.MessageBoxA
Vamos ao nosso debug então.
Linha 1: SUB EAX, EAX
SUB indica uma operação de subtração, seguida de seus 2 argumentos. EAX é um registrador, um local de armazenamento de dados temporário, onde normalmente são colocados valores para comparação, etc. Esse comando, mais especificamente, coloca no seu primeiro argumento, a subratração dele mesmo com o segundo elemento, algo como "EAX = EAX - EAX". Sim, isso dá zero ( é uma das maneiras de zerar um valor em ASM ).
Linha 2: CMP EAX, 0
CMP siginfica Compare. Ele compara seu primeiro argumento com o segundo. Se a comparação for verdadeira, ele seta uma "flag" indicando que é verdadeira. Nesse caso, ele está comparando EAX com 0 ( algo como "if ( eax == 0 )" em C ). Na linha anterior, EAX foi zerado, e agora ele está sendo comparado com 0, então, essa comparação é verdadeira.
Linha 3: JE SHORT fergo_ex.00401015
Jump if Equal. Como o nome já diz, se os argumentos da comparação anterior forem iguais ( comparação verdadeira ), ele realiza um salto para outra região do código. Nesse caso, como a comparação foi verdadeira, ele vai pular para o endereço 00401015 do executavel fergo_ex1.exe.
Vou pular as linhas 4, 5 e 6 para não matar a charada logo no começo. Falaremos dela depois
Linha 7: LEA EAX, DWORD PTR DS:[403000]
O comando LEA faz com que o primeiro argumento aponte para o segundo argumento. Ele não recebe o valor do segundo argumento, recebe apenas o "local" onde está esse valor. Nesse caso, ele vai mover para o registrador EAX, o endereço 403000 ( que corresponde a um valor de 32 bits ( DWORD )).
Linha 8: LEA EBX,DWORD PTR DS:[403009]
Mesma coisa que o comando de cima, só que ele move um endereço diferente para uma variável diferente ( 403009 para EBX )
Linha 9: PUSH 0
Apenas "puxa" o seu argumento ( 0 ) para um local temporario, não realiza nenhum comando. Veja mais abaixo.
Linha 10, 11, 12: PUSH ...
Faz a mesma cosia que a linha anterior, só alterando o seu argumento. Ele vai puxar a variável EAX, EBX e depois novamente um 0.
Linha 13: CALL <JMP.&user32.MessageBoxA>
Faz uma chamada para uma função qualquer. Nesse caso, ele vai chamar a função MessageBoxA, contida na DLL user32.dll. Como você já deve ter imaginado, essa função exibe uma mensagem de texto. Você deve ter imaginado também que essa é a mensagem de texto que aparece quando você inicia o programa.
Tá, mas onde ela pega o conteúdo para exibir? Vamos dar uma olhada nos argumentos que a função MessageBoxA recebe ( procure no google caso queira saber da onde eu tirei isso ):
MessageBoxA ( dono, endereço do texto, endereço do título, tipo )
Dono indica o dono da janela, não importa agora. Endereco do texto e endereço do titulo é o que o próprio nome já diz. Tipo é o tipo da mensagem ( botão OK/Cancel, Yes/No, etc... ). Mas onde são passados esses argumentos no nosso código? Toda a função Call em ASM vai pegar os argumentos que você "puxou" na ordem reversa. Ou seja, o Dono ele vai pegar do endereço 00401025, o Texto ele pega do 00401024 e assim por diante. Cada "Push" que você deu, ele colocou o valor no topo de uma pilha, sendo que para pegar os valores dessa pilha, você começa pelo último valor ( o último Push ). Imagine você empilhando livros e depois pegando eles para organizar numa ordem ;D
Linha 14: PUSH 0
Um novo valor é posto no "stack" ( agora você já deve imaginar que deve vir alguma outra função que fará o uso desse valor ). E vem mesmo!
Linha 15: CALL <JMP.&kernel32.ExitProcess>
Novamente é feita uma chamada a uma função. Desta vez, a função é ExitProcess. Você já deve ter percebido que essa função encerra o programa caso o seu argumento possua um determinado valor. Você notou quando rodou o executavel que assim que você clica em OK, o programa encerra, então faz sentido. Na linha anterior você colocou um valor 0 no "stack". Essa função recebe esse valor zero. Se o seu programa encerra quando você fecha a janela e a função de finalizar o processo recebe o valor 0, você sabe que caso a função receba o valor 0, ela encerra o programa.
Certo, o programa encerra por aqui. Mas e a minha mensagem escondida, onde está? Re-analise o código. Repare que na linha 7 e 8, você determina valores para o EAX e EBX, e logo depois você chama uma função que coloca esses 2 registradores como sendo Texto e o Título da mensagem. Agora ficou claro que essas 2 linhas colocam nos registradores o titulo e o texto da mensagem. Repare agora na linha 4 e 5. Também temos 2 LEA que fazem praticamente a mesma coisa. Oras, nessas 2 linhas ele deve atribuir a mensagem escondida para EAX e EBX, mas porque ele não faz isso? Veja que na linha 3 ele realiza um salto caso a condição seja verdadeira ( é o que ocorre, lembra? ). Como o salto ocorre, ele sequer passa por essas 2 linhas para poder atribuir a mensagem secreta. Agora pense no que você poderia fazer para que ele passasse por essas 2 linhas de código? Bom, tem várias alternativas, mas provavelmente vem na sua cabeça simplismente tirar aquele jump da linha 3 ou invertê-lo, fazer com que caso a condição NÃO seja verdadeira, ele realize o salto ( o pulo nunca vai ocorrer, visto que a comparação de 0 com 0 sempre vai ser verdadeira ).
Nós vamos "retirar" aquele jump, pois é mais simples ( na verdade o trabalho é o mesmo :P ). Você não pode "deletar" uma linha, pois isso alteraria o endereço de todas as intruções, e o programa iria parar de funcionar. Felizmente existe o comando "NOP" ( No OPeration ), que "anula" a linha sem alterar nenhum endereço. Para fazer isso, clique com o botão direito na linha 3, vá em "Binary->Fill with NOPs". Pronto, você anulou o pulo.
Repare que agora ele vai chegar na linha 3 e vai continuar seu caminho, sem um salto. Ele vai atribuir um valor ao EAX e ao EBX ( provavelmente nossa mensagem escondida ) e em seguida vai simplismente pular ( JMP, na linha 6 ) para o endereço 00401015 ( 00401021 após a alteração ), que é justamente onde ele começa a puxar os valores para a chada da função MessageBoxA ( repare que fazendo isso, ele evita que os valores originais das mensagens sejam re-atríbuidos a EAX e EBX ).
Que tal testar o que a gente fez? Clique com o botão direito sobre qualquer linha, vá em "Copy To Executable->All Modifications" e em seguida "Copy All". Uma nova janela se abrirá. Clique novamente com o botão direito sobre ela, selecione "Save File" e salve seu arquivo alterado.
Pronto, agora execute o seu arquivo recém salvo e você vai ver que a mensagem secreta era "Parabéns, você o encontrou!".
Espero ter dado o ponta pé inicial para aqueles que não sabiam por onde começar ou não sabiam o significado das instruções básicas do Assembly.
F3rGO!