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

はじめに

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

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

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

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

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

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

必要なデータたち

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

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

Game::PlayerMgr クラスを探そう

となれば、最初に探すべきはGame::PlayerMgrクラスのインスタンスですが、これはPlayerMgrとテキスト検索をかければ見つかります。

以下のような命令群が見つかると思うのですが、後半部分の ADRP 命令で読み込んでいるところがPlayerMgrクラスのインスタンスになります。

00A0EF0C                 ADRP            X8, #aPlayermgr@PAGE ; "PlayerMgr"
00A0EF10                 ADD             X8, X8, #aPlayermgr@PAGEOFF ; "PlayerMgr"
00A0EF14                 ADD             X0, SP, #0x80+var_70
00A0EF18                 MOV             X1, SP
00A0EF1C                 MOV             X2, XZR
00A0EF20                 STR             X8, [SP,#0x80+var_78]
00A0EF24                 BL              sub_1956EF4
00A0EF28                 ADRP            X8, #off_2CFDCF8@PAGE
00A0EF2C                 LDR             X8, [X8,#off_2CFDCF8@PAGEOFF]
00A0EF30                 LDR             X8, [X8]
1
2
3
4
5
6
7
8
9
10

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

SendSignalEvent() を探そう

バイナリ検索でA1 C3 1F B8 A8 C3 5F B8 F3 03 00 AAと調べると見つけられると思います。

以下のような命令群が、SendSignalEvent()です。

0104C94C                 STR             X19, [SP,#var_20]!
0104C950                 STP             X29, X30, [SP,#0x20+var_10]
0104C954                 ADD             X29, SP, #0x20+var_10
0104C958                 STUR            W1, [X29,#-4]
0104C95C                 LDUR            W8, [X29,#-4]
0104C960                 MOV             X19, X0
0104C964                 STRB            W2, [SP,#0x20+var_17]
0104C968                 STRB            W8, [SP,#0x20+var_18]
0104C96C                 BL              sub_5BC880
0104C970                 TBZ             W0, #0, loc_104C97C
0104C974                 MOV             W0, #1
0104C978                 B               loc_104C988
0104C97C                 LDR             X0, [X19,#0x10]
0104C980                 ADD             X1, SP, #0x20+var_18
0104C984                 BL              sub_104E590
0104C988                 LDP             X29, X30, [SP,#0x20+var_10]
0104C98C                 AND             W0, W0, #1
0104C990                 LDR             X19, [SP+0x20+var_20],#0x20
0104C994                 RET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

getControlledPerformer() を探そう

バイナリ検索で43 00 91 08 C8 85 B9 09 24 46 B9と調べると見つけられると思います。

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

010E6D2C                 STR             X19, [SP,#-0x10+var_10]!
010E6D30                 STP             X29, X30, [SP,#0x10+var_s0]
010E6D34                 ADD             X29, SP, #0x10
010E6D38                 LDRSW           X8, [X0,#0x5C8]
010E6D3C                 LDR             W9, [X0,#0x624]
010E6D40                 CMP             W9, W8
010E6D44                 B.LE            loc_10E6D74
010E6D48                 LDR             X10, [X0,#0x638]
010E6D4C                 LDR             W9, [X0,#0x630]
010E6D50                 ADD             X11, X10, X8,LSL#3
010E6D54                 CMP             W9, W8
010E6D58                 CSEL            X8, X11, X10, HI
010E6D5C                 LDR             X19, [X8]
010E6D60                 CBZ             X19, loc_10E6D78
010E6D64                 LDRB            W8, [X19,#0x430]
010E6D68                 CBZ             W8, loc_10E6D78
010E6D6C                 BL              sub_19F8C5C
010E6D70                 B               loc_10E6D78
010E6D74                 MOV             X19, XZR
010E6D78                 LDP             X29, X30, [SP,#0x10+var_s0]
010E6D7C                 MOV             X0, X19
010E6D80                 LDR             X19, [SP+0x10+var_10],#0x20
010E6D84                 RET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

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

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

クラス 5.4.0
Game::PlayerMgr::sInstance 02CFDCF8
Game::PlayerCloneHandle::sendSignalEvent 0104C94C
Game::PlayerMgr::getControlledPerformer 010E6D2C

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

sendSignalEvent() を書き換えよう

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

0104C94C                 STR             X19, [SP,#var_20]!
0104C950                 STP             X29, X30, [SP,#0x20+var_10]
0104C954                 ADD             X29, SP, #0x20+var_10
0104C958                 STUR            W1, [X29,#-4]
0104C95C                 LDUR            W8, [X29,#-4]
0104C960                 MOV             X19, X0
0104C964                 STRB            W2, [SP,#0x20+var_17]
0104C968                 STRB            W8, [SP,#0x20+var_18]
0104C96C                 BL              sub_5BC880
0104C970                 TBZ             W0, #0, loc_104C97C
0104C974                 MOV             W0, #1
0104C978                 B               loc_104C988
0104C97C                 LDR             X0, [X19,#0x10]
0104C980                 ADD             X1, SP, #0x20+var_18
0104C984                 BL              sub_104E590
0104C988                 LDP             X29, X30, [SP,#0x20+var_10]
0104C98C                 AND             W0, W0, #1
0104C990                 LDR             X19, [SP+0x20+var_20],#0x20
0104C994                 RET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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

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

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

コールスタックを書こう

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

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

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

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

0104C94C STP X29, X30, [SP, #-0x10]!
0104C950 MOV X29, SP
0104C954
0104C958
0104C95C
0104C960
0104C964
0104C968
0104C96C
0104C970
0104C974
0104C978
0104C97C
0104C980
0104C984
0104C988
0104C98C
0104C990 LDP X29, X30, [SP], #0x10
0104C994 RET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Game::PlayerMgr を呼び出そう

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

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

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

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

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

目的 Hook
02CFDCF8 0104C94C
  • XXXXX の求め方

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

目的 Hook 結果
02CFD000 0104C000 01CB1000

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

  • YYY の求め方

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

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

ADRP X0, #0x01CB1000
LDR X0, [X0, #0xCF8]
LDR X0, [X0]
1
2
3

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

0104C94C STP X29, X30, [SP, #-0x10]!
0104C950 MOV X29, SP
0104C954 ADRP X0, #0x01CB1000
0104C958 LDR X0, [X0, #0xCF8]
0104C95C LDR X0, [X0]
0104C960
0104C964
0104C968
0104C96C
0104C970
0104C974
0104C978
0104C97C
0104C980
0104C984
0104C988
0104C98C
0104C990 LDP X29, X30, [SP], #0x10
0104C994 RET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

getControlledPerformer() を呼び出そう

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

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

getControlledPerformer() BL 命令をコールするアドレス
010E6D2C 0104C960

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

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

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

0104C94C STP X29, X30, [SP, #-0x10]!
0104C950 MOV X29, SP
0104C954 ADRP X0, #0x01CB1000
0104C958 LDR X0, [X0, #0xCF8]
0104C95C LDR X0, [X0]
0104C960 BL #0x9A3CC
0104C964
0104C968
0104C96C
0104C970
0104C974
0104C978
0104C97C
0104C980
0104C984
0104C988
0104C98C
0104C990 LDP X29, X30, [SP], #0x10
0104C994 RET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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

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

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

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

STR XZR, [X0, #0x450]
1

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

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

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

二行かかる命令が一行で書けるので楽というわけですね。

ちなみに ID が 0 のスペシャルはマルチミサイルなので、このコードは「ナイスを押せばスペシャルがマルチミサイルになる」という効果を持つコードです。

意味があるんだかないんだかよくわかりませんね。

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

0104C94C STP X29, X30, [SP, #-0x10]!
0104C950 MOV X29, SP
0104C954 ADRP X0, #0x01CB1000
0104C958 LDR X0, [X0, #0xCF8]
0104C95C LDR X0, [X0]
0104C960 BL #0x9A3CC
0104C964 STR XZR, [X0, #0x450]
0104C968 NOP
0104C96C NOP
0104C970 NOP
0104C974 NOP
0104C978 NOP
0104C97C NOP
0104C980 NOP
0104C984 NOP
0104C988 NOP
0104C98C NOP
0104C990 LDP X29, X30, [SP], #0x10
0104C994 RET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

大量にある NOP 命令は「何もしない」という意味を持ちます。

とりあえず場所だけ確保しておいて、何かやりたいことが増えたら NOP を上書きしていけば良いです。

あとはこれを ARM to HEX Converter でちまちま変換していくだけです。

自分はコピペで入力するのがめんどくさすぎたので自作ツールでコマンド一発で変換できるようにしました。

// Change Special by Signal Hook [tkgling]
@disabled
0104C94C FD7BBFA9
0104C950 FD030091
0104C954 80E500B0
0104C958 007C46F9
0104C95C 000040F9
0104C960 F3680294
0104C964 1F2802F9
0104C968 1F2003D5
0104C96C 1F2003D5
0104C970 1F2003D5
0104C974 1F2003D5
0104C978 1F2003D5
0104C97C 1F2003D5
0104C980 1F2003D5
0104C984 1F2003D5
0104C988 1F2003D5
0104C98C 1F2003D5
0104C990 FD7BC1A8
0104C994 C0035FD6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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

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

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

ナイスを押すごとに変化させたければ「現在の値を読み取る」「値を書き換える」「現在の値を書き戻す」という三つの処理が必要になります。

メモリの値を直接書き換えることはできないので、一度レジスタにコピーする必要があります。

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

例えばこのように書けば現在の値を読み取って X1 レジスタにコピーし、その値に 1 を加えて書き戻すという動作ができます。

一見これでいいような気がするのですが、このままだとナイスを押すたびに値がどんどん大きくなってしまいます。

スプラトゥーンで定義されているスペシャルの数は決まっているので、それを超えるとバグの原因になるわけです。

実際、上の命令をそのままコード化するとスペシャルがガチホコになった段階でクラッシュしてしまいます。

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

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

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

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

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

が、適当に書いたのでいろいろなんか変です。

ここを直すのを宿題ということで。

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, LO // X1 = NZCV == 0 ? X2 : XZR
STR X1, [X0, #0x450] // X0[0x450] = X1
1
2
3
4
5
6

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

じゃあその NZCV レジスタにどこで値を代入したんだって話になるんですが、それを行うのが CMP 命令です。

ただし、 CMP 命令を実行するとレジスタの値が変化してしまうので再度読み込みが必要になります(ややこしい)。

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

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

0104C94C STP X29, X30, [SP, #-0x10]!
0104C950 MOV X29, SP
0104C954 ADRP X0, #0x01CB1000
0104C958 LDR X0, [X0, #0xCF8]
0104C95C LDR X0, [X0]
0104C960 BL #0x9A3CC
0104C964 LDR X1, [X0, #0x450]
0104C968 CMP X1, #13
0104C96C LDR X1, [X0, #0x450]
0104C970 ADD X2, X1, #1
0104C974 CSEL X1, X2, XZR, LO
0104C978 STR X1, [X0, #0x450]
0104C97C NOP
0104C980 NOP
0104C984 NOP
0104C988 NOP
0104C98C NOP
0104C990 LDP X29, X30, [SP], #0x10
0104C994 RET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

完成したもの

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

// RealTime Special Changer by Signal Hook [tkgling]
@disabled
0104C94C FD7BBFA9 // STP X29, X30, [SP, #-0x10]!
0104C950 FD030091 // MOV X29, SP
0104C954 80E500B0 // ADRP X0, #0x1CB1000
0104C958 007C46F9 // LDR X0, [X0, #0xCF8]
0104C95C 000040F9 // LDR X0, [X0]
0104C960 F3680294 // BL #0x9A3CC
0104C964 012842F9 // LDR X1, [X0, #0x450]
0104C968 3F3400F1 // CMP X1, #13
0104C96C 012842F9 // LDR X1, [X0, #0x450]
0104C970 22040091 // ADD X2, X1, #1
0104C974 41309F9A // CSEL X1, X2, XZR, LO
0104C978 012802F9 // STR X1, [X0, #0x450]
0104C97C 1F2003D5 // NOP
0104C980 1F2003D5 // NOP
0104C984 1F2003D5 // NOP
0104C988 1F2003D5 // NOP
0104C98C 1F2003D5 // NOP
0104C990 FD7BC1A8 // LDP X29, X30, [SP], #0x10
0104C994 C0035FD6 // RET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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

既存のバグ一覧

  • 発動しないスペシャルがある。

    • まともに使えるのはインクアーマー、スプラッシュボムピッチャー、スーパーチャクチのみ。
    • わかばシューターを使っている影響かもしれない。
  • ナイスを押すと何故か一回目にマルチミサイルになる。

    • 1 足されるはずなのに0で初期化されている。
    • 0x450 が間違っているか、まあなんか間違ってる。
    • 条件分岐かもしれない。
  • イカスフィアとバブルは普通に発動するとクラッシュする。

    • モデルデータ読み込んでないからとか多分そんなんの。
  • ナイスダマとウルトラハンコがない。

    • ID が離れたところにあるので1足してるだけではでてこない。
    • ID が何かは知らんが、やれば実装できる。
  • ガチホコを持つと何故かマルチミサイルを構える。

    • わけがわからん。

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

記事は以上。