なるほどえたきちのブログじゃねーの

チラ裏雑記帳

ポケモン過去作の解析とか乱数調整とかそこら辺。不定期更新。

【Em】セーブデータ改竄によるシステムハック

セーブデータに保存されるデータを利用して、ゲーム開始時に任意コードを実行できる方法を発見したので解説します。
この記事で紹介する方法ではゲーム開始時に自動的に実行される任意コードからバイナリエディタ起動準備を行い、L+Rの同時押しでいつでもバイナリエディタが起動できるようになります。


↓動画

また、システムハックの導入にはバイナリエディタの起動環境を整えていること、16進数を理解していることを前提条件とします。

導入手順解説の前に、具体的に何を行ったかについて解説します。

従来の手法では、技アニメスクリプトの不正アドレス参照をトリガーにしてボックス上に記述した任意コードを実行する、といった方式でチューリング完全の処理を実行させることができていました。

実行できる任意コードはGBAのハードウェアを完全に制御することが可能なので、割り込み処理等を変更することでシステムハック自体は簡単に行えます。
(この記事で行っていることも大体同じ原理です)
bzl.hatenablog.com

割り込み処理変更によるシステムハックを実行すれば、技アニメ再生後は任意のタイミングで任意コードを実行することができます。
しかし、割り込み処理のポインタを含む静的RAMの変更内容は基本的にゲーム終了と同時にリセットされてしまうため、システムハックを行ってもその効果をゲーム終了後も持続させることはできませんでした。

そこでゲーム開始時にメモリを改竄するため、ゲーム開始時の処理をトリガーに任意コードを実行する方法がないか調査した結果、主人公スプライトに紐付けられたオブジェクトアクションID(デフォルト0x0B)がセーブデータ上に保存され、開始時にセーブデータ依存でRAM展開されることに気付きました。
(オブジェクトアクションIDについての資料はこちら)
オブジェクトアクションIDに紐付けられたプログラムはNPCスプライトが描画されている限り実行され続けるので、ここで不正プログラムを参照させれば主人公スプライトが描画されるゲーム開始時に任意コードを実行させることができます。

紐付けられたプログラムの参照テーブル付近のデータを調査した結果、0x084DDA40にボックスRAM0x02030401(0x02030400に記述されたプログラムをTHUMBステートで参照する)へのポインタとなるデータが配置されていたので、ここを参照できるオブジェクトアクション0x6Eを任意コード実行のトリガーとして使用することにしました。

ゲーム開始時に行われる処理の流れは以下の通りです。

  1. 主人公スプライト描画
  2. オブジェクトアクション0x6Eに紐付けられたプログラム(0x02030400)の実行
  3. 静的RAM(0x0203D000)へのスクリプト展開
  4. ボックス1の2匹目から記述されているプログラムを静的RAM(0x0203D020)にコピー
  5. 静的RAM(0x0203CE00)への割り込み処理展開
  6. 割り込み処理参照ポインタ(0x03007FFC)の書き換え
  7. システムハックの完了


導入手順


1)バイナリエディタの導入



記事冒頭で前提条件として挙げた通り、データの打ち込みにはバイナリエディタを使用します。
(理論上はアニメスクリプトでも書き込めるが、使用できるバイナリデータに限りがあるのとボックス埋め込み用にコード組むのがめんどくさい)

バイナリエディタ未導入の場合、以下の記事を参考に導入してください。
http://bzl.hatenablog.com/entry/2019/07/16/075233bzl.hatenablog.com

2)データ書き込みアドレスの始点計算



バイナリエディタが導入できたら、バグ技0x2B5C、又はバグポケ0x085Fを使用してバイナリエディタを起動します。
今回データを書き込む領域はボックスRAM(動的RAM)になるので、乱数によって対応するメモリアドレスが変化してしまいます。
適当な領域に書き込むとダメタマゴフラグの影響でプログラムが破損するので、正確な位置にデータを書き込む必要があります。

まず、バイナリエディタで0x03005AF4、0x03005AF5、0x03005AF6、0x03005AF7のデータを読み込み、この時読み込んだデータを前から①、②、③、④として扱います。
f:id:BZL:20190922205425p:plain
画像で読み込まれたデータは①AC、②94、③02、④02となっています。
このデータを④③②①の順に並び替えると0x020294ACというアドレスになると思います。
これがボックスRAMの始点です。

ボックスRAMの始点に0x6ED8を加算するとデータ書き込み先のアドレスが算出できます。
この画像のケースではデータ書き込み先のアドレスは0x02030384になります。

この手順で開いたバイナリエディタは閉じずにそのまま手順3に移行してください。
もし閉じてしまった場合は、再度手順2の頭からやり直しです。(動的RAMの先頭アドレスは画面切り替え毎に変動するため)

3)データ書き込み



手順2で算出したアドレスをこれ以降pと表記します。

任意コード実行にバグポケ0x085Fを使用している場合p+0x00~p+0xBBまでの改変は必要ありません。
その代わりにp-0x34の位置を始点に以下のデータを書き込んでください。

書き込み位置 データ
p-0x34 00
p-0x33 48
p-0x32 00
p-0x31 47
p-0x30 F1
p-0x2F 17
p-0x2E 03
p-0x2D 02


p+0x00~p+0x7Fまでのメモリ領域を0x1A,0x00,0x1A,0x00...と埋めていきます。
バイナリエディタの導入時点で既にここのメモリは書き換えられているはずなので実際に書き込む手間はそこまで多くありません。

プログラムとして参照しても問題ないようにした上で0x2B5Cを引き続き使用できるようにする為にp+0x80の位置に以下のスクリプトを記述します。

書き込み位置 データ
p+0x80 03
p+0x81 DD
p+0x82 17
p+0x83 33
p+0x84 02
p+0x85 99
p+0x86 00
p+0x87 08

0x023317DDをポインタとして扱って大丈夫なのか?と思う方がいるかもしれませんが問題なく参照可能です。(実機、VBA1.7.2で確認済み)

p+0x88~p+0xBBまでの領域を0x00で埋めます。

p+0xBCを始点に以下のプログラムデータを書き込みます。
(長いのでスポイラーにしています)

書き込み位置 データ
p+0xBC F0
p+0xBD B4
p+0xBE 04
p+0xBF 1C
p+0xC0 29
p+0xC1 48
p+0xC2 01
p+0xC3 38
p+0xC4 01
p+0xC5 78
p+0xC6 00
p+0xC7 29
p+0xC8 47
p+0xC9 D1
p+0xCA 01
p+0xCB 21
p+0xCC 00
p+0xCD E0
p+0xCE AA
p+0xCF AA
p+0xD0 01
p+0xD1 70
p+0xD2 41
p+0xD3 1C
p+0xD4 26
p+0xD5 48
p+0xD6 00
p+0xD7 68
p+0xD8 54
p+0xD9 30
p+0xDA 20
p+0xDB 31
p+0xDC 78
p+0xDD 22
p+0xDE 92
p+0xDF 00
p+0xE0 00
p+0xE1 E0
p+0xE2 AA
p+0xE3 AA
p+0xE4 0B
p+0xE5 DF
p+0xE6 20
p+0xE7 48
p+0xE8 22
p+0xE9 49
p+0xEA 02
p+0xEB 1C
p+0xEC 00
p+0xED E0
p+0xEE AA
p+0xEF AA
p+0xF0 21
p+0xF1 32
p+0xF2 89
p+0xF3 23
p+0xF4 9B
p+0xF5 00
p+0xF6 03
p+0xF7 33
p+0xF8 00
p+0xF9 E0
p+0xFA AA
p+0xFB AA
p+0xFC 02
p+0xFD 25
p+0xFE 2E
p+0xFF C0
p+0x100 1D
p+0x101 48
p+0x102 1E
p+0x103 49
p+0x104 00
p+0x105 E0
p+0x106 AA
p+0x107 AA
p+0x108 1E
p+0x109 4A
p+0x10A 1F
p+0x10B 4B
p+0x10C 20
p+0x10D 4D
p+0x10E 21
p+0x10F 4E
p+0x110 22
p+0x111 4F
p+0x112 EE
p+0x113 C0
p+0x114 22
p+0x115 49
p+0x116 24
p+0x117 4A
p+0x118 24
p+0x119 4B
p+0x11A 25
p+0x11B 4D
p+0x11C 00
p+0x11D E0
p+0x11E AA
p+0x11F AA
p+0x120 24
p+0x121 4E
p+0x122 25
p+0x123 4F
p+0x124 EE
p+0x125 C0
p+0x126 26
p+0x127 49
p+0x128 26
p+0x129 4A
p+0x12A 27
p+0x12B 4B
p+0x12C 27
p+0x12D 4E
p+0x12E F4
p+0x12F 25
p+0x130 00
p+0x131 E0
p+0x132 AA
p+0x133 AA
p+0x134 AD
p+0x135 19
p+0x136 80
p+0x137 27
p+0x138 BF
p+0x139 00
p+0x13A 01
p+0x13B 37
p+0x13C 00
p+0x13D E0
p+0x13E AA
p+0x13F AA
p+0x140 EE
p+0x141 C0
p+0x142 09
p+0x143 49
p+0x144 03
p+0x145 31
p+0x146 23
p+0x147 4A
p+0x148 00
p+0x149 E0
p+0x14A AA
p+0x14B AA
p+0x14C 22
p+0x14D 4B
p+0x14E 0E
p+0x14F C0
p+0x150 09
p+0x151 48
p+0x152 23
p+0x153 49
p+0x154 00
p+0x155 E0
p+0x156 AA
p+0x157 AA
p+0x158 08
p+0x159 60
p+0x15A F0
p+0x15B BC
p+0x15C 20
p+0x15D 1C
p+0x15E 01
p+0x15F 49
p+0x160 08
p+0x161 47
p+0x162 00
p+0x163 00
p+0x164 FD
p+0x165 A2
p+0x166 08
p+0x167 08
p+0x168 00
p+0x169 D0
p+0x16A 03
p+0x16B 02
p+0x16C 00
p+0x16D E0
p+0x16E AA
p+0x16F AA
p+0x170 F4
p+0x171 5A
p+0x172 00
p+0x173 03
p+0x174 28
p+0x175 30
p+0x176 00
p+0x177 23
p+0x178 00
p+0x179 CE
p+0x17A 03
p+0x17B 02
p+0x17C 0F
p+0x17D 00
p+0x17E A0
p+0x17F E1
p+0x180 00
p+0x181 E0
p+0x182 AA
p+0x183 AA
p+0x184 05
p+0x185 00
p+0x186 80
p+0x187 E2
p+0x188 10
p+0x189 FF
p+0x18A 2F
p+0x18B E1
p+0x18C 00
p+0x18D E0
p+0x18E AA
p+0x18F AA
p+0x190 0A
p+0x191 48
p+0x192 40
p+0x193 78
p+0x194 03
p+0x195 21
p+0x196 88
p+0x197 42
p+0x198 00
p+0x199 E0
p+0x19A AA
p+0x19B AA
p+0x19C 0B
p+0x19D D1
p+0x19E 09
p+0x19F 4B
p+0x1A0 1B
p+0x1A1 78
p+0x1A2 01
p+0x1A3 2B
p+0x1A4 00
p+0x1A5 E0
p+0x1A6 AA
p+0x1A7 AA
p+0x1A8 07
p+0x1A9 D0
p+0x1AA 08
p+0x1AB 4B
p+0x1AC 00
p+0x1AD 20
p+0x1AE 18
p+0x1AF 60
p+0x1B0 D8
p+0x1B1 60
p+0x1B2 07
p+0x1B3 48
p+0x1B4 98
p+0x1B5 60
p+0x1B6 07
p+0x1B7 48
p+0x1B8 18
p+0x1B9 61
p+0x1BA 07
p+0x1BB 48
p+0x1BC 00
p+0x1BD E0
p+0x1BE AA
p+0x1BF AA
p+0x1C0 6E
p+0x1C1 21
p+0x1C2 81
p+0x1C3 71
p+0x1C4 06
p+0x1C5 4B
p+0x1C6 18
p+0x1C7 47
p+0x1C8 8C
p+0x1C9 23
p+0x1CA 00
p+0x1CB 03
p+0x1CC 38
p+0x1CD 0E
p+0x1CE 00
p+0x1CF 03
p+0x1D0 00
p+0x1D1 E0
p+0x1D2 AA
p+0x1D3 AA
p+0x1D4 F0
p+0x1D5 6F
p+0x1D6 03
p+0x1D7 02
p+0x1D8 F0
p+0x1D9 27
p+0x1DA 00
p+0x1DB 03
p+0x1DC 00
p+0x1DD E0
p+0x1DE AA
p+0x1DF AA
p+0x1E0 FC
p+0x1E1 7F
p+0x1E2 00
p+0x1E3 03


殿堂入り処理等、何らかの原因でシステムハックが解除された時にすぐ復帰できるように、バグ技0x2B5Cやバグポケ0x085Fで実行できる任意コードを記述します。
内容は0x02036FF6に0x6Eを代入するコードです。
ボックス名1の領域(p+0x146C)を始点に以下のデータを書き込みます。

書き込み位置 データ
p+0x146C 02
p+0x146D 48
p+0x146E 6E
p+0x146F 21
p+0x1470 81
p+0x1471 75
p+0x1472 70
p+0x1473 BC
p+0x1474 FF
p+0x1475 51
p+0x1476 80
p+0x1477 BD
p+0x1478 E0
p+0x1479 6F
p+0x147A 03
p+0x147B 02

ボックス名に直した場合の文字列データは以下の通りです。

ボックス ボックス名
ボックス1 いぶホむゥユミB
ボックス2 アィClマうい

最後に、以下のメモリ改変を行います。

メモリアドレス データ
0x0203CFFF 0x?? → 0x00
0x02036FF6 0x0B → 0x6E

以上でRAM改竄は完了です。


!注意
RAM改竄終了後は手順4の内容を終えるまで絶対にレポートを書かないこと。
もし入力ミスがあった場合ゲーム起動時にクラッシュするようになるので実質的なセーブ破損状態になる。
この状態になった場合『さいしょから はじめる』を選択してニューゲームするか外部ツールでセーブデータを改造する以外に回避する手段がない。


4)処理の実行



ポケモン選択画面やバッグ等を開いてから再度スタートメニュー画面に戻ります。
マップが再描画されオブジェクトアクションに紐付けられたプログラムが参照し直されると、前述の手順で書き込んだプログラムが実行され、割り込み処理の差し替えとバイナリエディタの静的RAMコピーが行われます。

フリーズせず正常に動作することを確認できたら、レポートを書いて終了してください。




以上の操作を完了させることにより、無改造のエメラルドをほぼ完全にハックすることができます。
ゲーム性は完全に崩壊しますが、メモリ弄りが大好きな方は是非試してみては如何でしょうか。
因みに、FRLGでも同じ手法でハックすることが可能です。