Jump to content

Unpack Me + Reverse Me | Nível 2


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

  • Curtir 2
Link to post
Share on other sites
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 😉

 

 

  • Curtir 2
Link to post
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,

Edited by Hakuoo
  • Curtir 1
Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...