Nginx For Windows の共有メモリのエラーについて調べる

公開:2013-08-25 08:19
更新:2020-02-15 04:37
カテゴリ:windows api,nginx,windows,c++

ASLRと共有メモリについて調べる

共有メモリのエラーを調べる前に、まずはALSRと共有メモリについて調べた。

ASLRとは

ASLRとはAddress Space Layout Randomaization(アドレス空間配置のランダム化)の略である。前回ポスト でも書いたが、ランダムなアドレスにモジュールを配置することでセキュリティを高めようとする技術である。Vista以降のOSでは/DYNAMICBASEでコンパイルされたEXEやDLLで有効になる。もちろんOSのAPIモジュールもランダムに配置される。

(左図 : メモリ位置の例) (右図 : 別の起動時のメモリ位置の例):
参照元:第 13 回 Windows Vista のセキュリティ機能 ~ Address Space Layout Randomization ~

これがなぜセキュリティ向上に寄与するのか。バッファオーバーフローを利用して自分のをコードを動かそうとする不正プログラムがある(昔ブラウザのURLにプログラムをエンコードして長い文字列にして送り込み、バッファをオーバーフローさせてスタックや例外アドレスを書き換えてURLの中に仕込んだプログラムを動かそうとする不正プログラムがたくさんあった)とする。そのコード中でOSのAPIを呼び出すようになっていても、API呼び出しアドレスが毎回違っているとそのコードはAPIを呼び出すことができなくなる。でもこれはAPIのアドレスが固定であると前提した決め打ちの場合で、LoadModuleとかGetProcAddressとか使えば結局はそういうプログラムからでもAPIへのアクセスは可能なように思う。あ、でもLoadModuleとかGetProcAddressの呼び出しアドレスも変わるので、無理だった。APIコードのバイト列をキーにメモリを総当たりで検索すればAPIを見つけ出すこともできるだろうけども、手間はかかるし、API呼び出しが出来ないので64Bitの広大なアドレス空間でビジーループを回すことになり、結果時間がかかるし重くなるのでユーザーも気づくだろう。いやでもそもそもプログラムがロードされているアドレス空間って読み取りできたんだっけ..。まあ知識不足なこと。。まあいいか。。

これに/GS(バッファセキュリティオプション。スタック破壊を検出する。)やDEP(データ領域のプログラム実行防止、文字列等に仕込んだプログラムが実行できなくなる)を組み合わせれば、アプリ側は手をかけなくてもそこそこにセキュリティを確保できるので、この機能とDEPは活用したほうがよいと思う。これだけで完璧なセキュリティになるということではないが、自転車に鍵を2つつけるのと同じで、1つの鍵を開けるのはたやすくても2つあると1つよりも面倒なので盗まれにくくなるのと同じような考え方でこの機能をとらえるのが良い。

ただ互換性を維持するために、古いDLLやEXE(/DYNAMICBASEでコンパイルされていないコード)は固定的なエントリポイント(つまりリンカーで指定した開始アドレス)にロードされる。古いプログラムはアドレス配置を固定されていると想定しているものもあるらしい。そのためそういう古いアプリはVista以降のOSでも脆弱性は残ったりする。VistaのASLRの機能が弱いといわれるのはこの点かもしれない。Windows 7以降であればどんなものでもASLRが有効にすることができるらしい。

共有メモリ

ここでいう共有メモリとは、プロセス間の共有メモリである。これはプロセス間でデータのやり取りをするときに使用する。物理メモリの一部を共有して各プロセスの仮想アドレスにマッピングすることでデータの共有を行う。当然だがプロセス間でアクセス競合が起こるので、データの一貫性や原子性を保持したいのであれば排他制御が必要である。概念の簡略化して描いたのが以下の図。

Windowsでは通常CreateFileMapping APIで共有メモリを作成する。

HANDLE CreateFileMapping(
  HANDLE hFile,                       // ファイルのハンドル
  LPSECURITY_ATTRIBUTES lpAttributes, // セキュリティ
  DWORD flProtect,                    // 保護
  DWORD dwMaximumSizeHigh,            // サイズを表す上位 DWORD
  DWORD dwMaximumSizeLow,             // サイズを表す下位 DWORD
  LPCTSTR lpName                      // オブジェクト名
);

ファイルハンドルを使用できることからわかるとおり、ファイルでもデータ共有を行うことが可能である。メモリを使う場合はhFileに INVALID_HANDLE_VALUEを指定する。オブジェクト名は文字列である。nginxではおそらくkeys_zoneで指定する名前が使われるのではないかと想像する。このAPIを呼び出すと、指定したオブジェクト名がない場合は指定したメモリサイズを確保してそのハンドルを返す。オブジェクト名が存在しない場合は単にハンドルが帰ってくる。このAPIは共有メモリを使用する全プロセスから呼び出さなくてはならない。

これで物理メモリに共有メモリ(正確にいうとファイルマッピングオブジェクト)が作成されるのだが、これを実際にアクセスするにはそれぞれのプロセスの仮想アドレス空間にマッピングしなければならない。それを行うAPIがMapViewOfFileもしくはMapViewOfFileEx APIを使用する。

LPVOID MapViewOfFile(
  HANDLE hFileMappingObject,   // ファイルマッピングオブジェクトのハンドル
  DWORD dwDesiredAccess,       // アクセスモード
  DWORD dwFileOffsetHigh,      // オフセットの上位 DWORD
  DWORD dwFileOffsetLow,       // オフセットの下位 DWORD
  SIZE_T dwNumberOfBytesToMap  // マップ対象のバイト数
);

このAPIが成功すると共有メモリアドレスへのポインタが帰ってくる。このポインタを使用して共有メモリにアクセスする。MapViewOfFileExはこのアドレス(ベースアドレス)を指定できる。メモリの利用が終わったらUnmapViewOfFileを使用してアドレス空間を解放する。指定するのはMapViewOfFileで得られたポインタアドレスである。

BOOL UnmapViewOfFile(
  LPCVOID lpBaseAddress   // 開始アドレス
);

nginxでの共有メモリの使われ方

とりあえずキャッシュが動かない原因となるキーワードであるASLRと共有メモリを調べたところでソースコードをのぞいてみる。やっぱりOSに依存するコードは関数でラップしてあって、それぞれのOS用に実装ファイルがある。なので共有メモリを使われているコードはCreateFileMappingをキーに検索することで容易に探し出すことができる。共有メモリのコードはngx_shmem.hおよびngx_shmem.cである。以下のソースコードはngx_shmem.cである。

ngx_shm_allocで共有メモリの作成、ngx_shm_freeで共有メモリの解放である。コードを見てもらえばわかるけれどもまあそのままである。Win32 APIを普通に使っている。ngx_shm_tは共有メモリ管理のための構造体で、定義はngx_shmem.hにある。下記はngx-shmem.hのソースコードである。

このコードを見る限りXPであってもVista以降のASLRが有効になっていても動作に違いはないように思われる。APIのリファレンスでも動作が異なるようにことは書かれていない。

この部分だけ見てもよくわからないので、ログでエラーになっているキーワード「has no equal addresses」で検索をかけてみる。そうするとnginx_cycle.cの中にあるngx_init_zone_pool関数のコード中でエラーログ出力されていることがわかった。このエラーログを吐き出すのはこの1か所のみである。

ngx_init_zone_pool関数はngx_shm_zone_t znを引数にとっている。そしてこのzn->shm.addrで共有メモリの先頭アドレスを取り出し、ngx_slab_pool_t にキャストしてspに格納している。 もし共有メモリが他のプロセス等ですでに作成している場合(zn->shm.existsが1の場合)、このspの値そのものと、sp->addr(他のプロセスが作成した共有メモリのマッピング仮想アドレス)を比較して違う場合はno equal addressesログを出力してエラーで終了する。後続のコードを見ると、もし共有メモリを今回新規に作成した場合はsp->addrに今回作成した共有メモリアドレスの仮想アドレスが入るようになっている。

他のプロセスでもマッピングされるビューの仮想アドレスが同じで、そうでない場合はERRORと想定していることはASLRが効いている環境下では問題である。なぜならばASLR配下ではプロセスごとにロードされるプログラムのアドレスは異なるし、共有メモリの物理アドレスは各プロセス間で同じだけれども得られる仮想アドレスは異なる。なのでほとんどの場合エラーで終了してしまうだろうから。

ただMapViewOfFileExにはlpBaseAddressパラメータがある。これは呼び出し側プロセスのアドレス空間でマッピングを開始するメモリアドレスへのポインタを指定するというものである。これを使うとプロセス間の共有メモリのアドレスを合わせることができるかもしれない。まあだけど仮想アドレスが異なるという前提で回避方法を考えたほうが根本的でいいかもね。どうするかは今後考えよう。その前にWindows上でビルドできるようにしないと。。