[Hack] Starlightで解析しよう!

Hack

Starlightとは

Shadowninja108氏が開発した神ツール。

ただ、公式レポジトリのは説明が全くなくてわかりにくいので個人的に説明を付け加えたフォーク版を公開しておきます。

tkgstrator/Starlight
An enviroment for linking to Splatoon 2’s executable and implementing hooks. - tkgstrator/Starlight

これで誰でも確実にStarlightの開発環境が整えられるはずなので頑張るのだ。

Starlightの仕組み

Starlightの大雑把な仕組みとしてはSplatoon2の実行ファイル(以後mainと呼ぶ)のgsys::SystemTask::invokeDrawTVをHookしてmain.cppで定義した独自の関数を呼びだすことです。

Starlightの仕組み

本来、gsys::SystemTask::invokeDrawTVは0x17BC78Cというアドレスから命令が実行されていきます。

その途中で製品版か開発版かのチェックを行う命令が0x17BCA10にあり、製品版ではこのチェックで引っかかって命令が最後まで実行されないままこの関数を抜けてしまいます。

Starlightではまずそのチェックを外すために0x17BCA10の命令をNOP命令で上書きして無効化します。それを行うのがCodehook.slpatchで、まあこれはARM命令が直接書けるIPSwitchと思ってもらえれば良いでしょう。

以下にIPSwitch形式とCodehook.slpatch形式の違いを載せておきます。どちらも同じ効果が得られます。

// Codehook.slpatch
[version=310, target=main]
gsys::SystemTask::invokeDrawTV_+284 NOP

// IPSwitch
@nsobid-034C8FA7A63B7A87F96F408B2AEFFF6C
@flag offset_shift 0x100
// Disable Checking
@enabled
017BCA10 1F2003D5 // NOP

Codehook.slpatch

Codehook.slpatchはmainの本来の命令を上書きしてStarlightにデータを渡す役割を担うパッチです。

先程も述べたように、原理自体はIPSwitchと全く同じなのでIPSwitchでも代用は可能なのですが、いちいち機械語に翻訳するよりも直接ARMで書いたほうが可読性もいいのでこちらを推奨しています。

[version=310, target=main]
gsys::SystemTask::invokeDrawTV_+284 NOP // enable display debug stuff (which is used for hook)
gsys::SystemTask::invokeDrawTV_+390:
	MOV X1, X0
	MOV X0, X25
	BL renderEntrypoint
	B #0x2B8
	
enl::NinInetMatchingManagerPlatformImpl::internetInitialize+BC BL nnSocketInitHook

では、Codehook.slpatchがどんな命令に上書きしているかということなのですが「表示したいデータのインスタンスのポインタをrenderEntrypointに渡している」のです。

ここで自分は結構躓いたので、わかりやすく解説していこうと思います。

インスタンスとポインタ

さて、先程Codehook.slpatchが表示したいデータのインスタンスのポインタを渡しているということは述べました。

プログラミング初心者の方だと思うかもしれません、「直接データを渡したほうが早くない?」と。

ところがよく考えてみるとこの「ポインタを渡す」が「直接データを渡す」ということよりも優れていることがわかるのです。

ポインタ渡しとデータ渡し

データ渡しというのは、直接データを関数に渡すコーディングを指します。

例えば、あるお兄さんがアパートに配達に向かうとします。アパートには四人の人が住んでいて、在宅しているかどうかはわかりません。

クラスの概念

もし、値渡ししかできないのであればお兄さんはこの四人(p1, p2, p3, p4)の各要素にアクセスしてenabledがtrueなら在宅、falseなら不在(これは現実世界でいうところのチャイムを鳴らすのと同義)を確認しなければいけません。

つまり、getEnabledApartment(Person)のような関数を最低でも四回呼び出す必要があります。

ところが、ポインタ渡しではこのアパートの住所(先頭アドレス)をまずお兄さんに教えます。

するとお兄さんは「その住所のアパートなら構造(全員の電話番号)を知っている」と答え、実際に訪問しなくても各住人がどのような状態にあるかを知ることができるのです。

要するに毎回住人のデータにアクセスしなくても、アパートの住所を教えるだけでapartment->p1.enabledのように書くだけで全データにアクセスできるのです。

もちろんこれは与えられたアパートの構造(人が何人いるか、どんなデータがあるか)を事前に知っておく必要があります。

この例え話超下手くそだからそのうち別のやつに変える!!

EventGeyserの場合

カンケツセンの挙動を司るクラスであるEventGeyserは以下のような構造を持つことが知られています。

EventGeyser

先頭アドレスから0x398の領域まではCmn::ActorLp::Sys::stateMachineというデータが入っているのですが、StarlightにLp::Sys::stateMachineのヘッダーファイルがないためこれらを扱うことができません。

なのでここではなんか適当なデータでその部分を埋めておきます。それが_BYTE gap[0x398]で、今後結構な頻度でお世話になる書き方です。

更にsead::Randomというクラスのデータを二つ持っており、これらはmSeed1, mSeed2, mSeed3, mSeed4という四つの数から線形合同法を使って疑似乱数を生成します。

そして最後にsead::PtrArrayImplという配列のクラスをもっており、ここにカンケツセンの位置などの配列が入っているというわけです。

ぶっちゃけるとぼくもクラスの概念については理解が不十分なので間違っていたら教えて下さい。

main.cpp+header

さて、Codehook.slpatchがインスタンスのポインタを渡すこと。インスタンスの構造についてあらかじめ知っていれば先頭アドレス(ポインタ)がわかれば全データにアクセスできることについては述べました。

では、それをmain.cppで受け取ってどのようにデータを扱うかを説明します。

// Before
void renderEntrypoint(agl::DrawContext *drawContext, sead::TextWriter *textWriter)
{
}

// After
static Game::Coop::EventGeyser *mEventGeyser;
void renderEntrypoint(agl::DrawContext *drawContext, sead::TextWriter *textWriter, Game::Coop::EventGeyser *eventGeyser)
{
  mEventGeyser = eventGeyser;
}

まず、表示したいデータを読み込む必要があるのでrenderEntrypointの引数に読み込みたいインスタンスのポインタを渡す必要があります。

また、受け取ったデータを永続的に保持するためstatic変数を使ってポインタ変数を宣言する必要もあります。

といっても、実はやるのはたったこれだけで、一度ポインタをコピーできたらあとは好き勝手に何でもできます。

例えば、疑似乱数を設定しているシードを調べたければ以下のコードで実装できます。

static Game::Coop::EventGeyser *mEventGeyser;
void renderEntrypoint(agl::DrawContext *drawContext, sead::TextWriter *textWriter, Game::Coop::EventGeyser *eventGeyser)
{
  mEventGeyser = eventGeyser;
  if (mEventGeyser != NULL) {
      textWriter->printf("%X\n", mEventGeyser->random.mSeed1);
  }
}

ポインタがNULLのときにアクセスするとゲームがクラッシュするので必ずNULLチェックはすること!

ここまでの流れ

  • Codehook.slpatchはmainを上書きして、インスタンスのポインタをStarlightに渡す
    • インスタンスのポインタがあれば、インスタンスの構造がわかっていればStarlightは全データにアクセスできる
  • main.cppの書き方は上のコードを参考にする

じゃあ「インスタンスの構造ってどうやって調べるのか?」ということに当然なるわけです。

クラスの構造を決めよう

さて、さんざん「インスタンスの構造」と言ってきましたが実際には「クラスの構造」と言った方が正しいと思います。

実は多くのクラスに関してはShadowninja108氏のおかげで既に定義されています。

shadowninja108/Starlight
An enviroment for linking to Splatoon 2’s executable and implementing hooks. - shadowninja108/Starlight

なので、基本的には呼び出したいインスタンスのポインタを探すことが課題になってきます。

が、サーモンランに関しては全くクラスの構造が定義されていないためこれは自分で頑張って構築する必要があります。

tkgstrator/Starlight
An enviroment for linking to Splatoon 2’s executable and implementing hooks. - tkgstrator/Starlight

いくつかぼくが定義してちゃんと動くことを確認したものがあるのでそれを載せておきます。

EventGeyser.hはEventDirector.hに名前変えないとですね…

まだクラスが定義されていない場合は、上のような感じで使いたいクラスのヘッダーファイルを作成する必要があります。

が、ここの話まで書いていると無限に長くなるので今回はクラスの構造がわかっている場合について解説します。

実際にやってみよう

ここまでずっと「これが何で必要なのか~」ということを説明してきましたが、実際にチュートリアルをやってみるのが一番だと思います。

やるべきことは以下の六つですので確認してみましょう

内容やること/データ難易度
表示したいインスタンス自分で決める
インスタンスのポインタ頑張って探す☆☆☆☆
インスタンスの定義頑張って定義する☆☆☆☆☆
Codehook.slpatch後で解説
main.cpp後で解説☆☆☆
exported.txt後で解説☆☆

表示したいデータについては自分がやりたいと思ったことからチャレンジしてみるのが一番です。

今回はサーモンランの設定を司っているGame::Coop::Settingをチョイスしました。何故ならチュートリアルとして最適なくらいに簡単だからです。

インスタンスの定義ですが、ここまで説明してるとやはり長くなりすぎるので今回はできているものと仮定して話を進めます。

コンストラクタを探す

ではインスタンスのポインタをどうやって調べるかということなのですがこれにはGHIDRAかIDA Proが必ず必要になります。

GHIDRAでSplatoon2を解析する方法については以下の記事で解説しているのでどうぞ。

探し方としてはまずはGame::Coop::Settingのメンバ関数を調べます。

このとき、以下のどちらかが見つかれば話は早いです。

Game::Coop::Setting::Setting()

いわゆるコンストラクタというやつで、これがあれば一番見つけやすいです。

が、今回はこのケースではありませんでした。

Cmn::Singleton<Game::Coop::Setting>

これも一応コンストラクタと読んでいいんでしょうか?

よくわからないのですが、今回はこちらが見つかりました。

インスタンスのアドレスを探す

さて、先程見つけたコンストラクタにジャンプすると以下のような命令が見つかります。

005C117C  ADRP X8, #off_4160E08@PAGE
005C1180  LDR  X8, [X8,#off_4160E08@PAGEOFF]
005C1184  MOV  X0, X19 ; this
005C1188  STR  XZR, [X8] ; Cmn::Singleton<Game::Coop::Setting>::GetInstance_(void)::sInstance

四行目のGetInstance_(void)がまさにビンゴ!

このとき、ADRP命令で参照されているoff_4160E08@PAGEがまさにインスタンスのポインタになります。

実際にそこにアクセスしてみると以下のようなデータが見れると思います。

004160E08 off_4160E08  DCQ 
 _ZZN3Cmn9SingletonIN4Game4Coop7SettingEE12GetInstance_EvE9sInstance

この、0x00460E08こそがまさに求めていたGame::Coop::Settingのポインタなのです。

Codehook.slpatchの編集

現状、以下のような状況であることを確認しましょう。

内容やること/データ
表示したいインスタンスGame::Coop::Setting
インスタンスのポインタ0x004160E08
Game::Coop::Settingの定義できていると仮定
Codehook.slpatch未着手←次ココ
main.cpp未着手
exported.txt未着手

さて、Codehook.slpatchはインラインアセンブラなのですが、一体どんなコードを書けばいいのでしょうか?

大雑把に説明すると、表示したいデータ一つにつき以下の三行で一組の(ような)コードをMOV X0, X25の下に追加する必要があります。

ADRP X2, ADDRESS // sInstance
LDR X2, [X2, OFFSET]
LDR X2, [X2]

今回の場合は追加すると以下のようになります。

[version=310, target=main]
gsys::SystemTask::invokeDrawTV_+284 NOP // enable display debug stuff (which is used for hook)
gsys::SystemTask::invokeDrawTV_+390:
	MOV X1, X0
	MOV X0, X25
	ADRP X2, ADDRESS // sInstance NEW
	LDR X2, [X2, OFFSET] // NEW
	LDR X2, [X2] // NEW
	BL renderEntrypoint
	B #0x2B8 // NEED TO CHANGE
	
enl::NinInetMatchingManagerPlatformImpl::internetInitialize+BC BL nnSocketInitHook

ここで必要になるのはADDRESSとOFFSETの値を求めることと、最後のB #0x2B8の編集です。

ADDRESSとOFFSET

さて、先程は追加する三行で一組のコードを大雑把に説明しましたが、実際には以下のパラメータを考えなければいけません。

必要なパラメータはそれぞれ赤・青・緑の箇所で、ここをそれぞれ変える必要があります。

じゃあどうやって変えた値を計算するかというと、元々のコードから何回コードを追加したかだけで決まります。

今回は初めて追加するので、追加する引数は1つ目ですね。

追加する引数青の計算式Bの値
1つ目X217BCB242AC
2つ目X317BCB302A0
3つ目X417BCB3C294
4つ目X517BCB48288
5つ目X617BCB5427C
パラメータの計算に必要なデータ

Bの値については後述!

青の数値の求め方

赤に入るのはすぐにX2だとわかるのですが、青に入る数値を計算するにはプログラマ電卓が必要です。

ここで、インスタンスのアドレスから表の数字を引きます。今回の場合だとインスタンスのアドレス4160000 - 17BC000 = 29A4000を計算すればいいわけです。

このとき、下の三桁を全部0に置き換えた数字が青い箇所に入る数字になります。よって、青い箇所に入る数字は29A4000になります。

緑に入る数字は簡単で、インスタンスのアドレスの下三桁です。

パラメータ
インスタンスのアドレス0x4160E08
赤の値X2
青の計算式4160000 – 17BC000
= 29A4000
緑の値E08
Bのオフセット2AC

B命令のオフセット

BのオフセットはX2を追加した場合には2B0なので、ここまでできてCodehook.slpatchの編集は終了です。

以下、完成したものを載せておくので参考にしてください。

[version=310, target=main]
gsys::SystemTask::invokeDrawTV_+284 NOP // enable display debug stuff (which is used for hook)
gsys::SystemTask::invokeDrawTV_+390:
  MOV X1, X0
  MOV X0, X25
  ADRP X2, #0x29A4000 // sInstance NEW
  LDR X2, [X2, #0xE08] // NEW
  LDR X2, [X2] // NEW
  BL renderEntrypoint
  B #0x2AC // NEED TO CHANGE
   
enl::NinInetMatchingManagerPlatformImpl::internetInitialize+BC BL nnSocketInitHook

おまけ

自分が調べたインスタンスのアドレスの値を載せておきます。

ClasssInstanceAddress
EnemyDirector0x5440140x4165740
EventDirector0x5B98680x4167BC0
CoopSetting0x5C28EC0x4160E08

main.cpp, main.hppの編集

内容やること/データ
表示したいインスタンスGame::Coop::Setting
インスタンスのポインタ0x004160E08
Game::Coop::Settingの定義できていると仮定
Codehook.slpatchできた!
main.cpp未着手←次ココ
exported.txt未着手

Codehook.slpatchを編集して、renderEntrypoint()に引数を追加するところまではできました。

引数を追加したということは関数の定義が変わったということなので、それをStarlight側に教えてあげなければいけません。なのでrenderEntrypointを書き換えましょう

// main.cpp
static Game::Coop::Setting *mCoopSetting;
void renderEntrypoint(agl::DrawContext *drawContext, sead::TextWriter *textWriter, Game::Coop::Setting *coopSetting)
{
}

同様に、main.hppも書き換えます。このときインスタンスの構造が書かれているSetting.hを読み込む必要があるのでそれも書きましょう。

// main.hpp
#include "Game/Coop/Setting.h"

void renderEntrypoint(agl::DrawContext *drawContext, sead::TextWriter *textWriter, Game::Coop::Setting *coopSetting);

exported.txtの編集

内容やること/データ
表示したいインスタンスGame::Coop::Setting
インスタンスのポインタ0x004160E08
Game::Coop::Settingの定義できていると仮定
Codehook.slpatchできた
main.cppできた
exported.txt未着手←次ココ

exported.txtはrenderEntrypoint()の関数の変更をスプラトゥーン側に伝える(多分そう)ためのファイルです。

これを定義しておかないとMakeの最後の関数をリンクするところで必ずコケます。

exported.txtはデフォルトでは以下のような内容になっているはずですが、_Z16renderEntrypointPN3agl11DrawContextEPN4sead10TextWriterEというところだけ書き換える必要があります。

// exported.txt
{
    global:
        _Z16renderEntrypointPN3agl11DrawContextEPN4sead10TextWriterE;
        _Z16nnSocketInitHookv;
        __custom_init;
        __custom_fini;
    local: *;
};

そのためにまずは、build310フォルダ内のStarlight310.mapという24MBくらいある巨大なテキストファイルを開きます。ここで_Z16renderEntrypointPN3agl11DrawContextEPN4sead10TextWriterEと検索すると2870行目付近でよく似た名前の関数が見つかると思います。

.text._Z16renderEntrypointPN3agl11DrawContextEPN4sead10TextWriterEPN4Game4Coop7SettingE;
                0x00000000000002fc      0xf84 main.o
                0x00000000000002fc                renderEntrypoint(agl::DrawContext*, sead::TextWriter*, Game::Coop::Setting*)

あとは見つかったコードを上書きして保存すれば成功です。

// exported.txt
{
    global: 
        _Z16renderEntrypointPN3agl11DrawContextEPN4sead10TextWriterEPN4Game4Coop7SettingE;
        _Z16nnSocketInitHookv;
        __custom_init;
        __custom_fini;
    local: *;
};

ここまでできたら再びmake installとコマンドを打てばエラーなくビルドできるはずです。

まとめ

まあここまで長々と書きましたが、ぼくのレポジトリにあるStarlightは既にGame::Coop::Settingを読み込む設定になっているので、そのままビルドすれば動きます。

ぜひとも活用してみて下さい。

コメント

  1. Anonymous より:

    What kind of mod is this?

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