問題は1行のコードに。
パスワードやメール内容、決済情報など、暗号化されるはずのデータが筒抜けになってしまうバグ「Heartbleed」が発見されました。カナダ政府が確定申告サービスを閉鎖するほどの緊急事態になっていますが、実際どんな仕組みのバグなんでしょうか? 米ギズのエリック・ライマー記者が、コードの核心部分についてわかりやすいメタファーで解説してくれています。
Heartbleedがどんなものかざっくりいうと、OpenSSLという、コンピュータとサーバにお互いの素性を知らせ合うためのセキュリティプロトコルのバグです。このOpenSSLのバグのために、米Yahoo!やFlickr、Imgurといったサイト(リストはここに)が数年間セキュリティ面で脆弱な状態のままになっていて、それを知っているハッカーならデータ取り放題の状態になっていたのです。
OpenSSLはオープンソースプロジェクトなので、誰でもコードの閲覧が可能です。なので、その気になればコードの意味を知らなくても、Heartbleedが生まれた場所やどこが修正されたのかも、把握できてしまうのです。
誰もが見られるコードなのに、Heartbleedが何年も発見されなかったのは驚愕に値します。2年間も気づかれないまま、経験豊富なプログラマーにすら知られていませんでした。でもいったん見つかってみると、問題は非常にクリアでシンプルです。
Heartbeatの仕組み
Heartbleedは、インターネットを暗号化するための基本的な技術であるTLSやSSLの問題ではありません。また、理論上はOpenSSLそれ自体の問題でもありません。単なるコーディング上のミスだったのです。
サーバとクライアントというふたつのコンピュータが暗号化のための握手をするとき、その準備として「Heartbeat」と呼ばれる手順が行われます(「Heartbleed(血を吹く心臓)」という怖い名前はここから来ています)。
Heartbeatは、ふたつのコンピュータがお互いの生存を確認するための手段で、これがあることで、処理の途中で問題が起きた際に進行を止めることができます。
クライアント(ユーザー)がHeartbeatをサーバ(たとえば銀行)に送ると、サーバはそれをすぐに返してきます。これで、取引の間に何か問題があったとき(たとえばどちらかのコンピュータがいきなり爆発したときなど)、Heartbeatが途切れることで相手側のコンピュータも異常に気づくことができるのです。それはカセットテープを再生しているときに、ふたつの穴が一緒に動き続けるようなもの。片方の穴が止まったときにもう片方も止まらなければ、何かしらが壊れてしまうはずですよね。
Heartbeatはシンプルな処理で、世界中で無数に行われています。でもなぜか、OpenSSLのバグ入りバージョンではそれがちゃんとできていませんでした。Sean Cassidyさんがブログexistential type crisisでその仕組みを異様なほど詳しく、美しく説明しています。全部説明すると長いんですが、実際の問題部分はたったこれだけです。
この1行だけでは、全く意味不明の文字の羅列です。技術的な話になりますが、以下にメタファーを使いながら説明を試みます。
簡単にいうと、上のコードの冒頭「memcpy」というのは、「データをコピーする」というコマンドです。そしてこのコマンドを実行するには3つの情報が必要になります。その3つが、カッコに入っている「bp,pl,payload」。このうち最初に書かれているのがコピーするデータの行き先、2番目がコピーするデータの所在地、3番目はコピー時に必要なデータ量です。つまり上のコードでは、bpがサーバの場所、plがHeartbeatデータの所在地、payloadがplのデータの大きさを表しています。
ここで大事なのは、コンピュータにデータをコピーするのは意外と厄介だということ。なぜなら「空のメモリ」ってものはないからです。つまり、データのコピー先であるbpは空っぽではなく、そこには既存のデータがいっぱい詰まっているのです。ただそのデータは「削除対象」としてマークされているので、コンピュータが「空のメモリ」として扱っているだけ。言い換えると、新しいデータが入ってくるまで行き先であるbpは上書きOKになった古いデータのかたまりみたいなものなんです。上書きしてもOKだけど、まだそこにデータは存在しています。
理想を言えば、memcpyでデータをplからbpにコピーする場合、bpにあった古いデータ全てに対し上書きするのが望ましいです。payloadでplのサイズが明示されるので、bpのスペースは全く同じサイズが用意されます。なので処理が滞りなく進めば、bpに元々あったものは全て壊れてplのデータで埋まり、同じデータがクライアント側に送り返されます。ここで言うデータとは、クライアントからサーバに送ったデータ、そのもの。つまり正しいHeartbeatは、送ったものがそのまま返ってくるという1:1のやりとりなんです。
これは良い仕組みですが、payloadが嘘である場合があるんです。たとえばpayloadがplは64KBだと言っていながら、実は0KBだったりするとまずいことになります。memcpyは64KBの空き地(実際は古いデータがある)をbpに作らせるんですが、plが0KBつまり空っぽということは、bpにある古いデータが上書きされないままになりますよね。これが実際起こると、bpに元々あった古いデータがplの代わりにクライアント側に送られてしまうんです。そのデータは無害な場合もありますが、それがオンラインバンク用パスワードであることもあります。どちらにせよ、本来起きてはならないことです。
メタファーで説明してみます
コードでの説明は難しいので、メタファーを使いますね。僕が写真の束を持っていると想像してみてください。何らかの箱を買って写真をしまおうとしているのですが、箱を売っているお店の人が数を数えられないんです。
僕はお店に写真100枚を持って行って「写真100枚持ってるんですけど」と言います。そうするとお店の人は喜んで「それなら良いものがあります! 写真が100枚入ってる箱です。この箱は誰かが写真いっぱい入れて置いてったものなんですけど、中身の写真はもう要りません」と言いながらカウンターの下から箱を出してきます。
お店の人はその箱から写真を1枚引っ張りだして燃やしてしまい、僕が持って行った写真を1枚入れます。彼は持ち込んだ写真がなくなるまでそれを続け、最後には僕の写真でいっぱいになった箱を渡してきます。箱には僕の写真100枚が入っていて、古い写真100枚は全て燃やされました。これで1:1の交換が成り立ちます。
でももし僕が写真100枚でなく、1枚だけ持って行ったらどうでしょうか。そしてカウンターに写真1枚をポンと出して「写真100枚あるんですけど」と言ってみます。お店の人はまた古い写真100枚入った箱を取り出し、その中から1枚取り出して、僕の写真1枚をしまいます。でも僕の様子を見て、彼はしまうべき新しい写真がないと判断します。そしてもう仕事は終わったと解釈し、古い写真99枚が入ったままの箱を返してきます。彼は僕の「100枚あるんだけど」という言葉をそのまま解釈しただけなんです。
つまりこれによって、誰かの古い写真99枚が手に入ります。そのうちもしかしたら1枚くらいは裸の写真かもしれないし、成績表かもしれません。さらにこのお店の人は0と0以外の違いもわからないので、「100枚ある」と言って0枚、つまり写真を全く出さなかったとしても、古い写真100枚入った箱を渡してくるんです。
Heartbleedの場合、この写真がデータのかけらにあたります。データのかけらを寄せ集めると、メールとかパスワード、ユーザーネームになる場合があるのです。または大規模なWebサイトのパスワードや、その名前の入った署名スタンプ、セキュリティシステムのキーコードかもしれません。可能性は実にさまざま。データのかけらはランダムに入手されますが、この手法は何回でも繰り返せるので、最終的には何かしら意味のあるものが得られるはずです。だって無限に箱をもらい続ければいいわけですから。
Heartbleedを知っているクラッカーにはそれができます。何かしら美味しいものを手に入れるまで、サーバに何度も情報を要求し続けるんです。
修正もシンプル
と、こんな風にシンプルなミスなので、修正も簡単です。
このコードのかたまりではごくシンプルな処理をしています。まず長さゼロのHeartbeatをチェックし、クライアントがサーバに対し「写真持ってるよ」と言ったときに写真の数が0でないことを確認します。次に、申告された写真の数が本当に正しいかどうかを見定める。必要なのはそれだけです。
この種のバグは、実はよくあることで「バッファオーバーフロー」という名がつけられています。コードを書いたことがある人ならご存知だと思いますが、「ユーザーからのインプット(この場合写真の枚数)のような、たいてい間違いないはずの当たり前すぎるデータについてチェックを忘れる」っていう一番ありがちなミスです。僕は高校でC++のクラスを取っていて、先生からは「つねにユーザー入力の長さを確認しろ」と口を酸っぱくして言われてました。
幸い、OpenSSLのバグはシンプルで、バグフィックスも簡単に出てきます。ただ、すでに漏れてしまったデータについてはどうしようもありません。結局、「コンピュータは言われた通りのことをし、それ以上でもそれ以下でもない」という、素晴らしくも恐ろしい原則の通りなんです。コンピュータは想像以上に従順で愚直でもあるから、人間は賢くならなきゃいけないんです。
source: existential type crisis、Thanks to Ryan Giglio and Nathan Halabuda
Eric Limer - Gizmodo US[原文]
(miho)