_ また勝手訳。だって誰もこの件を取りあげてくれないんだもん。適当なので原文にあたってください。原文は色もついてて綺麗だし。
heartbleed vs malloc.conf
約二年前に OpenSSL は新機能を導入しましたが、それは昨日まで皆さんが使ったことも聞いたこともなかったものでした。そう、プロセス内のメモリを読み出せるようにしてしまうバグが発見されるまでは。
heartbleed
そのバグ heartbleed のメインサイトには大量の情報がありますが、バグそのものの詳細な説明はありません。そちらは Diagnosis of the OpenSSL Heartbleed Bug をご覧ください。 ここにも参考のため擬似的なショートバージョンを載せておきます。
struct { unsigned short len; char payload[]; } *packet; packet = malloc(amt); read(s, packet, amt); buffer = malloc(packet->len); memcpy(buffer, packet->payload, packet->len); write(s, buffer, packet->len);packet はヒープのどこかにいて、我々はそこからユーザ指定の量だけ応答バッファにコピーしようとしています。そのユーザ指定のデータ長は 16 ビットだけですから安全ですし、書き込み先のバッファも常に確保されています。危険なのは、入力バッファが足りないかもしれないということです。バッファ以上に読み出せば、応答の中に本来あるはずのない情報が入ってしまいます。packet にいま雇われているメモリの前回の内容が interesting だったなら、応答の中身も interesting になってしまいます。
緩和方法
前回の内容が interesting でなければどうでしょう。たとえば再利用する前に malloc がメモリ内容を上書きしてくれていたら? まさに malloc.conf が J オプションで行なうとおりにです。そのオプションを付けていれば、攻撃しても 0xd0 を羅列したバッファしか得られませんから、明らかに uninteresting です。が、しかし……
物事には「しかし」がつきものです。libsslが OPENSSL_NO_BUF_FREELISTS オプション付きでコンパイルされていなければ (当時の OpenBSD では付いてなかった)、libssl は自前の freelist を管理し、malloc のあらゆる緩和策を無効にしてしまいます。そう、OpenSSL には自前の、攻撃緩和策の緩和策が組み込まれているのです。もちろん個人的にそのオプション付きで libssl をコンパイルすることはできます。が、しかし……
内部 freelist なしで libssl をビルドしてみたところ、それにリンクした nginx は、断続的かつ不規則に接続を拒否します。Firefox (nss) も ftp (libssl) もこのようなエラーを報告します:
1007048992:error:1409442E:SSL routines:SSL3_READ_BYTES:tlsv1 alert protocol version:libssl/src/ssl/s3_pkt.c:1255:SSL alert number 70ここで私はギブアップしました。 (そして……何が起こっているのかをあとで解明しました。)
入力パケットバッファの範囲を超えてデータをコピーしてしまうのは、防護ページを置く方法でも阻止できるはずです。マップされていないページを叩く → 即ドカーン。となるはずですが、まだドカーンを引き起こせていません; ということはlibssl のバッファはいつも十分に大きいように見えます。そう仮定するなら、読み出すメモリは必ず、以前に free されたメモリで、かつ利用中ではないメモリということになります。もちろん、free 済みメモリに大量の interesting なデータ (http ヘッダやフォームデータなど) があることは、お手軽なテストでも簡単にわかります。(libssl の入力パケットバッファが正確にどれほどの大きさなのかを確認するのは、やれば冥府の底より深くもぐることになるのでイヤです。挑戦者は心してかかるように。)
Posted 2014-04-08 18:36:16 by tedu Updated: 2014-04-10 13:52:22
openssl 内 freelist 再利用の分析
二日ほど前、Heartbleed を緩和する方法を探して OpenSSL をつついて回っていたところ、まずデフォルト設定の中に攻撃緩和策ブレイカが入っていることに気づきました。さらに、その邪魔なオプションを無効にしたところ OpenSSL は機能を完全に停止しました。とてもひどい状況ですが、そのときはもう我慢の限界でしたので、昨晩になって犯行現場に戻りました。
freelist
OpenSSL は接続バッファに独自の freelist を使っています。これは遠い昔、はるか彼方の malloc 速度が遅かったことに由来します。ユーザに自分でもっと良い malloc を探せと言うかわりに、OpenSSL は手作りの LIFO freelist を取り入れました。もうわかりますね。OpenSSL はその LIFO freelist を誤用しています。じつは、いまから説明するバグが存在し、また気づかれずにいる原因は、まさに freelist が LIFO であるというそのことにあるのです。
OpenSSL は接続からのデータを一時バッファに読み出しますが、バッファは必要に応じて新規に取得します。ソースは ssl/s3_pkt.c の関数 ssl3_read_n と ssl3_read_bytes を参照のこと。その際 record バッファ s->s3->rrec と read バッファ s->s3->rbuf の違いで混乱しないよう気をつけましょう。バッファの setup および release 関数本体は ssl/s3_both.c にあります。
1059 行目では、ヘッダを読み終わって ssl3_release_read_buffer を呼んでいます。これでバッファを free することになります。
if (type == rr->type) /* SSL3_RT_APPLICATION_DATA or SSL3_RT_HANDSHAKE */ { [...] if (!peek) { rr->length-=n; rr->off+=n; if (rr->length == 0) { s->rstate=SSL_ST_READ_HEADER; rr->off=0; if (s->mode & SSL_MODE_RELEASE_BUFFERS) ssl3_release_read_buffer(s); } }ちょっとした問題がひとつあります。このバッファはまだ使い終わっておらず、あとで読みたいデータが中に残っているのです。さいわい、LIFO freelist に言えばすぐ返してもらえるのですから、ほんの小さな問題ですね! 数ミリ秒もない一瞬だけ freelist で休んだら、すぐ ssl3_read_n が呼ばれて、そこから setup が呼ばれ、先程のところから続行です。同じバッファの、同じ中身で。
rb = &(s->s3->rbuf); if (rb->buf == NULL) if (!ssl3_setup_read_buffer(s)) return -1; left = rb->left;ただし、もちろんこれは freelist がない場合や release が本当にバッファをリリースする場合(!)を除いての話です。つまり OPENSSL_NO_BUF_FREELIST を付けてコンパイルすると動きません。最初のバッファは永遠に失われてしまい、まったく別のバッファを読み出し始めることになります。この新しいバッファが、前のバッファと同じデータを持っていることはあまり起きそうにありません。OpenSSL は、思っていたのと違うデータが来て混乱し、接続を強制終了するというわけです。
(謎: rb->left の値はなーんだ)
パッチ
解決策はとても簡単です。使い終わっていないならリリースしないことです。この毛玉を梳く方法はおそらく多数あるでしょう; そのひとつがこちらです:
diff -u -p -r1.20 s3_pkt.c --- s3_pkt.c 27 Feb 2014 21:04:57 -0000 1.20 +++ s3_pkt.c 10 Apr 2014 03:31:18 -0000 @@ -1054,8 +1054,6 @@ start: { s->rstate=SSL_ST_READ_HEADER; rr->off=0; - if (s->mode & SSL_MODE_RELEASE_BUFFERS) - ssl3_release_read_buffer(s); } } return(n);分析
もし OpenSSL 開発陣が面倒がらずにふつうの malloc (セキュリティ特化型 malloc でなくても、毎回ちゃんとメモリを free するもの) でテストしていたなら、このバグはまったく明白だったことでしょう。事実はその反対、私が心臓出血加速式アロケータをオフにする方法を探そうとするまで、何年も目覚めることなく横たわり続けたのです。
攻撃緩和策を構築するのは簡単なことではありません。それが難しいのは攻撃者たちが容赦なく狡猾だからです。それがイラッとするのは、攻撃を受けていないときでさえ正しく動作しない駄目ソフトがあまりにたくさんあって、多くの緩和策を完全には有効にできないからです。しかしセキュリティに関わる重要ソフトウェアの開発者が、世界で最も攻撃しやすいアロケーション手段を使ってこうした努力を妨害しておきながら、それを無効にできるかどうかのテストすらしないでいるときには、マジおこぷんぷん丸ですよ。
Update: ここにぶち当たったのは私が最初ではありませんでした。4年たつバグレポート があります。あともうひとつも。Piotr 教えてくれてありがとう!
Posted 2014-04-10 13:04:41 by tedu Updated: 2014-04-10 19:05:28
だから問題は、まず malloc に手をつけようと思った時点で「これは(セキュリティ的に)危険な作業だ」と思わなかったこと。freelist デザインの時点で「これって何かあったら危険なヤツだ」と思わなかったこと。freelist 無効での動作をテストしなかったこと。捨てたはずのメモリを freelist から拾ってこようと思った時点で「これ変だ」と思わなかったこと。この組み合わせだと思います。