みなさんこんにちは

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_d0sub_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 私達は試みます:

よろしくお願いします!