[IPSwitch] サーモンラン

本記事の内容は完全に開発者向けです。

バグコード

// Unknown [tkgling]
@enabled
00711BF8 E0031FAA

もし、IPSwitchを実行できる環境にあるのであればver4.3.1で上記のコードを適応するとイカッチャのサーモンランがバグります。

具体的には、

  • ブキがガチホコ(実際には支給されない)
  • インクタンク(浮き輪)がない
  • ボムが出せない
  • スペシャルパウチを使っても変化がない
  • ゼンメツしてもWAVEが終わらない
  • 時間が減らない
  • ノルマが-1になる
  • 最初のクマサンの船から飛んでくるモーションがない

と、デバッグモードのような状態になります。

このコード自体は、実行ファイルであるmain.nsoのアドレス711BF8にある命令を上書きしてしまうものです。

LDR W0, [X8,#0x738]
// W0 = mem[X8+#0x738]

MOVZ W0, WZR
// W0 = 0

本来の命令はメモリのあるアドレスの値をコピーしてくるものなのですが、この値を強制的に変更することでサーモンランのパラメータを弄ることができたのです。

最初はこの値をいろいろ変えることでWAVEなどの情報が変更できるかと思っていたのですが、それは甘い考えでした。

0を代入したらバグるということは「他の値を代入すればバグらないのではないか?」と考え、いくつか値を代入してみた結果が以下の結果になります。

// Value of W0
0 Bug
1 FakeMode(Sploosh-o-matic only)
2 Normal
3 Infinity loop
4 Bug
5 Bug
6 Bug

1はどんなブキを選んでいても必ずボールドマーカーが支給されます。

2はいかにも普通といった感じで、恐らく通常時には2が入ると思われます。

3はマッチング中のような無限ループになります。

4以降は0と同じでバグります。

Game::Coop::Utl::GetRule()

そもそもこのコードはGame::Coop::Utl::GetRule()という関数が返す値を上書きするものでした。

この関数は元々下のように実装されています。

005C3368  ADRP  X8, #off_4156130@PAGE
005C336C  LDR   X8, [X8,#off_4156130@PAGEOFF]
005C3370  LDR   X8, [X8] ; Cmn::StaticMem::sInstance
005C3374  CBZ   X8, loc_5C3380
005C3378  LDR   W0, [X8,#0x72C]
005C337C  RET
005C3380 ; ------------------------------------------------------------
005C3380
005C3380 loc_5C3380   ; CODE XREF: Game::Coop::Utl::GetRule(void)+C↑j
005C3380  MOV   X0, XZR
005C3384  RET

ここで注目すべきは005C3374の命令です。これは、C言語でいうところの以下のコードと等価です。

If(X8 == 0){
  goto loc_5C3380;
} else {
  W0 = mem[X8+0x72C];
}

loc_5C3380 {
  X0 = 0;
}

そして、バグコードを実行するとこの関数を次のように変更します。

If(X8 == 0){
  goto loc_5C3380;
} else {
  X0 = 0;
  // W0 = mem[X8+0x72C];
}

loc_5C3380 {
  X0 = 0;
}

しかし、よくコードを見るとジャンプ先ではX0に0を代入していることがわかります。

これは、X8が0ならX0に0を代入しろというコードになります。

W0, X0はそれぞれ32ビットか64ビットかという違いで全く同じレジスタを参照しています。

あれ、でもX0が0だとバグるんですよ?このジャンプをするような状況がありますか???

別のコードを書いてみる

loc_5C3880の意味がわからないので、バグコードと等価なコードを書いてみます。

// Unknown [tkgling]
@disabled
00711BF4 680000B5

このコードは、命令を次のように書き換えてしまいます。

005C3368  ADRP  X8, #off_4156130@PAGE
005C336C  LDR   X8, [X8,#off_4156130@PAGEOFF]
005C3370  LDR   X8, [X8] ; Cmn::StaticMem::sInstance
005C3374  CBNZ  X8, loc_5C3380
005C3378  LDR   W0, [X8,#0x72C]
005C337C  RET
005C3380 ; ------------------------------------------------------------
005C3380
005C3380 loc_5C3380   ; CODE XREF: Game::Coop::Utl::GetRule(void)+C↑j
005C3380  MOV   X0, XZR
005C3384  RET

アドレスは3.1.0のものを使用しているのでズレていますが、気にしないでください。

このままではわかりにくいので、C言語風に書き直すと以下のようになります。

If(X8 != 0){
  goto loc_5C3380;
} else {
  W0 = mem[X8+0x72C];
}

loc_5C3380 {
  X0 = 0;
}

2行目の条件文を変更し、”今までloc_5C3380にジャンプしていたならしないようにする”というコードに変えました。

すると、やっぱりバグりました…

このジャンプ命令、何のためにあるんですか?

Game::Coop::Setting::reset()

Game::Coop::Utl::GetRule()を解析した結果、2を返せば正常、1を返せばボールド固定(デバッグ?)、それ以外を返すとバグるという結論が得られました。

これ以上の解析をする(何故1と2だけバグらないか調べる)ためには、GetRule()の値を利用する関数を解析する他ありません。

調べたところ、この値を利用するのはreset()という関数だとわかりました。

これは、reset()という名前はつけられているものの、実際には”マッチングしてゲーム開始時にWAVEのすべての情報を決定する”重要な関数です。

さて、覚えておいていただきたいのは先程のGetRule()はX0を返り値としたことでした。

__int64 __fastcall Game::Coop::Setting::reset(Game::Coop::Setting *this){
__int64 v2 = Game::Coop::Utl::GetRule(this);
if ( v2 == 2 )
  {
  // State 1
  v2 = Game::Coop::Utl::GetRuleParam((Game::Coop::Utl *)v2);
  v3 = 5 * v2;
}
else
{
  // State 2
  v3 = 0;
  if ( v2 != 1 ) goto LABEL_19;
}

 // 中略

}

reset()の関数はとても長く、擬似コードにしても600行以上あるので大事なところだけ抜粋します。

いきなりGetRule()の値を参照してくれるのでこれはありがたいです。これによると返り値が2の場合だけ、GetRuleParam()に入ることがわかります。

つまり、返り値として2を設定した場合だけ上手く動いたように見えたのはここが理由だったわけです。

となると、ここに入らないと通常状態でもバグが起きるのでしょうか?

0070FA24  CMP   W8, #2
0070FA28  B.EQ  loc_70FAD4

該当箇所のアセンブラはこのようなコードになります。

CMPは比較命令でW8と2を比較して、一致していれば条件レジスタに0を代入します。

B.EQは分岐命令です。 ここでEQは条件サフィックスを意味します。EQは“等しければ”の意味なのですが、一体何と等しいという意味なのでしょう?

さっぱりわかりません。

というわけで、等しくなければこの分岐条件を満たすようにコードを変えます。

0070FA24  CMP   W8, #2
0070FA28  B.NE  loc_70FAD4

というわけで正反対になるようにコードを書き換えてみたのですが、ものすごく普通に動いてしまいました。ということはGetRuleParam()は全く関係ないメソッドということになるんでしょうか?

0070FA24  CMP   W8, #2
0070FA28  NOP

というわけで、GetRuleParam()に入らないようにNOP(何もしない)に書き換えます。

結果からいえば、この状態でも通常通りプレイができました。

ええ、どっから弄ればいいの…