Quantumleap
6334 words
32 minutes
誰でもできるコード開発 #01

IPSwitch で利用できるコードを開発しよう#

IPSwitch で様々なパッチを当てて遊んでいるときに「こういうコードがあればいいのに」と思ったことがある人が多いと思います。そのときに「誰かに作ってもらいたい」と思う人もいれば「できるなら自分で作ってみたい」と思う人もいるでしょう。

ところがコードを作ろうと思っても、コードの作成方法について日本語で解説している記事は殆どありません。というか、英語でも見たことがありません。

「どうやってこのコードを見つけたのか」、「どうやってこのコードを書いたのか」ということが気になっても、それを学ぶ方法も、誰にきけばいいのかもわからないわけです。

当ブログは学ぶ意欲のある方を応援するための努力を惜しまないので、以前のブログで公開していた IPSwitch 向けのコード開発記事を加筆修正し、これから初めてコード開発に取り組もうという方向けのチュートリアルを執筆することにしました。

当時の記事では「当時最新」のバージョン向けの内容になっていたのですが、今回それも「現在最新」のバージョン 5.5.0 向けに一新しました。

スプラトゥーン 3 がもう発売される段階ですので、今後スプラトゥーン 2 に大きなアップデートは恐らくないと思われます

このチュートリアルが皆さんのコード開発の手助けになれば執筆者として非常に嬉しいです。

パッチの種類#

本記事では IPSwitch 向けのコードの書き方を解説するのですが、コードときいて Edizon 向けのものを想像した方も多いと思います。

ではまず最初に IPSwitch 向けのパッチと Edizon 向けのパッチの違いについて解説します。

項目IPSwitchEdizon
対象main任意のメモリ
実行タイミング起動時任意
変更再起動するまで不可任意
解析バイナリメモリ
コードアドレス, 値アドレス, オフセット, 値

対象#

IPSwitch 形式のパッチは main にしか対象をとることができませんが、Edizon は main を含む任意のメモリにアクセスできます。何が違うのかというのを簡単に説明するのは難しいのですが、正確性を犠牲に解説します。

実行タイミング#

IPSwitch のパッチはゲームが起動する前に実行されるので、ゲームで使われるありとあらゆるデータを上書きすることができます。対して、Edizon のコードは任意のタイミングで実行するので、Overlay を使って手動でパッチを有効にする場合はゲーム起動時に初期化されるような値に対してはパッチを当てることができません。

ですが、Edizon には起動時にパッチを当てるような方法もあるので、できないわけではないです。

変更#

Edizon は Overlay を利用することでゲームが起動しているなら任意のタイミングでコードを有効にすることができます。対して、IPSwitch は一度コードを有効にするとゲームを終了するまで変更することができません。

何故 IPSwitch 向けのコード開発を学ぶか#

というわけで、基本的には Edizon のコードのほうができることが多いです。また、IPSwitch 向けのパッチは一部の例外を除いて Edizon 向けに書き換えることができます。

じゃあ IPSwitch 向けのパッチのチュートリアルでなく、Edizon 向けのチュートリアルを書けばいいじゃないかと思われるかもしれませんが、コードの実装に関しては IPSwitch のほうが遥かに楽です。これが、初学者が最初に IPSwitch 形式のコード開発の方法を学ぶべき理由です。

固定アドレス#

まず、IPSwitch のコードは解析する対象がバイナリです。簡単に説明すると、調べたい情報は常に同じアドレスにあります。対して Edizon は解析する対象がメモリなので、ゲームを起動する度にアドレスが変わります。ただ、アドレスは変わるのですがオフセットは変わらないので、基準となる位置がわかれば IPSwitch と同じように解析ができます。

とはいえ、何もしなくても常に同じ値を参照しているので IPSwitch の方が非常にわかりやすいです。

コードが読める#

IPSwitch 向けの解析は、ゲームの実行ファイルを直接解析します。要するに、プログラムを解析するということです。このプログラムを解析する行為を「逆アセンブル」といいます。つまり、プログラムが読めるのであれば、どういう処理が行われているかを簡単に知ることができるわけです。

逆アセンブルの本来の意味は異なりますが、まあ大雑把に「人が書いたプログラムを読む行為」と思ってください

対して Edizon はメモリを読むので「なにかの値がある」のはわかるのですが、その値がなんなのか、自分が求めている値なのかということはしっかりと解析しないとわかりません。

デバッグシンボルがある#

これはスプラトゥーン 3.1.0 まで限定ですが、実行ファイル内にデバッグシンボルが残っているため、何をする関数なのかということが簡単にわかります。

前提知識#

さて、いよいよ本格的なコード開発の内容に踏み込んでいきたいのですが、流石にコンピュータ関連の知識が 0 では無理です。ある程度コンピュータが動く仕組み、ゲームが動く仕組みを理解しておく必要があります。

コンピュータが動く仕組み#

コンピュータを構成する要素はいくつかあるのですが、最も基本的な構成は、

  • 中央処理装置(CPU)
  • 主記憶装置(メモリ)
  • 補助記憶装置(SSD/HDD)

の三つです。これに加えて、実際に動かすプログラム(バイナリ)があって初めて、コンピュータは何らかのプログラムを実行することができます。

プログラムのことをバイナリというのは、コンピュータが理解できる言語は二進数(バイナリ)であるためです

プログラムを実行したときに起こること#

では、何らかのプログラムをコンピュータに実行させたとしましょう。このとき、コンピュータと、それらを構成する要素はどのような働きをするかを考えてみます。

1. プログラムの読み込み#

まず、コンピュータは補助記憶装置からデータを読み込みます。

ニンテンドースイッチの場合、カートリッジを読み込む動作がこれにあたります。ただし、データを全て読み込むわけではなくデータの中のプログラムの部分だけを読み込みます。

スプラトゥーンの場合だと、ゲームのデータ自体は 3GB ほどあるのでこれを全て読み込んでからゲームを起動していては、ロード時間が長くなりすぎます。スプラトゥーンのプログラム(実行バイナリ)は 40MB ほどなので、起動自体は直ぐにできるというわけです。

2. プログラムの展開#

プログラムを読み込んで何をするのかというと、実行バイナリをより高速に読み込める記憶領域にコピーします。ゲームの実行中は何度も実行バイナリにアクセスするので、実行バイナリへのアクセスに時間がかかっているとゲームの動作に支障をきたします。よって、なるべく速い記憶装置にコピーしておこうというわけです。

もちろんコピーにはそれなりに時間がかかる。しかし、コピーに 30 秒かかったとしても(実際にはこの 100 万倍以上速いが)、コピーしたことによって 1 回あたりのアクセスが 1 秒短くなれば 31 回アクセスすればトータルとしてコピーしたほうが時間が節約できた、ということになる。実行バイナリをコピーするのはそういう理由がある

コンピュータが利用する記憶領域には CPU 内のキャッシュ、DRAM(メモリ)や SSD などいろいろありますが、この場合は DRAM が利用されます。

現在の SSD の読み込み速度は 3000MB/s を超えるものも珍しくなく、十分速いように思いますが DRAM や内部キャッシュと比べると恐ろしいほどに遅いです。

各記憶装置の遅延については諸説あるが、概ねこんな感じの値になります

領域遅延
L1/2 キャッシュ1ns
L3 キャッシュ10ns
DRAM100ns
NVMe SSD25000ns

1ns とは 10 億分の 1 秒のことである

カートリッジから毎回プログラムを呼び出すよりも、DRAM から読み込んだほうが 250 倍も速いので DRAM にコピーしておこうというわけですね。ただ、この表を見れば当然「キャッシュに保存したほうが 10~100 倍速くなるんじゃないの」と思われると思います。

しかし、残念ながらそれは不可能です。何故なら、キャッシュはスプラトゥーンの実行バイナリを保存できるほど容量が大きくないからです。SSD は 1TB の容量も全く珍しくないですが、メモリを 1TB(512GB でもいいが)積んでいる人は殆どいないと思います。当たり前ですが、速くなればなるほど値段は高くなり、搭載できる容量も減っていきます。

ニンテンドースイッチが二台買えてお釣りが返ってくる i7 12900K というハイエンドの CPU ですら一番大きい L3 キャッシュで 30MB しかありません。スプラトゥーン 2 の実行バイナリは 40MB 以上あるので、とてもキャッシュに乗らないというわけです。

3. メモリを確保する#

実行バイナリが DRAM(メモリ)にコピーされると、実行バイナリはゲームの起動に必要なメモリを確保します。これは実行バイナリをコピーした分とは別にメモリを消費します。メモリ上に保存されるのは所持金データ、プレイヤーの HP や攻撃力などのデータ、など様々なものがあります。ゲームが複雑になればなるほど、確保されるメモリの量は多くなります。

キャラクターの 3D データなどのモデルデータアセットは、頻繁に利用するようであればメモリにコピーしたほうが良いですが、これらの 3D データは一つが 10MB 以上あったりするので、たくさんコピーするとメモリを大量に消費することになります。なので、スプラトゥーンの場合はステージの 3D モデルなどはメモリにコピーせず、ゲーム開始時に毎回カートリッジから読み込んでいます。

ただし、頻繁にデータを利用し、高速に動作することを目的としている動画編集ソフトなどはそれらのデータをメモリにコピーしています。これらのソフトがメモリを大量に消費するのにはそういう理由があります。

で、メモリの確保は実行バイナリが最初に動作したその時一回だけ行われます。プログラムによってはメモリの動的確保といって必要になったときだけ余計に追加するなどの処理が行われますが「プログラムがメモリを要求する」「システムにメモリが確保可能かどうかチェックする」「メモリを確保する」といった余計なオペレーティングシステムへのアクセスが発生するため、遅くなります。

スプラトゥーンのゲームの場合は「急に追加でメモリ領域が必要になった」ということがないため、起動時に全ての必要なデータがメモリ上に確保されています。

と書いたけど、インスタンスの値が NULL だったりするので、ひょっとしたら動的確保しているかもしれない

余談ですが、ニンテンドースイッチでは 64 ビットのオペレーティングシステムが利用されているため、4GB 以上のメモリが利用可能です。何故 64 ビットだと 4GB 以上使えるのか、ということも簡単に解説します。

プログラムがメモリにアクセスする際には、アドレスと呼ばれるものを利用してメモリの位置を把握します。アドレスがわかりにくければ、個人を識別するための名前と思っていただいても結構です。クラスにたくさんの生徒がいて、それぞれ名前を持っているようなものです。

この場合、1 人の生徒が 1 バイトを表す

で、先生(プログラム)が生徒を呼び出す際に名前を呼ぶわけですが、先生が 32 ビットだと生徒の名前を最大で 2^32 通りしか管理することができません。これがおよそ 43 億通りなのですが、1 通りを 1 バイトとすると約 4.3GB になります。

43 億通りだと途方も無いので、ここでは仮に先生が何らかの事情で 43 通りしか名前を記憶できないとしましょう。すると、クラスに 44 人目の生徒がいたとしても、その子の名前を呼ぶことができず、仮にその子が先生(プログラム)にとって大事なデータを持っていたとしても、その子を呼んでデータを受け取ることができないわけです。

これが、32 ビットシステムが 4GB 以上のメモリを使えない、ひどく大雑把な解説です。

4. 実行バイナリがデータを要求する#

さて、ここまででニンテンドースイッチのゲーム(スプラトゥーン)が起動して、最初に起こることを大雑把に解説しました。

このあとは、ゲームをプレイする中で、実行バイナリがどのようなことを行い、どのような操作が行われているかを考えます。例えば、ブキを買うためにカンブリアームズに入店することを考えます。このとき、どのようなデータが必要になるでしょうか?

  • 3D データ
  • 所持しているブキデータ
  • 所持金

パッと思いつくのはこの三つだと思います。3D データがなければ何も画面に表示されませんし、所持しているブキデータがなければ購入済みであるにも関わらず、再度購入することができてしまいます。そして、ブキが購入できるかどうかを判定するために所持金データも必要になります。

今回は所持金のデータを改ざんして、所持金語りていないにもかかわらず、ブキを購入できてしまうようにするにはどうすればいいか、そのためにはどのような挙動をするパッチが必要かを考えていきます。

まず、プレイヤーが行き先としてカンブリアームズを指定した時点で、実行バイナリは上に述べた「必要な三つのデータ」を取得しようとします。大切なのは「それらのデータを実行バイナリが直接持っていない」ということです。もし仮に、実行バイナリがゲームのプレイに必要なすべてのデータを持ってしまうと、実行バイナリが巨大化(数 GB にもなる!)し、ゲーム起動時の実行バイナリのメモリへのコピーが著しく遅くなってしまいます。

特に理由もない限り、パフォーマンスが同じなら実行バイナリは小さければ小さいほどよいです。

では、所持金のデータはどこにあるのかと言うと「起動時に実行バイナリが確保したメモリ」に保存されています。そして、その値を取得するための「所持金取得関数」が実行バイナリには含まれています。

つまり、カンブリアームズに入ると、実行バイナリは所持金取得関数を呼び出し、メモリに保存されている所持金の値を取得させ、その取得された値をゲーム内で実際に利用、描画するということです。

5. どこにパッチをあてるか#

ここまでで、ゲームが起動したときに何が起こり、実際にゲームを遊んでいるときにどのようなことが起こっているかを説明しました。細かくは解説していなかったのですが、ここでより詳しく述べると、

  • 実行バイナリは基本的にゲームのデータを持たない
  • ゲームで使われるデータは確保されたメモリに保存されている
  • 重いデータはメモリにコピーしないことが多い

ということです。そして、昔のゲームでは常に確保されるメモリのアドレスが同じだったのですが、最近のゲームはオペレーティングシステムがASLRと呼ばれるコンピュータセキュリティ技術を取り入れているため、仮に所持金が保存されているメモリのアドレスが X だと突き止めて、X の値を書き換えるというコードを書いても無意味です。

何故なら、ASLR により次回起動したときには所持金が保存されているアドレスは X ではなくなっているからです。

じゃあパッチをあてることは不可能なのかといえばそうではなく。所持金が保存されているアドレスは毎回違うが、実行バイナリの先頭アドレスからのオフセット(ズレ)は毎回固定である、ということを利用します。これも例えて言うなら、A さんの住所がわからなくても A さんの家族の住所がわかれば同じところに住んでいるから住所が突き止められるといった感じです。

基準となる値がわかれば、そこからの差は常に一定なので、メモリのどこに確保されているかがわかるというわけですね。

IPSwitch/Edizon#

結局 IPSwitch 方式と Edizon 方式は何が違い、どんなことができるのかを解説します。

ちなみにこの章を読むには上の前提知識の半分くらいを理解している必要があります。

書き換える場所の違い#

IPSwitch 方式は実行バイナリの関数を書き換えることが多く、Edizon 方式では確保されたメモリを書き換えることが多いです。

なお、このプログラムは以下のような動作をすることを期待されています。

実行バイナリ自体は所持金データを持たないので、所持金取得関数に頼んでメモリのデータを読み込みにいってもらい、そのデータを利用するわけです。

IPSwitch での動作#

IPSwitch で所持金を書き換える場合は二通りの実装方法があるので、どちらも簡単に紹介します。

一つは、所持金取得関数を改ざんして、メモリから読み取った値に何らかの操作を行ってから実行バイナリに値を伝えるような方法です。

上の図の場合ではメモリは 10000 というデータを保存していて、正しくその値を読み込んでいるにも関わらず、その値を 100 倍にして返しています。IPSwitch 形式は、このように本来ある値に 1 を足したり 2 倍したりして返すような操作が得意です。この方式を利用したパッチに、獲得経験値 N 倍、獲得賞金 N 倍といったようなものが考えられます。

もう一つは、読み込んだ値とは関係なく、適当な値を返すような場合です。例えばブキをアンロックするようなパッチですと「アンロックフラグを反転させる」といったもとからあるデータを操作するやり方では、ロックされているブキはアンロックされますが、逆にアンロックされているブキがロックされてしまいます。

つまり、メモリのデータが「ロックされているかされていないか」に関わらず、常に「ロックされていない」というデータを返したいわけです。この場合、メモリのデータを読み込まないので、メモリへのアクセスは不要です。

Edizon での動作#

一方、Edizon では実行バイナリによって確保されたメモリを改ざんする事が多いです。実際には Edizon はそのゲームが利用している全てのメモリにアクセスできるので、実行バイナリ自体を改ざんすることもできます。

書き換えられたメモリには好きな値を保存することができるので、本来なら 10000 という値が保存されていたとしてもそれを無視して 1000000 という値を上書きすることができます。

こうなると、実行バイナリと所持金取得関数が正しく動作したとしても所持金を 1000000 円に改ざんすることができるというわけです。

Edizon 方式は確保されたメモリを書き換えるので、書き換える前にどんな値がそもそもメモリに入っていたのかを気にしないようなコードが多く、そのため、IPSwitch 方式のように元々あった値を操作するようなコードを書くのには向いていません。とはいえ、@p1atdev 氏によれば「シンプルな計算は可能」とのことで、理論上やってやれないことはないようです。

必要なもの#

必要なものは以下の五つです。とは言っても、二つはウェブサイトなので実質三つです。

逆アセンブラ#

主に以下の三つから選ぶことが多いと思います。潤沢な資金があるなら IDA Pro を選ぶと良いでしょう。

それ以外なら GHIDRA でいいのではないかと思います。

IDA Pro#

神、でも車が買えるくらい高いです。無料版もあるのですが ARM64(aarch64)が解析できないので無意味。

GHIDRA#

UI がカスなこと以外は優秀、しかもタダ。

今回はこれを利用した解析方法について解説します。

Hopper Disassembler#

使ったことないけど、それなりに使えるらしい。貧乏人向け IDA Pro とか言われていたりする。

解析したいバイナリ#

HACGUIまたはnxdumptoolで取得するのが良いと思います。

ニンテンドースイッチの実行バイナリはexefs/mainです。

今回の解析で必要なのは 5.5.0 のバイナリだけですが、別途 3.1.0 のものを持っておくと先述したようにデバッグシンボルがあるので解析が楽に行なえます。

取得方法は割愛します。

nx2elf#

暗号化されたバイナリを復号してELF(Executable and Linkable Format)と呼ばれるファイルに変換します。

GHIDRA で実行バイナリを読み込むために必要な処理ですが、別途Ghidra-Switch-Loaderを利用することでも対応できます。

GHIDRA の設定#

GHIDRA はデフォルトではメモリを 1024MB しか使ってくれない設定になっているので、解析するバイナリによってはメモリが足りずにクラッシュしてしまいます。なのでghidraRun.batを編集して利用できる最大メモリサイズを変更しましょう。

最近のパソコンはメモリを 8GB 以上積んでいるはずなので 4GB くらい割り当てても大丈夫なはずです。

:: Ghidra launch

@echo off
setlocal

:: Maximum heap memory size
:: Default for Windows 32-bit is 768M and 64-bit is 1024M
:: Raising the value too high may cause a silent failure where
:: Ghidra fails to launch.
:: Uncomment MAXMEM setting if non-default value is needed

set MAXMEM=4096M

call "%~dp0support\launch.bat" bg Ghidra "%MAXMEM%" "" ghidra.GhidraRun %*

実行バイナリを変換する#

ダウンロードした nx2elf にmainをドラッグアンドドロップします。するとmain.elfが出力されると思います。

以上です。

ELF を読み込ませる#

現在執筆中です

誰でもできるコード開発 #01
https://fuwari.vercel.app/posts/2019/05/ipswitch01/
Author
tkgling
Published at
2019-05-01