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)
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:
- Standardwert verwenden
- Kopieren Sie aus dem Bereich Banner / Symbol / Kommentar in die ROM-Datei
- 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, memcpy
die 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_process1
tatsä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 0x800C3180
Speicherdatenpuffer. 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_process1
auch 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 memcpy
tä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_process2
starten nie, da sie nur funktionieren, wenn der Zeiger nesinfo_rom_start
nicht 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_start
um 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_start
Verwendung vieler PAT-Markierungen zu berechnen .Es gibt jedoch zwei weitere Überprüfungen der Codewerte ...- Wenn der Code zwischen
0x80
und liegt 0xFF
, wird er hinzugefügt 0x7F80
und 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 0x80000000
bis 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 .0x04
Ist 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 malloc
ich die Implementierung auf die Möglichkeit der Verwendung eines Exploits mit Heap-Überlauf untersuchte, stellte ich fest, dass Implementierungsfunktionen malloc
mithilfe einer aufgerufenen Datenstruktur dynamisch deaktiviert werden können my_malloc
. my_malloc
Lädt einen Zeiger auf die aktuelle Implementierung malloc
oder free
von 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.