* かなりセキュアなポインタ -(by [[K]], 2018.08.14) ** (0) -Essenにはセキュアなポインタを実装しようと思っています。しかしセキュリティキャンプまでに実装できる自信がなくなったので、ここにアルゴリズムを詳しく書いておこうと思います。本当は、実際に実装して試せるようにしたかったのですが・・・。 -このポインタの仕組みは OSECPU-VM rev.2 で実装したことがあり、実際にテストもしたことがあるので、アルゴリズムの完成度は高いと思われます。 -[余談] OSECPU-VM rev.2 とは、セキュリティキャンプ2014の教材で、x86でもARMでもMIPSでもない理想のCPUの命令セットを妄想して、そのVM(仮想マシン)を作ったのですが、そのVMのことです。極めて高密度な機械語を書くことができます。 ~ -ここの話題は、[[page0004]]の(5)の「ポインタ関係」に対応しています。 -ポインタ関係 --ポインタは内部でアクセス可能範囲を持っていて、それをはみ出したらすぐに教えてくれる。 --freeしてしまった領域にアクセスしたら教えてくれる。 --freeしたのにまたfreeしようとしたら教えてくれる。 --mallocするときに、どのスコープを抜けるときまでには解放する予定なのかを指定することを標準とする。これで解放し忘れをなくすことができるはずだ。 ・ポインタ関係 ・ポインタは内部でアクセス可能範囲を持っていて、それをはみ出したらすぐに教えてくれる。 ・freeしてしまった領域にアクセスしたら教えてくれる。 ・freeしたのにまたfreeしようとしたら教えてくれる。 ・mallocするときに、どのスコープを抜けるときまでには解放する予定なのかを指定することを標準とする。これで解放し忘れをなくすことができるはずだ。 -このうちの最初の3つを実現するためのアルゴリズムです。 ** (1) -Essenでは32bitアーキテクチャを採用していますが、ポインタは32bitではなく224bitです。 -すごい量ですよね。単純に代入するだけでも28バイトの転送が必要で、これはかなりのオーバーヘッドです。でもこれだけあれば非常に固く守れるので、私はメリットを考えれば、これはアリだと思っています。 -Essenのポインタの中身 |フィールド名|ビット数|説明|h |ptr|32bit|普通の生ポインタ| |typ|32bit|型情報| |p0|32bit|アクセス可能なポインタの下限| |p1|32bit|アクセス可能なポインタの上限| |blk|32bit|メモリブロック情報構造体へのポインタ(下記参照)| |sig|32bit|シグネチャのコピー| |flg|32bit|フラグ(write権限があるかどうか、など)| -メモリブロック情報構造体の中身(これはグローバル領域にある) |フィールド名|ビット数|説明|h |typ|32bit|型情報| |sig|32bit|シグネチャ| |p0|32bit|メモリ領域の開始アドレス| |p1|32bit|メモリ領域の終了アドレス| |flg|32bit|フラグ(mallocで確保した領域かどうか、など)| ** (2) -[1] mallocしたり、配列のアドレスをポインタ変数に代入したりすると、224ビットのすべてのフィールドがセットされます。 --ptr: 普通のポインタなのでこれは難しくないです。 --typ: intのポインタなのか、charのポインタなのか、doubleのポインタなのか、そういうのを区別するための情報です。 --p0,p1: mallocした場合は、領域の上限と下限がセットされます。配列のポインタであれば、配列の先頭から末尾までアクセスできるように上限と下限がセットされます。 --blk: 配列にせよ、malloc時にせよ、とにかくメモリ領域を確保した時は、メモリブロック情報構造体にもメモリ領域に関する情報を登録しておきます。ここにシグネチャ情報がありますが、それはメモリ解放時に乱数で決めます(確保時ではない)。・・・で、その構造体へのポインタをポインタ変数のblkに書いておきます。 --sig: メモリブロック情報構造体のsigの内容をコピーしておきます。 --flg: writeアクセスを許すかどうかを反映させます。 -[2] ポインタ変数からポインタ変数に代入した場合、224ビットの情報を単純にコピーします。intにキャストして代入した場合は、ptrの部分だけが取り出されます。またintからキャストして代入した場合は、ptrの部分だけが上書きされます。 -[3] ポインタ変数に整数を加算したり減算したりしたときは、ptrの部分にだけC言語のルールで加算や減算をします。 -[4] つまり、プログラムはptr以外の部分の自由に書き換えることはできません。それどころか、参照することもできません。 -[5] ポインタを使ってメモリアクセスをするとき、以下のチェックをします。 --[5-1] writeアクセスの場合、flgを確認して、そもそも書き込みが許されているかどうかを確認します。ダメならセキュリティ例外です。 --[5-2] blkの値を使ってsigの値を読み取って、ポインタ変数内のsigと値が一致しているかどうかをチェックします。不一致なら、その領域はすでにfreeされてしまったということなので、もはやそのポインタは有効ではありません。そんなポインタを使ってアクセスするのはいけないことなので、セキュリティ例外にします。 --[5-3] p0,p1を使って、アクセス範囲が適切かどうかをチェックします。領域外であればセキュリティ例外にします。これでバッファオーバーランを検出できます。 --[5-4] typの情報がアクセスしたい型と一致しているかをチェックします。つまりintに対してcharやfloatでアクセスするのは基本的に許しません。またアラインが正しいかもチェックします。 --これらをすべてクリアして、やっとアクセスができます。チェックが多いのでメモリアクセスは遅くなりますが、これはしょうがないです。 -[6] メモリをfreeするときには、以下のチェックをします。 --[6-1] 上記の[5-2]と同じ方法でsigを確認します。これをやればdouble-freeを検出できます。・・・double-freeとはfreeした後にもう一度freeしてしまうバグで、何の対策もされていないシステムでこれをやるとmallocのチェーンが壊れて盛大におかしくなります。 --[6-2] メモリブロック情報構造体の中のflgをみて、そのメモリ領域がそもそもmallocで確保された領域なのかどうかを確認します。スタック上の変数であればfreeするべきではないので。 -[7] プログラムに謎のメモリリークがある場合、メモリブロック情報構造体を全部調べれば、freeし忘れている領域を効率よく探せるでしょう。 ** (3) -[Q] なぜメモリアクセスするまでp0,p1のチェックをしないのか?加算・減算したときにチェックできれば、不正な値になった瞬間で捕まえられるのだから、デバッグ支援のためにはその方がいいのではないか? -[A] 以下のようなプログラムは普通にあり得ると思うのだけど、これを許すためには加算時にチェックするわけにはいかないのです。 for (p = &a[0]; p < &a[10]; p++) { .... } -このプログラムでは、a[10]が実在しない要素で、だから p == &a[10] のときはアクセスしたらいけないのですが、p++しているうちに p == &a[10] になることはあって、でもその時にメモリアクセスがあるわけではないので、「アルゴリズムは問題ない」にもかかわらず、正常に実行できないということになってしまうのです。 -[Q] [5-4]でtypのチェックをするのはなぜか? -[A] このチェックすることで、データの内部表現の方法に依存しないプログラムになります。つまりエンディアンが異なるCPUで実行しても同一の結果になります。エンディアン依存は脆弱性ではないので、エンディアンに依存するプログラムでも構わないという考え方もありますが、私はエンディアンに依存しないプログラムが好きなので(その方が移植性が高いから)、これを採用しています。 -[Q] p0,p1はメモリブロック管理構造体の中にあるのだから、ポインタ変数の中に入れておかなくてもいいのでは? -[A] 確かにその通りです。でもそうしてしまうと、このポインタは確保した領域全部じゃなくて、その一部のみをアクセス許可したいのだけどな・・・と思ったときに、そういう制約をかけられないことになります。 -[Q] ptr以外の値を見せないのはなぜか? -[A] ptr以外の値は、システムがセキュリティ例外を検出するためだけに使います。それ以外の目的では使いません。ユーザプログラムから値を参照できればいろいろと便利なことがあることはわかりますが(たとえばアクセス可能な範囲を教えてもらえる)、そうするともうポインタ変数を普通の32bitに切り替えられなくなります。・・・もしポインタ変数を32bitにいつでも切り替えられるようにしておけば、デバッグが済んで「もうセキュリティ例外を起こすことはない」と確信できた時点で、セキュリティチェックをしないモードで高速に実行することができます。 -[Q] freeしたときにsigを乱数で書き換えるのはなぜか?インクリメントすれば十分なのではないか? -[A] インクリメントの場合、malloc-freeを0xffffffff回繰り返した後で、他のライブラリにmallocさせれば、free-after-use脆弱性を引き起こすことができてしまいます(検出できなくなる)。まあこれはバグが発見できなくなるというよりは、悪意あるプログラムを書けば攻撃できるというたぐいのものなので、この脆弱性を阻止する必要はないかもしれないですが、阻止できるのであればそれに越したことはないと思うので、ここでは乱数をお勧めします。 -まあインクリメントでも悪くはないかなとは思っていますが。 -[Q] sigを使ってポインタが生きているかどうかチェックするのは、ちょっとコストが高いのではないか?free以降のアクセスを禁止したいだけなら、free時にポインタ変数をNULLにするとかで十分なのでは? -[A] freeしたときに、その領域を指しているポインタを低負荷ですべてNULLにできるのであれば、確かにその方が優れています。・・・free(p);のときにNULLにすべきものはpだけなら話はとても簡単ですが、pと同じ値のものがどこかにあるかもしれません。それをすべて探してつぶすのはとても大変です。・・・sig方式なら、そうやって探し回る必要はなく、それでもきちんとアクセス違反を検出できます。 * こめんと欄 #comment