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

Hack

前回までの内容

このコード開発記事も七回目となるのですが、だんだん内容が難しくなってきているのでいきなりここから始めようとすると詰みます。

初心者の方は必ず第一回の内容から理解した上で本記事の内容に挑戦してください。

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

Hookの仕組み

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

今回も説明を簡単にするために3.1.0向けのコード開発の手順を解説し、それを応用して5.2.0向けに移植するという内容にしようと思います。

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

Hookしたい関数のアドレス

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

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

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

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

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

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

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

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

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

Hookしたい関数のアドレス

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

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

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

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

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

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

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

ではこのクラスがどこでインスタンスを生成しているか調べれば良いのですが、Cmn::Singleton::GetInstance_(void)::sInstanceのようなコメントをテキスト検索すれば見つかると思います。

005A6154  ADRP  X8, #off_4165DB8@PAGE
005A6158  LDR   X8, [X8,#off_4165DB8@PAGEOFF]
005A615C  STR   XZR, [X8]
Cmn::Singleton<Game::Coop::PlayerDirector>::GetInstance_(void)::sInstance

するとこんな感じで0x05A615C付近に見つかり、0x04165DB8からインスタンスのアドレスを読み込んでいることがわかります。

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

インスタンスの構造体

いきなりここに入るとものすごく難しいと思うので、詳しく知りたい方は以下のStarlightで解析しよう!の記事を御覧ください。

わからなかったらスルーしていただいて結構です。

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

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

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;
}

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

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

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

Game::Coop::PlayerDirector

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

Game::Coop::PlayerDirectorの構造

つまり、Game::Coop::PlayerDirectorのインスタンスのポインタが分かればそこから0x370バイト後ろにズラしたところに一人目のプレイヤーのmRoundBankedPowerIkuraNumのデータが入っています。二人目なら0x470という感じで、先頭さえわかればすべてのデータに自由にアクセスできます。

アセンブラを書こう

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

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

NameAddress
sendSignalEvent()0x00E797FC
Game::Coop::PlayerDirector0x04165DB8

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

目的アドレスHookしたいアドレス
0x041650000x00E79000
計算の結果

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

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

ADRP	X0, #0xXXXXX000
LDR	X0, [X0, #0xYYY]
LDR	X0, [X0]

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

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

XXXXXの求め方

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

目的Hook結果
0x041650000x00E790000x032EC000
計算の結果

YYYの求め方

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

データを取得する

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

ADRP  X0, #0x32EC000
LDR   X0, [X0, #0xDB8]
LDR   X0, [X0]

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

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

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

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

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

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

データの変更

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

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

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

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

ARM命令の書き方一覧

// MOV
MOV X0, #100    // X0 = 100
MOV X0, #0x100  // X0 = 256

MOV X0, #100    // X0 = 100
MOV X1, X0      // X1 = X0 = 100

// ADD
MOV X0, #100    // X0 = 100
ADD X0, X0, #50 // X0 = X0 +50

MOV X0, #100    // X0 = 100
ADD X0, X0, #50 // X0 = X0 +50

あとはまあだいたい同じ書き方するので省略。

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

命令長の問題から一度に9999増やすことはできないので8192増やしてから更に1807増やすことで9999増やすコードを実現しています。

Windowsのプログラマモードの電卓で9999を16進数に直すと0x270Fであることがわかるので、これを0x270F=0x2000+0x70Fというふうに分割します。

LDR     X1, [X0, #0x370] // X1 = mTotalBankedPowerIkuraNum
ADD     X1, X1, #0x2000  // X1 = X1 + 8192
ADD     X1, X1, #0x70F   // X1 = X1 + 1807

読み込んだ値に8192と1807を足すことで結果として元々の値から9999増えたことになるわけです。

データの書き込み

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

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

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

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

コード化する

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

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

ADRP    X0, #0x32EC000
LDR     X0, [X0, #0xDB8]
LDR     X0, [X0]         // X0 = PlayerDirector
LDR     X1, [X0, #0x370] // X1 = mTotalBankedPowerIkuraNum
ADD     X1, X1, #0x2000  // X1 = X1 + 8192
ADD     X1, X1, #0x70F   // X1 = X1 + 1807
STR     X1, [X0, #0x370] // mTotalBankedPowerIkuraNum = X1
RET                      // Return

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

あとはこのアセンブラをARM to HEX Converterで変換するだけです。このとき出力されるARM HEXという値が今回欲しかったコードになります。

60970190
00DC46F9
000040F9
01B841F9
21084091
213C1C91
01B801F9
C0035FD6

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

IPSwitch形式に書き換え

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

// Signal Nice Hook [tkgling]
@enabled
00E797FC 60970190
00E79800 00DC46F9
00E79804 000040F9
00E79808 01B841F9
00E7980C 21084091
00E79810 213C1C91
00E79814 01B801F9
00E79818 C0035FD6

ただ、今回のように連続するアドレスで命令を書いていく場合には以下のように書き換えることもできます。

// Signal Nice Hook 3.1.0 [tkgling]
@enabled
00E797FC 6097019000DC46F9
00E79804 000040F901B841F9
00E7980C 21084091213C1C91
00E79814 01B801F9C0035FD6

こうすれば指定するアドレスが四つで済むのでちょっとだけ楽になりますね。

3.1.0での動作確認

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

5.2.0に移植する

Hook自体はアドレスさえわかればバージョンを問わずに可能なので、当然5.2.0への移植も可能です。

ただし、インスタンスのアドレスがズレているのでテンプレの三命令が3.1.0とは異なっています。なので、普通のコードのようにアドレスだけを書き換える移植はできません。

つまり、sendSignalEvent()の5.2.0でのアドレスは0x01042818なのですが、以下のようには書き換えられないということです。

// Signal Nice Hook 5.2.0 [tkgling]
@enabled
01042818 6097019000DC46F9 // NG
01042820 000040F901B841F9 // OK
01042828 21084091213C1C91 // OK
01042830 01B801F9C0035FD6 // OK

演習問題1

ということで、折角なのでコード開発の演習を行いたいと思います。

5.2.0におけるGame::Coop::PlayerDirectorとsendSignalEvent()のアドレスは以下に載せておきます。

これを使ってSignal Nice Hookのコードが5.2.0でも正しく動作するように修正してください。

NameAddress
sendSignalEvent()0x01042818
Game::Coop::PlayerDirector0x2D02EE0

難易度としては☆☆くらいかな。本来めんどくさいのはアドレスを調べることなので。

5.2.0に移植したコード

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

演習問題2

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

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

5.2.0向けのコード

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

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

コメント

  1. 匿名 より:

    チーム変更コードと一緒にこのコードを使ったらエラーとか出ますかね?

    • えむいー より:

      チーム変更コードは3命令で書けるので、このコードと合わせても11命令となります。Hookできる命令長は16なのでちゃんとコードを書けばエラーなしに同時に使えます。

      が、違う目的のコードを同じ関数にHookさせるのは良くないので、ナイスではなくカモンに割り当てるなどしたほうが良いかもしれませんね。

  2. 匿名 より:

    チーム変更コードは同じように作れば行けるでしょうか?

    • えむいー より:

      原理としては同じ感じで実現できます。チーム情報はGame::Coop::PlayerDirector内では定義されていないので別のインスタンスを見つけてくる必要がありますが…

    • 匿名 より:

      一応それらしき関数?は見つけたんですが合ってるか分からないですよね…

    • 匿名 より:

      多分違うかなーて思ってます。
      PlayerClassかPlayer関係のインスタンスとかに入ってそうだと思ってます。

    • えむいー より:

      StarlightによるとCmn::ActorやCmn::PlayerInfoやCmn::StaticMemクラスでmTeamという変数が使われていますね。

      この中だとCmn::PlayerInfoが一番扱うのは簡単だとは思うのですが、自分がチーム変更コードをまだ完全に理解できていないので…
      理解でき次第記事は書きたいと思っているのですが…

  3. 匿名 より:

    寝て起き次第試してみたいと思います!
    進展があったらまたコメントすると思います!

タイトルとURLをコピーしました