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

Hack

リアルタイムスペシャル変更コード

なんとなくつくってみたくなったのでつくった。

Starlightだと簡単だったけど、それだと面白くないのでいつもどおりシグナルHookで5.3.1に移植してみました。

必要なデータたち

今回のコードは関数Hookなので開発難易度は高めです。

プレイヤーにセットされているスペシャル情報をとってくるためにはGame::Playerクラスが必要なのですが、これを取得するためにはGame::PlayerMgrを使ってgetControlledPerformer()を呼び出す必要があります。

Game::PlayerMgrクラスを探そう

となれば、最初に探すべきはGame::PlayerMgrクラスのインスタンスですが、これはPlayerMgrとテキスト検索をかければ見つかります。以下のような命令群が見つかると思うのですが、後半部分のADRP命令で読み込んでいるところがPlayerMgrクラスのインスタンスになります。

ADRP            X8, #aPlayermgr@PAGE ; "PlayerMgr"
ADD             X8, X8, #aPlayermgr@PAGEOFF ; "PlayerMgr"
ADD             X0, SP, #0x80+var_70
MOV             X1, SP
MOV             X2, XZR
STR             X8, [SP,#0x80+var_78]
BL              sub_1956EF4
ADRP            X8, #off_2CFDCF8@PAGE
LDR             X8, [X8,#off_2CFDCF8@PAGEOFF]
LDR             X8, [X8]

なので、今回の場合は2CFDCF8が求めているアドレスになります。

SendSignalEvent()を探そう

過去のバージョンのアドレスからバイナリ検索でA1 C3 1F B8 A8 C3 5F B8 F3 03 00 AA と調べると見つけられると思います。

以下のような命令群が見つかれば、それがSendSignalEvent()です。

STR             X19, [SP,#var_20]!
STP             X29, X30, [SP,#0x20+var_10]
ADD             X29, SP, #0x20+var_10
STUR            W1, [X29,#-4]
LDUR            W8, [X29,#-4]
MOV             X19, X0
STRB            W2, [SP,#0x20+var_17]
STRB            W8, [SP,#0x20+var_18]
BL              sub_5BC880
TBZ             W0, #0, loc_104CA88
MOV             W0, #1
B               loc_104CA94
LDR             X0, [X19,#0x10]
ADD             X1, SP, #0x20+var_18
BL              sub_104E69C
LDP             X29, X30, [SP,#0x20+var_10]
AND             W0, W0, #1
LDR             X19, [SP+0x20+var_20],#0x20
RET

getControlledPerformer()を探そう

最後に、自分が操作しているプレイヤー情報を取得するためのgetControlledPerformer()を探します。

どこにあるかわかりにくいのですが、sendSignalEventの少し下にあることがわかっているのでそこからバイナリ検索すると良いでしょう。

今回の場合はsendSignalEvent()がA0EEC8にあるのがわかっているのでまずそこへジャンプし、43 00 91 08 C8 85 B9 09 24 46 B9でバイナリ検索をします。

以下のような命令群があればそれがgetControlledPerformer()です。

STR             X19, [SP,#-0x10+var_10]!
STP             X29, X30, [SP,#0x10+var_s0]
ADD             X29, SP, #0x10
LDRSW           X8, [X0,#0x5C8]
LDR             W9, [X0,#0x624]
CMP             W9, W8
B.LE            loc_10E6E80
LDR             X10, [X0,#0x638]
LDR             W9, [X0,#0x630]
ADD             X11, X10, X8,LSL#3
CMP             W9, W8
CSEL            X8, X11, X10, HI
LDR             X19, [X8]
CBZ             X19, loc_10E6E84
LDRB            W8, [X19,#0x430]
CBZ             W8, loc_10E6E84
BL              sub_19F8C5C
B               loc_10E6E84
MOV             X19, XZR
LDP             X29, X30, [SP,#0x10+var_s0]
MOV             X0, X19
LDR             X19, [SP+0x10+var_10],#0x20

ここまでの情報をまとめよう

さて、ここまで調べたデータをまとめると以下のようになります。

3.1.05.3.1
Lp::Sys::Actor::create<Game::PlayerMgr>0x008A9F000x00A0EEC8
Game::PlayerCloneHandle::sendSignalEvent0x00E797FC0x0104CA58
Game::PlayerMgr::sInstance0x041575780x02CFDCF8
Game::PlayerMgr::getControlledPerformer0x00F07B1C0x010E6E38

Lp::Sys::Actorについてはついでに載せているだけなのでスルーしてもらってOK。

ではここからsendSignalEvent()の命令を上書きして、ナイスを押すとスペシャルを切り替えられるようにしましょう。

sendSignalEvent()を書き換えよう

シグナルを送るコードは上のようになっています。

STR             X19, [SP,#var_20]!
STP             X29, X30, [SP,#0x20+var_10]
ADD             X29, SP, #0x20+var_10
STUR            W1, [X29,#-4]
LDUR            W8, [X29,#-4]
MOV             X19, X0
STRB            W2, [SP,#0x20+var_17]
STRB            W8, [SP,#0x20+var_18]
BL              sub_5BC880
TBZ             W0, #0, loc_104CA88
MOV             W0, #1
B               loc_104CA94
LDR             X0, [X19,#0x10]
ADD             X1, SP, #0x20+var_18
BL              sub_104E69C
LDP             X29, X30, [SP,#0x20+var_10]
AND             W0, W0, #1
LDR             X19, [SP+0x20+var_20],#0x20
RET

ここに書かれている命令を、

  1. Game::PlayerMgrインスタンスを読み込む
  2. Game::PlayerMgr::getControlledPerformer()を呼び出してGame::Playerクラスを取得
  3. Game::PlayerクラスのスペシャルIDの値を上書きする

という命令に上書きすることが今回の目標です。

コールスタックを書こう

ここで注意するのは上三行と下三行はコールスタックで、BL命令などで分岐した際にスタックポインタが戻ってくる位置を保存しておくために必要な命令です。

上書きするコードが全くBL命令などを使わないのであれば消してしまって構わないのですが、今回はgetControlledPerformer()を呼び出すのでコールスタックが必要になります。

ただし、上のコードは二回の分岐命令に対応したコールスタックなので、一回しかBL命令を呼ばないのであればコールスタック自体を書き換えることは可能です。

その場合は以下のようにそれぞれ一行ずつコードを省略することができます。

0104CA58 STP X29, X30, [SP, #-0x10]!
0104CA5C MOV X29, SP
0104CA60
0104CA64
0104CA68
0104CA6C
0104CA70
0104CA74
0104CA78
0104CA7C
0104CA80
0104CA84
0104CA88
0104CA8C
0104CA90
0104CA94
0104CA98
0104CA9C LDP X29, X30, [SP], #0x10
0104CAA0 RET

Game::PlayerMgrを呼び出そう

インスタンスを呼び出すコードは何度か説明しているのですが今回も説明します!

これはテンプレートとして覚えたほうが早いのですが、以下の三手一組のコードがインスタンスを呼び出してX0レジスタに格納するコードです。

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

やることはXXXXXとYYYの値を求めるだけなので簡単ですね。

これらを求めるためには「目的アドレス」と「呼び出し元アドレス」の二つが必要になります。目的アドレスは今回呼び出したい「Game::PlayerMgrクラスのインスタンスのアドレス」、「呼び出し元アドレス」は本来は「命令を上書きしたいアドレス」なのですが0x1000以下のズレはオフセットで補正できるので「sendSignalEvent()のアドレス」と考えても問題ありません。

目的アドレス呼び出し元アドレス
0x02CFDCF80x0104CA58

XXXXXの求め方

まず、目的アドレスと呼び出し元アドレスの下三桁を全て0にし、目的アドレスから呼び出し元アドレスを引きます。

0x02CFDCF8 -> 0x02CFD000
0x0104CA58 -> 0x0104C000

0x02CFD000 - 0x0104C000 = 0x01CB1000

なので今回の場合は0x01CB1がXXXXXの値となります。

YYYの求め方

YYYの方は計算不要で、目的アドレスの下三桁になります。

なので今回の場合は0xCF8がYYYの値となります。

ここまでをまとめると、Game::PlayerMgrのインスタンスを呼び出すテンプレートの命令は以下のようになります。

ADRP X0, #0x01CB1000
LDR  X0, [X0, #0xCF8]
LDR  X0, [X0]

あとはこのコードを最初に書いた上書き命令のテンプレートにくっつけるだけです。

0104CA58 STP X29, X30, [SP, #-0x10]!
0104CA5C MOV X29, SP
0104CA60 ADRP X0, #0x01CB1000
0104CA64 LDR  X0, [X0, #0xCF8]
0104CA68 LDR  X0, [X0]
0104CA6C
0104CA70
0104CA74
0104CA78
0104CA7C
0104CA80
0104CA84
0104CA88
0104CA8C
0104CA90
0104CA94
0104CA98
0104CA9C LDP X29, X30, [SP], #0x10
0104CAA0 RET

getControlledPerformer()を呼び出そう

getControlledPerformer()はBL命令で呼び出すことができます。

BL命令で必要なのは「呼び出し先アドレス」と「呼び出し元アドレス」の二つです。先程のインスタンスを呼び出すときと違い、オフセットがないのでアドレスが一つでもズレると正しく呼び出せずにクラッシュすることに気をつけましょう。

呼び出し元アドレス呼び出し先アドレス
getControlledPerformer()
0x0104CA6C0x010E6E38

呼び出し先アドレスはすぐにわかるのですが「呼び出し元はどこか」となりますよね。

このとき呼び出し元というのはBL命令を書くアドレスそのもので、上のテンプレートを見ると0104CAB4までは命令が埋まっているのでBL命令を書くのであれば0104CAB8であることがわかります。

ここもWindows謹製の電卓を使って差を計算しましょう。

0104CA58 STP X29, X30, [SP, #-0x10]!
0104CA5C MOV X29, SP
0104CA60 ADRP X0, #0x01CB1000
0104CA64 LDR  X0, [X0, #0xCF8]
0104CA68 LDR  X0, [X0]
0104CA6C BL #0x9A380
0104CA70
0104CA74
0104CA78
0104CA7C
0104CA80
0104CA84
0104CA88
0104CA8C
0104CA90
0104CA94
0104CA98
0104CA9C LDP X29, X30, [SP], #0x10
0104CAA0 RET

さて、ここまででGame::PlayerMgrを呼び出し、getControlledPerformer()をコールし、自分が操作しているプレイヤー情報(Game::Player)のインスタンスのポインタがX0レジスタにコピーされました。

スペシャル情報を書き換えよう

スペシャル情報がどこにあるのかという問題になるのですが、これはStarlightによる解析からプレイヤー情報の0x450番目のアドレスに格納されていることがわかっています。

なので、スペシャルIDを0にしたければ以下のようなアセンブラを書けば良いことになります。

STR XZR, [X0, #0x450]

これはゼロレジスタをX0[0x450]に上書きする命令です。

ゼロレジスタということは、次の命令と等価になります。

MOV X1, #0
STR X1, [X0, #0x450]

二行かかる命令が一行で書けるので楽というわけですね。ちなみにIDが0のスペシャルはマルチミサイルなので、このコードは「ナイスを押せばスペシャルがマルチミサイルになる」という効果を持つコードです。意味があるんだかないんだかよくわかりませんね。

ここまでをまとめると以下のようになります。

0104CA58 STP X29, X30, [SP, #-0x10]!
0104CA5C MOV X29, SP
0104CA60 ADRP X0, #0x01CB1000
0104CA64 LDR  X0, [X0, #0xCF8]
0104CA68 LDR  X0, [X0]
0104CA6C BL #0x9A380
0104CA70 STR XZR, [X0, #0x450]
0104CA74 NOP
0104CA78 NOP
0104CA7C NOP
0104CA80 NOP
0104CA84 NOP
0104CA88 NOP
0104CA8C NOP
0104CA90 NOP
0104CA94 NOP
0104CA98 NOP
0104CA9C LDP X29, X30, [SP], #0x10
0104CAA0 RET

大量にあるNOP命令は「何もしない」という意味を持ちます。とりあえず場所だけ確保しておいて、何かやりたいことが増えたらNOPを上書きしていけば良いです。

あとはこれをOnline ARM to HEX Converterでちまちま変換していくだけです。自分はコピペで入力するのがめんどくさすぎたので自作ツールでコマンド一発で変換できるようにしました。

// Change Special by Signal Hook [tkgling]
@disabled
0104CA58 FD7BBFA9
0104CA5C FD030091
0104CA60 80E500B0
0104CA64 007C46F9
0104CA68 000040F9
0104CA6C F3680294
0104CA70 1F2802F9
0104CA74 1F2003D5
0104CA78 1F2003D5
0104CA7C 1F2003D5
0104CA80 1F2003D5
0104CA84 1F2003D5
0104CA88 1F2003D5
0104CA8C 1F2003D5
0104CA90 1F2003D5
0104CA94 1F2003D5
0104CA98 1F2003D5
0104CA9C FD7BC1A8
0104CAA0 C0035FD6

このコードは5.3.1で実施に動作し、ナイスを押すとスペシャルがマルチミサイルになります。

しかしこれでは意味がないので、ナイスを押せばどんどんスペシャルが変わるようにしましょう。

ナイスを押すごとに変化させよう

ナイスを押すごとに変化させたければ「現在の値を読み取る」「値を書き換える」「現在の値を書き戻す」という三つの処理が必要になります。メモリの値を直接書き換えることはできないので、一度レジスタにコピーする必要があります。

LDR X1, [X0, #0x450]
ADD X1, X1, #1
STR X1, [X0, #0x450]

例えばこのように書けば現在の値を読み取ってX1レジスタにコピーし、その値に1を加えて書き戻すという動作ができます。一見これでいいような気がするのですが、このままだとナイスを押すたびに値がどんどん大きくなってしまいます。

スプラトゥーンで定義されているスペシャルの数は決まっているので、それを超えるとバグの原因になるわけです。実際、上の命令をそのままコード化するとスペシャルがガチホコになった段階でクラッシュしてしまいます。

ガチホコはIDが13なので「読み取った値が13だったら0に戻す」という処理を書けば良いことになります。

これはC++だと三項演算子を使って以下のように上手くかけるのですが、アセンブラではそういう事はできないので地道に実装しましょう。

X1 = X1 == 13 ? 0 : ++X1;

アセンブラでIF文を書こう

結論からいってしまえば、次のコードでIF文は実現できます。

が、適当に書いたのでいろいろなんか変です。ここを直すのを宿題ということで。

LDR X1, [X0, #0x450]
CMP X1, #13
LDR X1, [X0, #0x450]
ADD X2, X1, #1
CSEL X1, X2, XZR, CC
STR X1, [X0, #0x450]

一応コードの解説をしておくと、

// LDR X1, [X0, #0x450]
X1 = X0[0x450];

// CMP X1, #13
NZCV = X1 >= 13 ? 1 : 0

// LDR X1, [X0, #0x450]
X1 = X0[0x450];

// ADD X2, X1, #1
X2 = X1 + 1;

// CSEL X1, X2, XZR, CC
X1 = NZCV == 0 ? X2 : XZR

// STR X1, [X0, #0x450]
X0[0x450] = X1

CSEL命令はNZCVレジスタという特別なレジスタの値をみて、条件フラグに応じて返す値を変える命令です。

じゃあそのNZCVレジスタにどこで値を代入したんだって話になるんですが、それを行うのがCMP命令です。ただし、CMP命令を実行するとレジスタの値が変化してしまうので再度読み込みが必要になります(ややこしい)

要するにCMP命令はNZCVレジスタにフラグをつけるだけの役目しかないということです。

ここまでのコードをまとめると以下のような感じになります。

0104CA58 STP X29, X30, [SP, #-0x10]!
0104CA5C MOV X29, SP
0104CA60 ADRP X0, #0x01CB1000
0104CA64 LDR  X0, [X0, #0xCF8]
0104CA68 LDR  X0, [X0]
0104CA6C BL #0x9A380
0104CA70 LDR X1, [X0, #0x450]
0104CA74 CMP X1, #13
0104CA78 LDR X1, [X0, #0x450]
0104CA7C ADD X2, X1, #1
0104CA80 CSEL X1, X2, XZR, PL
0104CA84 STR X1, [X0, #0x450]
0104CA88 NOP
0104CA8C NOP
0104CA90 NOP
0104CA94 NOP
0104CA98 NOP
0104CA9C LDP X29, X30, [SP], #0x10
0104CAA0 RET

完成したもの

めんどくさいのでIPSwitch形式でコード化したのも載せておきます。

// RealTime Special Changer by Signal Hook [tkgling]
@enabled
0104CA58 FD7BBFA9
0104CA5C FD030091
0104CA60 80E500B0
0104CA64 007C46F9
0104CA68 000040F9
0104CA6C F3680294
0104CA70 012842F9
0104CA74 3F3400F1
0104CA78 012842F9
0104CA7C 22040091
0104CA80 41309F9A
0104CA84 012802F9
0104CA88 1F2003D5
0104CA8C 1F2003D5
0104CA90 1F2003D5
0104CA94 1F2003D5
0104CA98 1F2003D5
0104CA9C FD7BC1A8
0104CAA0 C0035FD6

まあ動画を見てもらえばわかるのですが、色んなところがバグっています。

既存のバグ一覧

  1. 発動しないスペシャルがある
    1. まともに使えるのはインクアーマー、スプラッシュボムピッチャー、スーパーチャクチのみ
    2. わかばシューターを使っている影響かもしれない
  2. ナイスを押すと何故か一回目にマルチミサイルになる
    1. 1足されるはずなのに0で初期化されている
    2. 0x450が間違っているか、まあなんか間違ってる
    3. 条件分岐かもしれない
  3. イカスフィアとバブルは普通に発動するとクラッシュする
    1. モデルデータ読み込んでないからとか多分そんなんの
  4. ナイスダマとウルトラハンコがない
    1. IDが離れたところにあるので1足してるだけではでてこない
    2. IDが何かは知らんが、やれば実装できる
  5. ガチホコを持つと何故かマルチミサイルを構える
    1. わけがわからん

みなさんへの宿題はスペシャルをちゃんと発動できるようにすることと、切り替えをちゃんとできるようにすること、ということで!

記事は以上。

コメント

  1. より:

    よくHaxxie氏が使っている先輩キャノンのコードを作りたいんですけど(非公開だから)
    どうすればいいのかわかりません?まずスペシャルID1も知りません。
    5.3.0でやりたいです。もしかしてMushで、できるのかな?

    • えむいー より:

      何故最新が5.3.1なのに5.3.0用のパッチを作ろうとされているのでしょうか?

      パッチでも可能だと思われますが、Mushの編集で実現するほうがバージョンの差を吸収しやすく(違うバージョンでもそのまま使える場合が多い)オススメです。
      具体的なやり方はこの記事に書いているのでどうぞ。

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