Résoudre un Crackme simple pour Sega Mega Drive

Bonjour à tous



Malgré ma grande expérience dans les jeux inversés pour Sega Mega Drive , je n'ai jamais décidé de craquer pour cela, et ils ne m'ont pas été rencontrés sur Internet. Mais, l'autre jour, il y avait un drôle de crackie qui voulait résoudre. Je partage avec vous la décision ...


La description


La description de la tâche et le rhum lui-même peuvent être téléchargés ici .


Malgré le fait que la liste des ressources indique Hydra, le standard de facto parmi les outils pour le débogage et l'inversion de jeux sur Sega est Smd Ida Tools . Il a tout ce dont vous avez besoin pour résoudre cette crème:


  • Chargeur de rhum pour Ida
  • Débogueur
  • Afficher et modifier la mémoire RAM / VDP
  • Afficher des informations presque complètes sur VDP

Nous déposons la dernière version dans les plugins pour Ide et commençons à regarder ce que nous avons.


Solution


Le lancement de tout jeu Shogi commence par l'exécution du vecteur Reset . Un pointeur peut être trouvé dans le deuxième DWORD depuis le début du rhum.




Nous voyons quelques fonctions non identifiées commençant à l'adresse 0x27A . Voyons ce qu'il y a.


sub_2EA ()



D'après ma propre expérience, je dirai que cela ressemble généralement à la fonction d'attente de la VBLANK interruption VBLANK . Voyons où il y a d'autres appels à la variable byte_FF0026 :



Nous voyons que le bit zéro est juste défini dans l'interruption VBLANK . Nous appelons donc la variable vblank_ready , et la fonction où elle est vérifiée est wait_for_vblank .


sub_60E ()


Ensuite, la fonction sub_60E est appelée par code. Voyons ce qu'il y a:



Ce que la première commande écrit dans le VDP_CTRL est la commande de contrôle VDP . Pour savoir ce qu'elle fait, nous maintenons cette commande et appuyons sur la touche J :



Nous voyons que l'entrée dans CRAM (l'endroit où les palettes sont stockées) est initialisée. Cela signifie que tout le code de fonction suivant définit simplement une sorte de palette initiale. Par conséquent, la fonction peut être appelée init_cram .


sub_71A ()



Nous voyons que certaines commandes sont à nouveau transférées vers VDP_CTRL , puis appuyez à nouveau sur J et découvrez que cette commande initialise l'enregistrement dans la mémoire vidéo:



De plus, pour comprendre ce qui y est transféré dans la mémoire vidéo, cela n'a aucun sens. Par conséquent, nous appelons simplement la fonction load_vdp_data .


sub_C60 ()


Presque la même chose se produit ici que dans la fonction précédente, donc sans entrer dans les détails, nous appelons simplement la fonction load_vdp_data2 .


sub_8DA ()


Il y a déjà plus de code. Et d'ailleurs, une autre fonction est appelée dans cette fonction. Regardons juste là - dans sub_D08 .


sub_D08 ()



Nous voyons que dans le registre D0 commande pour VDP_CTRL , dans D1 - la valeur que VRAM remplira, et dans D2 et D3 - la largeur et la hauteur du remplissage (car il s'avère que deux cycles: interne et externe). Appelez la fonction fill_vram_by_addr.


sub_8DA ()


Nous revenons à la fonction précédente. Une fois la valeur du registre D0 transmise sous forme de commande pour VDP_CTRL , appuyez sur la touche J de la valeur. Nous obtenons:



Encore une fois, de l'expérience de l'inversion de jeux à Sega, je peux dire que cette commande initialise l'enregistrement des tuiles de mappage. Les adresses qui commencent sur $Fxxx , $Exxx , $Dxxx , $Cxxx dans 90% des cas seront des adresses de régions avec ces mêmes mappages. Que sont les mappages:
ce sont les valeurs avec lesquelles vous pouvez spécifier où afficher telle ou telle tuile à l'écran (une tuile est un carré de 8x8 pixels).


La fonction peut donc être appelée en tant que init_tile_mappings .


sub_CDC ()



La première commande initialise l'enregistrement à l'adresse $F000 . Une remarque: parmi les adresses du " mapping ", il y a encore une région où est stockée la table des sprites (ce sont leurs positions, les tuiles vers lesquelles elles pointent, etc.) Découvrez quelle région est responsable de ce qui peut être débogué. Mais pour l'instant, nous n'en avons pas besoin, appelons donc la fonction init_other_mappings .


De plus, nous voyons que dans cette fonction deux variables sont initialisées: word_FF000A et word_FF000C . D'après ma propre expérience (oui, il décide), je dirai que si deux variables sont proches dans l'espace d'adressage et sont associées au mappage, alors dans la plupart des cas, ce seront les coordonnées d'un objet (par exemple, un sprite). Par conséquent, je suggère de les appeler sprite_pos_x et sprite_pos_y . L'erreur en x et y admissible car plus loin sous le débogage, il sera facile à corriger.


VBLANK


Puisque la boucle va plus loin dans le code, nous pouvons supposer que nous avons terminé l'initialisation de base. Vous pouvez maintenant regarder l'interruption VBLANK .



On voit que deux variables s'incrémentent (ce qui est étrange, dans la liste des liens vers chacune d'elles, elle est absolument vide). Mais, puisqu'ils sont mis à jour une fois par trame, vous pouvez les appeler timer2 et timer2 .


Ensuite, la fonction sub_2FE est sub_2FE . Voyons ce qu'il y a:


sub_2FE ()



Et là - travaillez avec le port IO_CT1_DATA (responsable du premier joystick). L'adresse du port est chargée dans le registre A0 et transmise à la fonction sub_310 . On y va:


sub_310 ()



Mon expérience m'aide à nouveau. Si vous voyez le code qui fonctionne avec le joystick et deux variables en mémoire, alors l'une stocke pressed keys et la seconde contient les held keys , c.-à-d. touches enfoncées et maintenues. pressed_keys ces variables: pressed_keys et held_keys . Et puis la fonction peut être appelée en tant que update_joypad_state .


sub_2FE ()


Appelez la fonction en tant que read_joypad .


Boucle de gestionnaire


Maintenant, tout semble beaucoup plus clair:



Ce cycle répond donc aux touches enfoncées et effectue les actions correspondantes. Passons en revue chacune des fonctions appelées dans la boucle.


sub_4D4 ()



Il y a beaucoup de code. Commençons par la première fonction appelée: sub_60C .


sub_60C ()


Elle ne fait rien - cela peut sembler ainsi au premier abord. Le retour de la fonction actuelle est rts . Mais, parce que seuls des sauts ( bsr ) s'y produisent, ce qui signifie que rts nous ramènera à la boucle du gestionnaire. J'appellerais cette fonction comme retn_to_loop .


sub_4D4 ()


Ensuite, nous voyons l'appel à la variable word_FF000E . Il n'est utilisé nulle part sauf pour la fonction actuelle et, au début, son objectif n'était pas clair pour moi. Mais, si vous regardez de plus près, nous pouvons supposer que cette variable n'est nécessaire que pour un petit délai entre le traitement des frappes. ( Il est déjà mal implémenté dans ce rhum, mais je pense que sans cette variable ce serait bien pire ).



Ensuite, nous avons une grande quantité de code qui traite en quelque sorte les sprite_pos_y sprite_pos_x et sprite_pos_y , qui ne peuvent dire qu'une chose - cela est nécessaire pour afficher le sprite de sélection autour du caractère sélectionné dans l'alphabet.


Vous pouvez donc désormais nommer la fonction en toute sécurité update_selection . Continuons.



Le code vérifie si les bits de certaines touches enfoncées sont définis et appelle certaines fonctions. Regardons-les.


sub_D28 ()



Une sorte de magie chamanique. Tout d'abord, le WORD est tiré de la variable word_FF0018 , puis une instruction intéressante est exécutée:


 bsr.w *+4 

Cette commande passe simplement à l'instruction qui la suit.


Vient ensuite une autre magie:


 move.l d0,(sp) rts 

La valeur du registre D0 est placée en haut de la pile. Il convient de noter que, pour Shogi, ainsi que pour certains x86 , l'adresse de retour de la fonction lorsqu'elle est appelée est placée en haut de la pile. Par conséquent, la première instruction place une adresse en haut et la seconde la soulève de la pile et effectue une transition le long de celle-ci. Bon tour .


Vous devez maintenant comprendre quelle est cette valeur dans la variable, qui passe ensuite. Mais d'abord, appelons cette variable jmp_addr .


Et les fonctions seront appelées ceci:


  • sub_D38 : goto_to_d0
  • sub_D28 : jump_to_var_addr

jmp_addr


Découvrez où cette variable est remplie. Nous regardons la liste des références:



Il n'y a qu'un seul endroit pour écrire dans cette variable. Regardons-le.


sub_3A4 ()



Ici, en fonction des coordonnées du sprite (rappelez-vous qu'il s'agit très probablement de l'adresse du caractère sélectionné), telle ou telle valeur est saisie. Nous voyons la section de code suivante:



La valeur existante est décalée vers la droite de 4 bits, une nouvelle valeur est placée dans l'octet de poids faible et le résultat est à nouveau entré dans la variable. En théorie, notre variable jmp_addr stocke les caractères que nous pouvons saisir sur l'écran de saisie des touches. Notez également que la taille de la variable est WORD .


En fait, la fonction sub_3A4 peut être appelée update_jmp_addr .


sub_414 ()


Il ne nous reste plus qu'une fonction dans la boucle, qui n'est pas reconnue. Et cela s'appelle sub_414 .



Son code ressemble au code de la fonction update_jmp_addr , seulement à la fin nous avons un sub_45E fonction sub_45E . Regardons là-bas.


sub_45E ()



Nous voyons que le numéro #$4B1E2003 entré dans le registre D0 , qui est ensuite envoyé à VDP_CTRL , ce qui signifie que nous avons affaire à une autre commande de contrôle VDP . Nous $Cxxx sur J , nous recevons une commande d'enregistrement dans la région avec le mappage $Cxxx .


Ensuite, le code fonctionne avec la variable byte_FF0014 , qui n'est utilisée nulle part sauf la fonction actuelle. Si vous regardez attentivement comment il est utilisé, vous remarquerez que le nombre maximum pouvant y être installé est de 4 . J'ai l'hypothèse que c'est la longueur actuelle de la clé entrée. Voyons ça.


Exécutez le débogueur


J'utiliserai le débogueur de Smd Ida Tools , mais, en fait, quelques Gens KMod ou Gens ReRecording suffiront. L'essentiel est qu'il existe une fonctionnalité avec l'affichage des adresses en mémoire.



Ma théorie a été confirmée. Ainsi, la variable byte_FF0014 peut maintenant être key_length .


Il existe une autre variable: dword_FF0010 , qui est également utilisé uniquement dans la fonction actuelle, et son contenu, après avoir été ajouté à la commande initiale dans D0 (rappel, c'était le numéro #$4B1E2003 ), est envoyé à VDP_CTRL . Sans y réfléchir à add_to_vdp_cmd , j'ai nommé la variable add_to_vdp_cmd .


Alors, que fait cette fonction? Je suppose qu'elle dessine le caractère entré. La vérification est simple - en lançant le débogueur et en comparant l'état avant d'appeler la fonction sub_45E et après:


À:



Après:



J'avais raison - cette fonction dessine le caractère entré. Nous l'appelons do_draw_input_char , et la fonction qui l'appelle ( sub_414 ) est draw_input_char .


Et maintenant


Vérifions maintenant que la variable que nous avons appelée jmp_addr stocke vraiment la clé entrée. Nous utiliserons la même Memory Watch :



Comme vous pouvez le voir, la conjecture était vraie. Qu'est-ce que cela nous donne? Nous pouvons sauter à n'importe quelle adresse. Mais lequel? Dans la liste des fonctions, toutes sont triées après tout:



Ensuite, j'ai commencé à faire défiler le code jusqu'à ce que je trouve ceci:



L'œil entraîné a vu la séquence de $4E, $75 à la fin des octets non alloués. Il s'agit de l'opcode de l'instruction rts , c'est-à-dire retour de fonction. Ces octets non alloués peuvent donc être le code d'une fonction. Essayons de les désigner comme un code, appuyez sur C :



Évidemment, c'est un code de fonction. Vous pouvez également appuyer sur P pour faire du code une fonction. Rappelez-vous ce nom: sub_D3C .


Puis la pensée surgit: et si vous sautez sur sub_D3C ? Cela semble bien, même si un seul saut ici ne sera évidemment pas suffisant, car il n'y avait plus de lien vers la variable word_FF0020 .


Puis une autre pensée m'est venue: et si nous cherchions un autre code non alloué? Ouvrez la boîte de dialogue de Binary search (Alt + B), entrez-y la séquence 4E 75 , cochez la case Find all occurrences :



Cliquez sur pour lancer la recherche, nous obtenons les résultats suivants.



Au moins deux autres emplacements dans le rhum peuvent contenir un code de fonction, vous devez les vérifier. Nous cliquons sur la première des options, faisons défiler un peu vers le haut et nous voyons à nouveau une séquence d'octets non définis. Les dénoter en fonction? Oui! Appuyez sur P où les octets commencent:



Cool! Nous avons sub_34C fonction sub_34C . Nous essayons de répéter la même chose avec la dernière des options trouvées, et ... nous obtenons une déception. Il y a tellement d'octets avant 4E 75 qu'il n'est pas clair où la fonction démarre. Et, évidemment, tous ces octets ci-dessus ne sont pas du code, car beaucoup d'octets en double.


Déterminer le début de la fonction


Il nous sera plus facile de trouver le début de la fonction si nous trouvons où se terminent les données. Comment faire En fait pas du tout compliqué:


  1. On se tord avant le début des données (il y aura un lien vers elles depuis le code)
  2. Nous suivons le lien et recherchons un cycle dans lequel la taille de ces données devrait apparaître
  3. Marquer le tableau

Donc, nous effectuons le premier paragraphe ...:



... et nous voyons immédiatement que dans un cycle de notre tableau 4 octets de données sont copiés à la fois (parce que move.l ) vers VDP_DATA . Ensuite, nous voyons le nombre 2047 . Au début, il peut sembler que la taille finale du tableau est 2047 * 4 , mais la boucle basée sur dbf exécute +1 itération de plus, car La dernière valeur comparée n'est pas 0 , mais -1 .


Total: la taille du tableau est de 2048 * 4 = 8192 . Indique les octets sous forme de tableau. Pour ce faire, cliquez sur * et spécifiez la taille:



Nous tournons jusqu'à la fin du tableau, et nous y voyons des octets, qui sont exactement les octets du code:




Nous avons sub_2D86 fonction sub_2D86 , et nous avons tout pour résoudre cette fissure! Voyons ce que fait la fonction nouvellement créée.


sub_2D86 ()


Et il met simplement la valeur #$4147 dans le registre D1 et appelle la fonction sub_34C . Regardez-la.


sub_34C ()



On voit qu'ici la valeur de la variable word_FF0020 est word_FF0020 . Si vous regardez les liens vers celui-ci, nous verrons un autre endroit où l'enregistrement dans cette variable a lieu, et ce sera exactement l'endroit où je voulais sauter à travers la variable jmp_addr . Cela confirme l'intuition dont sub_D3C absolument besoin pour passer à sub_D3C .


Mais ce qui s'est passé ensuite était trop paresseux pour que je puisse comprendre, alors j'ai jeté le rhum dans GHIDRA , trouvé cette fonction et regardé le code décompilé:


 void FUN_0000034c(void) { ushort in_D1w; short sVar1; ushort *puVar2; if (((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,in_D1w ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; } 

On voit que la variable avec le nom étrange in_D1w , ainsi que la variable DAT_00ff0020 , qui avec son adresse ressemble au word_FF0020 mentionné word_FF0020 - word_FF0020 .


in_D1w nous indique que cette valeur est tirée du registre D1 , ou plutôt de sa moitié WORD plus jeune, et définit le registre D1 fonction qui le transmet. Rappelez-vous #$4147 ? Vous devez donc désigner ce registre comme argument d'entrée de la fonction.


Pour ce faire, dans la fenêtre avec le code décompilé, cliquez avec le bouton droit sur le nom de la fonction et sélectionnez l'élément de menu Edit Function Signature :



Afin d'indiquer que la fonction prend un argument à travers un registre spécifique, à savoir, pas par la méthode standard pour la convention d'appel actuelle, vous devez cocher la Use Custom Storage et cliquer sur l'icône avec un plus vert :



Une position pour le nouvel argument d'entrée apparaît. On double-clique dessus et on obtient une boîte de dialogue indiquant le type et le support de l'argument:



Dans le code décompilé, nous voyons que in_D1w est de type ushort , ce qui signifie que nous le spécifierons dans le champ type. Cliquez ensuite sur le bouton Add :



Une position apparaîtra pour indiquer le support de l'argument, nous devons spécifier le registre D1w dans Location , puis cliquez sur OK :



Le code décompilé prendra la forme:


 void FUN_0000034c(ushort param_1) { short sVar1; ushort *puVar2; if (((ushort)(param_1 ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(param_1 ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,param_1 ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; } 

Nous param_1 que notre valeur param_1 est constante, transmise par la fonction appelante et est égale à #$4147 . Alors quelle devrait être la valeur de DAT_00ff0020 ? Nous considérons:


 0x4147 ^ DAT_00ff0020 ^ 0x5e4e = 0x5a5a 0x4147 ^ DAT_00ff0020 ^ 0x4a44 = 0x4e50 

Parce que xor - l'opération est réversible, tous les nombres constants peuvent être disputés entre eux et obtenir la valeur souhaitée de la variable DAT_00ff0020 .


 DAT_00ff0020 = 0x4147 ^ 0x5e4e ^ 0x5a5a = 0x4553 DAT_00ff0020 = 0x4147 ^ 0x4a44 ^ 0x4e50 = 0x4553 

Il s'avère que la valeur de la variable doit être 0x4553 . Il semble que j'ai déjà vu un endroit où une telle valeur est définie ...



Conclusions et décision


Nous arrivons aux résultats suivants:


  1. Vous devez d'abord passer à l'adresse 0x0D3C , pour cela, vous devez entrer le code 0D3C
  2. 0x2D86 à la fonction à 0x2D86 , qui définit la valeur de D1 pour enregistrer #$4147 , pour cela, vous devez entrer le code 2D86

Expérimentalement, nous trouvons la touche qui doit être pressée pour vérifier la touche entrée: B Nous essayons:



Je vous remercie!

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


All Articles