[IPSwitch] 誰でもできるコード開発 #6

はじめに

今回の内容は以下の記事の続きになります。

[IPSwitch] 誰でもできるコード開発 #5 (opens new window)

この記事を読むにあたって必ず目を通して理解しておいてください。

Hook の仕組み

今回はナイスの動作を Hook して別の割り当てにしてしまおうという試みです。

Hook というのが自分でもよくわかってないのですが、本来の動作の命令を上書きして任意の関数を呼び出したりそういうのが Hook なんじゃないかとおもっています、違ったらごめんなさい。

まず、 Hook するコードを書くために必要なことは三つです。

  • Hook したい関数のアドレス

今回はナイスの動作を Hook したいのでそのアドレスを調べる必要があります。

これはバージョンごとに異なるので、アップデートのたびに更新しなければいけません。

  • 目的のインスタンスのアドレス

インスタンスのポインタを習得する必要があるので、インスタンスのアドレスが必要になります。

今回は、サーモンランにおけるプレイヤー情報をナイスを使って操作することを考えてみましょう。

これもバージョンごとに異なるので、アップデートのたびに更新する必要があります。

  • 目的のインスタンスの構造体

一番難しいのがこれで、仮に上の二つをクリアしたとしてもどこに何のデータが入っているのかがわからなければデータを使うことができません。

今回は目的のインスタンスの構造体はわかっているものとして話を進めます。

Hook したい関数のアドレス

ナイスを押したときに呼び出される関数はGame::PlayerCloneHandle::sendSignalEvent()で、これはアドレス 0104C94C にかかれています。

見ればわかるのですが、sendSignalEvent()自体は命令長が 17 の関数です。

17 もあるということはたくさん上書きしても大丈夫ということですね。

最後に RET 命令を必ず書かなければいけないので、実質 16 命令書くことができます。

というわけで、一つ目の目標であった「Hook したい関数のアドレス」はわかったことになります。

目的のインスタンスのアドレス

今回はサーモンランのプレイヤー情報を弄りたいのですが、それらを制御するクラスはGame::Coop::PlayerDirectorです。

このクラスがどこでインスタンスを生成しているか調べれば良いのです。

0073EC54                 ADRP            X8, #off_2D0CEE0@PAGE
0073EC58                 LDR             X8, [X8,#off_2D0CEE0@PAGEOFF]
0073EC5C                 STR             XZR, [X8]
1
2
3

調べると、こんな感じで 0073EC5C 付近に見つかり、02D0CEE0 からインスタンスのアドレスを読み込んでいることがわかります。

よって、インスタンスのアドレスは 02D0CEE0 ということがわかりました。

インスタンスの構造体

「インスタンスのポインタがわかれば何が便利なのか」ということなんですが、それは一言でいうと「インスタンスの構造がわかっていればポインタ(先頭アドレス)がわかれば好きなデータにアクセスできる」ということに尽きます。

例えば、サーモンランにおけるプレイヤー情報は以下のようになっています。

struct Game::Coop::PlayerDirector
{
  _BYTE gap[0x370];
  Game::Coop::Player player[4];
};
struct Game::Coop::Player
{
  uint32_t mRoundBankedPowerIkuraNum;
  uint32_t mGotGoldenIkuraNum;
  uint32_t mRoundBankedGoldenIkuraNum;
  uint32_t mTotalBankedGoldenIkuraNum;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

これはかなり大雑把な構造なので、実際にはもっといろんな要素がある。

つまり、PlayerDirectorのポインタを見つけたら先頭から 370 バイトまでは何が入っているかわからないが、その後に四人分のプレイヤー情報が入っていることがわかるのです。

正確には先頭の 880 バイトにはCmn::Actorsead::IDisposerが入っていますが、今回は使わないので無視します。

Game::Coop::PlayerDirector

よって、Game::Coop::PlayerDirectorの構造体をまとめると以下のようになります。

Game::Coop::PlayerDirector
  0x000 Cmn::Actor actor
  0x348 sead::IDisposer
  0x368 char char0x368
  0x370 Game::Coop::Player player[0]
    0x370 mRoundBankedPowerIkuraNum
    0x374 mGotGoldenIkuraNum
    0x378 mRoundBankedGoldenIkuraNum
    0x37C mTotalBankedGoldenIkuraNum
  0x470 Game::Coop::Player player[1]
    0x470 mRoundBankedPowerIkuraNum
    0x474 mGotGoldenIkuraNum
    0x478 mRoundBankedGoldenIkuraNum
    0x47C mTotalBankedGoldenIkuraNum
  0x570 Game::Coop::Player player[2]
    0x570 mRoundBankedPowerIkuraNum
    0x574 mGotGoldenIkuraNum
    0x578 mRoundBankedGoldenIkuraNum
    0x57C mTotalBankedGoldenIkuraNum
  0x670 Game::Coop::Player player[3]
    0x670 mRoundBankedPowerIkuraNum
    0x674 mGotGoldenIkuraNum
    0x678 mRoundBankedGoldenIkuraNum
    0x67C mTotalBankedGoldenIkuraNum
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

つまり、Game::Coop::PlayerDirectorのインスタンスのポインタが分かればそこから 370 バイト後ろにズラしたところに一人目のプレイヤーのmRoundBankedPowerIkuraNumのデータが入っています。

二人目なら 470 という感じで、先頭さえわかればすべてのデータに自由にアクセスできます。

アセンブラを書こう

IPSwitch 向けコードを書くといっても最終的に機械語に翻訳する作業が必要なだけで、元々のコードはアセンブラで書く必要があります。

いきなりアセンブラを考えると難しいのでゆっくり解説していきます。

Game::Coop::PlayerDirector sendSignalEvent()
02D0CEE0 0104C94C

インスタンスのアドレスを読み込む

まず最初にやらないといけないのはインスタンスを読み込むということです。

「どうすればいいんだ?」って思うかもしれませんが、どんなインスタンスを読み込む場合にも以下の三つの命令があれば読み込めます。

ADRP X0, #0xXXXXX000
LDR X0, [X0, #0xYYY]
LDR X0, [X0]
1
2
3

今回は X0 レジスタを使っても問題ないですが、Hook する関数によっては X1 や X2 など好きなレジスタを使ってください。

その際は全部 X0 から X1 や X2 などに置き換えること!

  • XXXXX の求め方

目的アドレスと Hook アドレスの下三桁を全て 0 にし、目的アドレス - Hook アドレスの計算結果が XXXXX になります。

目的 Hook 結果
02D0C000 0104C000 01CC0000

これは Windows 標準の電卓で簡単に計算することができます。

  • YYY の求め方

目的アドレスの下三桁なので EE0 になります。

データを取得する

さて、XXXXX と YYY の値がわかったので先程のテンプレの命令に当てはめると以下のようになります。

ADRP X0, #0x1CC0000
LDR X0, [X0, #0xEE0]
LDR X0, [X0]
1
2
3

実はこれで正しくPlayerDirectorのポインタが取得できており、その値が X0 レジスタに入っています。

ではさっそく、データを習得するコードを書いてみましょう。

実はデータ取得に必要なコードはたった一種類なので、使い方さえ覚えてしまえば非常に簡単です。

LDR X1, [X0, #0x370] // X1 = mTotalBankedPowerIkuraNum
1

それがこの LDR 命令で、これは X0 レジスタ(今回の場合はPlayerDirecotrのポインタ)から 370 ズラしたところにあるデータを X1 レジスタにコピーするという命令です。

370 ズラしたところには先ほど説明したように一人目のプレイヤーの赤イクラ数が入っています。

つまり、これだけでデータの読み込みができてしまうのです。

データの変更

ただ、これだと読み込んだだけで使いみちがないので、その値を更新したいと思います。

演算に使える命令はたくさんありますが、よく使うのはこの辺りでしょう。

命令 意味
MOV 代入
ADD 加算
SUB 減算
MUL 乗算
AND 論理積
ORR 論理和
EOR 排他的論理和

除算はあんまり使わないかな、多分。

ARM 命令の書き方一覧

今回は読み込んだ赤イクラ取得数を 9999 増やすコードを書いてみます。

Windows のプログラマモードの電卓で 9999 を 16 進数に直すと 270F であることがわかります。

LDR X1, [X0, #0x370] // X1 = mTotalBankedPowerIkuraNum
MOV X1, #0x270F      // X1 = 0x270F
1
2

元々の値を 9999 に代入します。

データの書き込み

さて、ここまではテンプレの三命令でインスタンスのポインタを読み込み、LDR 命令で赤イクラ数を習得し、9999 を足すところまで書くことができました。

でもこれだとただ計算をしただけなので、その結果を返さなければいけません。

データを戻す命令は STR 命令で、使い方は LDR 命令と全く同じです。

STR X1, [X0, #0x370] // X1 = mTotalBankedPowerIkuraNum
1

コード化する

今までの三工程をまとめると以下のようになります。

最後の RET 命令はおまじないのようなもので、Hook する関数にも依りますが基本的には必要になってきます。

ADRP X0, #0x1CC0000
LDR X0, [X0, #0xEE0]
LDR X0, [X0]         // X0 = PlayerDirector
LDR X1, [X0, #0x370] // X1 = mTotalBankedPowerIkuraNum
MOV X1, #0x270F      // X1 = 0x270F
STR X1, [X0, #0x370] // X1 = mTotalBankedPowerIkuraNum
RET
1
2
3
4
5
6
7

命令の長さは全部で 8 となり、sendSignalEvent()の長さである 17 以下で収めることができました。

あとはこのアセンブラを ARM to HEX Converter で変換するだけです。

このとき出力される ARM HEX という値が今回欲しかったコードになります。

00E60090
007047F9
000040F9
01B841F9
E1E184D2
01B801F9
C0035FD6
1
2
3
4
5
6
7

あとはこれを IPSwitch 形式に書き換えれば作業は終了です。

IPSwitch 形式に書き換え

sendSignalEvent()の先頭からドンドン上書きするだけなので以下のようになります。

// Signal Hook [tkgling]
@disabled
0104C94C 00E60090 // ADRP X0, #0x1CC0000
0104C950 007047F9 // LDR X0, [X0, #0xEE0]
0104C954 000040F9 // LDR X0, [X0]
0104C958 01B841F9 // LDR X1, [X0, #0x370]
0104C95C E1E184D2 // MOV X1, #0x270F
0104C960 01B801F9 // STR X1, [X0, #0x370]
0104C964 C0035FD6 // RET
1
2
3
4
5
6
7
8
9

Starlight(3.1.0)を使って左上に常に取得した赤イクラ数が表示されているのですが、ナイスを押すたびに 9999 増えていることがわかります。

ちなみに、 5.4.0 では赤イクラ数はプレイヤー一人あたり最大 9999 でカンストするので押すたびに 9999 増やすコードは最初の一回しか意味がなかったりします。

演習問題

ナイスを押すと一人目のプレイヤー(player[0] )のmRoundBankedGoldenIkuraNumの数が 999 になるコードを書いてください。

player[0] のmRoundBankedGoldenIkuraNumが先頭からいくらズレているかをチェックすれば難しくないはず。

ナイスを押した瞬間に納品数が 999 になるのでクリアできます。

ただ、何らかのチェックが働いているのか、リザルト画面でのスコアには正しく反映されません。

記事は以上。