KeyGen (Key Generator) como o próprio nome já diz, é um gerador de chaves ou seriais para registrar programas de forma ilegal. Para qualquer nome digitado ele gera um serial válido que quando inserido no programa oficial é aceito como se tivesse sido obtido pelos meios legais.
Um pouco de lógica
Entender a lógica por trás da validação do Nome e Serial do CrackMe ajudará quando estivermos lidando com os códigos.
Vimos que antes das mensagens de Erro ou Sucesso o programa chama uma função denominada “compare” que na verdade é o Offset 00401A08.
004019CF CALL CrackMe.00401A08
E baseado no retorno dela exibe a mensagem adequada.
Com isso podemos imaginar que a função que se inicia no Offset 00401A08 deve ser a responsável por validar os dados digitados. Sem ainda entrarmos nos códigos podemos imaginar também algumas operações que a função de validação deve executar.
Primeiro ela deve obter os valores que foram digitados nos dois campos. Depois, se cada Nome possui um Serial diferente, ela deve deve pegar o que foi digitado no campo Nome, fazer uma série de operações que não sabemos quais são, pegar o resultado e compará-lo com o que foi digitado no campo Serial. Caso os dois sejam iguais o Serial digitado é válido.
Um algoritmo simplificado para isso seria assim:
Leia NomeDigitado;
Leia SerialDigitado:
Resultado = OperaçõesCom(NomeDigitado);
Se (Resultado == SerialDigitado)
Serial Válido;
Senão
Seria Inválido;
De um modo geral muitos programas fazem a validação dessa forma. O “X” da questão, ou a proteção do programa, está na dificuldade de descobrir quais operações são realizadas, podem ser muitos cálculos, recursividade, combinações, atribuições, etc.
Dependendo do grau de complexidade dessas operações o algoritmo parece funcionar muito bem. Pense então na depuração desse algoritmo, onde vamos executando linha a linha com a ajuda de um Debugger e vendo os resultados. Conseguiu enxergar uma falha aí? Que valor seria exibido na variável Resultado?
A variável Resultado seria na verdade um Serial válido para o Nome digitado. Mesmo digitando um Serial qualquer o programa geraria uma Serial válido para comparar com o que digitamos. E através da execução linha a linha do algoritmo seríamos capazes de visualizá-lo.
O processo de obter um Serial válido através da execução do programa em um Debugger é conhecido como “pescar serial” ou “serial fishing”.
Pescar o Serial no CrackMe
Um dos desafios do artigo anterior era conseguir pelo menos um Nome e Serial válido para o CrackMe. Isso é possível através do debug do programa no OllyDbg.
Abre o CrackMe no OllyDbg, coloque um Breakpoint (BP) no Offset 00401A08 clicando uma vez na linha e pressionando F2. Agora pressione F9 (Run) para iniciar o debug. Com isso o CrackMe irá executar e quando chegar na linha que colocamos o BP irá parar e permitir que executemos linha a linha.
Insira um Nome e Serial qualquer e clique em Registrar, eu inseri “crimes ciberneticos” e “1234”. O debug irá parar no nosso BP.
Agora vá pressionando F8 e acompanhando a execução, lembre da lógica explicada anteriormente. Quando chegar no Offset 00401A71 você vai ver (ASCII “nomedigitado”), nessa linha está lendo o Nome que digitamos e armazenando em algum lugar. O mesmo acontece com o Serial no endereço 00401A7C.
Seguindo pressionando F8 vemos que a execução entra em um loop, pode ser as operações que falamos anteriormente. Continue no F8 até sair do loop.
Quando chega no Offset 00401ACF é exibido um número, no meu caso (ASCII “28065600”). Eis aí o que estávamos procurando, o Serial.
Para confirmar se realmente é válido, executamos o CrackMe, inserimos os dados obtidos e vemos o resultado.
KeyGen
O mecanismo por trás de um keygen é o mesmo utilizado pelo programa original. Acabamos de ver que o próprio programa gera um Serial válido para um Nome que foi digitado. Então a função que faz isso está dentro do programa, precisamos localizá-la e copiá-la para o nosso keygen.
Copiar como? Existem algumas formas de fazer isso. Uma forma é tentar entender o Assembly linha a linha e depois reescrever o código com a linguagem de programação predileta.
Outra forma é copiar o Assembly do jeito que está e colar dentro de nosso programa (Assembly Inline) com pequenas adaptações, isso nos poupa de tentar entender o que o código faz. Essa técnica é chamada de Ripping. Vejamos as duas.
Reescrevendo o Código
Essa solução foi fornecida pelo Gustavo, ele leu o artigo anterior e deixou um comentário com uma solução de keygen em Python. O código dele funciona perfeitamente, conseguiu entender o códigos em Assembly e em poucas linhas rescreveu toda a lógica em Python.
O código dele é esse:
import sys
acumula=0
c=0
while c<len(sys.argv[1]):
acumula=acumula+ord(sys.argv[1][c])
c=c+1
print (acumula*0x78)*0x78
No Python não é preciso compilar, é necessário apenas ter o interpretador instalado no computador, existem versões para Linux e Windows, mais informações no site oficial do projeto python.org.
Copie o código, salve em um arquivo com o nome de keygen.py por exemplo e depois execute a linha de comando:
python keygen.py nomedesejado
Como podemos ver abaixo, para o nome “crimes ciberneticos” gerou o mesmo Serial que pescamos anteriormente.
Assembly Inline - Ripping
Na técnica de Ripping precisamos localizar no Assembly do programa apenas as linhas relativas à geração do Serial. Depois copiamos o trecho e colamos em um programa que aceite códigos Assembly Inline. Nesse caso utilizarei a linguagem C.
Na instrução:
00401A71 MOV DWORD PTR SS:[EBP-30],EAX
Vimos que o Nome que digitamos é atribuído a um endereço na Pilha (Stack) que é o EBP-30. Então sabemos que todas as linhas que fizerem referência ao EBP-30 estará lidando com o Nome que digitamos.
No Offset 00401A7C também é lido o Serial que digitamos e atribuído a um endereço na Pilha, esse valor não tem utilidade para nosso keygen, o que importa para nós são as linhas após esse Offset.
O código que se segue é esse:
00401A7F | XOR EDX,EDX
00401A81 | MOV DWORD PTR SS:[EBP-38],EDX
00401A84 | XOR ECX,ECX
00401A86 | MOV DWORD PTR SS:[EBP-3C],ECX
00401A89 | JMP SHORT CrackMe.00401A9B
00401A8B | MOV EAX,DWORD PTR SS:[EBP-30]
00401A8E | MOV EDX,DWORD PTR SS:[EBP-3C]
00401A91 | MOVSX ECX,BYTE PTR DS:[EAX+EDX]
00401A95 | ADD DWORD PTR SS:[EBP-38],ECX
00401A98 | INC DWORD PTR SS:[EBP-3C]
00401A9B | MOV EAX,DWORD PTR SS:[EBP-30]
00401A9E | MOV EDX,DWORD PTR SS:[EBP-3C]
00401AA1 | CMP BYTE PTR DS:[EAX+EDX],0
00401AA5 | JNZ SHORT CrackMe.00401A8B
00401AA7 | IMUL ECX,DWORD PTR SS:[EBP-38],78
00401AAB | MOV DWORD PTR SS:[EBP-38],ECX
00401AAE | IMUL EAX,DWORD PTR SS:[EBP-38],78
00401AB2 | MOV DWORD PTR SS:[EBP-38],EAX
A vantagem do Ripping é que não precisamos entender o que todas essas linhas fazem para criarmos nosso keygen, basta copiarmos e fazermos algumas adaptações.
Além do endereço da Pilha EBP-30 que já sabemos se tratar do Nome digitado, vemos também no código os endereços EBP-38 e EBP-3C. No início do código vemos que os registradores EDX e ECX são zerados através do XOR e depois atribuídos a esses dois endereços da Pilha.
Isso quer dizer que possívelmente são duas variáveis que são zeradas no início da função. Sendo assim no nosso código Assembly Inline vamos substituir:
EBP-30 pela variável “Nome”.
EBP-38 pela variável “var38”.
EBP-3C pela variável “var3C”.
Essas são as primeiras adaptações que faremos no código.
No programa em C não teremos endereços de Offset, retiramos todos, mas isso causa problema com os Jumps no Assembly que usam os Offsets para desviar a execução do código. Para resolvermos isso inserimos rótulos nos locais originais onde o programa “pula” com o Jumps. A compreensão disso ficará mais clara logo abaixo quando eu aprensentar o código final.
Um último detalhe é que o C só entende valores hexadecimais com a máscara 0x999, então no código Assembly onde está o valor 78 substituiremos por 0x78.
Agora colocando em prática tudo o que foi dito, nosso keygen em C com código Assembly Inline ficará assim:
int gerarSerial(char* nome)
{
int var3c;
int var38;
int serial = 0;
asm
{
XOR EDX,EDX
MOV DWORD PTR SS:[var38],EDX
XOR ECX,ECX
MOV DWORD PTR SS:[var3c],ECX
JMP SHORT Jump1
Loop1:
MOV EAX,DWORD PTR SS:[nome]
MOV EDX,DWORD PTR SS:[var3c]
MOVSX ECX,BYTE PTR DS:[EAX+EDX]
ADD DWORD PTR SS:[var38],ECX
INC DWORD PTR SS:[var3c]
Jump1:
MOV EAX,DWORD PTR SS:[nome]
MOV EDX,DWORD PTR SS:[var3c]
CMP BYTE PTR DS:[EAX+EDX],0
JNZ SHORT Loop1
IMUL ECX,DWORD PTR SS:[var38],0x78
MOV DWORD PTR SS:[var38],ECX
IMUL EAX,DWORD PTR SS:[var38],0x78
MOV DWORD PTR SS:[serial],EAX
}
return serial;
}
int main(int argc, char* argv[])
{
printf("KeyGen CrackMe - crimesciberneticos.com\n\n");
if(argc>1){
printf("Nome: %s\n", argv[1]);
printf("Serial: %u\n", gerarSerial(argv[1]));
}
else{
printf("Use: keygen.exe <Nome>\n");
}
return 0;
}
Esse código foi compilado com êxito no Borland C++ Builder. Creio que também funcionará nos compiladores da Microsoft e em outros que utilizam o Assembly na sintaxe Intel.
Infelizmente o GCC e seus derivados como o Bloodshed Dev-C++ utilizam a sintaxe AT&T, então nesses compiladores o código não funcionará, seria necessário convertê-lo para a sintaxe AT&T. Um exemplo de como isso pode ser feito pode ser encontrado aqui: GCC-Inline-Assembly-HOWTO.
Como podemos ver o código Assembly é tratado dentro do C como se fosse uma função normal, inclusive pode-se combinar as variáveis das duas linguagens.
O keygen já compilado pode ser baixado aqui. Para utilizá-lo execute a linha de comando:
keygen.exe nomedesejado
Conclusão
Vimos nesse artigo mais uma técnica de cracking. Muitos mais do que aprender a crackear um programa esses conhecimentos podem ser úteis para protegê-lo. Ainda, o conhecimento de Assembly e engenharia reversa pode ser aproveitado em muitas áreas da segurança da informação.
Não são todos os softwares que possuem essa lógica de validação de Serial, mas muitos utilizam algo parecido.
O código original da função “compare” em C++ é esse:
bool TForm1::compare(){
String edit = Edit1->Text;
String edit2= Edit2->Text;
char* n = edit.c_str();
char* n2= edit2.c_str();
int s = 0;
for(int i=0;n[i]!=0;i++){
s+=n[i];
}
s = s * 5 * 4 * 3 * 2;
s = s * 5 * 4 * 3 * 2;
char saida[100];
sprintf(saida, "%d", s);
if(strcmp(saida,n2)==0)
return true;
else
return false;
}
Dúvidas, sugestões? Deixe um comentário. :)