Briser une simple «fissure» avec Ghidra - Partie 1

Beaucoup de gens savent probablement déjà de quel genre de bête il s'agit - Ghidra ("Hydra") et ce qu'il mange avec le programme de première main, bien que cet outil n'ait été rendu public que récemment - en mars de cette année. Je ne dérangerai pas les lecteurs avec une description d'Hydra, de ses fonctionnalités, etc. Ceux qui sont dans le sujet, j'en suis sûr, ont déjà étudié tout cela eux-mêmes, et ceux qui ne sont pas encore dans le sujet - ils peuvent le faire à tout moment, car maintenant il est facile de trouver des informations détaillées sur Internet. Soit dit en passant, l'un des aspects d'Hydra (le développement de plugins pour celui-ci) a déjà été couvert sur Habré (excellent article!) Je ne donnerai que les liens principaux:


Hydra est donc un désassembleur et décompilateur interactif multiplateforme gratuit avec une structure modulaire, avec la prise en charge de presque toutes les principales architectures CPU et une interface graphique flexible pour travailler avec du code désassemblé, de la mémoire, du code récupéré (décompilé), des symboles de débogage, et bien plus encore .

Essayons de casser quelque chose avec cette Hydra!

Étape 1. Trouvez et étudiez la fissure


En tant que «victime», nous trouvons un simple programme «crackme». Je suis juste allé sur crackmes.one , indiqué dans la recherche le niveau de difficulté = 2-3 ("simple" et "moyen"), la langue source du programme = "C / C ++" et la plateforme = "Multiplateforme", comme dans la capture d'écran ci-dessous:



La recherche a donné 2 résultats (en vert ci-dessous). La première fissure s'est avérée être 16 bits et n'a pas démarré sur mon Win10 64 bits, mais la seconde ( level_2 by seveb ) est apparue. Vous pouvez le télécharger à partir de ce lien .

Téléchargez et décompressez la fissure; Le mot de passe pour l'archive, tel qu'indiqué sur le site, est crackmes.de . Dans l'archive, nous trouvons deux répertoires correspondant à Linux et Windows. Sur ma machine, je vais dans le répertoire Windows et j'y rencontre le seul "exécutable" - level_2.exe . Courons et voyons ce qu'elle veut:



Cela ressemble à une déception! Au démarrage, le programme n'affiche rien. Nous essayons de l'exécuter à nouveau, en lui passant une chaîne arbitraire comme paramètre (tout d'un coup, attend-il une clé?) - et encore rien ... Mais ne désespérez pas. Supposons que nous devions également trouver les paramètres de lancement en tant que tâche! Il est temps de découvrir notre "couteau suisse" - Hydra.

Étape 2. Création d'un projet dans Hydra et analyse préliminaire


Supposons que Hydra soit déjà installé. Sinon, tout est simple.

Installer Ghidra
1) installez JDK version 11 ou supérieure (j'en ai 12 )

2) téléchargez Hydra (par exemple, d'ici ) et installez-le (au moment de la rédaction, la dernière version d'Hydra est 9.0.2, j'ai 9.0.1)

Nous lançons Hydra et dans le gestionnaire de projet ouvert, créons immédiatement un nouveau projet; Je lui ai donné le nom de crackme3 (c'est-à-dire que des projets crackme et crackme2 ont déjà été créés pour moi). Le projet est, en fait, un répertoire de fichiers, vous pouvez y ajouter tous les fichiers à étudier (exe, dll, etc.). Nous ajouterons immédiatement notre level_2.exe ( Fichier | Importer ou simplement la clé I ):



Nous voyons qu'avant l'importation, Hydra a identifié notre charlatan expérimental comme un PE 32 bits (exécutable portable) pour le système d'exploitation Win32 et la plate-forme x86. Après l'importation, nous attendons encore plus d'informations:



Ici, en plus de la profondeur de bits susmentionnée, nous pouvons toujours être intéressés par l' ordre d'endianness , qui dans notre cas est peu (de bas à élevé), qui était attendu pour la plate-forme Intel 86e.

Avec une analyse préliminaire, nous avons terminé.

Étape 3. Effectuez une analyse automatique


Il est temps de commencer une analyse entièrement automatique du programme dans Hydra. Cela se fait en double-cliquant sur le fichier correspondant (level_2.exe). Ayant une structure modulaire, Hydra fournit toutes ses fonctionnalités de base avec un système de plug-in qui peut être ajouté / désactivé ou développé indépendamment. Il en va de même pour l'analyse - chaque plugin est responsable de son type d'analyse. Par conséquent, tout d'abord, nous sommes confrontés à cette fenêtre où vous pouvez sélectionner les types d'analyse d'intérêt:

Fenêtre Paramètres d'analyse

Pour nos besoins, il est logique de laisser les paramètres par défaut et d'exécuter l'analyse. L'analyse elle-même est effectuée assez rapidement (cela m'a pris environ 7 secondes), bien que les utilisateurs des forums se plaignent que pour les grands projets, Hydra perd de la vitesse par rapport à IDA Pro . Cela peut être vrai, mais pour les petits fichiers, cette différence n'est pas significative.

Ainsi, l'analyse est terminée. Ses résultats sont affichés dans la fenêtre du navigateur de code:



Cette fenêtre est la principale pour travailler dans Hydra, vous devez donc l'étudier plus attentivement.

Présentation de l'interface du navigateur de code
Les paramètres d'interface par défaut divisent la fenêtre en trois parties.

Dans la partie centrale se trouve la fenêtre principale - une liste du désassembleur, qui est plus ou moins similaire à ses "frères" dans IDA, OllyDbg, etc. Par défaut, les colonnes de cette liste sont (de gauche à droite): adresse mémoire, opcode de la commande, commande ASM, paramètres de la commande ASM, référence croisée (le cas échéant). Naturellement, l'affichage peut être modifié en cliquant sur le bouton sous la forme d'un mur de briques dans la barre d'outils de cette fenêtre. Pour être honnête, je n'ai jamais vu une telle configuration flexible de la sortie du démonteur nulle part, c'est extrêmement pratique.

Dans la partie gauche du panneau 3:

  1. Sections du programme (cliquez sur la souris pour parcourir les sections)
  2. Arbre de caractères (importations, exportations, fonctions, en-têtes, etc.)
  3. Arbre de type des variables utilisées

Pour nous, la fenêtre la plus utile ici est une arborescence de symboles, qui vous permet de trouver rapidement, par exemple, une fonction par son nom et d'aller à l'adresse correspondante.

Sur le côté droit est une liste du code décompilé (dans notre cas, en C).

En plus des fenêtres par défaut, dans le menu Fenêtre , vous pouvez sélectionner et placer des dizaines d'autres fenêtres et affichages n'importe où dans le navigateur. Pour plus de commodité, j'ai ajouté une fenêtre d'octets et une fenêtre avec un graphique de fonction au centre, et à droite, des variables de chaîne (chaînes) et un tableau de fonctions (fonctions). Ces fenêtres sont désormais disponibles dans des onglets séparés. De plus, toutes les fenêtres peuvent être détachées et rendues "flottantes", en les plaçant et en les redimensionnant à votre discrétion - c'est aussi une solution très réfléchie, à mon avis.

Étape 4. Apprentissage de l'algorithme du programme - fonction main ()


Eh bien, procédons à une analyse directe de nos programmes de crack. Dans la plupart des cas, vous devez commencer par rechercher le point d'entrée du programme, c'est-à-dire La fonction principale qui est appelée au démarrage. Sachant que notre crack a été écrit en C / C ++, nous supposons que le nom de la fonction principale sera main () ou quelque chose comme ça :) C'est dit et fait. Entrez "main" dans le filtre de l'Arbre des Symboles (dans le panneau de gauche) et voyez la fonction _main () dans la section Fonctions . Allez-y avec un clic de souris.

Présentation de la fonction main () et changement de nom des fonctions obscures


Dans la liste des désassembleurs, la section de code correspondante est immédiatement affichée et à droite, nous voyons le code C décompilé de cette fonction. Il convient de noter une autre caractéristique pratique de la synchronisation Hydra - sélection: lorsqu'une souris sélectionne une plage de commandes ASM, la section de code correspondante dans le décompilateur est mise en surbrillance et vice versa. De plus, si la fenêtre d'affichage de la mémoire est ouverte, l'allocation est synchronisée avec la mémoire. Comme on dit, tout ingénieux est simple!

Immédiatement, je note une caractéristique importante du travail chez Hydra (contrairement, par exemple, au travail chez IDA). Le travail dans Hydra est principalement axé sur l'analyse de code décompilé . Pour cette raison, les créateurs d'Hydra (nous nous souvenons - nous parlons d'espions de la NSA :)) ont accordé une grande attention à la qualité de la décompilation et à la commodité de travailler avec du code. En particulier, on peut simplement aller à la définition des fonctions, des variables et des sections de mémoire en double-cliquant dans le code. De plus, n'importe quelle variable et fonction peut être immédiatement renommée, ce qui est très pratique, car les noms par défaut n'ont pas de sens et peuvent prêter à confusion. Comme vous le verrez plus loin, nous utiliserons souvent ce mécanisme.

Voici donc la fonction main () qu'Hydra a «disséquée» comme suit:

Liste 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; } 


Il semble que tout semble normal - définitions des variables, types C standard, conditions, boucles, appels de fonction. Mais en regardant de plus près le code, nous remarquons que pour une raison quelconque, les noms de certaines fonctions n'ont pas été définis et remplacés par la pseudo- fonction _text () (dans la fenêtre du décompilateur - .text () ). Commençons par définir quelles sont ces fonctions.

Double-cliquer sur le corps du premier appel

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

nous voyons que c'est juste une fonction wrapper autour de la fonction standard calloc () , qui est utilisée pour allouer de la mémoire pour les données. Renommons donc cette fonction en calloc2 () . Positionnez le curseur sur l'en-tête de la fonction, appelez le menu contextuel et sélectionnez Renommer la fonction (touche de raccourci - L ) et entrez un nouveau nom dans le champ qui s'ouvre:



On voit que la fonction a été immédiatement renommée. Nous revenons au corps principal () (le bouton Retour dans la barre d'outils ou Alt + <- ) et nous voyons qu'ici au lieu du mystérieux _text () calloc2 () se trouve déjà. Super!

Nous faisons de même avec toutes les autres fonctions wrapper: nous entrons dans leur définition une par une, voyons ce qu'elles font, renommons (j'ai ajouté l'index 2 aux noms standard des fonctions C) et revenons à la fonction principale.

Nous comprenons le code de la fonction main ()


D'accord, nous avons découvert des fonctions étranges. Nous commençons à étudier le code de la fonction principale. En sautant les déclarations de variables, nous voyons que la fonction renvoie la valeur de la variable iVar2, qui est zéro (signe de succès de la fonction) uniquement si la condition spécifiée par la chaîne est remplie

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

_Argc est le nombre de paramètres de ligne de commande (arguments) passés à main () . Autrement dit, notre programme «mange» 2 arguments (le premier argument, nous nous en souvenons, est toujours le chemin d'accès au fichier exécutable).

OK, passons. Ici, nous créons une chaîne C (tableau de caractères) de 256 caractères:

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

Ensuite, nous avons une boucle de 3 itérations. Dans ce document, nous vérifions d'abord si l'indicateur bVar1 est défini, et si c'est le cas, copiez l'argument (chaîne) de ligne de commande suivant dans _Dest :

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

Cet indicateur est défini lors de l'analyse de l'argument suivant:

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

La première ligne calcule la longueur de cet argument. De plus, la condition vérifie que la longueur de l'argument doit être 2, l'avant-dernier caractère == "-" et le dernier caractère == "f". Notez comment le décompilateur a "traduit" l'extraction des caractères de la chaîne à l'aide d'un masque d'octets.
Les valeurs décimales des nombres, et en même temps les caractères ASCII correspondants, peuvent être espionnés en maintenant le curseur sur le littéral hexadécimal correspondant. Le mappage ASCII ne fonctionne pas toujours (?), Je recommande donc de consulter la table ASCII sur Internet. Vous pouvez également directement dans Hydra convertir des scalaires de n'importe quel système numérique vers un autre (via le menu contextuel -> Convertir ), dans ce cas, ce nombre sera affiché partout dans le système numérique sélectionné (dans le désassembleur et dans le décompilateur); mais personnellement, je préfère laisser des hexagones dans le code pour l'harmonie du travail, car adresses mémoire, décalages, etc. les hexs sont placés partout.
Après la boucle vient ce code:

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

Ici, j'ai immédiatement ajouté des commentaires. Nous vérifions la validité des arguments ("-f path_to_file") et ouvrons le fichier correspondant (le 2ème argument passé, que nous avons copié dans _Dest). Le fichier sera lu au format binaire, comme indiqué par le paramètre "rb" de la fonction fopen () . Si la lecture échoue (par exemple, le fichier n'est pas disponible), un message d'erreur s'affiche dans le flux stderror et le programme se ferme avec le code 1.

Le suivant est le plus intéressant:

  /* !!!     !!! */ 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); 

Le descripteur de fichier ouvert ( _File ) est transmis à la fonction _construct_key () , qui, évidemment, effectue la vérification de la clé recherchée. Cette fonction renvoie un tableau d'octets à deux dimensions ( char ** ), qui est stocké dans la variable ppcVar3 . Si le tableau est vide, le «Nope» concis est affiché sur la console (c'est-à-dire, à notre avis, «Nope!») Et la mémoire est libérée. Sinon (si le tableau n'est pas vide), la clé apparemment correcte s'affiche et la mémoire est également libérée. À la fin de la fonction, le descripteur de fichier se ferme, la mémoire est libérée et la valeur de iVar2 est renvoyée .

Donc, maintenant nous avons réalisé que nous avons besoin de:

1) créez un fichier binaire avec la bonne clé;
2) passer son chemin dans la fissure après l'argument "-f"

Dans la deuxième partie de l'article, nous analyserons la fonction _construct_key () qui, comme nous l'avons découvert, est chargée de vérifier la clé souhaitée dans le fichier.

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


All Articles