メモリダンプの模様とはどのようなものなのか(入門編)

はじめに

最近バズった以下の記事について、補足のようなものを書きたくなったので書きます。

note.com

上記の記事に対して「模様って何…?」のようなコメントが散見されましたので、カーネルのメモリダンプ解析経験が数年ある筆者が、わたしの理解できる範囲でメモリの模様とはどんなものかについて書きます。なお、模様とはあくまで感覚的なものなので、上記記事で扱われているかたの定義とわたしの定義は違うかもしれませんのであしからず。また、LinuxカーネルやCPUについてのある程度の知識が必要な表現や用語が出てきますが、本記事ではそれらについての説明は割愛します。

メモリのさまざまな模様

メモリの模様とは(少なくとも私にとっては)16進バイナリの文字列の特定パターンです。ここでいうパターンとは正規表現マッチングできるようなパターンのことを指します。その中の代表的なパターンを見てみましょう。

ポインタ

linux/x86_64の仮想アドレスは、一般に最上位から数えて16ビットがすべて立った、0xffffではじまる8バイトの非常に大きな値となります。これによって「ここの8バイトは仮想アドレスのようなのでポインタ変数だな」というのがなんとなくわかるのです。

linux/x86の仮想アドレスマップについての詳細は以下の記事をごらんください。

github.com

数値

数値データはランダムなバイトになるかというとそうではありません。典型的なものはカウンタです。たとえばなんらかのイベント発生数を示す8バイトのカウンタ値があるとします。このときカウンタは0からはじまってひとつづつ増えます。このような方法でシステムが起動している間に8バイトのうちの上位ビットが埋めるのは至難の業です。したがって、0x00がいくつか続いた後にランダムなバイト列が続き、さらにそれが4バイトや8バイトなどの決まった単位で続いている場合は、それらは数値データである可能性が高いと考えられます。

文字列

バイナリの16進ダンプときに、同時にバイナリをASCII文字として表示する方法があります。0x00~0xffの中で表示可能文字として認識されるものは一部であり、かつ、人間にとって意味を持つかたちで表示可能文字が続く確率はそれほど高くないため、「このあたりは文字列が入るべき領域のようだ」というのは比較的容易にわかります*1

この判別方法には、さらに「パス名のような文字列が存在する場合はファイルシステム関連処理の可能性が高い」といったように応用できます。

フラグ

プログラミングにおいて、フラグなどの何らかの真偽値を表現するときには一つのバイトに1つのフラグに割り当てることもありますが、容量節約や判定高速化などのために1ビットで1つのon/offを表現し、かつ、そのようなビットを数バイトの変数に詰め込むという方法がよくとられます。たとえば8バイトの変数には(8*8bit=)64個のフラグを詰め込めます。このフラグ変数のどのビットがどのフラグに対応しているのか、および、どのフラグが立っている可能性が高いのか、などを知っておけば、「このパターンはこのフラグの可能性が高い」ということがわかります。

たとえばx86_64アーキテクチャのCPUにおいては、コード実行中に割り込みないし例外が発生すると、カーネルはスタックに8バイトのrflagsというレジスタを積みます。このレジスタは1ビットが1つのフラグに対応しています。詳細な説明は省略しますが、この値は典型的には0x0000000000000246、あるいはこれに少数のビットが立ったり落ちたりしているというパターンになっていることが多いです。したがって、これに近いパターンがあれば、割り込みフラグである可能性が高いといえます。

構造体

構造体はプログラマが定義した通りに、特定の型の値が特定の順番で並んでいます。したがって、この構造体の定義を頭に叩き込んでおき、かつ、これまで述べたようなパターンマッチング技術を身に着けていれば、「このパターンはこの構造体の可能性が高い」ということがわかります。

たとえばLinuxカーネルで双方向リストを表現するstruct listという構造体は、8バイトの仮想アドレス2つから構成されており、かつ、空リストは自分自身を指しています。したがって、仮想アドレスらしき値が2つ連続しており、かつ、それらが一つ目のデータと同じアドレスを指している場合は、この領域はstruct listである可能性が高いです。

模様を利用したバイナリダンプ解析

これまで述べたようなパターンの中に突然異なるパターンが出てくるようなことがあります。たとえば仮想アドレスらしき8バイトの下位4バイトに唐突に全然違うパターンが見られる、ということがあります。このような場合は当該領域が何者かの手によって破壊された可能性があると考えられます。しかも、その破壊パターンによっては、どのような処理が破壊したのかという推測ができることがあります。たとえばパス名らしき文字列でメモリが破壊されている場合は、ファイルシステム関連処理が有力な容疑者となるでしょう。

Linuxカーネルには、このアイデアを応用したデバッグ機能が組み込まれています。Linuxカーネルに所定のデバッグオプションをつけてビルドすると、カーネル内で動的メモリ管理において、未使用領域は0xdeadbeefという16進表記すると非常に読みやすいパターンで埋められます。これを利用すると、バグのある機能がこの領域を破壊した場合、破壊したビットパターンから容疑者が絞り込めるというわけです。

その先にあるもの

慣れてくると、「ここのパターンはおそらくタスク構造体、そうするとここにはリストが存在しているはずが別のパターンになっている。破壊パターンを見るにこれはstruct inodeっぽい。ということはファイルシステム関連処理が怪しい」というようなことができるようになります。

おわりに

本記事は入門編と書いたものの、今後それより詳しい続きの記事を書く予定はありません*2。なぜならわたしは門のところでうろうろしているだけで、それより先に進めないからです。その理由は次のようなものです。

  • 知っている模様の種類が限られている
  • 経験値が足りない
  • 理屈ではわかっていても脳が高速で処理できない
  • どうやら理屈では説明できない何らかのものもあるらしい

門をくぐってその先に行きたいというかたは、わたしはこれ以上お役に立てませんので、お近くの異能力者におたずねください。経験上、よく訓練されたメインフレーマーにこの手の人が多いようです。仮に秘術を答えてくれたとしても私がそうだったように何言ってるかわからなくて絶望するだけかもしれませんが…

*1:ACSII文字として表記せずに生の16進ダンプを文字列として解釈できる人もたまにいます

*2:このエントリをもう少し見やすく整形することはあるかもしれません