バトルKaitaiStruct
バトルKaitaiStruct
リプレイファイルのバイナリ解析を頑張った話
全ては黄昏酒場のために
僕は黄昏酒場が好きだ。 ほぼ全てパターンでありつつ、そのパターンがシビアなので研究のしがいもある。 確かに天井は決まっているかもしれない。 しかし、如何に自分のパターンを安定させるかという細かい技巧が試される、とても面白いゲームだと思う。
そんな黄昏酒場は何故か流行っていない。 確かにZUNが関わっているゲームではあるが東方というブランドに隠れてしまっている。 黄昏酒場が東方でないと言い張る人も出る始末である。1
結果世間はどうなっているかというと黄昏酒場以外の東方関連の外部ツールは数多く存在しているのに黄昏酒場のツールが少なくなり、結果黄昏酒場はツール類が少ないからさらに影が薄くなるという状況になっている。
自分はある程度の技術力を持つ側の人間だ。黄昏酒場を流行させるには自分から動いて黄昏酒場の環境を作る必要がある。 よって黄昏酒場のリプレイファイルの解析を行い、えるろだにステージ進行情報を表示させるような改修を行うようにし、黄昏酒場の発展に寄与しようと考えた。
KaitaiStructってなんすか
KaitaiStructはドメイン固有言語である ksy というYAML形式のファイルに解析したいバイナリの構造を書くことができるツールだ。
これを ksc というツールにかけると対応する任意の言語のバイナリ解析コードに変換することができる。
えるろだとサイレントセレナの使用状況
えるろだもKaitaiStructのksy をgitによる変更管理対象に入れており、 ksy をバックエンドのDockerコンテナイメージ作成時に ksc を使ってPythonコードに変換している。
何故えるろだがKaitaiStructを使っているかというとリプレイファイル定義をコピペさせてもらっているサイレントセレナがKaitaiStructを使用しているからだ。
サイレントセレナのKaitaiStruct使用状況は ksy と ksc による変換済コードの二重管理になっている。
何故えるろだは ksy だけの管理にしたか
この二重管理は流石にないと思った。何故なら ksy とその ksy を使って変換されたコードを管理するのは全く同じ情報のファイルを管理することになるからだ。片方だけ管理するのが情報の過不足がない。
この時『変換済Pythonコードを管理させればよいのでは』という疑問が出る。しかしこれは誤りだ。
何故なら仮に ksc に脆弱性があって出力されるPythonコードに脆弱性が含まれることが分かった場合、変換済Pythonコードという初見のコードを見ながら自分で脆弱性を治す必要がある。
であれば脆弱性対処ができている ksc を持ってきてそれを使って ksy を変換するした方が速い。よって ksy を管理しておく必要がある。
ただし ksy のみをgitリポジトリに入れるにあたって気を付けなければいけないことがある。IDEによる警告だ。
当然変換済Pythonコードがgitリポジトリにないうえでその変換済Pythonコードに依存したコードを作成することになる。
こうなると変換済Pythonコードを手元に置いておかないとIDEから『貴方が書いてるコードは依存先のコードが存在しません!』と言って怒ってくる。
このため、メンテナは各自 ksy を ksc を使ってPythonコードに変更しておく必要がある。少しだるいがここが現実的な妥協点だろう。
今わかる、サイレントセレナがKaitaiStructを採用した理由
KaitaiStruct、ゴミだろ
初見でこの存在を知った時、正直自分は『こんなややこしい管理をする必要があるKaitaiStruct、採用する必要があるか???』と思った。
冷静に考えてバイナリ解析プログラムを独自に作成する方が手っ取り早い気がする。
とは思いつつもサイレントセレナに依存している都合上えるろえだはKaitaiStructに依存しなければならないと思い、しゃーなし ksy を使った管理をしていた。
KaitaiStruct、神だろ
さて、この記事の本題は黄昏酒場のバイナリ定義を作ることだった。えるろだはサイレントセレナを通してバイナリ解析をKaitaiStructに依存しているので黄昏酒場の解析も周りに合わせてKaitaiStructを使う必要がある。
まず前提としてリプレイファイルは平文部と暗号部がある。よって平文部の ksy と暗号部の ksy を2段構えで作成する必要がある。
暗号部は使用されている定数さえわかれば簡単に復号できるが、その定数は実験したところ風神録と全く同じだった。2
その上でここからバイナリを解析していく必要がある。 要件として任意の黄昏酒場のリプレイファイルがえるろだにアップロードされる可能性があるので任意の黄昏酒場のリプレイが解析できる必要がある。 黄昏酒場のステージ進行を探すに当たってスコアさえひっこぬければいいのだが、その他東方作品を見るにステージ情報全体が構造体となっているようで、ステージのメタデータであるスコア情報等と、プレイヤーの1フレーム毎の操作情報が交互に書かれていることが予測できていた3
ここで、当然ながら黄昏酒場のゲーム自体もステージ情報を解析できる必要があるため、ステージのメタデータ内のどこかにプレイヤーの1フレーム毎の操作情報がどれだけのバイト長であるかという情報が入っていることが予測できる。 このデータの長さのデータをどこかから引き当てれば任意の黄昏酒場リプレイファイルを解析することができるようになる。
ここでキーになるのがKaitaiStructが公式提供しているウェブIDEだ。 ksy をサクッと書くとその場でその ksy に沿ったバイナリ解析を行って各バイナリ部分の情報を返してくれる。
プログラムを書いて一々デバッグするよりも容易に自分のバイナリ構造の予想が合っているか間違っているかが分かるので作業スピードが速かった。
なるほどこれは一次ソースであるサイレントセレナもKaitaiStructを使いたくなるはずだ。
これで無事に黄昏酒場のリプレイ解析が完了し、リプレイの構造が理解できた。
えるろだ適用時の事故
えるろだのユースケースと解析結果の内容
サイレントセレナはリプレイの解析方法を変えた時に全リプレイを解析し直す機構を持っているが、えるろだはその機構を持っていない。将来的に持つことも考えていない。
これは理由がある。 サイレントセレナは全世界に投稿者の真正性を保った状態で半永久的にリプレイを公開する場所であり、対してえるろだは投稿者の真正性は二の次にしつつ手軽にリプレイを一時的に公開できる場所として設計をしている。 よって昔に投稿されたリプレイはその存在価値は薄いと考えられるため、再解析される必要はない。
しかし昔を遡る人もいる。バックエンドは新しいリプレイに関して新解析定義で応答を返すが、当然遡ると再解析されていないので旧解析定義で応答を返してくる。 つまりフロントエンドは2つの解析定義の両方に対応している必要がある。 バックエンドはこのことを考慮して解析結果にパーサのバージョンを付けるような設計にした。これによってフロントエンドは解析の定義を振り分けて各々の情報をユーザに表示するようにしている。
よってフロントエンドを編集するにあたって旧解析定義を振り返る可能性があることが考えられる。よって旧解析定義はバックエンド上ではdeprecatedな内容として保存しておく必要がある。
本番適用時の事故の内容
上記理由から今まではalco.ksyという名前だったksyをalco_deprecated.ksyという名前にして同一ディレクトリ上に保管し、新しくalco.ksyとalco_userdata.ksyという暗号部/平文部のksyを作成した。
実験環境で実験したうえで本番適用すると事故が起きた。何故か上手くパースできない。
調べてみるとどうやらalco.ksyによって出力される新解析定義が旧解析定義の内容になっていた。
これは見てみると ksc は ksy のファイル名によってPythonコードファイルを出力するのではなく ksy 内の meta.id という場所の値を参照してコードファイル名を作成する。これが新 ksy と旧 ksy で被ってしまったので事故を起こしてしまった。
反省すべき内容
何故KaitaiStructはファイル名ではなく meta.id にファイル名が依存しているかを尋ねたところ、ファイル名はビルド時都合の偶然の産物であり、 meta.id を普遍的な名前であるとして管理する発想とのことで自分とは真逆の思想だということが分かった。
よって meta.id にバージョン名を記述するのがKaitaiStructの定義的には正しい。
あとそもそもdeprecatedな内容を本番環境に突っ込んで ksc がPythonコードに変換していたのも問題だ。これは本番の容量を使用しないコードのせいで無駄に圧迫してしまうことに繋がるので辞めた方がいい。どこかDocker管理されない場所にdeprecatedなファイルは移した方が良いという学びを得た。
結論
KaitaiStructは未知のファイルを解析する人にとってはとても嬉しいソフトウェアであるが、その恩恵を享受する下流のソフトウェア開発屋には扱いが難しいソフトウェアだということが分かった。 ZUNがリプレイの定義をオープンソースで提供してくれれば嬉しいかもしれないが、まぁ行われないだろう。 そう考えると東方のリプレイを扱うソフトウェア屋さんはしゃーなしKaitaiStructを使っていくことになるのだろうなと思った。
