Jump to content

Unpack Me + Reverse Me | Nível 2


Hakuoo

Recommended Posts

Olá Pessoal, tudo certo ?

Bom eu criei esse desafio para ajuda nos estudos do pessoal.

As regras são as seguintes:

  • Seu Unpack tem que ser funcional(OEP válido)
  • Seu Algoritmo revertido tem que bater com o esperado ele pode  ser similar(não igual), porem funcional

Aqui tem uma simples explicação de como funciona esse tipo de desafio:


UnpackMe - é um CrackMe Diferente, ou seja você vai receber um arquivo que está compactado ou protegido, pode ser um protector/packer comercial ou privado ou até mesmo um obfuscador e seu objetivo é limpar o binário de forma que seja possível recuperar o algoritmo de maneira clara e que ainda continue funcional, de maneira simplificada é recuperar ao estado antes do packer/protector, para isso basta atingir o "OEP".


ReverseMe - é um CrackMe mais gourmet, seu objetivo não é só reescrever o algoritmo de validação implementado pelo desenvolvedor mas também reconstruir todo o funcionamento original do software ou então explicar como ele funciona de maneira clara para as outras pessoas.

 

Sobre nosso desafio:

  • "Protegido" usando o NsPack 3
  • MinGW, original C
  • IA-32

Como mencionado seu objetivo é fazer o Unpack Corrigir a IAT e encontrar o OEP, e reverter ou explicar como funciona o algoritmo original, isso não é uma competição, então tenha calma e compartilhe seu progresso vamos treinar juntos, caso necessário postarei a solução original como resposta.

 

Não esqueçam de descrever corretamente os passos feitos.


Boa Sorte e Saudações.

UnpMe+RevMe_MENTEBINARIA.zip

Link to comment
Share on other sites

  • Supporter - Nibble
Spoiler

Unpack:

As instruções no inicio do entrypoint do nspack são:

  • pushfd -> empilha a eflag (zero flag, carry flag e etc)
  • pushad -> empilha todos os registradores
  • call $

Onde $ é um calculado para a instrução seguinte,

556687923_2021-01-2413_42_59-UnpMeRevMe.exe-PID_7688-Mdulo_unpmerevme.exe-Thread_ThreadPrincipal.thumb.png.ec58cba1ade5e0f118b21ceb66e68e20.png

 

Ocorre algumas instruções, o importante é ver que a VirtualAlloc é chamada para salvar o conteúdo que será usado para o unpack:116696958_2021-01-2413_44_42-UnpMeRevMe.exe-PID_7688-Mdulo_unpmerevme.exe-Thread_ThreadPrincipal.thumb.png.03b28a1360f48033ebd3aa28624ea0a4.png

 

Indo mais a fundo, é possivel acompanhar todo o unpack, porém o importante é: Quando é feito as primeiras instruções, é enviado como "parâmetro" para a rotina de descompressão onde deve ser o inicio do código descomprimido, o valor em si é o primeiro da pilha e pode ser achado no valor do ESP (0x4E6201) antes da call para a descompressão. Por isso, coloca-se um hardware breakpoint de acesso que será chamado ao final do unpack.

É possível também acompanhar toda a descompressão, basta acompanhar a saida de EAX do VirtualAlloc, lá irá o endereço da nova memória alocada, porém ao final da execução o conteúdo é movido para o valor acima, não chamarei de OEP por que ainda não é o OEP mas pode ser interpretado como um "segundo" entrypoint.

, 1806461319_2021-01-2413_49_26-UnpMeRevMe.exe-PID_3508-Mdulo_unpmerevme.exe-Thread_ThreadPrincipal.thumb.png.b7d3c17f70789387a9ed0ab5e9be1603.png

 

Pode-se continuar a execução, Após o breakpoint chegar iremos estar de cara com o famoso jump, esse jump está presente em quase todos os packers e ele irá finalizar o trabalho do unpack:

1879382667_2021-01-2413_54_46-UnpMeRevMe.exe-PID_9532-Mdulo_unpmerevme.exe-Thread_ThreadPrincipal.thumb.png.0635b08533857e75aa8d0510aa2c5996.png

Esse jump irá nos levar a uma outra função que fará o papel de reconstruir a IAT do binário que foi desconstruida no pack:

 

1661068224_2021-01-2413_57_02-UnpMeRevMe.exe-PID_9532-Mdulo_unpmerevme.exe-Thread_ThreadPrincipal.png.6fdb87d1001934441f587ad5f9b5e7ba.png

Note que após essa call, tem mais um jump, esse jump vai para o nosso OEP, localizado em 0x401180, assim podemos usar o Scylla para fazer o dump do nosso PE em memória e realizar o fix da IAT:

1019177673_2021-01-2413_58_29-Scyllax86v0.9.8.thumb.png.f1c284b36eb75c4abe9088c74a496d41.png

 

Desse jeito, fizemos o manual unpack e reconstruimos a IAT ?

533479176_2021-01-2414_00_00-C__Windows_System32_cmd.exe-_UnpMeRevMe_dump_SCY.exe_.png.784a422bfbed1b770d2285dd879debad.png

Visão do DIE:

2041808427_2021-01-2414_13_23-PE.thumb.png.771971a8bef3d7305fc56ef51ff92a52.png

Algoritmo:

O programa pede uma chave de acesso numérica e então checa com uma chave interna após realizar um calculo sobre a chave:

 

 

197327381_2021-01-2414_03_02-IDA-UnpMeRevMe_dump_SCY_exe.idb(UnpMeRevMe_dump_SCY_exe)C__Users_bill_Docu.thumb.png.06cf8897c611a6bb60e00f1336778c4d.png

 

Onde a chave é movida para a pilha em loc_401636:

1159129474_2021-01-2414_03_54-IDA-UnpMeRevMe_dump_SCY_exe.idb(UnpMeRevMe_dump_SCY_exe)C__Users_bill_Docu.png.0d92e2d27fbf770f8ca76a13ac3fd133.png

 

 

O algoritmo realizar um xor com 0x32, realiza um right shift e multiplica o resultado por 100:


key = 0x11D29450

if (100 * (( val ^ 0x32) >> 5)) == key:
    print("You win with {}".format(val))
else:
  	print("Not yet!")

Para descobrir o input valido, é possível realizar a operação inversa e com o valor aproximado da chave realizar um brute force, que seria:


key_dec = 299013200
almost_the_key = int(((key_dec ^ 0x32) << 5) / 100) # 95684229
for i in range(almost_the_key, key_dec):
    if (100 * ((i ^ 0x32) >> 5)) == key_dec:
          print("Correct input -> {}".format(i))
          break

Correct input -> 95684256

 

1753585887_2021-01-2414_28_01-C__Windows_System32_cmd_exe.thumb.png.403b57765174741c34b5233e6d928155.png

 

É isso ai, acredito que deva ter uma maneira mais rápida e direta de resolver em vez de brute force, mas talvez eu tenha tido alguns problemas com a precisão ?

 

 

Link to comment
Share on other sites

Olá Anderson,

Como combinado vamos a solução, eu separei por blocos e ocultei para que não prejudique quem ainda for fazer para estudos.

Fazendo o Unpack e Reconstruindo:
 

Spoiler

 

Visualizando o original com um Detector:
s0.thumb.PNG.6dde1667d036ca02bfa6cd62a83834b4.PNG

Vamos visualizar a IAT(Import Address Table):
s2.thumb.PNG.17b093b9fcb3e09c6fda0458d431db93.PNG
Não é possível encontrar nada muito relevante, além de funções de alocação de memória, porem isso pode ser mais útil do que se imagina.

Vamos carregar o Binário no IDA:
s1.thumb.PNG.e594844a109e3dfb64cb2a91705e9d68.PNG

Após ignorarmos os avisos do IDA sobre binário compactado, podemos visualizar a rotina de descompressão na memória.
Vamos nos atentar ao EntryPoint do NsPack:
 






pushfd    ;  Manda as Eflags para stack
pushad  ; Manda os registradores de uso geral para stack
call    $+5  ; Call para próxima instrução, ou inicio da rotina de descompressão

Uma maneira fácil de fazer o Unpack é filtrar por instruções que "Desempilham Eflags e Registradores", se procurarmos na rotina é possível encontrar uma única referência ao final da rotina:
s3.thumb.PNG.be9893321afe3a25abaf212e2ec5e045.PNG






popad      ; Faz um pop em todos os registradores de uso geral
popfd      ; Faz um pop em todos os registradores EFLAGS
jmp     near ptr _mainCRTStartup  ; Salto para o entrypoint OEP

Ok, vamos avançar ao debugger:
s4.thumb.PNG.299f57488d4875febf2981f76c2a1130.PNG
Estamos parados justamente no EntryPoint do NSPack.

Temos informações de onde suspeitamos ser o final da sua rotina, agora voltando ao IDA, vamos copiar esse endereço para procurar no debugger:
s5.thumb.PNG.4b3b7bd8b16e391fb1bf8a363f5c01f3.PNG

Vamos obter um endereço: 0x4E6471
Agora apertando os Atalhos: CTRL+G colamos o endereço e procuramos

s6.thumb.PNG.d4bbacea2081047259c8d794106b9185.PNG

Vamos nos deparar justamente ao final da rotina:
Faremos duas coisas, definir um breakpoint normal com F2, e executar com F9
s7.thumb.PNG.277eae11299f426d04ca36cf35260769.PNG
 

Vamos nos deparar com a seguinte situação:
1º Quando a execução parar no Breakpoint vamos avançar com F8
2º Quando chegarmos ao Salto, vamos apertar F7 e entrar
s8.thumb.PNG.53f9bf8d4f46251456c01ae32802d302.PNG

Agora nos depararemos com a seguinte situação:
s9.thumb.PNG.6964d1571f80c0c0b57b305023309312.PNG

Vamos abrir o Scylla:
s10.thumb.PNG.fad30bbb3ca622bb8d71a8b4b8d1fe1d.PNG

Agora basta reconstruir a IAT e Dumpar:
s11.thumb.PNG.fa37d923217680801a7619cf7fc1ecf9.PNG

Pronto o software está funcionando perfeitamente e não possui mais a proteção, Somente resquícios(Sim, o unpack é para analisar, mas ainda vamos nos deparar com funções usadas para esconder Strings e coisas do tipo, será explicado mais em breve). 

Curiosidade:
O NSPack assim como o UPX é muito utilizado pelos autores de Malware, é bom saber fazer o unpack desses packers mais comuns e a maneira mais divertida é resolvendo UnpackMes e CrackMes ?
 

 

Entendendo as Traps e Analisando o algoritmo
 

Spoiler

 

A primeira coisa que veremos quando abrirmos no IDA será:
S1.thumb.PNG.53c481bd4087d92454ab9888c01b4784.PNG
 






Assembly
mov     dword ptr [esp+4], offset Locale  ; "Portuguese"  ; <- IDIOMA
mov     dword ptr [esp], 0 ; Category                                        ; <- LC_ALL
call    setlocale                                                                                 ;<- SetLocale(LC_ALL, "Portuguese")

Equivalente em C
SetLocale(LC_ALL, "Portuguese")

Assembly
mov     eax, ds:IsDebuggerPresent                                           ;
call    eax ; IsDebuggerPresent                                                  ;
test    eax, eax                                                                                  ;
setnz   al                                                                                             
test    al, al
jz      short loc_40159A

ROTINA 2 - caso DBG
mov     dword ptr [esp], 1 ; uExitCode
mov     eax, ds:ExitProcess
call    eax ; ExitProcess


Equivalente em C
if(IsDebuggerPresent()){
     ExitProcess(1);
}

Com essas informações identificamos que temos diversas Traps Simples de IsDebuggerPresent, ao decorrer do programa.

Vamos entender a rotina que escondia as strings:






Assembly 1
mov     dword ptr [esp], offset asc_48C00C ; "***************************************"...
call    ESCONDER_STRING
mov     eax, ds:IsDebuggerPresent
call    eax ; IsDebuggerPresent

Assembly 2
mov     dword ptr [esp], offset aMentebinariaUn ; "   MENTEBINARIA | UNPACKME & REVERSE ME"...
call    ESCONDER_STRING
mov     eax, ds:IsDebuggerPresent
call    eax ; IsDebuggerPresent

Equivalente em C
ESCONDER_STRING(const char* STR);

Descendo mais um pouco encontraremos algo muito interessante e que nos ajudará a obter a senha correta:






loc_401636:
mov     [ebp+var_C], 11D29450h            ; Valor hexadecimal sendo armazenado em um local da memória
mov     eax, ds:IsDebuggerPresent
call    eax ; IsDebuggerPresent
test    eax, eax
setnz   al
test    al, al
jz      short loc_40165B

vamos guardar esse valor e construir um pseudocódigo:

11D29450h -> decimal -> 299013200
e vamos nomear essa posição com algo mais legal:
mov     [x], 11D29450h ; 299013200

vamos procurar abaixo para encontrar algo mais interessante.
Bingo! Encontrei:
 






xor     eax, 32h                                   ; Valor de EAX = Lido pelo usuário XOR 0x32 -> Decimal -> 50
sar     eax, 5                                        ; Deslocamento >> 5
imul    eax, 64h ; 'd'                          ; multiplicado por 64h -> Decimal -> 100
mov     [ebp+var_10], eax              ; Armazena o valor do registrador em uma posição da memória

e vamos nos divertir no nosso amado Pseudo C
 ((valor_lido ^ 0x50) >> 5)*100

Assim como anteriormente vamos dar um nome para esse local de memória:
mov     [y], valor_lido

Avançando mais um pouquinho encontraremos a seguinte rotina:
 






mov     eax, [y]
cmp     eax, [x]                     ;  Subtrai de EAX o valor de X e seta zero flag
jnz     short loc_40173C   ; salto para o final e exibe o erro

Arrumando para deixar entendivel para quem está iniciando:
mov     c, [y]
cmp     c, [x]                     ;  Subtrai de c o valor de X e seta zero flag
jnz     short loc_40173C   ; salto para o final e exibe o erro

pseudoC
if(y == x){
	EBA!
}else{
	HÁ, ADEUS!
}

Acredito que já ficou bem grande e quero organizar por tópicos elegantes, então vamos ao próximo que é reconstruir o algoritmo e encontrar sua chave.

 

Reconstruindo o algoritmo e encontrando a chave

Spoiler

 


Com as informações obtidas na nossa analise temos o seguinte pseudocódigo C
 





int main(){
	int x = 299013200;
	int y = 0;
	printf("Digite sua chave de acesso: \n");
	scanf("%d", &y);
	y= ((y^50)>>5)*100;
	if(y == x){
   			// EBA!
	}else{
   			//Ops..
	}
}

 

Pessoa: Então vamos digitar logo o valor de X no console e ser feliz
Eu: NÃO !
 
Vamos entender porque:
X é o valor esperado, então temos que ter uma chave para calcular seu valor final

Pessoa: Há, eu tiro de letra, isso está muito fácil, basta fazer o processo inverso.
Eu: Blz Vamos lá:

- O Processo inverso de uma multiplicação é a divisão
- O Processo inverso de um SHL(>>) é o SHR(<<)
- O XOR não tem processo diferença, basta fazer um XOR

Ok, vamos obter a seguinte solução:
 y= ((y^50)<<5)/100;
                <-----
               299013200 / 100
               << 5
               ^ 50
             = 95684274
Quando usamos essa chave:

"Parabéns !! agora reescreva esse algoritmo, faça o unpack do packer, de maneira funcional e poste sua solução."

Sucesso conseguimos a chave de forma reversa e o desafio foi resolvido!

Dúvidas frequentes:
Qual era a chave original que você usou:
95684262
Pessoa: Minha chave é diferente, mas eu cheguei no objetivo, estou errado ?
Eu: De maneira alguma, como expliquei no desafio todas as formas de pensar são corretas se você chegou no objetivo, compartilhe porque você conseguiu atingir por mérito próprio, e parabéns ?

 


Fonte original:

Spoiler

 






#include <iostream>
#include <locale.h>
#include <stdio.h>
#include <windows.h>

/*
chave
95684262
*/
int main(int argc, char** argv) {
	setlocale(LC_ALL, "Portuguese");
	if(IsDebuggerPresent()){
		ExitProcess(1);	
	}
	printf("************************************************\n");
	if(IsDebuggerPresent()){
		ExitProcess(1);	
	}
	printf("   MENTEBINARIA | UNPACKME & REVERSE ME    \n");
	if(IsDebuggerPresent()){
		ExitProcess(1);	
	}
	printf("************************************************\n");
	if(IsDebuggerPresent()){
		ExitProcess(1);	
	}
	if(IsDebuggerPresent()){
		ExitProcess(1);	
	}
	int senha = 299013200;
		if(IsDebuggerPresent()){
		ExitProcess(1);	
	}
	int leia = 0;
		if(IsDebuggerPresent()){
		ExitProcess(1);	
	}
	printf("Digite sua chave de acesso: \n");
		if(IsDebuggerPresent()){
		ExitProcess(1);	
	}
	scanf("%d", &leia);
		if(IsDebuggerPresent()){
		ExitProcess(1);	
	}
	leia = ((leia^50)>>5)*100;
		if(IsDebuggerPresent()){
		ExitProcess(1);	
	}
	if(leia == senha){
		if(IsDebuggerPresent()){
		ExitProcess(1);	
	}
		printf("Parabéns !!\n agora reescreva esse algoritimo, faça o unpack do packer, de maneira funcional e poste sua solução.\n");
	}else{
	if(IsDebuggerPresent()){
		ExitProcess(1);	
	}
		printf("Não foi dessa vez, vai pedir para sair ? VAMOS VOCÊ CONSEGUE !\n");
	}
	return 0;
}

 

 


Espero que tenham gostado do desafio, e até o próximo e quem resolveu, parabéns você pensou fora da caixa e isso que é realmente importante !


Saudações,

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...