Lösen eines einfachen Crackme für Sega Mega Drive

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:


  1. Wir drehen vor dem Beginn der Daten (es wird einen Link vom Code zu ihnen geben)
  2. Wir folgen dem Link und suchen nach einem Zyklus, in dem die Größe dieser Daten erscheinen soll
  3. 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:


  1. Zuerst müssen Sie zur Adresse 0x0D3C , dazu müssen Sie den Code 0D3C
  2. 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!

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


All Articles