たった1バイトの書き込みが引き起こすルート権限での実行の脆弱性

2016年9月22日(木)。こんなメールが私のもとに飛び込んできました。

件名: ares_create_query OOB write

c-aresプロジェクトのメンテナのひとりとして、c-aresにセキュリティ問題の恐れがあるというメールを受け取ったのです。実際、そのとおりでした。この問題を知らせてくれたのは、かつてChromeOSの脆弱性をGoogleに報告したこともある人でした。

このc-aresの不具合を悪用すると、ChromeOS上で、rootユーザーとしてJavaScriptのコードを実行できることがわかりました。ChromeOS史上最悪の脆弱性かもしれません。報告者には、相当な額の報奨金が贈られることでしょう。

この動きを実現したり、どうやって実現するのかを説明したりするのはとても面倒な作業だったでしょう。報告者がこれを発見したこと、そしてさらに深く掘り下げて、再現可能な方法を示したことに、私は大いに感銘を受けました。私にはそこまでできないかもしれないけど、ここではもう少し単純に説明してみます。たった1バイトのバッファに固定値を上書きするだけで、いったいなぜroot権限でのコード実行が可能になったのでしょうか。

この問題の根本原因であるGoogleのバグについてはまだ公表されていません。現在対策中だからです。しかし、c-aresの問題は修正済みであり、この件について公表することについては了承を得ています。

c-aresによるバッファ領域外への「1」の書き込み

c-aresにはares_create_queryという関数があります。これは2013年5月にリリースされた1.10で追加された関数で、古い関数であるares_mkqueryの後継版として追加されました。ここがポイントです。というのも、Googleが使っていたc-aresは1.10より前のバージョンで、この不具合は旧版の関数に存在するものだったのです。今回お話しする問題を含む関数は、このふたつです。もともとはares_mkquery関数にあった問題が、数年前にares_create_queryにも持ち越されたのです(そして新しい関数には、引数が追加されました)。中身のコードは大半がそのまま持ち越されて、バグもそのまま引き継がれました。実際には、このバグはc-aresのフォーク元であるオリジナルのaresプロジェクトの時代に生まれたものです。それは2003年10月のことでした。長い長い潜伏期間を経て、それが今ついに発見されたのです!

この関数の挙動をこまごまと説明したところで退屈でしょうから、要点だけお話しましょう。この関数は、名前を文字列として受け取ります。そして、DNSプロトコルのデータを含む送出パケット用のメモリ領域を確保して、確保したメモリ領域とその長さを戻します。

この関数のロジックに間違いがありました。末尾にエスケープしたピリオドを含む文字列を受け取ったときに、必要な長さより短いバッファしか確保しないようになっていたのです。つまり、たとえば”one.two.three.”のような文字列をこの関数に渡すと、確保されるメモリ領域は本来必要なサイズより1バイトだけ小さくなって、最後の1バイトは確保したメモリ領域以外のところに書き込まれてしまうのです。いわゆるバッファオーバーフローですね。確保した領域外に書き込まれるデータは、ほとんどの場合は「1」です。これは、DNSプロトコルのパケットデータの配置によるものです。

この不具合はCVE-2016-5180と名づけられ、2016年9月末に、修正版のc-ares 1.12.0のリリースとともに公開されました。不具合を修正したコミットは、こちらです。

「1」が及ぼす影響

ある関数を使えば、確保したバッファの範囲外に「1」というシングルバイトの値を書き込めることがわかりました。これがいったい、どんな悪用につながるのでしょう?

Redhatのセキュリティチームは、この問題を「セキュリティの影響度は中程度(Moderate)である」と判断しました。つまり、これを悪用しても大したことはできないだろうとみなしたのです。しかし、それなりの想像力を働かせたうえで運を味方につければ、悪用できてしまうのです!

ChromeOSに話題を戻しましょう。

まず知っておく必要があるのは、ChromeOSの内部ではHTTPプロキシーが動いていて、その入力値の制限はとても緩い(大抵の入力を受け付ける)ということです。このHTTPプロキシーのソフトウェアがc-aresを使っています。これが、悪意の持った攻撃者の必要とする重要なコンポーネントです。罠を仕込んだリクエストをこのプロキシーに送る方法さえ見つかれば、プロキシーはその文字列をc-aresに送り、そしてヒープバッファの外部に「1」を書き込むでしょう。

ChromeOSは、dlmallocでヒープメモリを管理しています。プログラムがメモリを確保するたびに、要求メモリ領域に戻ってポインタを取得します。そしてdlmallocは、確保したメモリ領域の直前に小さなヘッダを配置します。mallocでNバイトを確保しようとしたときに、dlmallocは「ヘッダサイズ+N」バイトを使って、要求されたNバイトへのポインタを返します。図で表すと、このようになります。
malloced-area

うまく仕込まれたさまざまなサイズのHTTPリクエストを使い、攻撃者はなんとかして解放済みメモリの穴を作ります。そして、c-aresがメモリを確保するときに確実にそこが使われるようにするのです。攻撃者は、ChromeOSのdlmallocやメモリアロケータの挙動を把握しています。c-aresのmallocが確保するメモリの大きさも、最終的に「1」が上書きされる場所もわかっているのです。範囲外に「1」が書き込まれたそのアドレスは、dlmallocが管理する次のメモリチャンクのヘッダ部になります。
two-mallocs

dmallocヘッダ上で「1」が書き込まれる場所は、各種のフラグとして使われている場所です。また、確保されたメモリチャンクのサイズの最下位ビットも含まれています。

ここに「1」が書き込まれると、二つのフラグをクリアして別のひとつのフラグをセットします。また、チャンクサイズの最下位ビットをクリアします。セットされるフラグはprev_inuseで、これは、メモリの解放時に隣接する領域をマージできるかどうかを表す重要なフラグです(もし書き込まれる値が「1」ではなく「2」だったとしたらこのフラグが立つことはなく、ここで紹介する方法での悪用は無理だったのです!)

騙されたdlmallocは、オーバーフローを起こしたc-aresのバッファを解放する際に、メモリ上の後続バッファもあわせて解放してしまいます(prev_inuseビットが立っているからです)。そして、後に解放されるはずのメモリの一部が使用中のまま残ってしまうのです。さあ、いたずらが始まりましたよ!
freed-malloc

混乱したメモリバッファの悪用

解放されたけれど実際には一部が使用中であるこのメモリ領域が、さらなる「お楽しみ」のための遊び場になります。うまく仕込んだHTTPリクエストを改めて送ると、このメモリブロックが再び確保されて、新しいデータの格納に使われるようになるのです。

攻撃者は、プログラムの別の部分でまだ使用されていたデータブロックの最後の部分に適切なデータを挿入することができました。ほとんどの場合、プロキシがリクエストに詰め込まれたものです。攻撃者は、そのデータブロックの末尾(まだプログラム内の別の部分で使われているところ)に適切なデータを送り込むでしょう。プロキシーは、リクエストに詰め込まれたものは何でもといっていいほど受け付けるからです。攻撃者は、自分のコードをこの領域に送り込んで実行させます。そこからさらに数段階の手順を経ると、任意のコードをroot権限で実行できるようになります。しかし、それだけではだめで、何とかして特定のJavaScriptコードを送り込んで実行させる必要があるというわけです。

この攻撃方法を見つけるためにいったいどれくらいの時間と労力を費やしたのか、私には想像もできません。私が受け取った報告は、37ページにわたる詳細なものでした。今まで読んだなかでも最高の報告のひとつだといえるでしょう!この件の全貌が公開されたときには、少なくとも今回紹介した部分だけでも皆さんに見てもらえるようになることを望みます。

ここから得られる教訓

第一印象で「影響範囲は限られる」「無害である」と感じたとしても、悪意を持った人が一連の操作の中のひとつのステップとして使えば、システムを攻撃できることがあります。世の中にはスキルの高い人たちがいて、攻撃に必要な手順を見つけようと血眼になっているのです。