[Hack] Starlightで解析しよう!

Starlightとは

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

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

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

レジスタの中身を表示

チャート

何故レジスタの中身を表示したいかというとデータを直接見ることができるからです。

特に、インスタンスのポインタ(アドレス)がわかれば、インスタンス内のデータは順番に並んでいるので調べるのが簡単です。

以下、インスタンスを調べるとなんで嬉しいのかわからない人や、クラスについてよくわかってない人向けの解説。

クラスの概念

まあ簡単にいうと例えばロボットを百台つくってくださいという仕事が来ました。ロボットには十個機能をつけて、その機能を実現するために百のセンサーがついていることが仕様として決まっていたとします。

で、その仕事をするために百人のエンジニアを用意したとして「それぞれのエンジニアに一人一台ロボットをつくらせますか?」ということです。

一人でつくらせたらミスをするかもしれないし、不具合があるかもしれません。

それなら百人でロボットの設計図をつくって不具合がないかを調べ、最後に機械でロボットを大量生産すればいいわけです。設計図があれば同じものをたくさん作るのはわけないですよね。

このロボットの設計図がまさにプログラムで言うところの「クラス」で、ロボットに搭載されている機能が「メンバ関数」、メンバ関数を働かせるために必要なセンサーが「メンバ変数」にあたるわけです。

そしてロボット自身が「インスタンス」というわけです。

設計図にはロボットのどこにどのセンサーがついているかは書かれているのですから、ロボットの位置さえわかればあとは自由に好きなところを調べることができるわけですね。

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

主な流れ

  • Splatoon2のmainファイルでプログラムが実行され、レジスタやメモリに様々なデータが保存されている。
  • Starlightは直接ニンテンドースイッチのレジスタにアクセスできない。
    • やろうと思えばできるけどここでは考えない。
  • codehook.slpatchでinvokeDrawTVをhookして命令を書き換える
    • codehook.slpatchはIPSwitch形式のパッチの元ファイルでインラインアセンブラで書かれている。
    • 書き換えることで本来の命令を無視してStarlightのコード(renderEntrypoint)を実行できる。
    • StarlightはrenderEntrypoint内で様々なコードを実行する。
  • renderEntrypointはX0レジスタの値を表示する関数である。
    • codehook.slpatch内でX0レジスタに表示したいデータのインスタンスを格納することで任意のデータが表示できる。

つまり、大まかに以下の三つの作業が必要になるわけです。

  • 表示したいデータのインスタンスのアドレスを調べる。
  • インスタンスをX0レジスタにロードするためのアセンブリ言語をcodehook.slpatchに書く。
  • Starlightのmain.cpp内にデータを表示するC++コードを書く。

codehook.slpatch

まずはpatches/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
    BL  renderEntrypoint
    B   #0x2B8
enl::NinInetMatchingManagerPlatformImpl::internetInitialize+BC BL nnSocketInitHook

IPSwitch形式のパッチのソースコードみたいなファイル。アセンブラが直接かけるのでIPSwitchよりちょっとだけ便利かもしれない。

Splatoon2はgsys::SystemTask::invokeDrawTVというデータを描画する関数があるのですが、0017BCA10にあるTBZ W8, #0xE, loc_17BCDECという命令で無効化されています。

なのでまずはこの無効化されている機能を有効化するために、この命令を上書きする必要があります。

それが、gsys::SystemTask::invokeDrawTV_+284 NOPという命令なわけなんですね。

じゃあその次の+390からの命令はなんなのかということなんですが、これは表示したいデータをStarlight側へ伝える命令です。

というのも、Starlightは直接Splatoon2のレジスタにアクセスできないので、パッチをあててrenderEntrypoint()に表示させたいデータを教えてあげる必要があります。

renderEntrypoint

renderEntrypoint()は二つの引数を必要とし、一つはtextWriterと呼ばれるコンストラクタ自身で、もう一方が表示したいデータdrawContextになります。

ちなみにX1==textWriterで、X0==drawContextになります。

普通は逆かな?っていう気がするんですが、まあこれでいきましょう。

つまり、renderEntrypoint(X0, X1)という関数になるわけですね。

インスタンスのアドレスを調べる

さて、インスタンスのアドレスを調べるのはGHIDRAかIDA Proが必要になります。

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

で、まずはインスタンスが知りたいのでGetInstance_(void)::sInstanceみたいなテキストを探します。

基本的にはコンストラクタで宣言されているので、コンストラクタを探せばOK!

コンストラクタというのはそのクラスを使うときに一番最初に宣言されるコピーを生み出すときに使うおまじないみたいなものです。

コンストラクタは基本的にクラス名と同じ名前なので、今回のようにGame::Coop::EnemyDirectorというクラスのインスタンスのアドレスを調べたいときはGame::Coop::EnemyDirector::EnemyDirector()を調べるのが手っ取り早いです。

ちなみに、クラスによってはコンストラクタがわかりにくいときもある。そのときは~Singleton()ってところを探してみよう。

0054400C  ADRP  X8,  #off_4165740@PAGE
00544010  LDR   X8,  [X8,#off_4165740@PAGEOFF]
00544014  STR   X19, [X8] ;
Cmn::Singleton<Game::Coop::EnemyDirector>::GetInstance_(void)::sInstance

EnemyDirectorの場合は0x00544014でインスタンスが生成されていることがわかります。

ということは、必要なのはこのインスタンスが生成されているアドレスなので、その上のoff_4165740@PAGEに注目します。そう、これが探し求めていたインスタンスのアドレスなのです。

さて、ここでようやくインスタンスのアドレスを調べることができ、それが0x04165740であることがわかりました。

インラインアセンブラを書こう

さて、調べたいアドレスはわかったのであとはそれをレジスタに入れるだけです。

MOV X0, #0x04165740でいけるんじゃないか?って思うかもしれませんが、これではダメです。これはただ整数がレジスタに入っているだけです。

実際にはメモリアドレス0x04165740がもつ値(ポインタ)をコピーしなければいけないのでLDR命令などが必要になります。ところがLDR命令は命令が書かれているところからあんまり遠いアドレスのデータにはアクセスできません。

今から命令を上書きしようとしている関数0x017BCB1C invokeDrawTVは取得したいデータである0x04165740から離れすぎているのでLDR命令は使えません。

なので、大雑把なオフセットが扱えるADRP命令と細かいオフセットが使えるLDRを組み合わせてデータをとってきます。

アドレスの計算

さて、アドレスの計算なのですが簡単にいえば「目的のアドレスから現在のアドレスを引く」という計算をするだけです。

目的のアドレスは今回の場合0x04165740とわかっているから問題ないのですが、現在のアドレスがやっかいです。この現在のアドレスというのは「命令が書かれているアドレス」だからです。

まあちょっとくらいズレても大丈夫なので一行や二行コードを追加したくらいでは問題ないです。

さて、ここで思い出してもらいたいのがcodehook.slpatchの中身です。

gsys::SystemTask::invokeDrawTV_+390:
    MOV X1, X0
    MOV X0, X25
    BL renderEntrypoint
    B   #0x2B8

gsys::SystemTask::invokeDrawTV_+390:というのは「invokeDrawTVの先頭アドレスから+390した場所ですよ」ということを意味しています。

invokeDrawTVの先頭アドレスは017BC78Cなのですから、これはIPSwitch風の形式で書くと以下のようになります。

gsys::SystemTask::invokeDrawTV_+390:
017BCB1C MOV X1, X0
017BCB20 MOV X0, X25
017BCB24 BL  renderEntrypoint
017BCB28 B   #0x2B8

017BC78C+390=017BCB1Cとなるわけやね。

ちなみに16進数の計算はめんどくさいので基本的に下記のサイトを利用させてもらっています。もちろんWindows標準の電卓をプログラマモードに変えて使うのでもOKです。

INPUT(目的)INPUT(現在)OUTPUT
4165740 -> 416500017BCB2029A84E0

このとき “とりあえず意味がわからなくてもいいので” 入力の目的アドレスの下三桁を全て0にした上で計算結果の下から四桁目を +1し、下三桁を全て0に置き換えます。

つまり、29A84E0 -> 29A9000となりこれがADRP命令で使う値になります。LDR命令で使うのは元々の目的アドレスの下三桁なので今回は740になります。

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

ADRPLDR
29A9000740

おまけ

ClasssInstanceADRPLDR
EnemyDirector0x54401429A9000740
EventGeyser0x55824429AB000AC0
CoopSetting0x5C28EC29A4000E08

アセンブラで書こう

IPSwitch風に書くと以下のようになるので、これをインラインアセンブラに直します。

gsys::SystemTask::invokeDrawTV_+390:
017BCB1C MOV  X1, X0
017BCB20 ADRP X0, #0x29A9000 // EnemyDirector
017BCB24 LDR  X0, [X0, #0x740]
017BCB28 LDR  X0, [X0]
017BCB2C BL   renderEntrypoint
017BCB30 B    #0x2B0 // 0xDE0-0xB30=0x2B0

最後のB命令のオフセットも変更する必要があり、これは常に0xDE0からB命令の下三桁の数字を引いたものになります。今回のケースですと、0xDE0-0xB30=0x2B0となるわけです。

なんでこんなコードになるのかわからないかもしれないですが、説明してると長いのでこうなるんだと覚えたほうが早いです。

といっても、アドレス部を消すだけなので簡単ですね。

gsys::SystemTask::invokeDrawTV_+390:
    MOV  X1, X0
    ADRP X0, #0x29A9000 // EnemyDirector
    LDR  X0, [X0, #0x740]
    LDR  X0, [X0]
    BL   renderEntrypoint
    B    #0x2B0

まとめ

とりあえず自分でも一番理解が難しかったインスタンスのところと、その探し方についてまとめてみました。

といっても、まだこれは肝心のデータをStarlight側で見るところまではできていません。

本当は書きたかったのですが、全部書いているとあまりにも記事が長くなりそうだったのでこのあたりで記事を分割しようと思います。