Pwning N64 for fun & speedrun : Power On
Coucou les pamplemoussesroux,
Dans mes articles précédents, j’expliquais pourquoi je m’intéressais à la N64.
A vrai dire, ça me servira pas spécialement dans la vie : qui de nos jours utilise un proc MIPS ? Pas grand monde à priori, tout le monde veut du x86_64 ou de l’ARM.
Cela dit, je pourrais me la péter en disant que je suis un expert reverse engineering n64, je trouverais peut être une lsdette comme ça. Surtout, ça me permet de chopper des mécanismes en reverse d’une manière générale, et ça, c’est pas un mal.
Ce coup ci, on va rentrer dans le vif du sujet et voir comment on commence l’analyse réelle d’un jeu from scratch.
Bref. Let’s dig into it, buddys !
Le démarrage
Reminder : il existe déjà la partie 1, et la partie 1.5. J’oblige personne à les lire, mais bon, vous risquez de pas tout comprendre :p
First step : se renseigner
Rappelez vous, j’y connais rien dans en RE console ! Du coup, l’idée c’est de montrer ma démarche, en expliquant un peu mes tribulations.
La première chose à faire, c’est de se renseigner un peu sur l’existant ! Le rom hacking, c’est une scène à part entière, il y a donc déjà sûrement des infos quelque part, sur la manière de commencer d’une façon générale, et même avec un peu de chance, des infos spécifiques à Star Wars Shadows Of The Empire.
Niveau infos générales, on a quelques tutos, mais principalement pour les consoles encore plus old school (SNES, NES, etc.). La N64, c’est le niveau du dessus, donc on trouve moins de ressources. Ou alors je ne sais pas les chercher, au choix.
Cela dit, je ne suis quand même pas le premier à m’intéresser à ça, et je suis tombé sur le blog d’un mec qui s’amusait à bidouiller un jeu de course, ainsi que quelques sites sympas, listés en fin d’article.
La plupart des gros jeux (Zelda OOT, Mario 64, etc) ont été ponçés plusieurs fois, et il y a pas mal de ressources dessus. Malheureusement, SOTE ne fait pas partie des “gros jeux”. On trouvera éventuellement quelques adresses mémoire utiles, mais pas de quoi casser trois pipes à un term nux. En plus, j’ai trouvé ce lien trop tard, ça aurait pu m’aider -_-.
Bref, comme l’existant n’existe pas, je suis parti from scratch, avec mon bit et mon couteau, et j’ai tatonné sur la manière de démarrer le reverse n64.
La base de la base
La première chose à faire, même si c’est pas d’une grande utilité, c’est de chercher des chaînes de caractères juicy : sur un malentedu, on peut trouver quelques chaînes de debug.
Pour ça, un basique coup de strings
nous retourne une palanquée de texte. Beaucoup de choses inutiles, principalement les textes descriptifs. Mais surtout… CA :
Une sorte de “Fuck U morron”, avec 25 ans de décallage x).
En vrai, hormis le coté troll du message, cela signifie surtout que, déjà en 1996, les devs se doutaient déjà que des petits cons malins comme moi s’amusaient déjà à faire du reverse. En 1996. Il y a près de 25 ans. On parle d’un temps où internet n’existait (quasi) pas, et où seuls quelques élus savaient faire du RE. Perso, je trouve ça fou.
Anyway. Rien de vraiment très utile via strings
pour SOTE, mais ça valait le coup d’essayer. On passe à la suite.
Trouver un point d’entrée
Je l’ai déjà dit, j’suis un peu moisi en reverse, donc je vais surement utiliser des techniques de boloss que personne n’utilise parce que c’est idiot.
MAIS : 1. “if it works, it ain’t stoopid” 1, 2. Si vous avez mieux à me proposer, je suis bien évidemment preneur :).
Mon but, c’est -pour rappel- de trouver des bugs à exploiter. Et pour ça, un point essentiel, c’est qu’il faut comprendre les mécanismes du jeu : où sont enregistrées les données du joueur, des ennemis, les stats, etc etc. En gros, trouver les données lues ou écrites. Le problème, c’est qu’on a un bin assez “gros” à reverser, donc analyser tout l’ASM en step by step depuis l’Entry Point pour trouver LA fonction sexy n’est pas forcément pertinent, limite chiant.
Du coup, pour trouver des datas intéressantes, on a la solution simple, que j’ai utilisé, et celle bien plus intelligente du pote sideway, qui fait un rom hack de Zelda A link to the past.
Ma méthode simple est de faire un gros diff des familles, au niveau de la mémoire. Ca tombe bien, Project64 nous permet de faire ça :)
Un quoi ? Un diff ? Mais pourquoi faire ?
Ca vient, ça vient, je t’explique jeune impatient. Un diff va nous permettre de faire une analyse de la RAM à deux instants différents. Pour savoir où sont stockées les infos, il “suffit” donc de sauvegarder la RAM deux fois ET dans deux états que l’on maitrise à peu près. Je vous z’explique :
Quand on joue à un jeu, les informations du joueur sont enregistrées en mémoire : points de vies, munitions, position, armes disponibles, etc.
En suivant cette idée, on va faire un premier snapshot de la mémoire, modifier une de ces informations (par exemple en changeant d’arme), puis faire un deuxième snapshot.
Ainsi, en analysant les deux sauvegardes de la RAM en parallèle, on trouvera quelles modifications on été effectuées par l’action faite.
Attention cependant, pour faire un diff efficace, il faut changer UNE stat à la fois. Si on fait plusieurs changements, on verra bien des modifs en ram, mais pour savoir à quoi ça correspond, bisou…
J’ai donc choisi de faire un diff sur le nombre de munitions restantes. Pour cela, j’ai chargé mon niveau, avancé jusqu’à un point me permettant de récupérer des munitions, sans ennemi à proximité (et oui, un ennemi qui est visible a ses stats aussi, donc il influencerait sur l’analyse), et j’ai fait une première sauvegarde de la RAM. Puis un bon coup de lance missile pour décrémenter le nombre de munitions, et boomz ! Une deuxième sauvegarde.
Le reste est ensuite assez “mainstream”. On lance un tool de diff (ici WinMerge, avec le plugin analyse de binaires) pour avoir une vue côte à côte des deux dumps mémoire, et… bah on lit quoi…
Comme on ne peut pas savoir précisément à quel endroit de la RAM sont sauvegardées les datas, j’ai tout dumpé (8mo, quand même2) et j’ai tout vérifié manuellement.
Pour information, un diff de 8mo, c’est genre super chiant à analyser : même si les deux dumps n’ont que quelques secondes d’écart, il se passe plein de choses en quelques secondes dans la RAM (des compteurs incrémentés, des pointeurs modifiés, etc)3, du coup, differ prend beaucoup de temps, car de nombreuses données ont changé.
Après une bonne heure de recherche, j’ai enfin pu trouver un endroit intéressant :
On voit ici que sur le dump de gauche, on a la valeur 70. A droite, on remarque qu’elle a été modifiée en 60. Alors, pourquoi cette adresse est intéressante ?
Déjà parce que je montre uniquement l’octet kivabien et pas tous ceux que j’ai testé et qui sont faux. Et ensuite parce que c’est un des rares octets qui décrémente de manière cohérente (on passe pas de FE à 32 par exemple).
Validation
Bon. Très bien. On a trouvé un octet, à l’adresse 0x801A92DC
qui semble être intéressant. Mais du coup, il va falloir valider cette théorie.
Comment ? Bah en jouant, tout simplement ^^.
Comme je l’expliquais dans mon premier article, la fenêtre mémoire s’amuse à faire Matrix quand on joue, parce que les données sont mises à jour en temps réel.
Du coup il suffit de lancer une game, ouvrir la fenêtre mémoire en affichant le bon octet, et de tirer. Si l’octet est modifié, on peut être quasiment certain que c’est le nombre de munitions. So, on va essayer :
Bingo !
Cela dit, afin d’être certain à 100%, un autre test est à effectuer : modifier directement l’octet en ram, et voir si cela change quelque chose dans le jeu. (On fait l’inverse du test précédent quoi ^^)
Re-bingo !
On sait maintenant où se situe le nombre de munitions pour les seekers (l’arme que j’utilise). Une petite danse de la victoire s’impose.
\o/ /o/ \o\ /o/ |o/ |o\ |o| \o| /o|
Cela dit, c’est bien mignon tout ça, mais il m’a fallu une paire d’heures pour trouve UNE information. Et des infos, il y en a pas mal. J’ai pas non plus envie de passer des jours entiers là dessus… On va essayer de trouver une autre manière de faire !
Gratter plus d’infos
Die & Retry
Afin d’aller plus vite pour détecter les différentes informations du joueur, on va partir d’un constat bête mais stupide : les données sont organisées. Et par organisées, j’entends “On va mettre toutes les infos relatives au joueur dans le même coin”.
En suivant cette idée, on va simplement jouer avec les octets autour de l’adresse 0x801A92DC
, afin de voir ce que ça peut fait :D
C’est un peu résumé à l’arrache, mais ça donne globalement ça :
Au bout de quelques minutes, on arrive donc globalement à extraire une sorte de structure, qui en pseudo code4 donne quelque chose de ce genre.
dash_infos = struct {
life_points = (half word) *0x801A92BC4
current_weapon_id = (half word) *0x801A92BC8
ammo_laser = (word) *0x801A92D0
ammo_pulsar = (word) *0x801A92D4
ammo_flame = (word) *0x801A92D8
ammo_seekers = (word) *0x801A92DC
ammo_stunner = (word) *0x801A92E0
ammo_disruptor = (word) *0x801A92E4
} // struct may begin at 0x801A9210
Ce qu’il est important de comprendre dans cette structure, c’est qu’elle est incomplète. A ce stade du reverse, on est totalement dans le noir. On ne peut pas savoir du tout où elle démarre, où elle se termine, et même si toutes les données entre l’adresse de début et de fin supposées appartiennent à cette structure.
Par exemple, les données de 0x801A92BCA
à 0x801A92BCF
(entre current_weapon_id
et ammo_laser
), peuvent être liées à la structure, mais n’ont à priori aucun effet sur les infos affichées in-game. On ne peut donc pas savoir ce qu’elles font et si elles sont utiles. A l’inverse, il peut également y avoir des valeurs liées au joueur qui sont totalement ailleurs en RAM et qui n’auraient donc pas été détectées.
Alors, oui, c’est possible d’avoir plus d’infos, mais pour ça, il faut gratter plus en profondeur dans le code ASM :)
from addr to Func
Très bien, très bien. On sait où sont rangées les datas liées à Dash Rendar. Ca nous fait une belle jambe hein…
Mais ce qu’on veut, c’est pas faire le ménage dans la RAM, c’est trouver du glitch. Donc c’est clairement pas suffisant. Alors quoi faire maintenant ?
Et bah, c’est plutôt logique. Maintenant qu’on sait où sont les données, on va pouvoir trouver facilement COMMENT elles sont utilisées.
Lorsqu’on effectue une action, par exemple en changeant d’arme, le jeu enregistre forcément l’info. En l’occurence, il va le faire dans le champ current_weapon_id
de la structure définie plus haut. Au niveau assembleur, il y aura donc une instruction qui fera en gros “Mets l’ID de l’arme en cours en 0x801A92BC8
”.
Donc, si on pose un BreakPoint en écriture5 sur cette adresse, dès qu’on va changer d’arme, elle sera modifiée, on va trigger le BP. En faisant ça, on pourra récupérer l’instruction ASM qui effectue cette écriture, ce qui fait qu’on aura -enfin- du code à reverse :)
Faisons ça!
Woot ! Comme on peut le voir, ça a l’air de fonctionner ! On a bien un bout d’ASM lié au changement d’arme (logique, non ? :D).
Analysons un peu ça ! Je pose mon BP en écriture sur 0x801A92C8
, puis je change d’arme ingame.
Dès le changement d’arme, la fenêtre d’instructions apparait pour me montrer à quelle instruction on a breaké; en l’occurence, en 0x8006EEFC
, et on voit notre première ligne d’assembleur \o/ :
SH V0, 0x00B8 (S0)
En gros, cette ligne nous dit : Tu Stores un Halfword qui est en V0
dans l’adresse S0
+ 0x00B8
.
Comme je vous ai maché le travail, j’ai mis des gros cadres rouges pour que vous voyez les bonnes infos dans mon gif, mais en gros V0
correspond à 3 et S0
à 0x801A9210
. Si on ajoute 0x00B8
, on obtient… 0x801A92C8
, l’adresse sur laquelle on a mis notre breakpoint :o !
On retombe bien sur nos pattes, tout va bien :)
Cela dit, on est en plein milieu d’un… “truc”, et on a juste aucune idée d’où ça commence, d’où ça finit, ni rien. Quoikonfé?
L’idée ici, c’est donc de trouver le début de la fonction qui utilise ce bout de code, fonction que j’ai sobrement nommée change_weapon
.
RRFPT
La manière la plus facile de trouver le début de la fonction change_weapon
, c’est d’utiliser le registre RA
:)
Souvenez vous, je l’avais expliqué dans un blogpost précédent : à chaque JAL
(call), on met PC+8
dans le registre RA
. A la fin du call, le JR
(Jump Return, un RET quoi) lit RA
afin de sauter vers l’adresse contenue dans ce registre, et donc revenir à la fonction appelante.
Bref, revenons à nos moutons (électriques). Là, on ne sait pas trop où on est, mais on peut supposer une chose : on est pas dans la boucle principale, mais dans une fonction qui est liée au changement d’arme.
Du coup, on a “juste” à utiliser le registre RA
pour savoir où se situe le début de la fonction.
Mais attention ! Encore une fois, on ne sait pas où on est exactement. On peut facilement imaginer quelque chose du genre :
main
\_ func_we_dont_care
\_func_check_input
\_ func_we_dont_need
\_ func_change_weapon <-- ce qu'on cherche
\_ subfunc1 [|
\_ subsubfunc1 |
\_ subsubsubfunc1 | on est dans cette partie plus ou moins
\_ subsubfunc2 |
\_ subfunc2 |]
\_ func_we_still_dont_care
[...]
L’idée, c’est donc de remonter les sous fonctions une à une en utilisant le RA, jusqu’à arriver à la fonction change_weapon
. L’idée est la suivante :
- on exécute le code, et on breake dans
subsubsubfunc1
: on récupère le RA. Ce qui nous donne l’endroit desubsubfunc1
- ensuite, on met un BP à l’adresse de
subsubfunc1
, puis on exécute. On breake au niveau desubsubfunc1
, ce qui nous donne le RA desubfunc1
- on fait pareil : BP sur à l’adresse de
subfunc1
et exécution. On breake, on récupère le RA, et boom, ça fait des chocapics, on a l’adresse dechange_weapon
Une technique que j’ai sobrement appelé le RRFPT, pour Reverse RA Following Path Trick. Aucune idée de si cette “technique” porte déjà un nom ou pas. Mais un acronyme idiot me paraissait une bonne idée.
Par contre, comme on peut le voir dans mon schéma, il est important de faire attention. Si on ne remonte pas assez avec le RRFPT, on sera toujours dans une sous fonction de change_weapon
. Si on remonte trop, on se retrouvera dans check_input
, ce qu’on ne veut pas.
Mais bref, assez de théorie, place à la pratique.
On pose donc notre Write BP sur l’adresse de current_weapon_id
, en 0x801A92BC8
, puis on va appuyer sur la touche pour changer d’arme, ce qui provoque le BP. On peut donc supposer que jusqu’à présent, je me suis pas trop planté dans ce que je dis, et qu’on est dans la bonne routine.
Si vous regardez l’image très attentivement, vous verrez que je sélectionne RA
, qui a la valeur 0x8006EEE4
.
On va donc faire plusieurs choses :
- aller regarder à l’adresse de
RA-8
pour récupérer l’adresse du JAL (0x8006EEDC
) - supprimer le BP actuel, pour en mettre un nouveau en
RA-8
. Gaffe, maintenant, il faut mettre un BP en exec, puisqu’on veut breake quand on va appeler le JAL. - changer d’arme ingame une fois de plus
- récupérer le
RA
En effectuant une seconde fois la manipulation, on trouve l’adresse 0x80073178
. On sait donc maintenant que le call en 0x8006EEDC
était bien une sous fonction de change_weapon
.
Si on continue de faire ça une paire de fois, on remarque assez vite qu’on breake même sans changer d’arme. Ca signifie qu’on est remonté trop loin.
En effet, comme aucune action n’a été faite de notre part, c’est que ce break est lié à autre chose.
Comme c’est un peu galère à expliquer, j’ai fait un beau schéma pas piqué des hannetons !
On peut donc tenter de résumer nos trouvailles de cette manière :
while(1):
if on_a_appuyé_sur_la_touche():
change_weapon(some args) # call vers 0x80073178
def change_weapon(some args): # 0x80073178
unknown_subfunction(some args) # call vers 0x8006EEDC
def unknown_subfunction(some args): # 0x8006EEDC
dash_infos[current_weapon_id] = some_int # 0x801A92BC8
Avec tout ça, on sait donc où commence la fonction permettant de changer d’arme, ce qui veut dire que… CA Y EST ! ON A ENFIN DU CODE A REVERSE ! \o/
Maiiiiiis on verra ça dans l’article suivant ;).
Quelques tips au passage
Dans ce blogpost, j’ai expliqué ma manière de faire, c’est à dire en apprenant sur le tas. Cela dit, avec le recul, il aurait été possible d’utiliser d’autres méthodes (que je testerai peut être plus tard), dont je vais parler succintement.
Monitoring de l’input manette
De manière assez logique, on peut facilement supposer qu’il existe quelque part dans le code un endroit qui vérifie les inputs de la manette, et qu’un morceau de code est exécuté lorsqu’un bouton est appuyé. Il aurait donc été possible de prendre le check de l’input comme entry point, et d’analyser step by step pour trouver l’input qui correspond au bouton de changement d’arme, et donc la fonction change_weapon
. Entre temps, j’ai découvert comment ça marchait à ce niveau, mais à l’époque, j’étais jeune et bête.
RTJMT
A la place du RRPFT, on aurait pu utiliser le RTJMT : Real Time JAL Monitoring Trick. L’idée, derrière ce nom barbare que je viens de trouver, c’est d’utiliser l’API de debug pour enregistrer en temps réel tous les JAL. Pour cela, deux étapes sont nécessaires :
- un monitoring “témoin”. On ne touche à rien dans le jeu, et on enregistre tous les JAL qui se déclenchent.
- un monitoring “analyse”. Cette fois, on change d’arme.
En faisant un diff des deux listings de JAL, on peut voir les calls appelés uniquement dans le deuxième monitoring, et donc trouver les addresses de JAL qui ne sont utilisés que pour le changement d’arme.
Ca rejoint en partie la manière de faire de Sideway. J’ai pas inventé le concept, juste mis un nom débile dessus…
Diff multiple
Lors de mon diff, il y avait de nombreuses valeurs différentes qui n’avaient aucun rapport avec ce que je cherchais. Il aurait été moins teubé de faire 3 diffs à la place de 2 :
- un premier “témoin”
- un deuxième “faux positif”
- un troisième “analyse”
Un diff entre le témoin et le faux positif aurait permis de révéler les octets modifiés entre deux instants et n’ayant pas de rapport avec la valeur cherchée. Un second diff par la suite entre le témoin et l’analyse, en supprimant les faux positifs détectés juste avant aurait permis d’accélerer la recherche. Th3_l5Dumb, c’est mon nom.
RTDMT
Une troisième manière de trouver où sont les octets modifiés en RAM lorsqu’on effectue une action serait de faire un RTDMT -un Real Time Diff Monitoring Trick : on monitore TOUTE la ram en temps réel et on laisse tourner le jeu.
Comme ça, on voit tous les octets modifiés. Ensuite, on enlève du monitoring ces octets et on effectue notre action. Ainsi, on détecterait automatiquement les octets modifiés lorsqu’on fait quelque chose.
Cela dit, il y a quand même deux gros drawbacks : déjà, on a 8mo à monitorer en permanence, j’imagine pas la chute de perf ; ensuite, une action peut modifier plusieurs choses en RAM. Par exemple, un saut va avoir un bit “is_jumping” ainsi que la hauteur actuelle du perso. Ca nous laisse plus que quelques données à analyser, mais on est pas sur un match à 100%.
C’est d’ailleurs ce qu’à fait liveroverflow sur son reverse de pokemon missingno :)
Le mot de la fin
On commence à rentrer doucement dans l’analyse de la rom. Pour l’instant, la seule information réellement intéressante, c’est l’adresse de la fonction change_weapon
. Le reste n’a finalement été que tests et hints.
Au passage, je dis ça à tout fins utiles : à chaque fois que je lis un article de ce genre, je me suis toujours dit “le mec c’est un tueur !”. Les autres peut être, moi non. Ce que je présente en quelques paragraphes ici représente principalement les choses intéressantes. Les échecs critiques, j’en ai pas parlé plus que ça. Et ça peut donner l’impression que je sais ce que je fais. Mais pas du tout. Je suis totalement dans le noir, et je fais juste croire que je suis bon pour avoir un peu de fame.
Next time : Analyse de toute la fonction change_weapon
. On va vraiment parler d’ASM ;)
Liens utiles
- n64.dev : une petite perle, qui contient de nombreuses ressources et docs sur la N64
- So you want to mod your favorite game : une autre liste de liens, qui peut compléter n64.dev
- GitHub Mario Kart 64 : un repo github qui contient plusieurs docs et notes sur le reverse de mario kart 64
- The Cutting Room Floor : un site sur lequel on peut perdre des heures à regarder tous les oublis des devs JV
- Blog de JayTheHam : le blog d’un mec qui reverse un jeu de voiture, dont je parlais en début d’article
- Blog de Sideway : je l’ai déjà mis en début d’article, mais son reverse de Zelda est tellement cool, qu’il faut le mettre une deuxième fois :)
- Vidéos de lifeoverflow sur pokemon partie 1 et partie 2 : Comme d’hab, un excellent boulot d’explication de la part de liveoverflow sur la manière de reverse un jeu !
-
L’idée, c’est de se raccrocher aux branches comme on peut ^^’ ↩
-
Tel l’idiot que je suis, je n’ai pas pensé une seule seconde que la N64 contient 8mo de RAM UNIQUEMENT quand le ram pack est pluggué. Par défaut, il n’y a que 4mo, j’aurais donc pu désactiver le ram pack dans la conf de l’émulateur et gagner du temps… ↩
-
J’ai même découvert depuis que ce con de jeu pioche apparemment des fichiers de la ROM pour les écrire en RAM toutes les secondes. Niveau efficacité du diff, on est sur un bon zéro pointé du coup. ↩
-
Quand je dis pseudocode, c’est même pas réellement du pseudocode. C’est un mélange de C, de python, de cacaprout, et de ce que vous voulez. L’idée, c’est juste de rendre compréhensible la RAM et l’ASM. Donc le “pseudocode” que je montre ne sera jamais fonctionnel ni même normé, mais il est facile à lire. ↩
-
Il est en fait plus intéressant de mettre un BP en écriture, plutôt qu’en lecture. Pourquoi ? Car il est possible que la valeur soit lue souvent, et donc on risque des faux positifs, car le BP se ferait trigger inutilement (j’ai fait le test hein, faut me croire). A moins que les devs aient codé avec le cul, la valeur est modifiée uniquement lorsqu’on effectue une action. Et c’est bien le cas. ↩