Rétro-ingénierie de l'émulateur NES dans le jeu pour GameCube

image

En cherchant des moyens d'activer les menus du développeur laissés dans Animal Crossing, y compris le menu de sélection de jeu pour l'émulateur NES, j'ai trouvé une fonction intéressante qui existe dans le jeu original et était constamment active, mais n'a jamais été utilisée par Nintendo.

En plus des jeux NES / Famicom en jeu, vous pouvez télécharger de nouveaux jeux NES à partir d'une carte mémoire.

J'ai également réussi à trouver un moyen d'utiliser ce chargeur de démarrage ROM pour patcher mon code et mes données dans le jeu, ce qui vous permet d'exécuter du code via une carte mémoire.

Introduction - Objets de console NES


Les jeux NES ordinaires, qui peuvent être obtenus auprès d'Animal Crossing, sont des meubles séparés sous la forme d'une console NES avec une cartouche posée dessus.

Après avoir localisé cet objet dans votre maison et interagir avec lui, vous pouvez exécuter ce seul jeu. L'image ci-dessous montre Excitebike et Golf.


Il existe également un objet NES Console commun dans lequel il n'y a pas de jeux intégrés. Il peut être acheté auprès de Redd, et parfois obtenu par le biais d'événements aléatoires, par exemple, en lisant sur le tableau d'affichage de la ville que la console est enterrée à un endroit aléatoire de la ville.


Cet objet ressemble à une console NES sur laquelle il n'y a pas de cartouches.


Le problème avec cet objet est qu'il était considéré comme injouable. Chaque fois que vous interagissez avec lui, vous voyez juste un message disant que vous n'avez pas de logiciel de jeu.


Il s'est avéré que cet objet essayait réellement de scanner la carte mémoire pour des fichiers spécialement conçus contenant des images ROM pour NES! L'émulateur NES utilisé pour exécuter des jeux intégrés semble être l'émulateur NES standard complet pour le GameCube, et est capable de lancer la plupart des jeux.

Avant de démontrer ces fonctionnalités, je vais expliquer le processus de rétro-ingénierie.

Rechercher le chargeur de démarrage ROM sur la carte mémoire


Nous recherchons un menu développeur


Au départ, je voulais trouver un code qui active divers menus de développeur, tels que le menu de sélection de carte ou le menu de sélection de jeu pour l'émulateur NES. Le menu Forest Map Select , grâce auquel vous pouvez facilement charger instantanément différents endroits du jeu, était assez simple à trouver - je viens de chercher la ligne FOREST MAP SELECT qui apparaît en haut de l'écran (on peut la voir dans différentes vidéos et captures d'écran sur Internet )

Dans «FOREST MAP SELECT», il existe des références croisées de données à la fonction select_print_wait , ce qui conduit à un tas d'autres fonctions qui ont également le préfixe select_* , y compris la fonction select_init . Ils se sont avérés être des fonctions qui contrôlent le menu de sélection de carte.

La fonction select_init mène à une autre fonction intéressante appelée game_get_next_game_dlftbl . Cette fonction relie tous les autres menus et «scènes» que vous pouvez exécuter: un écran avec le logo Nintendo, l'écran principal, le menu de sélection de carte, le menu d'émulateur NES (Famicom), etc. Il commence au début de la procédure de jeu principale, recherche la fonction d'initialisation de scène à exécuter et trouve son entrée dans la structure de données de la table appelée game_dlftbls . Ce tableau contient des liens vers les fonctions de traitement de diverses scènes, ainsi que d'autres données.


Une étude attentive du premier bloc de la fonction a montré qu'il charge la fonction "next game init", puis commence à le comparer avec une série de fonctions init bien connues:

  • first_game_init
  • select_init
  • play_init
  • second_game_init
  • trademark_init
  • player_select_init
  • save_menu_init
  • famicom_emu_init
  • prenmi_init


L'un des pointeurs de fonction qu'il recherche est famicom_emu_init , qui est responsable de l'exécution de l'émulateur NES / Famicom. game_get_next_game_init résultat game_get_next_game_init à famicom_emu_init ou select_init dans le débogueur Dolphin, j'ai pu afficher des menus spéciaux. L'étape suivante consiste à déterminer comment ces pointeurs sont définis de manière normale lors de l'exécution du programme. La seule chose que fait la fonction game_get_next_game_init est de charger la valeur à l'offset 0xC premier argument dans game_get_next_game_dlftbl .

Garder une trace de ces valeurs définies dans diverses structures de données était un peu ennuyeux, donc je vais aller directement au cœur. La chose la plus importante que j'ai trouvée:

  • Lorsque le jeu démarre de la manière habituelle, il exécute la séquence d'actions suivante:
    • first_game_init
    • second_game_init
    • trademark_init
    • play_init
  • player_select_init définit le prochain init sur select_init . Cet écran devrait vous permettre de sélectionner un joueur immédiatement après avoir choisi une carte, mais il semble que cela ne fonctionne pas correctement.

J'ai également trouvé une fonction sans nom qui définit la fonction init de l'émulateur, mais je n'ai rien trouvé qui définit la fonction init sur la valeur init du joueur ou du choix de carte.

À ce stade, j'ai réalisé que j'avais un autre problème stupide avec la façon dont je chargeais les noms de fonctions dans l'IDA: en raison de l'expression régulière utilisée pour couper les lignes dans le fichier de symboles de débogage, j'ai raté tous les noms de fonctions commençant par une lettre majuscule . La fonction mise en famicom_emu_init ressemblait à des transitions entre les scènes et, bien sûr, s'appelait Game_play_fbdemo_wipe_proc .

Game_play_fbdemo_wipe_proc gère les transitions entre les scènes, telles que les Game_play_fbdemo_wipe_proc d'écran et les pannes.

Dans certaines conditions, la transition d'écran a été effectuée du gameplay habituel à l'affichage de l'émulateur. C'est lui qui a défini la fonction init de l'émulateur.

Gestion des objets de la console


En fait, les gestionnaires d'objets de mobilier pour consoles NES font basculer le gestionnaire de transition d'écran vers l'émulateur. Lorsqu'un joueur interagit avec l'une des consoles, aMR_FamicomEmuCommonMove est aMR_FamicomEmuCommonMove .

Lors de l'appel de la fonction, r6 contient la valeur d'index correspondant aux nombres dans les noms des fichiers de jeu NES dans famicom.arc :

  • 01_nes_cluclu3.bin.szs
  • 02_usa_balloon.nes.szs
  • 03_nes_donkey1_3.bin.szs
  • 04_usa_jr_math.nes.szs
  • 05_pinball_1.nes.szs
  • 06_nes_tennis3.bin.szs
  • 07_usa_golf.nes.szs
  • 08_punch_wh.nes.szs
  • 09_usa_baseball_1.nes.szs
  • 10_cluclu_1.qd.szs
  • 11_usa_donkey3.nes.szs
  • 12_donkeyjr_1.nes.szs
  • 13_soccer.nes.szs
  • 14_exbike.nes.szs
  • 15_usa_wario.nes.szs
  • 16_usa_icecl.nes.szs
  • 17_nes_mario1_2.bin.szs
  • 18_smario_0.nes.szs
  • 19_usa_zelda1_1.nes.szs

( .arc est un format d'archivage de fichiers propriétaire.)

Lorsque r6 pas égal à zéro, il est transmis dans aMR_RequestStartEmu appel à aMR_RequestStartEmu . Dans ce cas, la transition vers l'émulateur est déclenchée.


Cependant, si r6 est nul, la fonction aMR_RequestStartEmu_MemoryC est appelée à la aMR_RequestStartEmu_MemoryC . En définissant la valeur dans le débogueur à 0, j'ai reçu le message «Je n'ai aucun logiciel». Je ne me souvenais pas immédiatement que je devais vérifier l'objet console NES pour m'assurer qu'il réinitialise la valeur r6 , mais il s'est avéré que l'index zéro est utilisé pour l'objet console sans cartouche.

Bien que aMR_RequestStartEmu stocke simplement la valeur d'index dans une sorte de structure de données, aMR_RequestStartEmu_MemoryC effectue des opérations beaucoup plus complexes ...


Ce troisième bloc de code appelle aMR_GetCardFamicomCount et vérifie un résultat différent de zéro, sinon il ignore la plupart des choses intéressantes sur le côté gauche du graphique de fonction.

aMR_GetCardFamicomCount appelle famicom_get_disksystem_titles , qui appelle ensuite memcard_game_list , et ici tout devient très intéressant.

memcard_game_list monte la carte mémoire et commence à se déplacer dans le cycle d'écriture de fichier, en vérifiant chacune de certaines valeurs. En suivant la fonction dans le débogueur, j'ai pu comprendre qu'il comparait les valeurs avec chacun de mes fichiers sur la carte mémoire.


La fonction décide de télécharger ou non le fichier, en fonction des résultats de la vérification de plusieurs lignes. Tout d'abord, il vérifie la présence des lignes «GAFE» et «01», qui sont les identifiants du jeu et de la société. 01 signifie Nintendo, GAFE signifie Animal Crossing. Je pense que cela signifie GameCube Animal Forest English.

Elle vérifie ensuite les lignes «DobutsunomoriP_F_» et «SAVE». Dans ce cas, la première ligne doit correspondre, mais pas la seconde. Il s'est avéré que "DobutsunomoriP_F_SAVE" est le nom du fichier qui stocke les données des jeux intégrés pour NES. Par conséquent, tous les fichiers sauf celui-ci seront chargés avec le préfixe «DobutsunomoriP_F_».

En utilisant le débogueur Dolphin pour ignorer les comparaisons de chaînes avec "SAVE" et en faisant le truc du jeu pour croire que mon fichier "SAVE" peut être téléchargé en toute sécurité, j'ai obtenu ce menu après avoir utilisé la console NES:


J'ai répondu «Oui» et j'ai essayé de charger le fichier de sauvegarde en tant que jeu, après quoi j'ai vu pour la première fois l'écran de blocage du jeu intégré:


Super! Maintenant, je sais qu'elle essaie en fait de télécharger des jeux à partir d'une carte mémoire et je peux commencer à analyser le format des fichiers de sauvegarde pour voir si une vraie ROM peut être téléchargée.

La première chose que j'ai essayé de faire était d'essayer de trouver où le nom du jeu est lu dans le fichier de la carte mémoire. En recherchant la ligne «FEFSC» qui était présente dans le message «Voulez-vous jouer <nom>?», J'ai trouvé l'offset auquel il a été lu dans le fichier: 0x642 . J'ai copié le fichier de sauvegarde, changé le nom du fichier en «DobutsunomoriP_F_TEST», changé les octets à l'offset 0x642 en «TEST» et importé la sauvegarde modifiée, après quoi le nom dont j'avais besoin apparaissait dans le menu.

Après avoir ajouté quelques fichiers supplémentaires dans ce format, quelques options supplémentaires sont apparues dans le menu:


Télécharger la ROM


Si aMR_GetCardFamicomCount retourné différent de zéro, puis la mémoire est allouée sur le tas, famicom_get_disksystem_titles est directement appelé à famicom_get_disksystem_titles , après quoi un tas de décalages aléatoires est spécifié dans la structure de données. Au lieu de déchiffrer où ces valeurs seront lues, j'ai commencé à étudier la liste des fonctions famicom .

Il s'est avéré que j'avais besoin de famicom_rom_load . Il contrôle le chargement de la ROM, soit depuis une carte mémoire, soit depuis les ressources internes du jeu.


La chose la plus importante dans ce bloc "démarrage à partir de la carte mémoire" est qu'il appelle
memcard_game_load . Elle monte à nouveau le fichier sur la carte mémoire, le lit et analyse. C'est là que les options de format de fichier les plus importantes deviennent apparentes.

Valeur de somme de contrôle


La première chose qui se produit après le téléchargement du fichier est le calcul de la somme de contrôle. La fonction calcSum est calcSum , qui est un algorithme très simple qui additionne les valeurs de tous les octets dans les données de la carte mémoire. Les huit derniers bits du résultat doivent être nuls. Autrement dit, pour réussir cette vérification, vous devez additionner les valeurs de tous les octets dans le fichier source, calculer la valeur qui doit être ajoutée pour que les huit derniers bits deviennent nuls, puis affecter cette valeur à l'octet de somme de contrôle dans le fichier.

Si la vérification échoue, vous recevez un message sur l'impossibilité de lire correctement la carte mémoire et rien ne se passe. Pendant le débogage, tout ce que j'ai à faire est de sauter cette vérification.

Copier la ROM


Vers la fin de memcard_game_load , une autre chose intéressante se produit. Il existe plusieurs blocs de code plus intéressants entre celui-ci et la somme de contrôle, mais aucun d'entre eux ne conduit à une ramification qui ignore l'exécution de ce comportement.


Si une certaine valeur entière de 16 bits lue sur la carte mémoire n'est pas égale à zéro, une fonction est appelée qui vérifie l'en-tête de compression dans le tampon. Il vérifie les formats de compression Nintendo propriétaires en examinant le début du tampon Yay0 ou Yaz0. Si l'une de ces lignes est trouvée, la fonction de décompression est appelée. Sinon, une simple fonction de copie copie est exécutée. Dans tous les cas, après cela, une variable appelée nesinfo_data_size .

Un autre indice de contexte ici est que les fichiers ROM pour les jeux NES intégrés utilisent la compression Yaz0, et cette ligne est présente dans les en-têtes de leurs fichiers.

Après avoir observé la valeur vérifiée pour zéro et le tampon passé aux fonctions de vérification de la compression, j'ai rapidement découvert d'où le jeu était lu dans le fichier sur la carte mémoire. La vérification du zéro est effectuée pour une partie du tampon de 32 octets copié à partir de l'offset 0x640 dans le fichier, qui est très probablement l'en-tête ROM. Cette fonction vérifie également d'autres parties du fichier, et c'est en elles que se trouve le nom du jeu (en commençant par le troisième octet de l'en-tête).

Dans le chemin d'exécution du code que j'ai trouvé, le tampon ROM est situé immédiatement après ce tampon d'en-tête de 32 octets.


Ces informations suffisent pour essayer de créer un fichier ROM fonctionnel. Je viens de prendre l'un des autres fichiers de sauvegarde d'Animal Crossing et de le DobutsunomoriP_F_TEST dans un éditeur hexadécimal pour remplacer le nom du fichier par DobutsunomoriP_F_TEST et effacer toutes les zones où je voulais coller les données.

Pour un essai, j'ai utilisé la ROM du jeu Pinball, qui est déjà dans le jeu, et j'ai inséré son contenu après l'en-tête de 32 octets. Au lieu de calculer la valeur de la somme de contrôle, j'ai défini des points d'arrêt de sorte que je saute simplement calcSum et observe également les résultats d'autres vérifications qui peuvent conduire à une branche qui ignore le processus de démarrage de la ROM.

Enfin, j'ai importé le nouveau fichier via le gestionnaire de cartes mémoire Dolphin, redémarré le jeu et essayé de lancer la console.



Ça a marché! Il y avait quelques petits bugs graphiques liés aux paramètres de Dolphin, qui affectaient le mode graphique utilisé par l'émulateur NES, mais en général le jeu fonctionnait très bien. (Dans les nouvelles versions de Dolphin, cela devrait fonctionner par défaut.)

Pour m'assurer que d'autres jeux démarrent également, j'ai essayé d'écrire plusieurs autres ROM qui n'étaient pas dans le jeu. Battletoads a commencé, mais a cessé de fonctionner après le texte de l'écran de démarrage (après d'autres réglages, j'ai réussi à le rendre jouable). Mega Man, en revanche, fonctionnait parfaitement:


Afin d'apprendre à générer de nouveaux fichiers ROM qui pourraient être chargés sans l'intervention de débogueurs, j'ai dû commencer à écrire du code et mieux comprendre l'analyse du format de fichier.

Format de fichier ROM externe


La partie la plus importante de l'analyse des fichiers se produit à memcard_game_load . Il existe six sections principales de blocs d'analyse de code dans cette fonction:

  • Somme de contrôle
  • Enregistrer le nom du fichier
  • En-tête de fichier ROM
  • Tampon inconnu copié sans aucun traitement
  • Commentaires texte, icône et chargeur de bannière (pour créer un nouveau fichier de sauvegarde)
  • Chargeur de démarrage ROM


Somme de contrôle


Les huit derniers bits de la somme de toutes les valeurs d'octets dans le fichier de sauvegarde doivent être nuls. Voici un code Python simple générant l'octet de somme de contrôle nécessaire:

 checksum = 0 for byte_val in new_data_tmp: checksum += byte_val checksum = checksum % (2**32) # keep it 32 bit checkbyte = 256 - (checksum % 256) new_data_tmp[-1] = checkbyte 

Il y a probablement un endroit spécial pour stocker l'octet de somme de contrôle, mais l'ajouter à l'espace vide à la toute fin du fichier de sauvegarde fonctionne très bien.

Nom du fichier


Encore une fois, le nom du fichier de sauvegarde doit commencer par "DobutsunomoriP_F_" et se terminer par quelque chose qui ne contient pas "SAVE". Ce nom de fichier est copié plusieurs fois et dans un cas, la lettre «F» est remplacée par «S». Ce sera le nom des fichiers de sauvegarde pour le jeu NES ("DobutsunomoriP_S_NAME").

En-tête ROM


Une copie directe de l'en-tête de 32 octets est chargée en mémoire. Certaines des valeurs de cet en-tête sont utilisées pour déterminer comment gérer les sections suivantes. Fondamentalement, ce sont des valeurs de taille de 16 bits et des bits de paramètres compressés.

Si vous tracez le pointeur copié par l'en-tête jusqu'au début de la fonction et trouvez la position de son argument, la signature de la fonction ci-dessous montrera qu'elle a en fait le type MemcardGameHeader_t* .

 memcard_game_load(unsigned char *, int, unsigned char **, char *, char *, MemcardGameHeader_t *, unsigned char *, unsigned long, unsigned char *, unsigned long) 

Tampon inconnu


Vérifie la valeur de taille 16 bits de l'en-tête. S'il n'est pas égal à zéro, le nombre d'octets correspondant est directement copié du tampon de fichiers vers un nouveau bloc de mémoire allouée. Cela déplace le pointeur de données dans le tampon de fichier afin que la copie puisse continuer à partir de la section suivante.

Bannière, icône et commentaire


Une autre valeur de taille est vérifiée dans l'en-tête, et si elle n'est pas égale à zéro, la fonction de vérification de compression de fichier est appelée. Si nécessaire, l'algorithme de décompression sera lancé, après quoi SetupExternCommentImage est SetupExternCommentImage .

Cette fonction fait trois choses: «commentaire», image de bannière et icône. Pour chacun d'eux, il y a un code dans l'en-tête ROM montrant comment les gérer. Il existe les options suivantes:

  1. Utiliser la valeur par défaut
  2. Copier de la bannière / icône / section de commentaires dans le fichier ROM
  3. Copier à partir d'un autre tampon

Les valeurs par défaut du code entraînent le chargement de l'icône ou de la bannière à partir de la ressource sur le disque, et le nom du fichier de sauvegarde et le commentaire (description textuelle du fichier) reçoivent les valeurs «Animal Crossing» et «NES Cassette Save Data». Voici à quoi ça ressemble:


La deuxième valeur du code copie simplement le nom du jeu à partir du fichier ROM (une alternative à "Animal Crossing"), puis essaie de trouver la chaîne "] ROM" dans le commentaire du fichier et de la remplacer par "] SAVE". Apparemment, les fichiers que Nintendo voulait publier étaient censés être au format des noms «ROM de nom de jeu [NES]» ou quelque chose de similaire.

Pour l'icône et la bannière, le code essaie de déterminer le format d'image, d'obtenir une valeur de taille fixe correspondant à ce format, puis de copier l'image.

À la dernière valeur de code, le nom et la description du fichier sont copiés sans modifications à partir du tampon, et l'icône et la bannière sont également chargées à partir du tampon alternatif.

ROM


Si vous regardez attentivement la capture d'écran de la ROM de copie memcard_game_load , vous pouvez voir que la valeur de 16 bits vérifiée pour l'égalité à zéro est décalée vers la gauche de 4 bits (multipliée par 16), puis elle est utilisée comme taille de la fonction memcpy si la compression n'est pas détectée. Il s'agit d'une autre valeur de taille présente dans l'en-tête.

Si la taille n'est pas égale à zéro, les données ROM sont vérifiées pour la compression, puis copiées.

Recherche de tampon et de bogue inconnue


Bien que le téléchargement de nouvelles ROM soit assez curieux, la chose la plus intéressante à propos de ce chargeur de ROM pour moi était qu'en fait c'est la seule partie du jeu qui reçoit des entrées utilisateur de taille variable et les copie dans différents emplacements de mémoire. Presque tout le reste utilise des tampons de taille constante. Des choses comme les noms et les textes des lettres peuvent sembler de longueur différente, mais essentiellement l'espace vide est simplement rempli d'espaces. Les chaînes terminées par zéro sont rarement utilisées, ce qui évite les bogues de corruption de mémoire courants, tels que l'utilisation de strcpy avec un tampon trop petit pour copier les chaînes.

J'étais très intéressé par la possibilité de trouver un exploit du jeu basé sur des fichiers de sauvegarde, et il semblait que c'était la meilleure option.

La plupart des opérations de fichiers ROM décrites ci-dessus utilisent des copies de taille constante, à l'exception d'un tampon inconnu et de données ROM. Malheureusement, le code qui traite ce tampon alloue exactement autant d'espace qu'il est nécessaire pour le copier, il n'y a donc pas de débordement et la définition de très grandes tailles de fichiers ROM n'était pas très utile.

Mais je voulais quand même savoir ce qui arrive à ce tampon, qui est copié sans aucun traitement.

Gestionnaires d'étiquettes d'informations NES


Je suis retourné à famicom_rom_load . Après avoir chargé la ROM à partir d'une carte mémoire ou d'un disque, plusieurs fonctions sont appelées:

  • nesinfo_tag_process1
  • nesinfo_tag_process2
  • nesinfo_tag_process3

Après avoir suivi l'endroit où le tampon inconnu est copié, je me suis assuré que cette tâche est effectuée par ces fonctions. Ils commencent par un appel à nesinfo_next_tag , qui exécute un algorithme simple:

  • Vérifie si le pointeur spécifié nesinfo_tags_end pointeur dans nesinfo_tags_end . S'il est inférieur à nesinfo_tags_end ou que nesinfo_tags_end est nul, il vérifie la présence de la chaîne "END" dans l'en-tête du pointeur.

    • Si "END" est atteint, ou si le pointeur s'est élevé vers ou au-dessus de nesinfo_tags_end , la fonction retourne null.
    • Sinon, l'octet à l'offset 0x3 pointeur est ajouté à 4 et au pointeur actuel, après quoi la valeur est retournée.

Cela nous indique qu'il existe une sorte de format d'étiquette à partir d'un nom à trois lettres, d'une valeur de taille de données et des données elles-mêmes. Le résultat est un pointeur sur l'étiquette suivante, car l'étiquette actuelle est ignorée ( cur_ptr + 4 ignore le nom à trois lettres et un octet, et size_byte ignore les données).

Si le résultat n'est pas nul, la fonction de traitement d'étiquette effectue une série de comparaisons de chaînes pour déterminer quelle étiquette doit être traitée. Certains des noms d'étiquette vérifiés dans nesinfo_tag_process1 : VEQ, VNE, GID, GNO, BBR et QDS.


Si une correspondance d'étiquette est trouvée, du code de gestionnaire est exécuté. Certains gestionnaires ne font rien d'autre que d'afficher une étiquette dans le message de débogage. D'autres ont des gestionnaires plus complexes. Après avoir traité l'étiquette, la fonction essaie d'obtenir l'étiquette suivante et de poursuivre le traitement.

Heureusement, il existe de nombreux messages de débogage détaillés qui s'affichent lorsque des balises sont détectées.Ils sont tous en japonais, ils doivent donc d'abord être décodés de Shift-JIS et traduits. Par exemple, un message pour QDS peut indiquer «Chargement d'une zone de sauvegarde de disque» ou «Comme il s'agit de la première exécution, créez une zone de sauvegarde de disque». Les messages pour le BBR se lisent «chargement d'une sauvegarde de la batterie» ou «puisque c'est le premier démarrage, nous effectuons un nettoyage».

Ces deux codes chargent également certaines valeurs de la section de données de leurs étiquettes et les utilisent pour calculer le décalage dans les données ROM, après quoi ils effectuent des opérations de copie. De toute évidence, ils sont chargés de déterminer les parties de la mémoire ROM associées à la conservation de l'état.

Il y a également une balise «HSC» avec un message de débogage indiquant qu'elle traite les enregistrements de points. Elle obtient un décalage en ROM à partir de ses données de balise, ainsi que de la valeur d'enregistrement du score d'origine. Ces marques peuvent être utilisées pour indiquer une place dans la mémoire du jeu NES pour le stockage des meilleurs scores, éventuellement pour les sauvegarder et les restaurer à l'avenir.

Ces balises créent un système de téléchargement de métadonnées ROM assez complexe. De plus, beaucoup d'entre eux conduisent à des appels memcpybasés sur les valeurs transmises dans les données d'étiquette.

Chasse aux insectes


La plupart des balises qui conduisent à une manipulation de la mémoire ne sont pas très utiles pour les exploits, car toutes ont des valeurs de décalage et de taille maximales spécifiées sous forme d'entiers 16 bits. Cela suffit pour travailler avec l'espace d'adressage NES 16 bits, mais pas assez pour écrire des valeurs cibles utiles, telles que des pointeurs vers des fonctions ou renvoyer des adresses sur la pile dans l'espace d'adressage GameCube 32 bits.

Cependant, il existe plusieurs cas où les valeurs des décalages de taille transmis memcpypeuvent dépasser 0xFFFF.

QDS

QDS charge un décalage de 24 bits à partir de ses données de balise, ainsi qu'une valeur de taille de 16 bits.

La bonne chose ici est que le décalage est utilisé pour calculer l'adresse de destination de l'opération de copie. L'adresse de base du décalage est le début des données téléchargées, la source de la copie se trouve dans le fichier ROM de la carte mémoire et la taille est définie par la valeur de taille de 16 bits de l'étiquette.

La valeur 24 bits a une valeur maximale 0xFFFFFF, bien supérieure à ce qui est nécessaire pour écrire en dehors des données ROM chargées. Cependant, il y a certains problèmes ...

Le premier est que bien que la valeur de taille maximale soit égale 0xFFFF, elle est initialement utilisée pour réinitialiser la partition mémoire. Si la valeur de la taille est trop élevée (pas beaucoup plus grande 0x1000), cela réinitialisera la marque «QDS» dans le code du jeu.

Et c'est là que réside le problème, car il nesinfo_tag_process1est en fait appelé deux fois. Pour la première fois, elle reçoit des informations sur l'espace dont elle a besoin pour préparer les données stockées. Les balises QDS et BBR ne sont pas entièrement traitées lors de la première exécution. Après la première exécution, un emplacement est préparé pour les données de sauvegarde et la fonction est à nouveau appelée. Cette fois, les balises QDS et BBR sont entièrement traitées, mais si les chaînes de nom de balise sont effacées de la mémoire, il est impossible de faire correspondre à nouveau les balises!

Cela peut être évité en définissant une valeur de taille plus petite. Un autre problème est que la valeur de décalage ne peut avancer que dans la mémoire et que les données ROM NES sont situées dans le tas assez près de la fin de la mémoire disponible.

Après eux, il n'y a que quelques tas, et aucun d'entre eux n'a quelque chose de particulièrement utile, comme des pointeurs de fonction évidents.

Dans le cas normal, vous pouvez l'utiliser pour exploiter un débordement de tas, mais dans l'implémentation mallocutilisée pour ce tas, pas mal d'octets de vérification de l'état des blocs ont été ajoutés malloc. Nous pouvons écraser les valeurs de pointeur dans les blocs de tas suivants. Sans contrôles d'intégrité, cela pourrait être utilisé pour écrire dans une zone de mémoire arbitraire lorsqu'il est appelé freepour un bloc de segment de mémoire impliqué.

Cependant, l'implémentation utilisée ici mallocvérifie un modèle d'octet spécifique ( 0x7373) au début des blocs suivant et précédent qu'elle manipulera lorsqu'elle sera appeléefree. Si elle ne trouve pas ces octets, elle appelle OSPanicet le jeu se bloque.


Impossible d'influencer la présence de ces octets à un emplacement cible, il n'est pas possible d'écrire ici. En d'autres termes, il est impossible d'enregistrer quelque chose dans un endroit arbitraire sans pouvoir enregistrer quelque chose près de cet endroit. Il peut y avoir un moyen de rendre la valeur 0x73730000stockée sur la pile directement en face de l'adresse de retour et de l' emplacement auquel la valeur se réfère, que nous voulons écrire à l'adresse de destination (elle sera également vérifiée comme s'il s'agissait d'un pointeur vers un bloc de tas), mais cela c'est difficile à réaliser et à utiliser dans un exploit.

nesinfo_update_highscore

Une autre fonction concernant les balises QDS, BBR et HSC est la suivante nesinfo_update_highscore. Les tailles des repères QDS, BBR et OFS (décalage) sont utilisées pour calculer le décalage auquel enregistrer, et le repère HSC inclut l'enregistrement à cet emplacement. Cette fonction est effectuée pour chaque trame traitée par l'émulateur NES.

La valeur de décalage maximale pour chaque étiquette dans ce cas, même pour QDS, est égale 0xFFFF. Cependant, pendant le cycle de traitement des étiquettes, les valeurs de dimension des étiquettes BBR et QDS s'accumulent réellement . Cela signifie que plusieurs marques peuvent être utilisées pour calculer presque n'importe quelle valeur de décalage. La limitation est le nombre d'étiquettes qui peuvent tenir dans la section de données des étiquettes ROM dans un fichier sur une carte mémoire, et il a également une taille maximale 0xFFFF.

L'adresse de base à laquelle le décalage est ajouté est le 0x800C3180tampon de sauvegarde des données. Cette adresse est bien inférieure aux données ROM, ce qui nous donne plus de liberté dans le choix d'un emplacement d'enregistrement. Par exemple, il sera assez simple de réécrire l'adresse de retour dans la pile à l'adresse 0x812F95DC.

Malheureusement, cela n'a pas fonctionné non plus. Il s'avère qu'il nesinfo_tag_process1vérifie également la taille cumulée des décalages de ces étiquettes et utilise cette taille pour initialiser l'espace:

 bzero(nintendo_hi_0, ((offset_sum + 0xB) * 4) + 0x40) 


Avec la valeur de décalage que j'essayais de calculer, cela a conduit au fait que 0x48D91EC(76 386 796) octets de mémoire ont été effacés , c'est pourquoi le jeu s'est écrasé de façon spectaculaire.

Marque PAT


J'avais déjà commencé à perdre espoir, car toutes ces balises qui effectuaient des appels non protégés memcpyavaient échoué avant même que je ne parvienne à les utiliser. J'ai décidé de simplement documenter le but de chaque balise et j'ai progressivement accédé aux balises nesinfo_tag_process2.

La plupart des gestionnaires d'étiquettes nesinfo_tag_process2ne démarrent jamais, car ils ne fonctionnent que lorsque le pointeur est différent nesinfo_rom_startde zéro. Rien dans le code n'assigne une valeur différente de zéro à ce pointeur. Il est initialisé avec une valeur nulle et n'est plus jamais utilisé. Lorsque le chargement de la ROM est défini uniquement nesinfo_data_start, il ressemble donc à du code mort.

Cependant, il existe une étiquette qui peut toujours fonctionner lorsqu'elle est différente de zéro nesinfo_rom_start: PAT. Il s'agit de l'étiquette la plus difficile d'une fonction nesinfo_tag_process2.


Il l'utilise également comme pointeur nesinfo_rom_start, mais ne le vérifie jamais pour zéro. La balise PAT lit son propre tampon de données de balise, traitant les codes qui calculent les décalages. Ces décalages sont ajoutés au pointeur nesinfo_rom_startpour calculer l'adresse de destination, puis les octets sont copiés du tampon de correctifs vers cet emplacement. Cette copie se fait en chargeant et en enregistrant des octets, sans utiliser d'instructions memcpy, donc je ne l'ai pas remarqué auparavant.

Chaque tampon de données de marque PAT a un code de type 8 bits, une taille de patch 8 bits et une valeur de décalage 16 bits, suivis des données de patch.

  • Si le code est 2, la valeur de décalage est ajoutée à la somme actuelle des décalages.
  • Si le code est 9, le décalage est décalé de 4 bits vers le haut et ajouté à la somme actuelle des décalages.
  • Si le code est 3, la somme des décalages est réinitialisée à 0.

La taille maximale de l'étiquette d'information NES est de 255, c'est-à-dire que la plus grande taille de patch PAT est de 251 octets. Cependant, plusieurs marques PAT peuvent être utilisées, c'est-à-dire que vous pouvez patcher plus de 251 octets, ainsi que patcher des espaces non contigus.

Tant que nous avons une série de semelles PAT avec le code 2 ou le code 9, le décalage du pointeur de destination continue de s'accumuler. Lors de la copie des données de patch, elles sont remises à zéro, mais si vous utilisez une taille de patch zéro, cela peut être évité. Il est clair que cela peut être utilisé pour calculer un décalage arbitraire avec un pointeur nul en nesinfo_rom_startutilisant de nombreuses marques PAT.

Cependant, il existe deux autres vérifications des valeurs de code ...

  • Si le code est compris entre 0x80et 0xFF, il est ajouté à 0x7F80, puis décalé de 16 bits. Ensuite, il est ajouté à la valeur de décalage 16 bits et utilisé comme adresse de fin pour le patch.

Cela nous permet d'attribuer une adresse de destination pour le patch dans la plage de 0x80000000à 0x807FFFFF! C'est là que la majeure partie du code Animal Crossing réside en mémoire. Cela signifie que nous pouvons corriger le code Animal Crossing lui-même en utilisant des étiquettes de métadonnées ROM à partir d'un fichier sur une carte mémoire.

À l'aide d'un petit chargeur de patchs, vous pouvez même facilement télécharger des patchs plus volumineux d'une carte mémoire vers n'importe quelle adresse.

Pour une vérification rapide, j'ai créé un patch qui comprenait le «mode zuru 2» (mode développeur de jeu, décrit dans mon article précédent) lorsqu'un utilisateur charge une ROM à partir d'une carte de jeu. Il s'est avéré que la combinaison de triche des touches active uniquement le mode «zuru mode 1», qui n'a pas accès aux fonctions du mode 2. Avec ce patch, grâce à la carte mémoire, nous pouvons avoir un accès complet au mode développeur sur du matériel réel.


Les marques de patch seront traitées au démarrage de la ROM.


Après avoir chargé la ROM, vous devez quitter l'émulateur NES pour voir le résultat.


Ça marche!

Format d'étiquette d'informations sur le patch


Les marques d'information dans le fichier de sauvegarde qui exécutent ce correctif ressemblent à ceci:

000000 5a 5a 5a 00 50 41 54 08 a0 04 6f 9c 00 00 00 7d >ZZZ.PAT...o....}<
000010 45 4e 44 00 >END.<


  • ZZZ \x00: marque de départ ignorée. 0x00Est la taille de son tampon de données: zéro.
  • PAT \x08 \xA0 \x04 \x6F\x9C \x00\x00\x00\x7D: patch 0x80206F9Cin 0x0000007D.
    • 0x08 Correspond à la taille du tampon d'étiquettes.
    • 0xA0lorsqu'elles sont ajoutées aux 0x7F80devenant 0x8020, soit les 16 bits supérieurs de l'adresse de destination.
    • 0x04Correspond à la taille des données de patch ( 0x0000007D).
    • 0x6F9C Sont les 16 derniers bits de l'adresse de destination.
    • 0x0000007D Est les données du patch.
  • END \x00 : marque de fin de marqueur.

Si vous voulez expérimenter par vous-même la création d'un fichier de sauvegarde de patcher ou de ROM, alors sur https://github.com/jamchamb/ac-nesrom-save-generator, j'ai publié un code très simple pour générer des fichiers. Un patch comme celui illustré ci-dessus peut être généré avec la commande suivante:

$ ./patcher.py Patcher /dev/null zuru_mode_2.gci -p 80206F9c 0000007D

Exécution de code arbitraire


Grâce à cette balise, vous pouvez réaliser l'exécution de code arbitraire dans Animal Crossing.

Mais voici le dernier obstacle: l'utilisation de correctifs pour les données fonctionne bien, mais des problèmes surviennent lors de la correction des instructions de code.

Lorsque les patchs sont enregistrés, le jeu continue de suivre les anciennes instructions qui étaient à sa place. Cela semble être un problème de mise en cache, et en fait c'est le cas. Le CPU GameCube possède des caches d'instructions, comme décrit dans les spécifications .

Pour comprendre comment vous pouvez vider le cache, j'ai commencé à étudier les fonctions liées au cache de la documentation du SDK GameCube, et j'ai découvert ICInvalidateRange. Cette fonction invalide les blocs d'instructions mis en cache à l'adresse de mémoire spécifiée, ce qui permet à la mémoire d'instructions modifiée d'être exécutée avec un code mis à jour.

Cependant, sans la possibilité d'exécuter le code d'origine, nous ne pouvons toujours pas appeler ICInvalidateRange. Pour une exécution réussie du code, nous avons besoin d'une astuce supplémentaire.

En étudiant l'implémentation mallocpour la possibilité d'utiliser un exploit avec débordement de tas, j'ai appris que les fonctions d'implémentation mallocpeuvent être désactivées dynamiquement à l'aide d'une structure de données appelée my_malloc. my_malloccharge un pointeur sur l'implémentation actuelle mallocou à freepartir d'un emplacement statique en mémoire, puis appelle cette fonction, en passant tous les arguments passés à my_malloc.

L'émulateur NES utilise activementmy_mallocpour allouer et libérer de la mémoire pour les données NES liées à la ROM, j'étais donc sûr qu'il serait lancé plusieurs fois à peu près en même temps que les marques PAT.

Puisqu'il my_malloccharge un pointeur de la mémoire et y fait une transition, je peux changer le processus d'exécution du programme en écrasant simplement le pointeur pour qu'il pointe vers la fonction actuelle mallocou free. La mise en cache des outils n'empêchera pas cela de se produire, car aucune instruction ne doit être modifiée my_malloc.

Le développeur du projet de fan Dōbutsu no Mori e +, nommé Cuyler, a écrit un tel chargeur dans l'assembleur PowerPC et a démontré son utilisation pour injecter du nouveau code dans cette vidéo: https://www.youtube.com/watch?v=BdxN7gP6WIc. (Dōbutsu no Mori e + était la dernière itération d'Animal Crossing sur le GameCube, qui avait le plus de mises à jour. Publié uniquement au Japon.) Le patch télécharge du code qui permet au joueur de créer des objets en entrant son identifiant par lettre et en appuyant sur le bouton Z.


Grâce à cela, vous pouvez télécharger des mods, des astuces et des homebrews dans une copie régulière d'Animal
Crossing sur un vrai GameCube.

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


All Articles