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

コードの自作の目的と意味

IPSwitchでいろいろなコードを試している人はたくさんいるように思います。

中にはコードを使うだけで楽しい人もいるかも知れませんが、「自分でもコードを見つけたい」と思っている人もいるかも知れません。

というのは、コードを見つければそのコードはくだらないものであったとしても自身の名前が残る可能性があるからです。

ぼくが見つけたのは金イクラドロップ数変更とサーモンランでのスペシャル使用回数変更のコードなので、たまに動画の紹介で名前が載っていることがありますね。

ぼくより先にコード見つけた人がいるかも知れませんが、こういうのは公開しないと名前が載りません。

実際のところコードはチーム内以外では非公開だったので「なんで勝手に動画化されてるん?」っていう気はしないでもないですが、ちゃんとクレジットがついていてコード自体は公開されていないことを考えると最低限のモラルを持っている人は多いように思います。

自作しやすいコード

コードの中にも自作しやすいものとそうでないものが存在します。

最も簡単なのは実行ファイル内で定義されている値を変更するもので、これはとてつもなく簡単です。

基本的には必要なパラメータが記述されているアドレスを探すだけの作業になります。

当然、「実行ファイルのどこにそんな定数が載っているのか」という話になるのですが、 “ELF String Table” という箇所があるのでそこを見れば簡単に見つけることができます。

どのあたりにそれらのテーブルがあるかは下のテーブルに載せておくので参考にしてください。

VerELF(IDA)NSO(IDA)
3.1.037106C471037106C4
4.7.023C57E471023C57E4

“Shooter_Normal_H” でテキスト検索をすれば割りと早く見つけられると思います。

これらは文字列なのでソースコードのコメントが削除されたVer3.1.0以降のNSOであっても比較的読みやすいはずです。

ここから自分がいじりたいと思うパラメータを検索する必要があります。

パラメータ名はどこかで見たことがあるものばかりだと思うので、見つけること自体は簡単だと思います。

以下にパラメータ名の一例を挙げておきます。

文字列意味
Coopサーモンラン
SpecialCostスペシャル必要量
InkConsumeインク消費量
Playerイカちゃん
Npc_NPC
Stateメッセージなど

とにかくパラメータは多すぎるので、全部書ききることはできません。

値を0にするコード

先程、最も書きやすいコードは実行ファイル内で定義されている変数を変更するものだと述べました。

それらのうち、その値を0にするコードは最も書きやすいです。

これを100や1000にするのは意外と難しいのです。

例えば、スペシャル必要量を0にするコードを書いてみましょう。

GHIDRAでELFを分析する場合、メモリ使用量がデフォルトだと1024MBしか使えないためオーバーフローしてしまいます。

4096MBを指定すれば最後まで実行できたので、大きめに設定してください。

ghidraRun.bat

set MAXMEM=4096M

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

GHIDRAを起動させるためのバッチファイル内で最大メモリ使用量が指定できるので、変更しましょう。

SpecialCos

アドレスは7103711e8aで、その右を見るとXREF[2]の記述から7100038e98でこの値が使われていることがわかります。

ダブルクリックするとこのアドレスにとぶことができるので、実際にそのサブルーチンを見てみましょう。

サブルーチンというのは、関数(メソッド)内で動く関数みたいなイメージです。

サブルーチンの中身

ここのサブルーチンの上五行の命令を覚えておきましょう。

オフセット

逆アセンブラの種類と解析するファイルの種類によってELFをIDAで解析したときに比べてアドレスがズレるのでその値を覚えて置かなければいけません。

逆アセンブラアドレスオフセット
IDA Pro (ELF)847A00
IDA Pro (NSO)71000847A0+0x710000000
GHIDRA (ELF)1847A0+0x000100000
GHIDRA (NSO)7100038E8C

これを見るとELFをGHIDRAで逆アセンブルしたときはきれいにアドレスがズレるのですが、NSOを直接解析するとオフセットがおかしな値になってしまうことがわかります。

ここで上手い解決法が見つからなかったので、アドレスのズレを上手く固定できる設定を見つけた方は教えてください。

以後、記事ではIDA ProでELFを解析したときのアドレス(IPSwitchで直接指定できるアドレス)に変換したものを記述します。

自身の環境で同じサブルーチンを参照したい場合は、ここのオフセット値を加えたものを使用してください。

アセンブラを理解する

loc_847A0
LDR             X1, [SP,#0x6C0+var_660]
ADRP            X2, #aSpecialcost@PAGE ; "SpecialCost"
SUB             X0, X29, #-var_C8
ADD             X2, X2, #aSpecialcost@PAGEOFF ; "SpecialCost"
STR             X19, [SP,#0x6C0+var_468]
BL              sub_19A32AC

この五行がスペシャル必要量を設定しているサブルーチンになります。

で、そのサブルーチンを実行したときのレジスタの動きはだいたいこんな感じ(自信がないので間違ってたら指摘ください)

で、細かいことはさておいて最終的にはX1レジスタに入っているアドレスが指し示すメモリの値がSpecialCostとして使われます。

サブルーチン19A32AC

レジスタはこのあとも何度も更新されてしまうので、ここの値を変えることに意味はありません。

では、メモリのアドレスXXXのデータが持つ値を変更するにはどのようなアセンブラを書けば良いでしょうか?

もちろん、直接メモリに値を代入するアセンブラが書ければよいのですが、それはできません。

何故なら、メモリに値を代入するためには必ずレジスタからコピーしなければいけないからです。

つまり、データのコピー先としてメモリのアドレスを直接指定することは不可能です。

ではどうすればいいのかというと、メモリのアドレスを保持しているレジスタを利用するのです。

上の例の場合は、X1レジスタが参照先のメモリのアドレスを保持しているのでこれが利用できます。

MOV             X20, #0
STR             X20, [X1]

例えばこのように書けばアドレスXXXに保存されているデータの値を0に上書きすることができます。

MOV X20, X1と書いてしまうと、参照したいメモリのアドレス自体が0に更新されてしまって、データを正しく読み込むことができなくなってしまいます。

ただし、これでは二行も使ってしまうのでできれば一行で済ませたいところです。

そこで、読み込むと必ず0を返すゼロレジスタを利用します。

STR             WZR, [X1]

こう書けばX1の値を0にするコードが一行で書けてしまいます。

コードの実装

X1レジスタが指し示すアドレスの値を0にするアセンブラは “STR WZR, [X1]” と書くことができました。

そして、本来こういった動作はサブルーチン19A32ACで行われます。

つまり、サブルーチンに入らずに単にX1レジスタの値を変更してしまえば期待通りの動作をするはずです。

よって、サブルーチン19A32ACに分岐するための命令 “BL sub_19A32AC” 自体を書き換えてしまいましょう。

// Before
LDR             X1, [SP,#0x6C0+var_660]
ADRP            X2, #aSpecialcost@PAGE ; "SpecialCost"
SUB             X0, X29, #-var_C8
ADD             X2, X2, #aSpecialcost@PAGEOFF ; "SpecialCost"
STR             X19, [SP,#0x6C0+var_468]
BL              sub_19A32AC

// After
LDR             X1, [SP,#0x6C0+var_660]
ADRP            X2, #aSpecialcost@PAGE ; "SpecialCost"
SUB             X0, X29, #-var_C8
ADD             X2, X2, #aSpecialcost@PAGEOFF ; "SpecialCost"
STR             X19, [SP,#0x6C0+var_468]
STR             WZR, [X1]

“BL sub_19A32AC” の命令が書かれているアドレスは “847B4” ですので、これを “STR WZR, [X1]” という命令で上書きします。

これはこのままではアセンブラですので、ARM64が解釈できる機械語に治す必要があります。

以下のサイトで簡単に変換できるので試してみましょう。

ARM HEX

このとき出力されるARM HEXの値をコピーします。

これがARM64が解釈できる “STR WZR, [X1]” という命令になります。

// Special Cost 0 [tkgling]
@disabled
000847B4 340000F9

最終的に、このようなコードが得られたら正解です。

全てのスペシャルポイントが0

応用

実は今のサブルーチンは各ブキのパラメータが載っているbprmファイルを読み込んでその値を読み取るという関数でした。

ブキのパラメータはアップデータでコロコロと変わるので、実行ファイルに書くよりもパラメータ名だけ定義しておいて外部ファイルから読み込んだほうが合理的というわけです。

そして、今回のサブルーチンと全く同じ構造を持ったサブルーチンは多数あります。

LDR             X1, [SP,#0xXXX]
ADRP            X2, #XYZ ; Parameter
SUB             X0, X29, #-var_C8
ADD             X2, X2, #XYZ ; Parameter
STR             X19, [SP,#0xXXX]
BL              sub_ABCDEFG

つまり、こういうサブルーチンであるなら、BLの命令を上書きしてパラメータの値を0にすることは簡単だということです。

基本的にはX1レジスタが指し示す値を変更すれば反映されるはずです。

最後に

IPSwitchのコード開発第一回は少々長くなりましたが、パラメータ名が実行ファイル内で定義されている値を0に変更するコードの書き方を学習しました。

次回はある特定の値に変更する方法を学びたいと思います。

記事は以上。