Contents
前回までの内容
このコード開発記事も七回目となるのですが、だんだん内容が難しくなってきているのでいきなりここから始めようとすると詰みます。
初心者の方は必ず第一回の内容から理解した上で本記事の内容に挑戦してください。
今回はナイスの動作を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のインスタンスのポインタが分かればそこから0x370バイト後ろにズラしたところに一人目のプレイヤーのmRoundBankedPowerIkuraNum
のデータが入っています。二人目なら0x470という感じで、先頭さえわかればすべてのデータに自由にアクセスできます。
アセンブラを書こう
IPSwitch向けコードを書くといっても最終的に機械語に翻訳する作業が必要なだけで、元々のコードはアセンブラで書く必要があります。
いきなりアセンブラを考えると難しいのでゆっくり解説していきます。
Name | Address |
sendSignalEvent() | 0x00E797FC |
Game::Coop::PlayerDirector | 0x04165DB8 |
インスタンスのアドレスを読み込む
目的アドレス | Hookしたいアドレス |
0x04165000 | 0x00E79000 |
まず最初にやらないといけないのはインスタンスを読み込むということです。
「どうすればいいんだ?」って思うかもしれませんが、どんなインスタンスを読み込む場合にも以下の三つの命令があれば読み込めます。
ADRP X0, #0xXXXXX000 LDR X0, [X0, #0xYYY] LDR X0, [X0]
今回はX0レジスタを使っても問題ないですが、Hookする関数によってはX1やX2など好きなレジスタを使ってください。
その際は全部X0からX1やX2などに置き換えること!
XXXXXの求め方
目的アドレスとHookアドレスの下三桁を全て0にし、目的アドレス-Hookアドレス
の計算結果がXXXXXになります。
目的 | Hook | 結果 |
0x04165000 | 0x00E79000 | 0x032EC000 |
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
こうすれば指定するアドレスが四つで済むのでちょっとだけ楽になりますね。
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でも正しく動作するように修正してください。
Name | Address |
sendSignalEvent() | 0x01042818 |
Game::Coop::PlayerDirector | 0x2D02EE0 |
難易度としては☆☆くらいかな。本来めんどくさいのはアドレスを調べることなので。
ちなみに、5.2.0では赤イクラ数はプレイヤー一人あたり最大9999でカンストするので押すたびに9999増やすコードは最初の一回しか意味がなかったりします。
演習問題2
ナイスを押すと一人目のプレイヤー(player[0])のmRoundBankedGoldenIkuraNumの数が999になるコードを書いてください。
player[0]のmRoundBankedGoldenIkuraNumが先頭からいくらズレているかをチェックすれば難しくないはず。難易度は☆☆☆くらいかな?
ナイスを押した瞬間に納品数が999になるのでクリアできます。
ただ、何らかのチェックが働いているのか、リザルト画面でのスコアには正しく反映されません。
自身を天才と信じて疑わないマッドサイエンティスト。二つ上の姉は大英図書館特殊工作部勤務、額の十字架の疵は彼女につけられた。
コメント
チーム変更コードと一緒にこのコードを使ったらエラーとか出ますかね?
チーム変更コードは3命令で書けるので、このコードと合わせても11命令となります。Hookできる命令長は16なのでちゃんとコードを書けばエラーなしに同時に使えます。
が、違う目的のコードを同じ関数にHookさせるのは良くないので、ナイスではなくカモンに割り当てるなどしたほうが良いかもしれませんね。
チーム変更コードは同じように作れば行けるでしょうか?
原理としては同じ感じで実現できます。チーム情報はGame::Coop::PlayerDirector内では定義されていないので別のインスタンスを見つけてくる必要がありますが…
一応それらしき関数?は見つけたんですが合ってるか分からないですよね…
多分違うかなーて思ってます。
PlayerClassかPlayer関係のインスタンスとかに入ってそうだと思ってます。
StarlightによるとCmn::ActorやCmn::PlayerInfoやCmn::StaticMemクラスでmTeamという変数が使われていますね。
この中だとCmn::PlayerInfoが一番扱うのは簡単だとは思うのですが、自分がチーム変更コードをまだ完全に理解できていないので…
理解でき次第記事は書きたいと思っているのですが…
寝て起き次第試してみたいと思います!
進展があったらまたコメントすると思います!