Quebrando um simples "crack" com Ghidra - Parte 1

Muitas pessoas provavelmente já sabem em primeira mão que tipo de animal é - Ghidra ("Hydra") e o que ele come com o programa em primeira mão, embora essa ferramenta tenha sido disponibilizada publicamente apenas recentemente - em março deste ano. Não incomodarei os leitores com uma descrição do Hydra, sua funcionalidade etc. Aqueles que estão no tópico, tenho certeza, já estudaram tudo isso, e aqueles que ainda não estão no tópico - podem fazê-lo a qualquer momento, pois agora é fácil encontrar informações detalhadas na Internet. A propósito, um dos aspectos do Hydra (o desenvolvimento de plugins para ele) já foi abordado em Habré (excelente artigo!) Vou dar apenas os principais links:


Portanto, o Hydra é um desmontador e descompilador interativo entre plataformas, com uma estrutura modular, com suporte para quase todas as principais arquiteturas de CPU e uma interface gráfica flexível para trabalhar com código desmontado, memória, código recuperado (descompilado), símbolos de depuração e muito, muito mais .

Vamos tentar quebrar algo com esta Hydra!

Etapa 1. Encontre e estude o crack


Como "vítima", encontramos um programa simples "crackme". Acabei de ir ao crackmes.one , indicado na pesquisa o nível de dificuldade = 2-3 (“simples” e “médio”), o idioma de origem do programa = “C / C ++” e a plataforma = “Multiplataforma”, como na captura de tela abaixo:



A pesquisa retornou 2 resultados (em verde abaixo). O primeiro crack acabou sendo de 16 bits e não foi iniciado no meu Win10 de 64 bits, mas o segundo ( level_2 por seveb ) surgiu. Você pode baixá-lo neste link .

Baixe e descompacte o crack; A senha do arquivo, conforme indicado no site, é crackmes.de . No arquivo, encontramos dois diretórios correspondentes ao Linux e Windows. Na minha máquina, vou ao diretório do Windows e encontro nele o único "executável" - level_2.exe . Vamos correr e ver o que ela quer:



Parece uma chatice! Na inicialização, o programa não exibe nada. Tentamos executá-lo novamente, passando uma string arbitrária como parâmetro (de repente, ele está esperando por uma chave?) - e novamente nada ... Mas não se desespere. Vamos assumir que também precisamos descobrir os parâmetros de inicialização como uma tarefa! É hora de descobrir nossa "faca suíça" - Hydra.

Etapa 2. Criando um projeto no Hydra e análise preliminar


Suponha que você já tenha o Hydra instalado. Se ainda não, então tudo é simples.

Instale o Ghidra
1) instale o JDK versão 11 ou superior (tenho 12 )

2) faça o download do Hydra (por exemplo, daqui ) e instale-o (no momento em que escrevo, a versão mais recente do Hydra é 9.0.2, tenho 9.0.1)

Lançamos o Hydra e, no Gerente de Projeto aberto, criamos imediatamente um novo projeto; Eu dei o nome crackme3 (ou seja, os projetos crackme e crackme2 já foram criados para mim). O projeto é, de fato, um diretório de arquivos, você pode adicionar todos os arquivos para estudar (exe, dll, etc.). Adicionaremos imediatamente nosso level_2.exe ( Arquivo | Importar ou apenas a tecla I ):



Vimos que, antes da importação, a Hydra identificou nosso charlatão experimental como um PE (executável portátil) de 32 bits para os sistemas operacionais Win32 e plataforma x86. Após a importação, estamos aguardando ainda mais informações:



Aqui, além da profundidade de bits acima mencionada, ainda podemos estar interessados ​​na ordem de endianness , que no nosso caso é Little (de baixo a alto byte), que era esperado para a 86ª plataforma da Intel.

Com uma análise preliminar, terminamos.

Etapa 3. Execute a análise automática


Hora de iniciar uma análise automática completa do programa no Hydra. Isso é feito clicando duas vezes no arquivo correspondente (level_2.exe). Com uma estrutura modular, o Hydra fornece toda a sua funcionalidade básica com um sistema de plug-in que pode ser adicionado / desativado ou desenvolvido de forma independente. O mesmo acontece com a análise - cada plug-in é responsável por seu tipo de análise. Portanto, primeiro, somos confrontados com esta janela onde você pode selecionar os tipos de análise de interesse:

Janela Configurações de análise

Para nossos propósitos, faz sentido deixar as configurações padrão e executar a análise. A análise em si é realizada rapidamente (levei cerca de 7 segundos), embora os usuários dos fóruns se queixem de que, para grandes projetos, o Hydra perde em velocidade para o IDA Pro . Isso pode ser verdade, mas para arquivos pequenos essa diferença não é significativa.

Então, a análise está completa. Seus resultados são exibidos na janela Navegador de código:



Esta janela é a principal para trabalhar no Hydra, portanto, você deve estudá-lo com mais cuidado.

Visão geral da interface do navegador de código
As configurações padrão da interface dividem a janela em três partes.

Na parte central está a janela principal - uma lista do desmontador, que é mais ou menos semelhante aos seus "irmãos" na IDA, OllyDbg, etc. Por padrão, as colunas nesta listagem são (da esquerda para a direita): endereço de memória, código de operação do comando, comando ASM, parâmetros do comando ASM, referência cruzada (se aplicável). Naturalmente, a exibição pode ser alterada clicando no botão na forma de uma parede de tijolos na barra de ferramentas desta janela. Para ser sincero, nunca vi uma configuração tão flexível da saída do desmontador em nenhum lugar, é extremamente conveniente.

Na parte esquerda do painel 3:

  1. Seções do programa (clique com o mouse para percorrer as seções)
  2. Árvore de caracteres (importações, exportações, funções, cabeçalhos, etc.)
  3. Árvore de tipos de variáveis ​​usadas

Para nós, a janela mais útil aqui é uma árvore de símbolos, que permite encontrar rapidamente, por exemplo, uma função pelo nome e ir para o endereço correspondente.

No lado direito, há uma lista do código descompilado (no nosso caso, em C).

Além das janelas padrão, no menu Janela, você pode selecionar e colocar dezenas de outras janelas e exibições em qualquer lugar do navegador. Por conveniência, adicionei uma janela Bytes e uma janela com um gráfico de funções ao centro e à direita, variáveis ​​de string (Strings) e uma tabela de funções (Functions). Essas janelas estão agora disponíveis em guias separadas. Além disso, qualquer janela pode ser desanexada e tornada "flutuante", colocando-a e redimensionando-a a seu critério - essa também é uma solução muito atenciosa, na minha opinião,.

Etapa 4. Aprendendo o algoritmo do programa - função main ()


Bem, vamos prosseguir com uma análise direta de nossos programas de crack. Na maioria dos casos, você deve começar pesquisando o ponto de entrada do programa, ou seja, A principal função que é chamada quando é iniciada. Sabendo que nosso crack foi escrito em C / C ++, achamos que o nome da função principal será main () ou algo assim :) Já foi dito e feito. Digite "main" no filtro da Árvore de símbolos (no painel esquerdo) e veja a função _main () na seção Funções . Vá com um clique do mouse.

Visão geral da função main () e renomeação de funções obscuras


Na lista do desmontador, a seção de código correspondente é exibida imediatamente e, à direita, vemos o código C descompilado dessa função. Outro recurso conveniente do Hydra é a sincronização da seleção: quando um mouse seleciona um intervalo de comandos ASM, a seção de código correspondente no descompilador é destacada e vice-versa. Além disso, se a janela de visualização da memória estiver aberta, a alocação será sincronizada com a memória. Como se costuma dizer, tudo engenhoso é simples!

Imediatamente, noto uma característica importante de trabalhar na Hydra (em oposição a, digamos, trabalhar na AID). O trabalho no Hydra é focado principalmente na análise de código descompilado . Por esse motivo, os criadores do Hydra (lembramos - estamos falando de espiões da NSA :)) prestaram muita atenção à qualidade da descompilação e à conveniência de trabalhar com código. Em particular, pode-se simplesmente ir para a definição de funções, variáveis ​​e seções da memória clicando duas vezes no código. Além disso, qualquer variável e função pode ser renomeada imediatamente, o que é muito conveniente, pois os nomes padrão não têm significado e podem ser confusos. Como você verá mais adiante, frequentemente usaremos esse mecanismo.

Então, aqui está a função main () , que Hydra "dissecou" da seguinte maneira:

Listagem main ()
int __cdecl _main(int _Argc,char **_Argv,char **_Env) { bool bVar1; int iVar2; char *_Dest; size_t sVar3; FILE *_File; char **ppcVar4; int local_18; ___main(); if (_Argc == 3) { bVar1 = false; _Dest = (char *)_text(0x100,1); local_18 = 0; while (local_18 < 3) { if (bVar1) { _text(_Dest,0,0x100); _text(_Dest,_Argv[local_18],0x100); break; } sVar3 = _text(_Argv[local_18]); if (((sVar3 == 2) && (((int)*_Argv[local_18] & 0x7fffffffU) == 0x2d)) && (((int)_Argv[local_18][1] & 0x7fffffffU) == 0x66)) { bVar1 = true; } local_18 = local_18 + 1; } if ((bVar1) && (*_Dest != 0)) { _File = _text(_Dest,"rb"); if (_File == (FILE *)0x0) { _text("Failed to open file"); return 1; } ppcVar4 = _construct_key(_File); if (ppcVar4 == (char **)0x0) { _text("Nope."); _free_key((void **)0x0); } else { _text("%s%s%s%s\n",*ppcVar4 + 0x10d,*ppcVar4 + 0x219,*ppcVar4 + 0x325,*ppcVar4 + 0x431); _free_key(ppcVar4); } _text(_File); } _text(_Dest); iVar2 = 0; } else { iVar2 = 1; } return iVar2; } 


Parece que tudo parece normal - definições de variáveis, tipos C padrão, condições, loops, chamadas de função. Mas, olhando mais de perto o código, notamos que, por algum motivo, os nomes de algumas funções não foram definidos e substituídos pela pseudo- função _text () (na janela do descompilador - .text () ). Vamos começar definindo quais são essas funções.

Clique duas vezes no corpo da primeira chamada

  _Dest = (char *)_text(0x100,1); 

vemos que essa é apenas uma função de wrapper em torno da função calloc () padrão, usada para alocar memória para dados. Então, vamos renomear essa função para calloc2 () . Configurando o cursor no cabeçalho da função, chame o menu de contexto e selecione Renomear função (tecla de atalho - L ) e digite um novo nome no campo que se abre:



Vemos que a função foi renomeada imediatamente. Voltamos ao corpo main () (o botão Voltar na barra de ferramentas ou Alt + <- ) e vemos que aqui, em vez do misterioso _text () calloc2 () já está de pé. Ótimo!

Fazemos o mesmo com todas as outras funções do invólucro: analisamos suas definições uma a uma, analisamos o que elas fazem, as renomeia (adicionei o índice 2 aos nomes padrão das funções C) e voltamos à função principal.

Compreendemos o código da função main ()


Ok, descobrimos algumas funções estranhas. Começamos a estudar o código da função principal. Ignorando as declarações da variável, vemos que a função retorna o valor da variável iVar2, que é zero (um sinal de sucesso da função) somente se a condição especificada pela sequência for satisfeita

 if (_Argc == 3) { ... } 

_Argc é o número de parâmetros da linha de comando (argumentos) passados ​​para main () . Ou seja, nosso programa “come” 2 argumentos (o primeiro argumento, lembramos, é sempre o caminho para o arquivo executável).

OK, vamos seguir em frente. Aqui, criamos uma string C (matriz de caracteres ) de 256 caracteres:

 char *_Dest; _Dest = (char *)calloc2(0x100,1); //  new char[256]  C++ 

Em seguida, temos um loop de 3 iterações. Nele, primeiro verificamos se o sinalizador bVar1 está definido e , em caso afirmativo, copiamos o seguinte argumento de linha de comando (string) para _Dest :

 while (i < 3) { /*    .  */ if (bVar1) { /*   */ memset2(_Dest,0,0x100); /*    _Dest    */ strncpy2(_Dest,_Argv[i],0x100); break; } ... } 

Esse sinalizador é definido ao analisar o seguinte argumento:

 n_strlen = strlen2(_Argv[i]); if (((n_strlen == 2) && (((int)*_Argv[i] & 0x7fffffffU) == 0x2d)) && (((int)_Argv[i][1] & 0x7fffffffU) == 0x66)) { bVar1 = true; } 

A primeira linha calcula o comprimento desse argumento. Além disso, a condição verifica se o comprimento do argumento deve ser 2, o penúltimo caractere == "-" e o último caractere == "f". Observe como o decompilador "traduziu" a extração de caracteres da string usando uma máscara de bytes.
Valores decimais de números e, ao mesmo tempo, os caracteres ASCII correspondentes, podem ser espionados mantendo o cursor sobre o literal hexadecimal correspondente. O mapeamento ASCII nem sempre funciona (?); Portanto, recomendo examinar a tabela ASCII na Internet. Você também pode diretamente converter no Hydra escalares de qualquer sistema numérico para outro (via menu de contexto -> Converter ); nesse caso, esse número será exibido em qualquer lugar do sistema numérico selecionado (no desmontador e no descompilador); mas pessoalmente, prefiro deixar azarações no código da harmonia do trabalho, porque endereços de memória, compensações etc. hexágonos são definidos em todos os lugares.
Após o loop, vem este código:

 if ((bVar1) && (*_Dest != 0)) { /*    1) "-f"  2)  -         */ _File = fopen2(_Dest,"rb"); if (_File == (FILE *)0x0) { /*  1    */ perror2("Failed to open file"); return 1; } ... } 

Aqui eu imediatamente adicionei comentários. Verificamos a exatidão dos argumentos ("-f caminho_para_arquivo") e abrimos o arquivo correspondente (o segundo argumento passado, que copiamos para _Dest). O arquivo será lido em formato binário, conforme indicado pelo parâmetro "rb" da função fopen () . Se a leitura falhar (por exemplo, o arquivo estiver indisponível), uma mensagem de erro será exibida no fluxo stderror e o programa sairá com o código 1.

Em seguida é o mais interessante:

  /* !!!     !!! */ ppcVar3 = _construct_key(_File); if (ppcVar3 == (char **)0x0) { /*    ,  "Nope" */ puts2("Nope."); _free_key((void **)0x0); } else { /*    -      */ printf2("%s%s%s%s\n",*ppcVar3 + 0x10d,*ppcVar3 + 0x219,*ppcVar3 + 0x325,*ppcVar3 + 0x431); _free_key(ppcVar3); } fclose2(_File); 

O descritor de arquivo aberto ( _File ) é passado para a função _construct_key () , que, obviamente, executa a verificação da chave procurada. Essa função retorna uma matriz de bytes bidimensional ( char ** ), que é armazenada na variável ppcVar3 . Se a matriz estiver vazia, o "Nope" conciso é exibido no console (ou seja, em nossa opinião, "Nope!") E a memória é liberada. Caso contrário (se a matriz não estiver vazia), a chave aparentemente correta será exibida e a memória também será liberada. No final da função, o descritor de arquivo é fechado, a memória é liberada e o valor de iVar2 é retornado .

Então, agora percebemos que precisamos:

1) crie um arquivo binário com a chave correta;
2) passe seu caminho no crack após o argumento "-f"

Na segunda parte do artigo, analisaremos a função _construct_key () , que, como descobrimos, é responsável por verificar a chave desejada no arquivo.

Source: https://habr.com/ru/post/pt447450/


All Articles