Reverse Engineering des NES-Emulators im Spiel für GameCube

Bild

Während ich nach Möglichkeiten suchte, die in Animal Crossing verbleibenden Entwicklermenüs zu aktivieren, einschließlich des Spielauswahlmenüs für den NES-Emulator, fand ich eine interessante Funktion, die im ursprünglichen Spiel vorhanden war und ständig aktiv war, aber von Nintendo nie verwendet wurde.

Zusätzlich zu den NES / Famicom-Spielen im Spiel können Sie neue NES-Spiele von einer Speicherkarte herunterladen.

Ich habe auch einen Weg gefunden, diesen ROM-Bootloader zu verwenden, um meinen Code und meine Daten in das Spiel zu patchen, wodurch Sie Code über eine Speicherkarte ausführen können.

Einführung - NES-Konsolenobjekte


Gewöhnliche NES-Spiele, die bei Animal Crossing erhältlich sind, sind separate Möbelstücke in Form einer NES-Konsole mit einer darauf liegenden Patrone.

Nachdem Sie dieses Objekt in Ihrem Haus gefunden und mit ihm interagiert haben, können Sie dieses einzige Spiel ausführen. Das Bild unten zeigt Excitebike und Golf.


Es gibt auch ein allgemeines NES-Konsolenobjekt, in dem keine integrierten Spiele vorhanden sind. Es kann bei Redd gekauft und manchmal durch zufällige Ereignisse erhalten werden, indem beispielsweise an der Pinnwand der Stadt gelesen wird, dass die Konsole an einem zufälligen Punkt in der Stadt vergraben ist.


Dieses Objekt sieht aus wie eine NES-Konsole, auf der sich keine Kassetten befinden.


Das Problem mit diesem Objekt ist, dass es als nicht spielbar angesehen wurde. Jedes Mal, wenn Sie mit ihm interagieren, sehen Sie eine Meldung, dass Sie keine Spielesoftware haben.


Es stellte sich heraus, dass dieses Objekt tatsächlich versucht, die Speicherkarte nach speziell entwickelten Dateien zu durchsuchen, die ROM-Images für NES enthalten! Der zum Ausführen eingebetteter Spiele verwendete NES-Emulator scheint der vollständige Standard-NES-Emulator für den GameCube zu sein und kann die meisten Spiele starten.

Bevor ich diese Funktionen demonstriere, werde ich den Prozess des Reverse Engineering erläutern.

Suchen Sie den ROM-Bootloader auf der Speicherkarte


Wir suchen ein Entwicklermenü


Zunächst wollte ich einen Code finden, der verschiedene Entwicklermenüs aktiviert, z. B. das Kartenauswahlmenü oder das Spielauswahlmenü für den NES-Emulator. Das Forest Map Select- Menü, mit dem Sie problemlos verschiedene Orte des Spiels laden können, war recht einfach zu finden. Ich habe nur nach der Zeile FOREST MAP SELECT gesucht, die oben auf dem Bildschirm angezeigt wird (sie ist in verschiedenen Videos und Screenshots im Internet zu sehen )

In „FOREST MAP SELECT“ gibt es Querverweise von Daten auf die Funktion select_print_wait , was zu einer Reihe anderer Funktionen führt, die ebenfalls das Präfix select_* , einschließlich der Funktion select_init . Es stellte sich heraus, dass es sich um Funktionen handelte, die das Kartenauswahlmenü steuern.

Die Funktion select_init führt zu einer weiteren interessanten Funktion namens game_get_next_game_dlftbl . Diese Funktion verknüpft alle anderen Menüs und „Szenen“, die Sie ausführen können: einen Bildschirm mit dem Nintendo-Logo, den Hauptbildschirm, das Kartenauswahlmenü, das NES-Emulatormenü (Famicom) usw. Es beginnt am Anfang der Hauptspielprozedur, findet heraus, welche game_dlftbls soll, und findet seinen Eintrag in der Tabellendatenstruktur mit dem Namen game_dlftbls . Diese Tabelle enthält Links zu den Verarbeitungsfunktionen verschiedener Szenen sowie einige andere Daten.


Eine sorgfältige Untersuchung des ersten Blocks der Funktion ergab, dass die Funktion "next game init" geladen wird, und beginnt dann, sie mit einer Reihe bekannter init-Funktionen zu vergleichen:

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


Einer der Funktionszeiger, nach denen er sucht, ist famicom_emu_init , der für die Ausführung des NES / Famicom-Emulators verantwortlich ist. game_get_next_game_init Ergebnisses von game_get_next_game_init auf famicom_emu_init oder select_init im Dolphin-Debugger konnte ich spezielle Menüs anzeigen. Der nächste Schritt besteht darin, zu bestimmen, wie diese Zeiger während der Programmausführung auf normale Weise gesetzt werden. Die Funktion game_get_next_game_init lädt nur den Wert bei Offset 0xC ersten Arguments in game_get_next_game_dlftbl .

Das Verfolgen dieser in verschiedenen Datenstrukturen festgelegten Werte war etwas langweilig, daher gehe ich direkt zum Kern. Das Wichtigste, was ich gefunden habe:

  • Wenn das Spiel auf die übliche Weise startet, führt es die folgende Abfolge von Aktionen aus:
    • first_game_init
    • second_game_init
    • trademark_init
    • play_init
  • player_select_init setzt den nächsten Init auf select_init . Dieser Bildschirm sollte es Ihnen ermöglichen, einen Spieler unmittelbar nach der Auswahl einer Karte auszuwählen, aber es scheint, dass er nicht richtig funktioniert.

Ich habe auch eine namenlose Funktion gefunden, die die Init-Funktion des Emulators definiert, aber ich habe nichts gefunden, das die Init-Funktion auf den Init-Wert des Players oder der Kartenauswahl setzt.

Zu diesem Zeitpunkt wurde mir klar, dass ich ein weiteres dummes Problem mit dem Laden von Funktionsnamen in die IDA hatte: Aufgrund des regulären Ausdrucks zum Ausschneiden von Zeilen in der Debug-Symboldatei habe ich alle Funktionsnamen verpasst, die mit einem Großbuchstaben beginnen . Die von famicom_emu_init eingerichtete Funktion sah aus wie Übergänge zwischen Szenen und hieß Game_play_fbdemo_wipe_proc .

Game_play_fbdemo_wipe_proc verarbeitet Übergänge zwischen Szenen, wie z. B. Bildschirmlöschungen und Stromausfälle.

Unter bestimmten Bedingungen wurde der Bildschirmübergang vom üblichen Gameplay zur Anzeige des Emulators durchgeführt. Er hat die Emulator-Init-Funktion eingestellt.

Umgang mit Konsolenobjekten


Tatsächlich wechseln Handler von Möbelobjekten für NES-Konsolen den Bildschirmübergangshandler zum Emulator. Wenn ein Spieler mit einer der Konsolen aMR_FamicomEmuCommonMove , wird aMR_FamicomEmuCommonMove .

Beim Aufrufen der Funktion enthält r6 den Indexwert, der den Zahlen in den Namen der NES- famicom.arc in 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 ist ein proprietäres Dateiarchivformat.)

Wenn r6 ungleich Null ist, wird es in aMR_RequestStartEmu Aufruf von aMR_RequestStartEmu . In diesem Fall wird der Übergang zum Emulator ausgelöst.


Wenn jedoch r6 Null ist, wird aMR_RequestStartEmu_MemoryC die Funktion aMR_RequestStartEmu_MemoryC aufgerufen. Wenn Sie den Wert im Debugger auf 0 setzen, wird die Meldung "Ich habe keine Software" angezeigt. Ich habe mich nicht sofort daran erinnert, dass ich das NES-Konsolenobjekt überprüfen musste, um sicherzustellen, dass es den r6 Wert zurücksetzt, aber es stellte sich heraus, dass der Nullindex für das Konsolenobjekt ohne Kassette verwendet wird.

Obwohl aMR_RequestStartEmu den Indexwert einfach in einer Art Datenstruktur aMR_RequestStartEmu_MemoryC führt aMR_RequestStartEmu_MemoryC viel komplexere Operationen aus ...


Dieser dritte Codeblock ruft aMR_GetCardFamicomCount und aMR_GetCardFamicomCount nach einem Ergebnis ungleich Null. Andernfalls werden die meisten interessanten Dinge auf der linken Seite des Funktionsdiagramms übersprungen.

aMR_GetCardFamicomCount ruft famicom_get_disksystem_titles , das dann memcard_game_list , und hier wird alles sehr interessant.

memcard_game_list die Speicherkarte memcard_game_list und beginnt im memcard_game_list , wobei jeder der einzelnen Werte memcard_game_list . Durch Verfolgen der Funktion im Debugger konnte ich verstehen, dass die Werte mit jeder meiner Dateien auf der Speicherkarte verglichen wurden.


Die Funktion entscheidet abhängig von den Ergebnissen der Überprüfung mehrerer Zeilen, ob die Datei heruntergeladen werden soll oder nicht. Zunächst wird das Vorhandensein der Zeilen „GAFE“ und „01“ überprüft, die die Kennungen des Spiels und des Unternehmens darstellen. 01 steht für Nintendo, GAFE steht für Animal Crossing. Ich denke, es steht für GameCube Animal Forest English.

Sie überprüft dann die Zeilen "DobutsunomoriP_F_" und "SAVE". In diesem Fall sollte die erste Zeile übereinstimmen, nicht jedoch die zweite. Es stellte sich heraus, dass "DobutsunomoriP_F_SAVE" der Name der Datei ist, in der die Daten der eingebetteten Spiele für NES gespeichert sind. Daher werden alle Dateien außer dieser mit dem Präfix "DobutsunomoriP_F_" geladen.

Mit dem Dolphin-Debugger, um Zeichenfolgenvergleiche mit "SAVE" zu überspringen und den Spieltrick zu machen, um zu glauben, dass meine "SAVE" -Datei sicher heruntergeladen werden kann, habe ich dieses Menü nach Verwendung der NES-Konsole erhalten:


Ich antwortete mit "Ja" und versuchte, die Sicherungsdatei als Spiel zu laden. Danach sah ich zum ersten Mal den eingebauten Spielabsturzbildschirm:


Großartig! Jetzt weiß ich, dass sie tatsächlich versucht, Spiele von einer Speicherkarte herunterzuladen, und ich kann mit der Analyse des Formats für die Sicherungsdateien beginnen, um festzustellen, ob ein echtes ROM heruntergeladen werden kann.

Als erstes habe ich versucht herauszufinden, wo der Name des Spiels aus der Speicherkartendatei gelesen wird. Beim Durchsuchen der Zeile "FEFSC", die in der Meldung "Möchten Sie <Name> spielen?" 0x642 war, fand ich den Offset, mit dem es aus der Datei gelesen wurde: 0x642 . Ich habe die Sicherungsdatei kopiert, den Dateinamen in "DobutsunomoriP_F_TEST" geändert, die Bytes am Offset 0x642 in "TESTING" geändert und die geänderte Speicherung importiert. 0x642 der gewünschte Name im Menü angezeigt.

Nach dem Hinzufügen einiger weiterer Dateien in diesem Format wurden im Menü einige weitere Optionen angezeigt:


ROM herunterladen


Wenn aMR_GetCardFamicomCount ungleich Null zurückgegeben wird, wird der Speicher auf dem Heap zugewiesen. famicom_get_disksystem_titles wird direkt famicom_get_disksystem_titles aufgerufen. Anschließend werden in der Datenstruktur eine Reihe zufälliger Offsets angegeben. Anstatt zu entschlüsseln, wo diese Werte gelesen werden, begann ich, die Liste der famicom Funktionen zu studieren.

Es stellte sich heraus, dass ich famicom_rom_load brauchte. Es steuert das Laden des ROM entweder von einer Speicherkarte oder von internen Ressourcen des Spiels.


Das Wichtigste in diesem Block "Boot von Speicherkarte" ist, dass er aufruft
memcard_game_load . Sie legt die Datei erneut auf die Speicherkarte, liest sie und analysiert sie. Hier werden die wichtigsten Dateiformatoptionen deutlich.

Prüfsummenwert


Das erste, was nach dem Hochladen der Datei passiert, ist die Prüfsummenberechnung. Die Funktion calcSum wird calcSum ist ein sehr einfacher Algorithmus, der die Werte aller Bytes in den Daten von der Speicherkarte summiert. Die unteren acht Bits des Ergebnisses müssen Null sein. Das heißt, um diese Prüfung zu bestehen, müssen Sie die Werte aller Bytes in der Quelldatei summieren, den Wert berechnen, der hinzugefügt werden muss, damit die unteren acht Bits Null werden, und diesen Wert dann dem Prüfsummenbyte in der Datei zuweisen.

Wenn die Überprüfung fehlschlägt, erhalten Sie eine Meldung über die Unmöglichkeit, die Speicherkarte korrekt zu lesen, und es passiert nichts. Während des Debuggens muss ich nur diese Prüfung überspringen.

ROM kopieren


Gegen Ende von memcard_game_load passiert eine weitere interessante Sache. Es gibt mehrere weitere interessante Codeblöcke zwischen ihr und der Prüfsumme, aber keiner von ihnen führt zu einer Verzweigung, die die Ausführung dieses Verhaltens überspringt.


Wenn ein bestimmter 16-Bit-Ganzzahlwert, der von der Speicherkarte gelesen wird, nicht gleich Null ist, wird eine Funktion aufgerufen, die den Komprimierungsheader im Puffer überprüft. Es sucht nach proprietären Nintendo-Komprimierungsformaten, indem es sich den Anfang des Yay0- oder Yaz0-Puffers ansieht. Wird eine dieser Zeilen gefunden, wird die Entpackfunktion aufgerufen. Andernfalls wird eine einfache Kopierkopierfunktion ausgeführt. In jedem Fall wird danach eine Variable namens nesinfo_data_size .

Ein weiterer Hinweis auf den Kontext ist, dass ROM-Dateien für eingebettete NES-Spiele die Yaz0-Komprimierung verwenden und diese Zeile in den Kopfzeilen ihrer Dateien vorhanden ist.

Nachdem ich den Wert beobachtet hatte, der auf Null geprüft und der Puffer an die Komprimierungsprüfungsfunktionen übergeben wurde, fand ich schnell heraus, von wo das Spiel in der Datei auf der Speicherkarte gelesen wurde. Die Nullprüfung wird für einen Teil des 32-Byte-Puffers durchgeführt, der vom Offset 0x640 in der Datei kopiert wurde, bei der es sich höchstwahrscheinlich um den ROM-Header handelt. Diese Funktion überprüft auch andere Teile der Datei und in ihnen befindet sich der Name des Spiels (beginnend mit dem dritten Byte des Headers).

In dem Code-Ausführungspfad, den ich gefunden habe, befindet sich der ROM-Puffer unmittelbar nach diesem 32-Byte-Header-Puffer.


Diese Informationen reichen aus, um zu versuchen, eine funktionierende ROM-Datei zu erstellen. Ich habe gerade eine der anderen Animal Crossing-Sicherungsdateien genommen und sie in einem Hex-Editor bearbeitet, um den Dateinamen durch DobutsunomoriP_F_TEST zu ersetzen und alle Bereiche zu löschen, in denen ich die Daten einfügen wollte.

Für einen Testlauf habe ich das Pinball-Spiel-ROM verwendet, das sich bereits im Spiel befindet, und seinen Inhalt nach dem 32-Byte-Header eingefügt. Anstatt den Prüfsummenwert zu berechnen, setze ich Haltepunkte, sodass ich calcSum einfach überspringe und auch die Ergebnisse anderer Überprüfungen beobachte, die zu einem Zweig führen können, der den ROM-Startvorgang überspringt.

Schließlich importierte ich die neue Datei über den Dolphin-Speicherkarten-Manager, startete das Spiel neu und versuchte, die Konsole zu starten.



Es hat funktioniert! Es gab einige kleine Grafikfehler im Zusammenhang mit Dolphin-Parametern, die sich auf den vom NES-Emulator verwendeten Grafikmodus auswirkten, aber im Allgemeinen lief das Spiel einwandfrei. (In neueren Versionen von Dolphin sollte dies standardmäßig funktionieren.)

Um sicherzustellen, dass auch andere Spiele gestartet werden, habe ich versucht, mehrere andere ROMs zu schreiben, die nicht im Spiel enthalten waren. Battletoads wurden gestartet, funktionierten jedoch nach dem Begrüßungsbildschirmtext nicht mehr (nach weiteren Einstellungen konnte ich ihn spielbar machen). Mega Man hingegen hat perfekt funktioniert:


Um zu lernen, wie man neue ROM-Dateien generiert, die ohne das Eingreifen von Debuggern geladen werden können, musste ich anfangen, Code zu schreiben und das Parsen des Dateiformats besser zu verstehen.

Externes ROM-Dateiformat


Der wichtigste Teil des Parsens von Dateien findet unter memcard_game_load . Diese Funktion besteht aus sechs Hauptabschnitten von Code-Analyseblöcken:

  • Prüfsumme
  • Dateinamen speichern
  • ROM-Datei-Header
  • Unbekannter Puffer ohne Verarbeitung kopiert
  • Textkommentar, Symbol und Bannerlader (um eine neue Sicherungsdatei zu erstellen)
  • ROM-Bootloader


Prüfsumme


Die unteren acht Bits der Summe aller Bytewerte in der Sicherungsdatei müssen Null sein. Hier ist einfacher Python-Code, der das erforderliche Prüfsummenbyte generiert:

 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 

Es gibt wahrscheinlich einen speziellen Ort zum Speichern des Prüfsummenbytes, aber das Hinzufügen zum leeren Bereich ganz am Ende der Sicherungsdatei funktioniert recht gut.

Dateiname


Auch hier sollte der Name der Sicherungsdatei mit "DobutsunomoriP_F_" beginnen und mit etwas enden, das nicht "SAVE" enthält. Dieser Dateiname wird einige Male kopiert, und in einem Fall wird der Buchstabe "F" durch "S" ersetzt. Dies ist der Name der Sicherungsdateien für das NES-Spiel ("DobutsunomoriP_S_NAME").

ROM-Header


Eine direkte Kopie des 32-Byte-Headers wird in den Speicher geladen. Einige der Werte in diesem Header werden verwendet, um zu bestimmen, wie nachfolgende Abschnitte behandelt werden sollen. Grundsätzlich sind dies einige 16-Bit-Größenwerte und gepackte Parameterbits.

Wenn Sie den vom Header kopierten Zeiger bis zum Start der Funktion verfolgen und die Position ihres Arguments ermitteln, zeigt die Signatur der folgenden Funktion, dass sie tatsächlich den Typ MemcardGameHeader_t* .

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

Unbekannter Puffer


Überprüft den 16-Bit-Größenwert aus dem Header. Wenn es nicht gleich Null ist, wird die entsprechende Anzahl von Bytes direkt aus dem Dateipuffer in einen neuen Block des zugewiesenen Speichers kopiert. Dadurch wird der Datenzeiger in den Dateipuffer verschoben, sodass das weitere Kopieren ab dem nächsten Abschnitt fortgesetzt werden kann.

Banner, Symbol und Kommentar


Ein anderer Größenwert wird im Header überprüft. Wenn er ungleich Null ist, wird die Funktion zur Überprüfung der Dateikomprimierung aufgerufen. Bei Bedarf wird der SetupExternCommentImage gestartet, wonach SetupExternCommentImage .

Diese Funktion führt drei Dinge aus: "Kommentar", Bannerbild und Symbol. Für jeden von ihnen gibt es einen Code im ROM-Header, der zeigt, wie man mit ihnen umgeht. Es gibt folgende Optionen:

  1. Standardwert verwenden
  2. Kopieren Sie aus dem Bereich Banner / Symbol / Kommentar in die ROM-Datei
  3. Aus alternativem Puffer kopieren

Die Standardwerte des Codes bewirken, dass das Symbol oder Banner von der Ressource auf der Festplatte geladen wird. Dem Namen der Sicherungsdatei und dem Kommentar (Textbeschreibung der Datei) werden die Werte "Animal Crossing" und "NES Cassette Save Data" zugewiesen. So sieht es aus:


Der zweite Wert des Codes kopiert einfach den Namen des Spiels aus der ROM-Datei (eine Alternative zu "Animal Crossing") und versucht dann, die Zeichenfolge "] ROM" im Dateikommentar zu finden und durch "] SAVE" zu ersetzen. Anscheinend sollten die Dateien, die Nintendo veröffentlichen wollte, das Format „Game Name [NES] ROM“ oder ähnliches haben.

Für das Symbol und das Banner versucht der Code, das Bildformat zu bestimmen, einen diesem Format entsprechenden festen Größenwert zu erhalten und das Bild dann zu kopieren.

Beim letzten Codewert werden der Dateiname und die Beschreibung ohne Änderungen aus dem Puffer kopiert, und das Symbol und das Banner werden ebenfalls aus dem alternativen Puffer geladen.

ROM


Wenn Sie sich den Screenshot des memcard_game_load Kopieren von memcard_game_load genau ansehen, sehen Sie, dass der auf Gleichheit auf Null geprüfte 16-Bit-Wert um 4 Bit nach links verschoben wird (multipliziert mit 16) und dann als Größe der memcpy Funktion verwendet wird, wenn keine Komprimierung erkannt wird. Dies ist ein weiterer Größenwert, der im Header vorhanden ist.

Wenn die Größe ungleich Null ist, werden die ROM-Daten auf Komprimierung überprüft und dann kopiert.

Unbekannte Puffer- und Fehlersuche


Obwohl das Herunterladen neuer ROMs ziemlich merkwürdig ist, war das Interessanteste an diesem ROM-Loader für mich, dass dies tatsächlich der einzige Teil des Spiels ist, der Benutzereingaben variabler Größe empfängt und an verschiedene Speicherorte kopiert. Fast alles andere verwendet Puffer mit konstanter Größe. Dinge wie Namen und Buchstabentexte mögen unterschiedlich lang erscheinen, aber im Wesentlichen ist der leere Raum einfach mit Leerzeichen gefüllt. Nullterminierte Zeichenfolgen werden selten verwendet, um häufige Speicherbeschädigungsfehler zu vermeiden, z. B. die Verwendung von strcpy mit einem Puffer, der zu klein ist, um Zeichenfolgen zu kopieren.

Ich war sehr interessiert an der Möglichkeit, einen Exploit des Spiels basierend auf gespeicherten Dateien zu finden, und es schien, dass dies die beste Option war.

Die meisten oben beschriebenen ROM-Dateivorgänge verwenden Kopien mit konstanter Größe, mit Ausnahme eines unbekannten Puffers und von ROM-Daten. Leider weist der Code, der diesen Puffer verarbeitet, genau so viel Speicherplatz zu, wie zum Kopieren erforderlich ist, sodass kein Überlauf auftritt und das Festlegen sehr großer ROM-Dateigrößen nicht sehr nützlich war.

Aber ich wollte immer noch wissen, was mit diesem Puffer passiert, der ohne Verarbeitung kopiert wird.

Handler für NES-Informationsetiketten


Ich kehrte zu famicom_rom_load . Nach dem Laden des ROM von einer Speicherkarte oder Festplatte werden verschiedene Funktionen aufgerufen:

  • nesinfo_tag_process1
  • nesinfo_tag_process2
  • nesinfo_tag_process3

Nachdem ich den Ort verfolgt hatte, an dem der unbekannte Puffer kopiert wurde, stellte ich sicher, dass diese Aufgabe von diesen Funktionen ausgeführt wird. Sie beginnen mit einem Aufruf von nesinfo_next_tag , der einen einfachen Algorithmus ausführt:

  • Überprüft, ob der angegebene Zeiger nesinfo_tags_end Zeiger in nesinfo_tags_end . Wenn es kleiner als nesinfo_tags_end oder nesinfo_tags_end Null ist, prüft es, nesinfo_tags_end die Zeichenfolge "END" im Header des Zeigers vorhanden ist.

    • Wenn "END" erreicht ist oder der Zeiger auf oder über nesinfo_tags_end , gibt die Funktion null zurück.
    • Andernfalls wird das Byte am Offset 0x3 Zeigers zu 4 und zum aktuellen Zeiger addiert, wonach der Wert zurückgegeben wird.

Dies sagt uns, dass es eine Art Etikettenformat aus einem aus drei Buchstaben bestehenden Namen, einem Datengrößenwert und den Daten selbst gibt. Das Ergebnis ist ein Zeiger auf die nächste Beschriftung, da die aktuelle Beschriftung übersprungen wird ( cur_ptr + 4 überspringt den dreistelligen Namen und ein Byte und size_byte überspringt die Daten).

Wenn das Ergebnis nicht Null ist, führt die Etikettenverarbeitungsfunktion eine Reihe von Zeichenfolgenvergleichen durch, um herauszufinden, welches Etikett verarbeitet werden muss. Einige der in nesinfo_tag_process1 überprüften nesinfo_tag_process1 : VEQ, VNE, GID, GNO, BBR und QDS.


Wenn eine Beschriftungsübereinstimmung gefunden wird, wird ein Handlercode ausgeführt. Einige der Handler tun nichts anderes, als eine Bezeichnung in der Debug-Nachricht anzuzeigen. Andere haben komplexere Handler. Nach der Verarbeitung des Etiketts versucht die Funktion, das nächste Etikett zu erhalten und die Verarbeitung fortzusetzen.

Glücklicherweise gibt es viele detaillierte Debugging-Meldungen, die angezeigt werden, wenn Tags erkannt werden.Sie sind alle auf Japanisch, daher müssen sie zuerst aus Shift-JIS dekodiert und übersetzt werden. Beispielsweise könnte eine Nachricht für QDS lauten: "Laden eines Festplattenspeicherbereichs" oder "Da dies die erste Ausführung ist, erstellen Sie einen Festplattenspeicherbereich". Die Meldungen für den BBR lauten "Laden eines Backups der Batterie" oder "Da dies der erste Start ist, führen wir eine Bereinigung durch".

Beide Codes laden auch einige Werte aus dem Datenabschnitt ihrer Beschriftungen und verwenden sie, um den Versatz in den ROM-Daten zu berechnen, wonach sie Kopiervorgänge ausführen. Offensichtlich sind sie für die Bestimmung der Teile im ROM-Speicher verantwortlich, die mit der Zustandserhaltung verbunden sind.

Es gibt auch ein "HSC" -Tag mit einer Debug-Nachricht, die besagt, dass Punktdatensätze verarbeitet werden. Sie erhält einen Versatz im ROM aus ihren Tag-Daten sowie den ursprünglichen Wert des Score-Datensatzes. Diese Markierungen können verwendet werden, um einen Platz im Speicher des NES-Spiels zum Speichern von Highscores anzuzeigen, möglicherweise um sie in Zukunft zu speichern und wiederherzustellen.

Diese Tags erstellen ein ziemlich komplexes ROM-Metadaten-Download-System. Darüber hinaus führen viele von ihnen zu Anrufen, memcpydie auf den in den Etikettendaten übertragenen Werten basieren.

Insektenjagd


, , , , 16- . 16- NES, , 32- GameCube.

, , memcpy , 0xFFFF .

QDS

QDS lädt einen 24-Bit-Offset aus seinen Tag-Daten sowie einen 16-Bit-Größenwert.

Das Gute dabei ist, dass der Offset verwendet wird, um die Zieladresse des Kopiervorgangs zu berechnen. Die Basisadresse des Offsets ist der Anfang der heruntergeladenen Daten, die Kopierquelle befindet sich in der ROM-Datei der Speicherkarte und die Größe wird durch den 16-Bit-Größenwert auf dem Etikett festgelegt.

Der 24-Bit-Wert hat einen Maximalwert 0xFFFFFF, der viel mehr ist als zum Schreiben außerhalb der geladenen ROM-Daten erforderlich. Es gibt jedoch bestimmte Probleme ...

Das erste ist, dass der maximale Größenwert zwar gleich ist 0xFFFF, aber zunächst zum Zurücksetzen der Speicherpartition verwendet wird. Wenn der Größenwert zu hoch ist (nicht viel größer 0x1000), wird die „QDS“ -Markierung im Spielcode zurückgesetzt.

Und darin liegt das Problem, weil es nesinfo_tag_process1tatsächlich zweimal aufgerufen wird. Zum ersten Mal erhält sie einige Informationen über den Speicherplatz, den sie zur Vorbereitung der gespeicherten Daten benötigt. QDS- und BBR-Tags werden beim ersten Start nicht vollständig verarbeitet. Nach dem ersten Lauf wird ein Ort für die Sicherungsdaten vorbereitet und die Funktion erneut aufgerufen. Dieses Mal werden die QDS- und BBR-Tags vollständig verarbeitet. Wenn jedoch die Tag-Namenszeichenfolgen aus dem Speicher gelöscht werden, ist es unmöglich, die Tags erneut zuzuordnen!

Dies kann vermieden werden, indem ein kleinerer Größenwert eingestellt wird. Ein weiteres Problem besteht darin, dass sich der Versatzwert nur im Speicher vorwärts bewegen kann und sich die ROM-NES-Daten im Heap ziemlich nahe am Ende des verfügbaren Speichers befinden.

, , .

, malloc , , malloc . . free .

malloc ( 0x7373 ) , free . , OSPanic .


, - , . , - , - . - , 0x73730000 , , ( , ), .

nesinfo_update_highscore

, QDS, BBR HSC — nesinfo_update_highscore . QDS, BBR OFS (offset, ) , , HSC . , NES.

, QDS, 0xFFFF . BBR QDS . , . , ROM , 0xFFFF .

Die Basisadresse, zu der der Offset hinzugefügt wird, ist der 0x800C3180Speicherdatenpuffer. Diese Adresse ist viel niedriger als die ROM-Daten, was uns mehr Freiheit bei der Auswahl eines Aufnahmeorts gibt. Beispielsweise ist es recht einfach, die Rücksprungadresse im Stapel in die Adresse umzuschreiben 0x812F95DC.

Leider hat das auch nicht funktioniert. Es stellt sich heraus, dass es nesinfo_tag_process1auch die akkumulierte Größe der Offsets dieser Labels überprüft und diese Größe verwendet, um den Speicherplatz zu initialisieren:

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


Mit dem Versatzwert, den ich zu berechnen versuchte, führte dies dazu, dass 0x48D91EC(76.386.796) Bytes Speicher gelöscht wurden , weshalb das Spiel spektakulär abstürzte.

PAT-Marke


Ich hatte bereits begonnen, die Hoffnung zu verlieren, weil all diese Tags, die ungeschützte Anrufe memcpytätigten, fehlgeschlagen waren, noch bevor ich sie verwenden konnte. Ich beschloss, nur den Zweck jedes Tags zu dokumentieren, und kam nach und nach zu den Tags in nesinfo_tag_process2.

Die meisten Label-Handler nesinfo_tag_process2starten nie, da sie nur funktionieren, wenn der Zeiger nesinfo_rom_startnicht Null ist. Nichts im Code weist diesem Zeiger einen Wert ungleich Null zu. Es wird mit einem Nullwert initialisiert und nie wieder verwendet. Beim Laden wird nur das ROM eingestellt nesinfo_data_start, so dass es wie toter Code aussieht.

Es gibt jedoch eine Bezeichnung, die auch dann funktionieren kann, wenn sie nicht Null ist nesinfo_rom_start: PAT. Dies ist die schwierigste Bezeichnung in einer Funktion nesinfo_tag_process2.


Es wird auch als Zeiger verwendet nesinfo_rom_start, überprüft es jedoch nie auf Null. Das PAT-Tag liest seinen eigenen Tag-Datenpuffer und verarbeitet Codes, die Offsets berechnen. Diese Offsets werden dem Zeiger hinzugefügt, nesinfo_rom_startum die Zieladresse zu berechnen, und dann werden die Bytes aus dem Patch-Puffer an diesen Speicherort kopiert. Dieses Kopieren erfolgt durch Laden und Speichern von Bytes ohne Verwendung von Anweisungen memcpy, sodass ich es vorher nicht bemerkt habe.

Jeder PAT-Markierungsdatenpuffer hat einen 8-Bit-Typcode, eine 8-Bit-Patchgröße und einen 16-Bit-Offsetwert, gefolgt von den Patchdaten.

  • Wenn der Code 2 ist, wird der Versatzwert zur aktuellen Summe der Versätze addiert.
  • Wenn der Code 9 ist, wird der Versatz um 4 Bits nach oben verschoben und zur aktuellen Summe der Versätze addiert.
  • Wenn der Code 3 ist, wird die Summe der Offsets auf 0 zurückgesetzt.

Die maximale Größe des NES-Informationsetiketts beträgt 255, d. H. Die größte PAT-Patchgröße beträgt 251 Bytes. Es können jedoch mehrere PAT-Markierungen verwendet werden, dh Sie können mehr als 251 Byte patchen sowie nicht zusammenhängende Leerzeichen patchen.

Solange wir eine Reihe von PAT-Sohlen mit Code 2 oder Code 9 haben, sammelt sich der Versatz des Zielzeigers weiter an. Beim Kopieren von Patch-Daten werden diese auf Null zurückgesetzt. Wenn Sie jedoch eine Patch-Größe von Null verwenden, kann dies vermieden werden. Es ist klar, dass dies verwendet werden kann, um einen beliebigen Versatz mit einem Nullzeiger unter nesinfo_rom_startVerwendung vieler PAT-Markierungen zu berechnen .

Es gibt jedoch zwei weitere Überprüfungen der Codewerte ...

  • Wenn der Code zwischen 0x80und liegt 0xFF, wird er hinzugefügt 0x7F80und dann um 16 Bit nach oben verschoben. Dann wird es zum 16-Bit-Offsetwert addiert und als Endadresse für das Patch verwendet.

Dadurch können wir eine Zieladresse für den Patch im Bereich von 0x80000000bis zuweisen 0x807FFFFF! Hier befindet sich der Großteil des Animal Crossing-Codes im Speicher. Dies bedeutet, dass wir den Animal Crossing-Code selbst mithilfe von ROM-Metadatenbezeichnungen aus einer Datei auf einer Speicherkarte patchen können.

Mit Hilfe eines kleinen Patch-Loaders können Sie sogar problemlos größere Patches von einer Speicherkarte an eine beliebige Adresse herunterladen.

Zur schnellen Überprüfung habe ich einen Patch erstellt, der "zuru mode 2" (Spieleentwicklermodus, beschrieben in meinem vorherigen Artikel) enthielt ) ROM . , - «zuru mode 1», , 2. .


ROM.


ROM NES, .


Es funktioniert!


, , :

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 : . 0x00 — : .
  • PAT \x08 \xA0 \x04 \x6F\x9C \x00\x00\x00\x7D : 0x80206F9C 0x0000007D .
    • 0x08 — .
    • 0xA0 0x7F80 0x8020 , 16 .
    • 0x04Ist die Größe der Patch-Daten ( 0x0000007D).
    • 0x6F9C Sind die unteren 16 Bits der Zieladresse.
    • 0x0000007D Sind die Patch-Daten.
  • END \x00 : Endmarkierungsmarkierung.

Wenn Sie selbst mit der Erstellung von Patcher- oder ROM-Sicherungsdateien experimentieren möchten, habe ich unter https://github.com/jamchamb/ac-nesrom-save-generator einen sehr einfachen Code zum Generieren von Dateien veröffentlicht. Ein Patch wie der oben gezeigte kann mit dem folgenden Befehl generiert werden:

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

Beliebige Codeausführung


Dank dieses Tags können Sie in Animal Crossing eine beliebige Codeausführung erreichen.

Aber hier kommt die letzte Hürde: Die Verwendung von Patches für Daten funktioniert gut, aber beim Patchen von Codeanweisungen treten Probleme auf.

Wenn Patches aufgezeichnet werden, folgt das Spiel weiterhin den alten Anweisungen, die an seiner Stelle waren. Dies scheint ein Caching-Problem zu sein, und tatsächlich ist es das auch. Die GameCube-CPU verfügt über Anweisungs-Caches, wie in den technischen Daten beschrieben .

Um zu verstehen, wie Sie den Cache leeren können, habe ich begonnen, die Cache-bezogenen Funktionen aus der GameCube SDK-Dokumentation zu untersuchen, und festgestellt ICInvalidateRange. Diese Funktion macht die zwischengespeicherten Befehlsblöcke an der angegebenen Speicheradresse ungültig, wodurch der geänderte Befehlsspeicher mit aktualisiertem Code ausgeführt werden kann.

Ohne die Möglichkeit, den ursprünglichen Code auszuführen, können wir jedoch immer noch nicht aufrufen ICInvalidateRange. Für eine erfolgreiche Codeausführung benötigen wir noch einen Trick.

Als mallocich die Implementierung auf die Möglichkeit der Verwendung eines Exploits mit Heap-Überlauf untersuchte, stellte ich fest, dass Implementierungsfunktionen mallocmithilfe einer aufgerufenen Datenstruktur dynamisch deaktiviert werden können my_malloc. my_mallocLädt einen Zeiger auf die aktuelle Implementierung mallocoder freevon einer statischen Stelle im Speicher und ruft diese Funktion auf, wobei alle übergebenen Argumente übergeben werden my_malloc.

NES-Emulator wird aktiv verwendetmy_malloc ROM NES , , , PAT.

my_malloc , , , malloc free . , my_malloc .

- Dōbutsu no Mori e+ Cuyler PowerPC : https://www.youtube.com/watch?v=BdxN7gP6WIc . (Dōbutsu no Mori e+ Animal Crossing GameCube, . .) , ID Z.


, homebrew Animal
Crossing GameCube.

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


All Articles