Hallo an alle,

Trotz meiner großartigen Erfahrung beim Umkehren von Spielen für Sega Mega Drive
ich mich nie dafür entschieden, dafür zu knacken, und sie sind mir im Internet nicht aufgefallen. Aber neulich gab es einen lustigen Crackie, der lösen wollte. Ich teile mit Ihnen die Entscheidung ...
Beschreibung
Beschreibung der Aufgabe und Rum selbst können hier heruntergeladen werden .
Trotz der Tatsache, dass in der Ressourcenliste Hydra angegeben ist, ist Smd Ida Tools der Standard unter den Tools zum Debuggen und Umkehren von Spielen auf Sega . Es hat alles, was Sie brauchen, um diese Creme zu lösen:
- Rumlader für Ida
- Debugger
- RAM-VDP-Speicher anzeigen und ändern
- Zeigen Sie fast vollständige Informationen zu VDP an
Wir legen die neueste Version in den Plugins für Ide ab und schauen uns an, was wir haben.
Lösung
Der Start eines Shogi-Spiels beginnt mit der Ausführung des Reset
Vektors. Ein Zeiger darauf befindet sich im zweiten DWORD vom Anfang des Rums.


Wir sehen einige nicht identifizierte Funktionen ab Adresse 0x27A
. Mal sehen, was da ist.
sub_2EA ()

Aus eigener Erfahrung möchte ich sagen, dass dies normalerweise so aussieht, als würde man darauf warten, dass der VBLANK
Interrupt abgeschlossen ist. Mal sehen, wo sonst die Variable byte_FF0026
wird:

Wir sehen, dass das Nullbit gerade im VBLANK
Interrupt gesetzt ist. Wir rufen also die Variable vblank_ready
und die Funktion, in der sie überprüft wird, ist wait_for_vblank
.
sub_60E ()
Als nächstes wird die Funktion sub_60E
per Code aufgerufen. Mal sehen, was da ist:

Was der erste Befehl in die VDP_CTRL
schreibt, ist der VDP
Steuerbefehl. Um herauszufinden, was sie tut, stehen wir auf diesem Befehl und drücken die J
Taste:

Wir sehen, dass der Eintrag in CRAM
(dem Ort, an dem die Paletten gespeichert sind) initialisiert wird. Dies bedeutet, dass der gesamte nachfolgende Funktionscode einfach eine Art Anfangspalette festlegt. Dementsprechend kann die Funktion init_cram
.
sub_71A ()

Wir sehen, dass ein Befehl erneut an VDP_CTRL
übertragen wird. VDP_CTRL
dann erneut J
und stellen Sie fest, dass dieser Befehl die Aufzeichnung im Videospeicher initialisiert:

Um zu verstehen, was dort in den Videospeicher übertragen wird, macht es keinen Sinn. Deshalb rufen wir einfach die Funktion load_vdp_data
.
sub_C60 ()
Hier passiert fast das Gleiche wie in der vorherigen Funktion. Ohne auf Details load_vdp_data2
, rufen wir einfach die Funktion load_vdp_data2
.
sub_8DA ()
Es gibt bereits mehr Code. Außerdem wird in dieser Funktion eine andere Funktion aufgerufen. Schauen wir genau dort hin - in sub_D08
.
sub_D08 ()

Wir sehen, dass im D0
Register der Befehl für VDP_CTRL
, in D1
- dem Wert, mit dem VRAM
gefüllt wird, und in D2
und D3
- der Breite und Höhe der Füllung (weil sich zwei Zyklen herausstellen: intern und extern). Rufen Sie die Funktion fill_vram_by_addr auf.
sub_8DA ()
Wir kehren zur vorherigen Funktion zurück. Sobald der Wert im D0
Register als Befehl für VDP_CTRL
, drücken Sie die Taste J
für den Wert. Wir bekommen:

Aus der Erfahrung mit dem Umkehren von Spielen zu Sega kann ich wieder sagen, dass dieser Befehl die Aufzeichnung von Mapping-Kacheln initialisiert. Adressen, die in 90% der Fälle mit $Fxxx
, $Exxx
, $Dxxx
, $Cxxx
$Dxxx
, sind Adressen von Regionen mit denselben Zuordnungen. Was sind Zuordnungen:
Mit diesen Werten können Sie festlegen, wo diese oder jene Kachel auf dem Bildschirm angezeigt werden soll (eine Kachel ist ein Quadrat mit 8x8
x 8x8
Pixel).
Die Funktion kann also als init_tile_mappings
.
sub_CDC ()

Der erste Befehl initialisiert den Datensatz unter der Adresse $F000
. Ein Hinweis: Unter den Adressen der " Zuordnung " befindet sich noch eine Region, in der die Sprite-Tabelle gespeichert ist (dies sind ihre Positionen, Kacheln, auf die sie zeigen usw.). Finden Sie heraus, welche Region für das Debuggen verantwortlich ist. init_other_mappings
benötigen wir dies jedoch nicht. Rufen Sie einfach die Funktion init_other_mappings
.
Wir sehen auch, dass in dieser Funktion zwei Variablen initialisiert werden: word_FF000A
und word_FF000C
. Aus eigener Erfahrung (ja, entscheidet er) werde ich sagen, dass wenn zwei Variablen im Adressraum in der Nähe sind und mit der Zuordnung verknüpft sind, sie in den meisten Fällen die Koordinaten eines Objekts sind (z. B. eines Sprites). Daher schlage ich vor, sie sprite_pos_x
und sprite_pos_y
. Der Fehler in x
und y
seitdem zulässig weiter unter Debugging wird es leicht zu beheben sein.
VBLANK
Da die Schleife im Code weiter geht, können wir davon ausgehen, dass wir die grundlegende Initialisierung abgeschlossen haben. Jetzt können Sie sich den VBLANK
Interrupt VBLANK
.

Wir sehen, dass zwei Variablen inkrementieren (was seltsam ist, in der Liste der Links zu jeder von ihnen ist es absolut leer). Da sie jedoch einmal pro Frame aktualisiert werden, können Sie sie als timer1
und timer2
.
Als nächstes wird die Funktion sub_2FE
. Mal sehen, was da ist:
sub_2FE ()

Und dort - arbeiten Sie mit dem IO_CT1_DATA
Port (verantwortlich für den ersten Joystick). Die sub_310
wird in das Register A0
geladen und an die Funktion sub_310
. Wir gehen dorthin:
sub_310 ()

Meine Erfahrung hilft mir wieder. Wenn Sie den Code sehen, der mit dem Joystick funktioniert, und zwei Variablen im Speicher, speichert eine die pressed keys
und die zweite die held keys
, d. H. nur Tasten gedrückt und gehalten. Nennen wir also diese Variablen: pressed_keys
und held_keys
. Und dann kann die Funktion als update_joypad_state
.
sub_2FE ()
Rufen Sie die Funktion als read_joypad
.
Handler-Schleife
Jetzt sieht alles viel klarer aus:

Dieser Zyklus reagiert also auf die gedrückten Tasten und führt die entsprechenden Aktionen aus. Lassen Sie uns jede der in der Schleife aufgerufenen Funktionen durchgehen.
sub_4D4 ()

Es gibt viel Code. Beginnen wir mit der ersten Funktion namens: sub_60C
.
sub_60C ()
Sie tut nichts - es mag zunächst so scheinen. Nur von der aktuellen Funktion zurückzukehren ist rts
. Aber weil es treten nur Sprünge ( bsr
) auf, was bedeutet, dass rts
uns zurück zur rts
zurückbringt. Ich würde diese Funktion als retn_to_loop
.
sub_4D4 ()
Als nächstes sehen wir den Aufruf der Variablen word_FF000E
. Es wird nirgendwo verwendet, außer für die aktuelle Funktion, und der Zweck war mir zunächst nicht klar. Wenn Sie genau hinschauen, können wir davon ausgehen, dass diese Variable nur für eine kleine Verzögerung zwischen der Verarbeitung von Tastenanschlägen benötigt wird. ( Es ist in diesem Rum bereits schlecht implementiert, aber ich denke, ohne diese Variable wäre es viel schlimmer ).

Als nächstes haben wir eine große Menge Code, der die sprite_pos_y
sprite_pos_x
und sprite_pos_y
irgendwie verarbeitet, was nur eines sagen kann - dies ist erforderlich, um das Auswahlsprite um das im Alphabet ausgewählte Zeichen anzuzeigen.
Jetzt können Sie die Funktion sicher als update_selection
. Lass uns weitermachen.

Der Code prüft, ob die Bits einiger gedrückter Tasten gesetzt sind, und ruft bestimmte Funktionen auf. Schauen wir sie uns an.
sub_D28 ()

Eine Art schamanische Magie. Zuerst wird das WORD
aus der Variablen word_FF0018
entnommen, dann wird eine interessante Anweisung ausgeführt:
bsr.w *+4
Dieser Befehl springt einfach zu der darauf folgenden Anweisung.
Als nächstes kommt eine weitere Magie:
move.l d0,(sp) rts
Der Wert im Register D0
wird oben auf den Stapel gelegt. Es ist erwähnenswert, dass sowohl für Shogi als auch für einige x86
die Rücksprungadresse der Funktion, wenn sie aufgerufen wird, oben auf dem Stapel abgelegt wird. Dementsprechend setzt der erste Befehl eine Adresse oben, und der zweite hebt sie vom Stapel ab und macht einen Übergang entlang dieser. Guter Trick .
Jetzt müssen Sie verstehen, was dieser Wert in der Variablen ist, die dann durchlaufen wird. Aber zuerst rufen wir diese Variable jmp_addr
.
Und die Funktionen werden so heißen:
sub_D38
: goto_to_d0
sub_D28
: jump_to_var_addr
jmp_addr
Finden Sie heraus, wo diese Variable ausgefüllt ist. Wir sehen uns die Referenzliste an:

Es gibt nur einen Ort, an den in diese Variable geschrieben werden kann. Schauen wir ihn uns an.
sub_3A4 ()

Hier wird abhängig von der Koordinate des Sprites (denken Sie daran, dass dies höchstwahrscheinlich die Adresse des ausgewählten Zeichens ist) dieser oder jener Wert eingegeben. Wir sehen den folgenden Codeabschnitt:

Der vorhandene Wert wird um 4 Bits nach rechts verschoben, ein neuer Wert wird in das niedrige Byte eingefügt und das Ergebnis wird erneut in die Variable eingegeben. jmp_addr
speichert unsere Variable jmp_addr
die Zeichen, die wir auf dem Schlüsseleingabebildschirm eingeben können. Beachten Sie auch, dass die Größe der Variablen WORD
.
Tatsächlich kann die Funktion update_jmp_addr
als update_jmp_addr
.
sub_414 ()
Jetzt haben wir nur noch eine Funktion in der Schleife, die nicht erkannt wird. Und es heißt sub_414
.

Sein Code ähnelt dem Code der Funktion update_jmp_addr
, nur am Ende haben wir einen Funktionsaufruf sub_45E
. Schauen wir uns das an.
sub_45E ()

Wir sehen, dass die Nummer #$4B1E2003
in das D0
Register eingetragen ist, das dann an VDP_CTRL
gesendet VDP_CTRL
, was bedeutet, dass es sich um einen anderen VDP
Steuerbefehl handelt. Wir drücken J
, wir erhalten einen Befehl zum Aufzeichnen in der Region mit der Zuordnung von $Cxxx
.
Als nächstes arbeitet der Code mit der Variablen byte_FF0014
, die nur in der aktuellen Funktion verwendet wird. Wenn Sie sich die Verwendung genau ansehen, werden Sie feststellen, dass maximal 4
installiert werden können. Ich gehe davon aus, dass dies die aktuelle Länge des eingegebenen Schlüssels ist. Lass es uns überprüfen.
Führen Sie den Debugger aus
Ich werde den Debugger von Smd Ida Tools
, aber tatsächlich werden einige Gens KMod oder Gens ReRecording ausreichen. Die Hauptsache ist, dass es eine Funktion zur Anzeige von Adressen im Speicher gibt.

Meine Theorie wurde bestätigt. Die Variable byte_FF0014
kann nun key_length
.
Es gibt noch eine andere Variable: dword_FF0010
, die ebenfalls nur in der aktuellen Funktion verwendet wird, und deren Inhalt wird nach dem Hinzufügen zum ursprünglichen Befehl in D0
(Rückruf, dies war die Nummer #$4B1E2003
) an VDP_CTRL
gesendet. Ohne nachzudenken, habe ich die Variable add_to_vdp_cmd
.
Was macht diese Funktion? Ich gehe davon aus, dass sie das eingegebene Zeichen zeichnet. Dies zu sub_45E
ist einfach - indem Sie den Debugger starten und den Status vergleichen, bevor Sie die Funktion sub_45E
aufrufen und danach:
An:

Nachher:

Ich hatte recht - diese Funktion zeichnet das eingegebene Zeichen. Wir nennen es do_draw_input_char
und die Funktion, die es aufruft ( sub_414
), ist draw_input_char
.
Was jetzt?
Lassen Sie uns zunächst überprüfen, ob die Variable, die wir jmp_addr
genannt jmp_addr
den eingegebenen Schlüssel wirklich speichert. Wir werden dieselbe Memory Watch
:

Wie Sie sehen können, war die Vermutung wahr. Was gibt uns das? Wir können zu jeder Adresse springen. Aber welches? In der Liste der Funktionen sind schließlich alle sortiert:

Dann habe ich einfach angefangen, durch den Code zu scrollen, bis ich Folgendes gefunden habe:

Das trainierte Auge sah die Folge von $4E, $75
am Ende nicht zugeordneter Bytes. Dies ist der Opcode des rts
Befehls, d.h. Rückkehr von der Funktion. Diese nicht zugewiesenen Bytes können also der Code einer Funktion sein. Versuchen wir, sie als Code zu kennzeichnen, und drücken Sie C
:

Dies ist offensichtlich ein Funktionscode. Sie können auch P
drücken, um den Code zu einer Funktion zu machen. Merken Sie sich diesen Namen: sub_D3C
.
Dann entsteht der Gedanke: Was ist, wenn Sie auf sub_D3C
springen? Es hört sich gut an, obwohl ein einziger Sprung hier offensichtlich nicht ausreicht, weil Es gab keine Links mehr zur Variablen word_FF0020
.
Dann kam mir ein anderer Gedanke: Was wäre, wenn wir nach einem anderen solchen nicht zugewiesenen Code suchen würden? Öffnen Sie den Binary search
(Alt + B), geben Sie die Sequenz 4E 75
ein und aktivieren Sie das Kontrollkästchen Find all occurrences
Binary search
:

Klicken Sie auf
, um die Suche zu starten. Wir erhalten die folgenden Ergebnisse.

Mindestens zwei weitere Stellen im Rum können einen Funktionscode enthalten. Sie müssen diese überprüfen. Wir klicken auf die erste der Optionen, scrollen ein wenig nach oben und sehen wieder eine Folge von undefinierten Bytes. Bezeichnen Sie sie als Funktion? Ja! Drücken Sie P
wo die Bytes beginnen:

Cool! Jetzt haben wir die Funktion sub_34C
. Wir versuchen, dasselbe mit den letzten gefundenen Optionen zu wiederholen, und ... wir bekommen einen Mist. Es gibt so viele Bytes vor 4E 75
dass nicht klar ist, wo die Funktion beginnt. Und natürlich sind nicht alle dieser obigen Bytes Code, weil viele doppelte Bytes.
Bestimmen Sie den Beginn der Funktion
Es ist für uns am einfachsten, den Anfang der Funktion zu finden, wenn wir herausfinden, wo die Daten enden. Wie kann man das machen? Eigentlich gar nicht kompliziert:
- Wir drehen vor dem Beginn der Daten (es wird einen Link vom Code zu ihnen geben)
- Wir folgen dem Link und suchen nach einem Zyklus, in dem die Größe dieser Daten erscheinen soll
- Markieren Sie das Array
Also führen wir den ersten Absatz durch ...:

... und wir sehen sofort, dass in einem Zyklus von unserem Array 4 Datenbytes gleichzeitig (weil move.l
) nach VDP_DATA
. Als nächstes sehen wir die Nummer 2047
. Auf den ersten dbf
scheint die endgültige Größe des Arrays 2047 * 4
, aber die dbf
basierte Schleife führt mehr +1
Iteration aus, weil Der zuletzt verglichene Wert ist nicht 0
, sondern -1
.
Gesamt: Die Größe des Arrays beträgt 2048 * 4 = 8192
. Bezeichnen Sie Bytes als Array. Klicken Sie dazu auf *
und geben Sie die Größe an:

Wir drehen uns zum Ende des Arrays und sehen dort Bytes, die genau die Bytes des Codes sind:


Jetzt haben wir die Funktion sub_2D86
und wir haben alles, um diesen Riss zu lösen! Mal sehen, was die neu erstellte Funktion macht.
sub_2D86 ()
Und es legt einfach den Wert #$4147
in das Register D1
und ruft die Funktion sub_34C
. Schau sie dir an.
sub_34C ()

Wir sehen, dass hier der Wert der Variablen word_FF0020
. Wenn Sie sich die Links dazu ansehen, sehen wir eine andere Stelle, an der der Datensatz in dieser Variablen stattfindet, und genau an dieser Stelle wollte ich durch die Variable jmp_addr
springen. Dies bestätigt die Vermutung, dass sub_D3C
definitiv zu sub_D3C
springen sub_D3C
.
Aber was als nächstes geschah, war zu faul, um es zu verstehen, also warf ich den Rum in GHIDRA , fand diese Funktion und sah mir den dekompilierten Code an:
void FUN_0000034c(void) { ushort in_D1w; short sVar1; ushort *puVar2; if (((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,in_D1w ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; }
Wir sehen, dass die Variable mit dem seltsamen Namen in_D1w
, und auch die Variable DAT_00ff0020
, die mit ihrer Adresse an das word_FF0020
erwähnte DAT_00ff0020
erinnert.
in_D1w
sagt uns, dass dieser Wert aus dem Register D1
oder vielmehr aus seiner jüngeren WORD-Hälfte entnommen wird, und setzt das Register D1
Funktion, die es übergibt. #$4147
du dich an #$4147
? Sie müssen dieses Register also als Eingabeargument für die Funktion festlegen.
Klicken Sie dazu im Fenster mit dem dekompilierten Code mit der rechten Maustaste auf den Funktionsnamen und wählen Sie den Menüpunkt Edit Function Signature
:

Um anzuzeigen, dass die Funktion ein Argument über ein bestimmtes Register führt, und zwar nicht über die Standardmethode für die aktuelle Aufrufkonvention, müssen Sie das Kontrollkästchen Use Custom Storage
und auf das Symbol mit dem grünen Plus klicken:

Eine Position für das neue Eingabeargument wird angezeigt. Wir doppelklicken darauf und erhalten einen Dialog, der den Typ und das Medium des Arguments angibt:

Im dekompilierten Code sehen wir, dass in_D1w
vom Typ in_D1w
ist, was bedeutet, dass wir es im ushort
angeben. Klicken Add
dann auf die Schaltfläche Add
:

Eine Position zeigt das Medium des Arguments an. Wir müssen das D1w
Register in Location
angeben und auf OK
klicken:

Der dekompilierte Code hat folgende Form:
void FUN_0000034c(ushort param_1) { short sVar1; ushort *puVar2; if (((ushort)(param_1 ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(param_1 ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,param_1 ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; }
Wir param_1
dass unser param_1
Wert konstant ist, von der aufrufenden Funktion übergeben wird und gleich #$4147
. Was sollte dann der Wert von DAT_00ff0020
? Wir betrachten:
0x4147 ^ DAT_00ff0020 ^ 0x5e4e = 0x5a5a 0x4147 ^ DAT_00ff0020 ^ 0x4a44 = 0x4e50
Weil xor
- die Operation ist reversibel, alle konstanten Zahlen können miteinander gestritten werden und erhalten den gewünschten Wert der Variablen DAT_00ff0020
.
DAT_00ff0020 = 0x4147 ^ 0x5e4e ^ 0x5a5a = 0x4553 DAT_00ff0020 = 0x4147 ^ 0x4a44 ^ 0x4e50 = 0x4553
Es stellt sich heraus, dass der Wert der Variablen 0x4553
sein 0x4553
. Es scheint, ich habe bereits einen Ort gesehen, an dem ein solcher Wert festgelegt ist ...

Schlussfolgerungen und Entscheidung
Wir kommen zu folgenden Ergebnissen:
- Zuerst müssen Sie zur Adresse
0x0D3C
, dazu müssen Sie den Code 0D3C
0x2D86
zu der Funktion bei 0x2D86
, die den Wert von D1
auf Register #$4147
setzt. Dazu müssen Sie den Code 2D86
Experimentell finden wir die Taste heraus, die gedrückt werden muss, um die eingegebene Taste zu überprüfen: B
Wir versuchen:

Vielen Dank!