Das Passwortsystem von MegaMan 4
aus RHWiki, der freien Romhacking-Enzyklopädie
| Inhaltsverzeichnis |
ASM um das Passwortsystem von Megaman4 zu verstehen
Benötigt werden:
- fceud (oder fceuxd) (=NES Emulator mit Debugfunktionen)
- Megaman 4 Rom
- nes asm Referenz (6502 ASM) von nesdev.parodius.com (hier nicht unbedingt da ich quasi alles erkläre, aber unbedingt nötig um selbst was zu hacken)
Einleitung
Das war mein erstes und auch irgendwie bisher einziges NES ASM Projekt. Wenn ich jetzt wo ich sehe wie lang das Tut geworden ist denke ich, ich hätte vielleicht doch besser was Einfacheres (wie ne einfache Pokemon Checksum) nehmen sollen.
Naja egal - ach und noch etwas: Wir tun hier nur so genial :) Als ich das selbst gehackt habe war ich einen Tag (mit Pausen natürlich) oder so beschäftigt (hat Spaß gemacht :) )
Naja wie auch immer enjoy:
Vorgehen
Wir Starten das Rom und versuchen ein Passwort einzugeben. Naja geht offensichtlich nicht. Also ändern wir ein paar Mal die erste Position und finden (per Tools->Cheats, wie das geht sollte klar sein, sonst ein Tutorial über Cheatcodes lesen) so die Ram Adresse. (die roten Punkte nenne ich ab sofort Dots) Die Stelle sollte $201 sein.
Gut Debugger anschmeißen: Tools->Debug Unter Breakpoints: Add... CPU Mem, 201-201 und "read" aktivieren. Das der Debugger stoppt das Game wenn von $201 gelesen wird
Im Game gehen wir auf END und bestätigen. Voila wir landen hier:
$9731:B9 FD 01 LDA $01FD,Y @ $0201 = #$01 $9734:F0 05 BEQ $973B $9736:E8 INX $9737:E0 07 CPX #$07 $9739:F0 37 BEQ $9772 //REF $9734: $973B:88 DEY $973C:88 DEY $973D:88 DEY $973E:88 DEY $973F:D0 F0 BNE $9731 -> NotZero $9741:E0 06 CPX #$06 $9743:D0 2D BNE $9772
Nun was macht das? Lade Daten von $01FD+Y in A. Wenn die Zero flag gesetzt ist jumpt die Funktion zu $973B (überspringt also 3 Zeilen), Und Subtrahiert dann 4 von Y. Wenn Y noch nicht 0 ist Springt das ding wieder zu $9731. Was Passiert wenn da keine 0 sondern eine 1 steht? Naja dann wird X um 1 vergrößert. Dann wird X mit 7 verglichen. Wenn X=7 wird zu $9772 gesprungen. Wenn die Schleife Durchgelaufen ist wird X mit 6 verglichen. Wenn X nicht 6 ist wird ebenfalls nach $9772 gejumpt.
Ne Idee was das macht? Jo, das zählt alle Dots die man gesetzt hat (die stehen wie sich schnell feststellen lässt im Ram ja als 1) Wenn jemand mehr oder weniger als 6 Dots eingegeben hat ist sein Passwort auf jeden fall falsch. Das wird hier geprüft. 6 Dots sind pflicht.
Gehen wir also weiter:
$9745:A0 04 LDY #$04 -> Y=4 //REF $9750 $9747:BE AA 99 LDX $99AA,Y @ $99AE = #$8C $974A:BD 01 02 LDA $0201,X @ $0201 = #$01 $974D:D0 23 BNE $9772 -> drop failed $974F:88 DEY $9750:10 F5 BPL $9747 -> Branch if result Plus op upper byte
Eine neue Schleife! BPL Springt solange das Resultat der letzten Instruction kleiner ist. Naja das wird bei DEY immer der Fall sein außer DEY von Y=0 das ist FF. Die Schleife läuft also 5x da am Anfang x=4 (Erste Zeile). In der Schleife wird $99AA + Y in X geladen und dann geprüft ob an dieser stelle eine 1 steht. Wenn das der Fall ist, ist das Passwort anscheinend falsch. Bleibt die Frage, welche Dots das sind. Wenn man $99AA anschaut sieht man:
0x58,0x5c,0x84,0x70,0x8c Okay. Was fehlt uns jetzt noch? Naja wir müssen diese Bytes die wir in diesen Tabellen (später kommen noch welche) finden in Positionen umrechnen, das ist aber einfach dafür braucht man kein asm Durch 4 Teilen (da ja nur jeder 4te byte verwentet wird naja und das layout ist dann:
|-----------------------| |201|205|209|20D|211|215| |-----------------------| |219|...
So findet man den Punkt 201+0x58 und dann nachgucken wo der ist.
Gehen wir weiter. Von jetzt an werde ich nicht mehr ganz so genau beschreiben sondern nur noch direkt
den Code kommentieren. Die Prinzipien sind wieder ähnlich wer soweit alles verstanden hat sollte folgen
können.
$9752:A0 0A LDY #$0A ; Y=0xA (bzw 10) $9754:A9 00 LDA #$00 ; A=0 $9756:85 00 STA $00 = #$00 ; Ram an ADR $0=0 //REF $976A $9758:BE AF 99 LDX $99AF,Y @ $99B3 = #$30 ; lese Byte von $99AF (siehe $9747) $975B:BD 01 02 LDA $0201,X @ $028D = #$00 ; lade den "Dot-Status" $975E:F0 09 BEQ $9769 ; wenn 0 (also kein Dot) springen $9760:A5 00 LDA $00 = #$00 ; lade data vom ram an ADR $0=0 $9762:D0 0E BNE $9772 ; wenn das geladene nicht null ist gehe zu Passwort falsch! $9764:98 TYA ; A=Y $9765:09 80 ORA #$80 ; A = A OR 0x80 $9767:85 00 STA $00 = #$00 ; Schreibe A an $00 //REF $975E $9769:88 DEY ; Y-- $976A:10 EC BPL $9758 ; Wenn Resultat kleiner: Springen!
Das ist ja fast genauso: wie das vorher nur ist diesmal anscheinend 1 Dot in der 11er Gruppe erlaubt (dann wird $00 !=0 und $9762 bricht ab)
$976C:A0 0F LDY #$0F ;Y=0xF (=15) $976E:A5 00 LDA $00 = #$00 ;Lade $9770:30 03 BMI $9775 ; Springe wenn höchstes Bit gesetzt $9772:4C 1D 98 JMP $981D ; Das ist das berüchtigte $9772 da wollen wir bestimmt nicht hin... :)
Okay also brauchen wir einen dot in der 11er Gruppe!
$9775:A9 00 LDA #$00 ; A=0 $9777:99 80 07 STA $0780,Y @ $078F = #$9A ;($0780,Y)=0 $977A:88 DEY ;Y-- $977B:10 F8 BPL $9775 ;loop
Das schreibt einen Haufen (15) Nullen in 0780+... laaaangweilig
$977D:A9 9C LDA #$9C $977F:8D 80 07 STA $0780 = #$22 ; Okay 0x780 soll nun doch 9C werden. $9782:A5 00 LDA $00 = #$81 $9784:29 0F AND #$0F ; Das Mühevoll (nicht wirklich) an den $00 geklebte $9786:85 00 STA $00 = #$81 ; höchste bitt wird wieder entfernt - war nur für den BMI-Jump $9788:A0 00 LDY #$00 ; $978A:84 04 STY $04 = #$20 ; $978C:84 05 STY $05 = #$03 ; Und noch mehr ram wird 0, diesmal wird dazu aber Y und nicht A verwendet.
Okay jetzt kommt der schwierigste Teil des Ganzen:
$978E:20 01 98 JSR $9801 ; hier wird erstmals eine Unterfunktion aufgerufen wir wissen noch nicht was die macht $9791:A5 01 LDA $01 = #$00 ; wir lesen $01 $9793:C9 01 CMP #$01 ; ist das 1? $9795:D0 DB BNE $9772 ; wenn nicht: PW Falsch! (das muss die Unterfunktion feststellen!) $9797:A5 02 LDA $02 = #$FF ; Lade $02 $9799:29 03 AND #$03 ; nur die letzten 2 Bits bitte! $979B:C9 03 CMP #$03 ; $979D:F0 38 BEQ $97D7 ; alle Bits gesetzt? Dann Jump $979F:29 02 AND #$02 ; wenn nicht ignoriere das erste Bit! $97A1:D0 02 BNE $97A5 ; Springt bei nicht-null also wenn 2nd Bit Set -> also A=2 $97A3:A9 01 LDA #$01 ; wenn gesetzt $01 in A lesen, das ist mit Sicherheit 1, sonst wären wir schon gejumpt! //REF: $97A1 $97A5:18 CLC $97A6:65 04 ADC $04 = #$00 ; 0..A + 0..2 -> 0..E $97A8:85 04 STA $04 = #$00 ; Schreibe wieder in $04 $97AA:A6 02 LDX $02 = #$03 ; Und den unkommentierten Kram ignorieren wir erst mal $97AC:A5 05 LDA $05 = #$00 $97AE:1D CE 99 ORA $99CE,X @ $99D2 = #$04 $97B1:85 05 STA $05 = #$00 $97B3:A5 02 LDA $02 = #$03 $97B5:0A ASL $97B6:65 02 ADC $02 = #$03 $97B8:AA TAX $97B9:84 06 STY $06 = #$96 $97BB:A9 9C LDA #$9C $97BD:BC E2 99 LDY $99E2,X @ $99E6 = #$03 $97C0:F0 13 BEQ $97D5 $97C2:99 80 07 STA $0780,Y @ $0784 = #$00 $97C5:BC E3 99 LDY $99E3,X @ $99E7 = #$00 $97C8:F0 0B BEQ $97D5 $97CA:99 80 07 STA $0780,Y @ $0784 = #$00 $97CD:BC E4 99 LDY $99E4,X @ $99E8 = #$0C $97D0:F0 03 BEQ $97D5 $97D2:99 80 07 STA $0780,Y @ $0784 = #$00 //REF (by a lot of crap): $97D5:A4 06 LDY $06 = #$96 REF: $979D $97D7:C0 14 CPY #$14 $97D9:D0 B3 BNE $978E ; Y=!0x14 (=20)? wenn ja machen wir die ganze große Schleife noch einmal! $97DB:A5 00 LDA $00 = #$01 $97DD:C5 04 CMP $04 = #$00 ; Vergleiche den Berechneten mist mit $04 $97DF:D0 3C BNE $981D ; wenn die nicht übereinstimmen gibt es haue und das Passwort ist Falsch! ...
Das ist die Checksum! Endlich! Okay nur wie wird sie berechnet? In der Langen hässlich Komplizierten schleife *meh* Fangen wir langsam an erstmal die Unterfunktion
SUB9801: $9801:A9 00 LDA #$00 $9803:85 01 STA $01 = #$00 $9805:85 02 STA $02 = #$FF $9807:A9 04 LDA #$04 $9809:85 03 STA $03 = #$1C ; Einmal 0x04 in $03 //REF $981A ; Schleife Läuft 4x (siehe $981A) $980B:BE BA 99 LDX $99BA,Y @ $99BA = #$34 ; wieder Kram aus Tabelle Lesen $980E:BD 01 02 LDA $0201,X @ $0289 = #$00 ; Dots... $9811:F0 04 BEQ $9817 ; Jump wenn kein dot $9813:84 02 STY $02 = #$FF ; Speichere Derzeitigen Y Wert in $02 $9815:E6 01 INC $01 = #$00 ; $01++ (*g*) $9817:C8 INY ; Y++ $9818:C6 03 DEC $03 = #$1C $981A:D0 EF BNE $980B $981C:60 RTS
Naja wir wissen das nach der Funktion 01 geprüft wird. Das muss 1 sein. Folglich muss in einer 4er Gruppe ein Dot sein. Außerdem erinnern wir uns an diese Zeilen:
$9797:A5 02 LDA $02 = #$FF ; Lade $02 $9799:29 03 AND #$03 ; nur die letzten 2 Bits bitte!
Warum nur die letzten zwei Bits gefragt sind ist klar, die Funktion wird mehrfach aufgerufen, zunächst mit Y=0 dann mit Y=4 usw. Weil hier nur interessiert, ob der der erste zweite dritte oder vierte dot in Gruppe X gesetzt ist, wird der Rest ignoriert.
Und letztendlich verrät uns dann:
$979D:F0 38 BEQ $97D7 ; 3? Dann spring weit nach unten $979F:29 02 AND #$02 ; wenn nicht ignoriere das erste Bit! $97A1:D0 02 BNE $97A5 ; Springen wenn A = 2 $97A3:A9 01 LDA #$01 ; wenn gesetzt $01 in A lesen, das ist mit Sicherheit 1, sonst wären wir schon gejumpt! //REF: $97A1 $97A5:18 CLC $97A6:65 04 ADC $04 = #$00 ; 0..A + 0..2 -> 0..E $97A8:85 04 STA $04 = #$00 ; Schreibe wieder in $04
im Prinzip wird die Checksum also so berechnet:
if DOT_POSITION1 chk++; if DOT_POSITION2 chk++; if DOT_POSITION3 chk=chk+2; if DOT_POSITION3 chk=chk;
Und das mehrmals (5x) siehe unten. Am anfang ist die checksum=0.
Wir wissen das bei Y=20 abgebrochen wird Pro durchlauf wird Y um 4 erhöht. 20/4=5. Es gibt also 5 dieser 4er "Pakete" die jeweils 2Bit Informationen (0-3) Speichern. Jetzt wäre es an der Zeit einen Generator zu schreiben, der die ganzen Tabellendaten nutzt aufgrund von 5 Eingangswerten ganze Passwörter ausgibt. Und damit festzustellen was die Bewirken. Wäre eigentlich eine gute Übung, wenn was nicht geht einfach im Debugger laufen lassen und schauen wo falsch gesprungen wird und überlegen woran das liegt.
Mein Megaman4Pass.c (c, TextConsole) ist schon vollständig mit den Androidnamen (also ich hab schon ausprobiert was die bewirken, aber das ist einfach)
Schlusswort
Okay das wars. Ich hoffe es hat euch einigermaßen Spaß gemacht und ihr habt was gelernt. Falls ja: Es gibt einige solcher Passwörter (zB in Megaman2-6, Metroid, Kid Ikarus) Jedoch ist das mehr eine Übung, denn irgendwer hat diese Spiele bestimmt schon dokumentiert. Selbstmachen macht aber mehr Spaß! loadingNOW

