Monday, June 10, 2013

/3GBスイッチによってカーネルアドレス空間で起こる事

Kernel address space consequences of the /3GB switch@The Old New Thing の翻訳

/3GBスイッチによる不幸な結果のひとつは、それがカーネルにずっと小さな空間で動作することを強いる事である。

制限されたアドレス空間の最大の被害者がのひとつがビデオドライバーである。ビデオカード上のメモリーを管理するために、ドライバーはそれにアドレスを与えられなければならず、その要求範囲は典型的にかなり大きい。ビデオドライバーが256MBの大きさを要求したとき、その要求は、おそらく、単にこのような大きなアドレス空間を与えることができないために失敗する。

すべてのカーネルのデータは1ギガバイトの中に収まる必要がある。ページテーブル、ページディレクトリ、ビットマップ、ビデオドライバーメモリー。これは非常に厳しい押し込みだが、もしあなたが削減したいのであれば(例えば、このような大きなビデオメモリーを求めないことによって)なんとか達成できるだろう。(後の記事で、減らされたアドレス空間の、別の被害者について議論する)

これは、小さなクローゼットの中で着替えをしようとするようなものだ。それは可能だが大変な苦労だ。あなたは犠牲を払わなければならないし、その結果は、いつもとても素晴らしいというわけではない。

Sunday, June 9, 2013

カーネルハンドルは参照カウントされていない

Kernel handles are not reference-counted@The Old New Thing の翻訳。

ここにしばらく前の質問がある。

私のコードに、同じハンドルで(DeviceIoControl関数を通して)デバイスと会話したい複数のオブジェクトがある。私がオブジェクトを作るときはいつも、ハンドルの参照カウントを増やすためにDuplicateHandle関数を使う。これにより、各オブジェクトがCloseHandle関数を呼んでも、最後のオブジェクトの時だけ実際にハンドルが閉じられる。ところが実際にコードを走らせてみると、最初のオブジェクトがCloseHandle関数を呼ぶや否や、このハンドルはもはや有効ではなく、誰も使用する事が出来なくなる。これを機能させるために、どのようなフラグをCreateFile関数に渡す必要があるのか?

言い換えると、コードはこのようになっている:

// hは新しいCFredオブジェクトによって共有したいハンドル

CFred *MakeFred(HANDLE h)
{
 //  "参照カウントを増やすためにハンドルを複製する"
 //  このコードは正しくない - 以下の議論を見よ
 //  すべてのエラーチェックは説明目的のために削除した
 HANDLE hDup;
 DuplicateHandle(GetCurrentProcess(), h,
                 GetCurrentProcess(), &hDup,
                 0, FALSE, DUPLICATE_SAME_ACCESS);
 return new CFred(h);
}

カーネルハンドルは参照カウントされていない。CloseHandle関数を呼ぶと、それはハンドルを閉じる。物語はこれで終わりだ。

もとの問題の記述から、CFredオブジェクトはそれが削除されたときハンドルを閉じる事が読み取れる。純粋に議論のために、呼び出し元はこのようになっているとしよう:

CFred *pfred1 = MakeFred(h);
CFred *pfred2 = MakeFred(h);
delete pfred1;
delete pfred2;

このコードを実行したとき、実際には何が起きるのだろうか?

最初にMakeFred関数を呼ぶと、私たちはもともとのハンドルhを受け取り、それを複製するが、私たちはもとのハンドルをCFredコンストラクターに渡し、hDupはリークする。もとの投稿者は、ハンドルの複製は、単にハンドルの想像上の参照カウントをインクリメントするものだ、と想定した。つまりh == hDupであると(加えてこれは、もとの投稿者を、そもそもなぜlpTargetHandleパラメーターがあるんだ、と不思議に思わせただろう)。

pfred1が削除されたとき、それはそのハンドルhを閉じる。これはhを閉じ、それを無効にし、CreateFileや他のハンドルを作成する操作によって再利用される事を可能にする。

pfred2が削除されたとき、それもそのハンドルhを閉じる。これは既に閉じられたハンドルを閉じている。エラーだ。もし私たちが敢えてこのハンドルを使用するpfred2のメソッドを呼んだなら、そのハンドルがもはや有効ではないために、それらの操作は同様に失敗するだろう(あー、もし私たちが幸運ならエラーになるだろう。もし私たちが不幸なら、そのハンドルは再利用されていて、私たちは誰かのハンドルでDeviceIoControlを行う結果になるだろう)。

ところで、pfred1が削除されるとき、それはhを閉じるので、呼び出しコードにおけるhのコピーも良くない。

私たちが本当にここでしたい事は、ハンドルを複製し、その複製物を各オブジェクトに渡す事だ。DuplicateHandle関数は、もとのハンドルと同じオブジェクトを参照する新しいハンドルを作成する。この新しいハンドルは、もとのハンドルへの影響なしに閉じる事が出来る。

// hは新しいCFredオブジェクトによって共有したいハンドル

CFred *MakeFred(HANDLE h)
{
 //  "h"と同じオブジェクトを参照する別のハンドルを作成する
 //  すべてのエラーチェックは説明目的のために削除した
 HANDLE hDup;
 DuplicateHandle(GetCurrentProcess(), h,
                 GetCurrentProcess(), &hDup,
                 0, FALSE, DUPLICATE_SAME_ACCESS);
 return new CFred(hDup);
}

この修正は青で強調された1語だ。私たちはCFredオブジェクトに複製されたハンドルを与える。これにより、オブジェクトは独自の、いつでも閉じたいときに自由に閉じる事が出来るハンドルを得、それは他の誰のハンドルにも影響しない。

あなたはDuplicateHandleを、カーネルオブジェクト用のAddRefのようなものとみなすことができる。あなたがハンドルを複製するたびに、カーネルオブジェクトの参照カウントが1つ増え、あなたは新しい参照(新しいハンドル)を得る。あなたがハンドルを閉じるたびに、カーネルオブジェクトの参照カウントが1つ減る。

要するに、ハンドルは参照カウントされたオブジェクトではない。ハンドルを閉じると、それは無くなる。ハンドルを複製すると、あなたは、もとのハンドルを閉じなければならないという義務に加えて、その複製を閉じなければならないという新しい義務を負う。複製されたハンドルはもとのハンドルと同じオブジェクトを参照し、それが参照カウントされたオブジェクトである(なお、カーネルオブジェクトは、ハンドルではないものからの参照を持つ事が出来る。例えば、実行スレッドはスレッドオブジェクトへの参照を保持する。スレッドはそれが動作し続けている限り自身への参照を保持するので、スレッドに対する最後のハンドルを閉じても、スレッドオブジェクトは破棄されない)。

なぜカーネルハンドルは常に4の倍数なのか

Why are kernel HANDLEs always a multiple of four?@The Old New Thing の翻訳。 

あまり良く知られていない事は、カーネルハンドルの下位2ビットが常に0であるという事だ。言い換えると、それらの数値は常に4の倍数である。なお、これはカーネルハンドについてだけ当てはまる。疑似ハンドルやあらゆる他のタイプのハンドル(USERハンドルやGDIハンドル、マルチメディアハンドルなど)には当てはまらない。カーネルハンドルとは、CloseHandle関数に渡す事が出来るものを指す。

この下位2ビットの用途はntdef.hヘッダーファイルの中に埋められている。

//
// ハンドルの下位2ビットはシステムからは無視され、
// アプリケーションからタグビットとして使用される。残りのビットは不透明で、
// シリアルナンバーとテーブルインデックスの格納に使用される。
//

#define OBJ_HANDLE_TAGBITS  0x00000003L

カーネルハンドルの下位2ビットが常に0である事は、GetQueuedCompletionStatus関数(完了ポートの通知を抑制するために、イベントハンドルの下位ビットに値を設定できる)によって暗示されている。これが機能するためには、下位ビットは通常0でなければならない。
この情報はほとんどの(ハンドルを不透明な値として扱うべき)アプリケーション開発者にとって有用ではない。タグビットに興味のあるのは、低レベルクラスライブラリーを実装している者達か、カーネルオプジェクトを巨大なフレームワークの中に包み込んでいる者達だ。

Wednesday, May 29, 2013

SecureZeroMemoryの目的は何か?

What's the point of SecureZeroMemory?@The Old New Thing の翻訳。

SecureZeroMemory関数は、コンパイラの最適化によって削除される事なくメモリーをゼロで埋める。しかし、その目的は何だろうか? これは本当にアプリケーションをよりセキュアにするのだろうか? 私が言いたいのは、確かにデータはスワップファイルやハイバネーションファイルに行くのだが、それらのファイルにアクセスするには管理者権限が必要で、悪意のある管理者に対して防御する事は不可能であるということだ。それに、もしメモリーがゼロで埋められるまえにスワップアウトしたならば、結局その値はスワップファイルに行くことになる。他のアプリケーションからプロセスメモリーを読まれることを防ぐのだという者もいるが、それらのアプリケーションはSecureZeroMemory関数が呼ばれる前にいつでもメモリーを読む事ができる。ならば、なにが目的なのか?

SecureZeroMemory関数は物事をセキュアにするわけではない。単に、よりセキュアにするのだ。これは程度の問題であり、絶対性の問題ではない。

もし悪意のある管理者や、あなたのメモリーを監視する他のアプリケーションがいた場合、悪意のある者は、機微な情報が生成されてからゼロで埋められるまでの間にデータを吸い出さなくてはならない。これは一般的には長い時間ではないため、SecureZeroMemory関数は攻撃者がデータを得ることを難しくする。同様に、データは、機微な情報が生成されてからゼロで埋められるまでの間にスワップアウトされなければならない。これに対して、もしあなたがSecureZeroMemory関数を呼ばなければ、機微な情報は何か別の内容で上書きされるまで保持されるため、攻撃者はその情報を探すための良い時間を得る事になる。

加えて、情報の漏洩は悪意のある操作によってではなく、あなたのアプリケーションによって行われるかもしれない。もしあなたのプログラムがクラッシュし、あなたがWindows Error Reportingプログラムにサインアップしたならば、なぜあなたのプログラムが失敗しているのかダウンロードし調査するために、クラッシュダンプファイルが生成、Microsoftにアップロードされる。アップロードの準備のために、クラッシュダンプがユーザーのハードディスクにファイルとして保存され、攻撃者は機微な情報を得るためにそのクラッシュダンプを調査できるかもしれない。機微な情報を持つメモリーをゼロで埋める事は、その情報がクラッシュダンプに含まれてしまう可能性を減らすのだ。

不注意により機微な情報が公開されうる他の場所は、未初期化バッファの使用である。もし完全に初期化されていない変数を使用するバグがあるならば、機微な情報がそこに漏れだし、偶然にネットワークに転送されたりディスクに書かれるかもしれない。使い終わった機微な情報に対してSecureZeroMemory関数を使用する事は、機微な情報が意図しないところに行ってしまうことを難しくする多層防御の手段なのだ。