みなさんこんにちは
Sega Mega Drive
ゲームを逆転した豊富な経験にもかかわらず、私はそれをクラックすることを決して決めませんでした、そして、彼らはインターネットで私に出くわしませんでした。 しかし、先日、解決したかったおかしなクラッカーがいました。 私はあなたと決定を共有します...
説明
タスクの説明とラム酒自体はここからダウンロードできます 。
リソースのリストにはHydraと書かれていますが、 Segaでゲームをデバッグおよびリバースするためのツールの標準的なデファクトはSmd Ida Toolsです。 このクリームを解決するために必要なものはすべて揃っています。
- Idaのローマローダー
- デバッガー
- RAM / VDPメモリの表示と変更
- VDPに関するほぼ完全な情報を表示する
Ideのプラグインに最新リリースをドロップし、私たちが持っているものを調べ始めます。
解決策
将giゲームの起動は、 Reset
ベクターの実行から始まります。 それへのポインタは、ラムの先頭から2番目のDWORDにあります。
アドレス0x27A
から始まる未確認の関数がいくつかあります。 そこに何があるか見てみましょう。
sub_2EA()
私自身の経験から言うと、これは通常、 VBLANK
割り込みの完了を待つ機能のように見えます。 byte_FF0026
変数への呼び出しがまだある場所を見てみましょう。
VBLANK
割り込みでVBLANK
設定されていることがVBLANK
ます。 したがって、変数vblank_ready
を呼び出し、変数がチェックされる関数はwait_for_vblank
です。
sub_60E()
次に、 sub_60E
関数がコードによって呼び出されます。 何があるか見てみましょう:
最初のコマンドがVDP_CTRL
は、 VDP
制御コマンドです。 彼女が何をしているかを調べるために、このコマンドを実行してJ
キーを押します。
CRAM
(パレットが保存されている場所)のエントリが初期化されていることがわかります。 これは、後続のすべての関数コードが単に初期パレットを設定することを意味します。 したがって、関数はinit_cram
と呼ぶことができます。
sub_71A()
いくつかのコマンドが再びVDP_CTRL
に転送され、次にJ
をもう一度押すと、このコマンドがビデオメモリの記録を初期化することがわかります。
さらに、そこでビデオメモリに転送される内容を理解することは意味がありません。 したがって、関数load_vdp_data
呼び出すだけload_vdp_data
。
sub_C60()
ここでは、前の関数とほぼ同じことが起こります。したがって、詳細を説明することなく、 load_vdp_data2
関数を呼び出します。
sub_8DA()
すでに他のコードがあります。 さらに、この関数ではもう1つの関数が呼び出されます。 そこを見てみましょうsub_D08
。
sub_D08()
D0
レジスタにはVDP_CTRL
のコマンドがVDP_CTRL
、 D1
はVRAM
される値が、 D2
およびD3
は入力の幅と高さが表示されます(内部と外部の2サイクルになるため)。 関数fill_vram_by_addr
ます。
sub_8DA()
前の機能に戻ります。 D0
レジスタの値がVDP_CTRL
コマンドとして送信されたら、値のJ
キーを押します。 取得するもの:
繰り返しになりますが、セガにゲームを逆転した経験から、このコマンドはマッピングタイルの記録を初期化すると言うことができます。 ケースの90%で$Fxxx
、 $Exxx
$Cxxx
、 $Dxxx
、 $Cxxx
で$Cxxx
アドレスは、これらの同じマッピングを持つリージョンのアドレスになります。 マッピングとは:
これらは、このタイルまたはそのタイルを画面上のどこに表示するかを指定できる値です(タイルは8x8
ピクセルの正方形です)。
したがって、関数はinit_tile_mappings
としてinit_tile_mappings
ことができます。
sub_CDC()
最初のコマンドは、アドレス$F000
レコードを初期化します。 注:「 マッピング 」のアドレスの中には、まだスプライトテーブルが格納されている領域があります(これらは、位置、ポイントするタイルなどです)。どの領域がデバッグできるのかを調べます。 しかし、今のところ、これは必要ないので、関数init_other_mappings
呼び出しましょう。
また、この関数では、 word_FF000A
とword_FF000C
2つの変数が初期化されていることがword_FF000C
ます。 私自身の経験から(はい、彼が決定します)、いくつかの2つの変数がアドレス空間に近く、マッピングに関連付けられている場合、ほとんどの場合、それらは何らかのオブジェクト(スプライトなど)の座標になります。 したがって、それらをsprite_pos_x
およびsprite_pos_y
と呼ぶことをお勧めします。 x
とy
のエラーy
許容されます さらにデバッグを行うと、簡単に修正できます。
VBLANK
ループはコード内でさらに進むため、基本的な初期化が完了したと想定できます。 これで、 VBLANK
割り込みを確認できます。
2つの変数が増加していることがわかります(奇妙なことに、各変数へのリンクのリストでは絶対に空です)。 ただし、これらはフレームごとに1回更新されるため、 timer2
およびtimer2
と呼ぶことができます。
次に、 sub_2FE
関数がsub_2FE
ます。 何があるか見てみましょう:
sub_2FE()
そしてそこにIO_CT1_DATA
ポートをIO_CT1_DATA
ます(最初のジョイスティックを担当します)。 ポートアドレスはレジスタA0
ロードされ、 sub_310
関数に渡されます。 私たちはそこに行きます:
sub_310()
私の経験は再び私を助けます。 ジョイスティックで動作するコードと2つの変数がメモリにある場合、1つはpressed keys
保存し、2つ目はheld keys
保持しheld keys
。 キーを押したままにしました。 これらの変数をpressed_keys
とheld_keys
と呼びましょう。 そして、関数はupdate_joypad_state
としてupdate_joypad_state
ことができます。
sub_2FE()
read_joypad
として関数をread_joypad
ます。
ハンドラーループ
これですべてがより明確になりました。
したがって、このサイクルは押されたキーに応答し、対応するアクションを実行します。 ループで呼び出される各関数を見ていきましょう。
sub_4D4()
たくさんのコードがあります。 最初の関数sub_60C
から始めましょう。
sub_60C()
彼女は何もしません-最初はそう見えるかもしれません。 現在の関数から戻るのはrts
です。 しかし、なぜなら ジャンプ( bsr
)のみが発生します。つまり、 rts
はハンドラループに戻ります。 この関数をretn_to_loop
ます。
sub_4D4()
次に、 word_FF000E
変数の呼び出しをword_FF000E
ます。 現在の機能以外の場所では使用されておらず、最初はその目的が明確ではありませんでした。 しかし、よく見ると、この変数はキーストロークの処理間のわずかな遅延にのみ必要であると想定できます。 ( このラムではすでに実装が不十分ですが、この変数がなければ、さらに悪化すると思います )。
次に、 sprite_pos_y
とsprite_pos_y
を何らかの方法で処理する大量のコードがありsprite_pos_y
。これらのsprite_pos_y
、1つのことしか言えません。これは、アルファベットで選択された文字の周りに選択スプライトを表示するために必要です。
そのため、関数にupdate_selection
という名前を安全に付けることができます。 続けましょう。
コードは、押されたキーの一部が設定されているかどうかを確認し、特定の機能を呼び出します。 それらを見てみましょう。
sub_D28()
ある種のシャーマニスティックマジック。 最初に、 word_FF0018
変数からWORD
が取得され、次に1つの興味深い命令が実行されます。
bsr.w *+4
このコマンドは、それに続く命令にジャンプするだけです。
次は別の魔法です:
move.l d0,(sp) rts
レジスタD0
の値は、スタックの最上部に配置されます。 将giにとっては、 x86
ように、関数が呼び出されたときの関数からの戻りアドレスがスタックの一番上に置かれることに注意する価値があります。 したがって、最初の命令はアドレスを先頭に配置し、2番目の命令はそのアドレスをスタックから持ち上げて、それに沿って遷移します。 良いトリック 。
ここで、変数内のこの値が何であるかを理解する必要があります。 しかし、最初に、この変数をjmp_addr
と呼びましょう。
そして、関数はこれと呼ばれます:
-
sub_D38
:goto_to_d0
-
sub_D28
:jump_to_var_addr
jmp_addr
この変数が入力されている場所を見つけます。 参考文献のリストを見てください。
この変数に書き込む場所は1つだけです。 彼を見てみましょう。
sub_3A4()
ここでは、スプライトの座標に応じて(これは選択した文字のアドレスである可能性が高いことに注意してください)、この値またはその値が入力されます。 次のコードセクションが表示されます。
既存の値は右に4ビットシフトされ、新しい値が下位バイトに配置され、結果が再び変数に入力されます。 理論的には、 jmp_addr
変数には、キー入力画面で入力できる文字が格納されています。 変数のサイズがWORD
であることにも注意してください。
実際、 sub_3A4
関数はsub_3A4
と呼ぶことができます。
sub_414()
これで、ループに残っている関数が1つだけになりましたが、これは認識されません。 そして、それはsub_414
と呼ばれsub_414
。
そのコードはupdate_jmp_addr
関数のコードに似ていますが、最後にsub_45E
関数を呼び出します。 見てみましょう。
sub_45E()
番号#$4B1E2003
D0
レジスタに入力され、 VDP_CTRL
に送信されることがVDP_CTRL
。これは、別のVDP
制御コマンドを処理していることを意味します。 J
を押すと、 $Cxxx
をマッピングした地域のレコードのコマンドを受け取ります。
さらに、コードは現在の関数以外では使用されない変数byte_FF0014
で動作します。 使用方法をよく見ると、インストールできる最大数は4
であることがわかります。 これは入力されたキーの現在の長さであるという仮定があります。 見てみましょう。
デバッガーを実行する
Smd Ida Tools
デバッガーを使用しますが、実際には、 Gens KModまたはGens ReRecordingで十分です。 主なことは、メモリ内のアドレスを表示する機能があることです。
私の理論は確認されました。 したがって、変数byte_FF0014
はkey_length
とkey_length
ようになりkey_length
。
別の変数があります: dword_FF0010
は現在の関数でのみ使用され、その内容はD0
の初期コマンドに追加した後(これは番号#$4B1E2003
)、 VDP_CTRL
送信されVDP_CTRL
。 考え直すことなく、変数にadd_to_vdp_cmd
という名前を付けました。
それでは、この関数は何をするのでしょうか? 私は彼女が入力されたキャラクターを描くと仮定しています。 これの確認は簡単です-デバッガーを実行し、 sub_45E
関数を呼び出す前と後の状態を比較することにより:
宛先:
後:
私は正しかった-この関数は入力されたキャラクターを描く。 do_draw_input_char
と呼び、それを呼び出す関数( sub_414
)はdraw_input_char
です。
今何
jmp_addr
と呼ばれる変数が実際に入力されたキーを保存していることを確認しましょう。 同じMemory Watch
を使用します。
ご覧のとおり、推測は真実でした。 これにより何が得られますか? 任意のアドレスにジャンプできます。 どれだけ? 関数のリストでは、すべてが結局ソートされます:
次に、これを見つけるまでコードをスクロールし始めました:
訓練された目では、 $4E, $75
シーケンス、未割り当てバイトの最後に$4E, $75
シーケンスが見られました。 これは、 rts
命令のオペコードです。 関数から戻ります。 したがって、これらの未割り当てバイトは、何らかの関数のコードになる可能性があります。 それらをコードとして指定して、 C
押してみましょう。
明らかに、これは機能コードです。 P
を押して、コードを機能にすることもできます。 この名前を覚えておいてください: sub_D3C
その後、考えがsub_D3C
ます: sub_D3C
ジャンプしsub_D3C
どうなるでしょうか? ここでの1回のジャンプでは明らかに十分ではありませんが、いいですね。 word_FF0020
変数へのリンクword_FF0020
これ以上word_FF0020
ませんでした。
次に、別の考えが浮かびました。そのような未割り当てコードを探したらどうでしょうか。 Binary search
ダイアログ(Alt + B)を開き、シーケンス4E 75
入力して、[ Find all occurrences
ボックスFind all occurrences
。
[
をクリックして検索を開始すると、次の結果が得られます。
ラムの少なくとも2つの場所に関数コードが含まれている可能性があるため、それらを確認する必要があります。 最初のオプションをクリックし、少し上にスクロールすると、未定義のバイトのシーケンスが再び表示されます。 それらを関数として示しますか? はい! バイトが始まるP
ヒットします。
かっこいい! これでsub_34C
関数ができました。 最後に見つかったオプションを使用して同じことを繰り返しますが、...残念なことになります。 4E 75
前に非常に多くのバイトがあるので、関数がどこから始まるのかは明確ではありません。 そして、明らかに、上記のこれらのバイトのすべてがコードであるわけではありません。 大量の重複バイト。
関数の始まりを決定する
データの終わりを見つけると、関数の始まりを見つけるのが最も簡単になります。 どうやってやるの? 実際にはまったく複雑ではありません:
- データの開始前にツイストします(コードからそれらへのリンクがあります)
- リンクをたどり、このデータのサイズが表示されるサイクルを探します
- アレイをマークアップする
したがって、最初の段落を実行します...:
...そして、配列からのサイクルで、4バイトのデータが一度に( move.l
) move.l
コピーされることがmove.l
にVDP_DATA
ます。 次に、番号2047
ます。 最初は、配列の最終サイズは2047 * 4
ように見えますが、 dbf
ベースのループは+1
反復をより多く実行します。 最後に比較された値は0
ではなく、 -1
です。
合計:配列サイズは2048 * 4 = 8192
です。 バイトを配列として示します。 これを行うには、 *
をクリックしてサイズを指定します。
配列の最後までツイストすると、そこにはバイトがあります。これはまさにコードのバイトです。
これでsub_2D86
関数があり、このクラックを解決するためのすべてがあります! 新しく作成された関数が何をするのか見てみましょう。
sub_2D86()
そして、 D1
レジスタに値#$4147
入れて、 sub_34C
関数を呼び出します。 彼女を見てください。
sub_34C()
ここでword_FF0020
変数の値がword_FF0020
ことがword_FF0020
ます。 リンクを見ると、この変数のレコードが発生している別の場所が表示され、 jmp_addr
変数をジャンプしたい場所になります。 これにより、 sub_D3C
にジャンプする必要があるsub_D3C
確実にsub_D3C
ます。
しかし、次に起こったことは私が理解するのが面倒だったので、ラム酒をGHIDRAに投げて、この関数を見つけ、逆コンパイルされたコードを見ました:
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; }
奇妙な名前in_D1w
変数in_D1w
、変数DAT_00ff0020
もin_D1w
いることがDAT_00ff0020
。これは、前述のword_FF0020
アドレスで思い出させます。
in_D1w
は、この値がレジスタD1
から取得されること、またはその若いWORDハーフから取得されることを示し、それを渡す関数にレジスタD1
設定します。 #$4147
覚えていますか? したがって、このレジスタを関数の入力引数として指定する必要があります。
これを行うには、逆コンパイルされたコードを含むウィンドウで、関数名を右クリックし、[ Edit Function Signature
のEdit Function Signature
]メニュー項目を選択します。
関数が特定のレジスタを介して、つまり現在の呼び出し規則の標準的な方法ではなく、引数を受け入れることを示すには、[ Use Custom Storage
] Use Custom Storage
をオンにし、 緑色のプラス記号が付いたアイコンをクリックUse Custom Storage
必要があります:
新しい入力引数の位置が表示されます。 それをダブルクリックすると、引数のタイプとメディアを示すダイアログが表示されます。
逆コンパイルされたコードでは、 in_D1w
がushort
型であることがわかります。つまり、typeフィールドで指定します。 次に、[ Add
]ボタンをクリックします。
引数の媒体を示す位置が表示されますD1w
レジスタを指定し、[ OK
]をクリックする必要があります。
逆コンパイルされたコードの形式は次のとおりです。
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; }
param_1
値は定数であり、呼び出し側の関数によって渡され、 #$4147
等しいことがparam_1
ています。 次に、 DAT_00ff0020
の値はDAT_00ff0020
ますか? 私達は考慮します:
0x4147 ^ DAT_00ff0020 ^ 0x5e4e = 0x5a5a 0x4147 ^ DAT_00ff0020 ^ 0x4a44 = 0x4e50
なぜなら xor
操作は可逆的であり、すべての定数は互いにけんかされ、変数DAT_00ff0020
目的の値を取得できます。
DAT_00ff0020 = 0x4147 ^ 0x5e4e ^ 0x5a5a = 0x4553 DAT_00ff0020 = 0x4147 ^ 0x4a44 ^ 0x4e50 = 0x4553
変数の値は0x4553
なければなり0x4553
。 私はすでにそのような値が設定されている場所を見たようです...
結論と決定
次の結果に到達します。
- まず、アドレス
0x0D3C
にジャンプする必要があります。そのためには、コード0D3C
を入力する必要があります - アドレス
0x2D86
関数にジャンプします。これにより、レジスターD1
に#$4147
値が設定されます。これには、コード2D86
を入力する必要があります
実験的に、入力されたキーを確認するために押す必要があるキーを見つけますB
私達は試みます:
よろしくお願いします!