バイナリアンに出会った話

飲み会の席で隣に座っていた人が、聞いてみるとゲームのデバッガーをやっているらしい。でも開発プロジェクトの一員という訳ではなくて、コードそのものは見られない立場にいるらしい。なんだそりゃ、と思ったが、そういえばテストプレイヤーの募集を見たことがある。そういうものなのかもしれない、と思ってそのまま話を続けることにした。

ところが彼はただのテストプレイヤーではなかった。彼はバイナリハッカーだったのである。というのも、彼はデバッグするに当たって、愚直に色々な状況を作って試すのではなく、自分でバイナリを解析するようになったらしいのだ。チートをするプレイヤーが出ないように、チートが可能になりそうな箇所を先んじて見つけることもあるという。

彼には酒とその場の勢いで色々なハックを教えてもらった。彼はプレイの最中にそのプロセスが使っているメモリの内容をダンプしたものと、逆アセンブラを主に使っているらしい。

例えば、ゲーム中で戦闘が始まり、自分が敵にダメージ10を与えたとする。するとメモリのどこかの値が10減ったりする。そうすると、メモリ上のどの位置がその時の敵のHPを意味しているかがわかる。これは基本的な戦略だ。メモリ上のどの位置が何を意味しているかわかれば、その分干渉が可能になる。

彼曰く、アマチュアハッカーはその値を直接書き換えるのだという。敵のHPを直接0にしてしまえば、次のフレームでHP判定が入って敵が倒れる。自分のHPを9999にしておけば、死ぬ心配がなくなる。私も知っている基本的なハックだ。だが彼に言わせるとこれは甘いらしく、というのも効果が一回限りだからだという。確かにそうだ。例えば次の戦闘が始まれば別の敵のHPを0にしなければならないし、自分が全回復すれば正常な値に上書きされてしまう。

ではプロのハッカーはどうするのかというと、そのメモリ領域を指すポインタを探すのだという。メモリアクセスも解析できるので、どのコードブロックからアクセスがあるかがわかる。すると、そこを逆アセンブルすれば、概ねどういう処理をしているかがわかる。例えば自分が死んでいないか判定するルーチンがあったなら、そこで自分のHPに即値9999を代入するコードに変えてしまえば、毎ターン自分のHPが9999になるのだという。

そういうことをするためには、書き換えてもバイナリのサイズが変わらないように気を使う必要がある。というのも、バイナリコードの場所は非常に重要なので、1byteでもずれると例えばjmpの先がずれたり、命令がCPUのページ境界を跨いで割り込みが発生したり、全体がクラッシュするからだ。なので、書き換えても構わないコードの存在は、それ自体がセキュリティホールになるのだ、と彼は教えてくれた。

ではどういう部分なら書き換えられるのか。彼によると、デッドコードがその筆頭で、リリースされていない新機能やDLC用の処理などは格好の餌食になるらしい。ゲームなのでそういう部分は存在してしまう。そこを書き換えて自分のコードを埋め込み、また別のどこかを書き換えてそこへ飛ばす。他にも、滅多に呼ばれないルーチン、例えば正常なプレイの範疇では起きないオーバーフローを直すためのルーチンや、特殊なイベントのための処理なども餌食になるらしい。

より面白かったのは、もっと微妙なケースだ。あるレジスタ、例えばeaxに即値0を入れたいとする。素直に書くとmov eax 0になる。だが、このeaxレジスタは4byteなので即値0も4byte整数が埋め込まれる。これはロスになると彼は言う。というのも、xor eax eaxeaxの値はクリアできるからだ。これは確かにバイナリでよく見るオペレーションである。mov eax 0のバイナリコードをxor eax eaxにすると、数バイト浮く。そのような空きを集めてjmpなりcallなりの色々な命令を埋め込んでいくらしい。酒の酔いのせいか、目が回りそうだった。

なので無駄のないサイズの小さなバイナリは、それ自体が結構セキュアになるのだ。ハッカーがコードを埋め込める場所が少ないからだ。バイナリのサイズそのものがセキュアかどうかをある程度表しているというのは予想外だったので、これは意外な発見で、面白いなと思った。

では実際jmpだけを埋め込めるようなケースがそれ単体でも役に立つことがあるかどうかは気になるところだろう。彼曰く、戦闘のルーチンで即脱出できるなら当然これはチートの役に立つ。この話を聞いて当然浮かぶ質問は、メモリリークはないのかということだ。なので質問してみた。すると彼は、その場合は「正しい場所」を探してそのアドレスへ飛ぶのだと言う。つまり、もし戦闘のためのサブルーチンにjzなどがあったなら、そこではおそらく敵のHPが0になったかどうか、つまり死亡判定をしている確率が高く、そこまでジャンプできれば戦闘が正常に終わって勝利した場合のコードまで飛べるのだ。なのでメモリ確保が走っている場合は(これは単に逆アセンブルした内容を追っていけばわかる)、cmpjzなどを探してそこまで飛べば、解放処理もするはずだ。元のコードがメモリリークしていない限りは。

ところで、チートで使われる技法にはメモリ上のどこが何を意味しているか解析できる前提のハックが多い。非常にシンプルだが効果的なチート対策として、コードではなく、変数の値の難読化があるのだという。例えば、HPに定数を足しておく。そして死亡判定処理の時だけその定数を引く。あるいは、HPを負の値にしておいて、表示する時だけ符号を変える。また、整数値でいいところをわざと浮動小数点数で持っておく。こうすると、ビットパターンがチーターの探す値のビットパターンと変わってくるので、メモリダンプの内容からそこを見つけ出すことが難しくなるのだ。逆アセンブリの結果を難読化する手法は聞き覚えがあったが、実際に働いている人から聞くとまた格別に面白い。ずっとコードを難読化するのだろうと思っていたのだが、思いの外シンプルな方法で、しかし劇的な効果があるのだという。

彼は慣れているので、バイナリのどのあたりがデータセグメントでどのあたりがコードセグメントなのか16進ダンプしただけでなんとなく見当がつくのだという。多くのゲームで使われる定数はそんなに大きな値ではない(ゲームによるがせいぜい10000程度)ので、非常に桁が大きいバイナリが続いているところはデータセグメントではなさそうに見えるらしい。ただ、テクスチャや音楽がコードされている部分はこの方法では見分けられず、コードセグメントだと勘違いしてクラッシュさせてしまったことがあると彼は語っていた。

データセグメントが見つかると、その中に定数、例えば敵のHPの上限などが入っていたりすることがある。その場合、その定数を0にしてしまうと、敵が出てきた次のフレームでは勝利するようになる。そういった書き換えも立派なチートだ。

しかし、彼はコンパイラの最適化などが謎のマジックナンバーを生成することがあるなどの話は知らなかった。私がいくつかの事例や、そもそも人間が謎のビット演算によって一部の重い処理を置き換える話などを話題に挙げたところ、彼はそのほとんどを知らなかった。彼はバイナリを解読する人間であって、コンパイラが行う最適化には詳しくないらしい。それはそれで不思議な話だ。私のこれまでの知り合いにはいなかったパターンなので、非常に話が弾んだ。彼も職場ではあまり低レイヤーの話で盛り上がることは少ないらしく(これも私にとっては非常に疑問だったが、どう言う会社なのだ?)、私との会話を楽しんでくれているように見えた。久しぶりにほぼ初対面の人間と楽しい会話ができた気がする。

ただし思い出して欲しい。最初に言った通り、私はその時飲み会に参加していたのだ。もちろん、レジスタの名前やニーモニックが飛び交う私と彼の会話が非常に弾んでいる横で、他の人々が我々を奇異な目で見ていたことは、わざわざ説明する必要もないだろう。


ところで『楽しいバイナリの歩き方』という本があり、この手のことが少し平易に解説されている。一年くらい前に読んだが、低レイヤーのことに興味を持ついいきっかけになる本だと思う。他に、『低レベルプログラミング』と言う本を今読んでいる。これは結構ガチな本で、少し時間をかけて読んでいる。『BINARY HACKS』はだいぶ前にパラパラ見て、一度読むのを諦めて塩漬けにしてしまったが、今なら楽しめそうな気がしてきた。『Hacking: 美しき策謀』は友人が持っていて、色々と勧めてくれたが、まだ読む機会を得ていない。当時の私には難しすぎた。あと次に本屋に行く機会があれば『ハッカーのたのしみ』を買おうと思っている。前に近くの中規模の本屋に行った時には置いていなかったので。