本稿は 2016年2月5日に Talos Group のブログに投稿された「Vulnerability Spotlight: Libgraphite Font Processing Vulnerabilities」の抄訳です。
本脆弱性は、Cisco Talos の Yves Younan が発見しました。
この記事では、Libgraphite ライブラリ内で見つかった 4 件の脆弱性に関する Talos の勧告について記載します。Libgraphite ライブラリは Linux、Firefox、OpenOffice などの主要なアプリケーションで、フォント処理に使用されています。最も深刻な脆弱性は領域外メモリ参照(out-of-bounds read)によるものです。攻撃者はこの脆弱性を悪用して任意のコードを実行できてしまいます。2 つ目の脆弱性は、悪用可能なヒープ オーバーフローです。残る 2 つの脆弱性は、サービス拒否(DoS)の原因となりうるものです。攻撃者は、Graphite を使用し、上記のいずれかの脆弱性を引き起こすよう細工したフォントを含むページをレンダリングするアプリケーションをユーザに実行させるだけで、脆弱性を悪用できてしまいます。バージョン 11 ~ 42 の Mozilla Firefox は Graphite を直接サポートしており、また Graphite はローカルとサーバベースの両方のフォントをサポートしています。このため、攻撃者はまずサーバに侵入して、ユーザがサーバ上のページをレンダリングした際に、こうした細工したフォントを配信するようにできてしまいます。
この記事では、次の脆弱性について記載します。
- CVE-2016-1521
- CVE-2016-1522
- CVE-2016-1523
- CVE-2016-1526
概要
Graphite は、動作が複雑かつ多彩な表記体系の表示が可能な「スマート フォント」を作成できるパッケージです。基本的には、Graphite のスマート フォントは拡張子が付加された TrueType フォント(TTF)にすぎません。Talos では、以下のような問題を発見しています。
- Libgraphite のフォント処理には、サービス妨害(DoS)に悪用される可能性のある脆弱性が存在します。細工したフォントを使用すれば、領域外メモリ参照を引き起こし、情報漏えいやサービス拒否を発生させることができます。
- 細工したフォントを使ってバッファ オーバーフローを引き起こし、任意のコードを実行できます。
- Libgraphite の双方向フォント処理機能には、悪用可能な Null ポインタ参照が存在します。細工したフォントを使って Null ポインタ参照を引き起こし、クラッシュさせることができてしまいます。
いずれの場合においても、攻撃者は悪意のあるフォントを使ってそれぞれの脆弱性を引き起こすことができます。
領域外メモリ参照の詳細
悪意のあるフォントを使って、フォントのオペコードの解釈時に領域外メモリ参照を引き起こすことができます。
問題は、directmachine.cpp ファイルの directrun 関数にある、さまざまなオペコードの実行時に発生します。85 行目で、オペコードのインタープリタが GOTO 文で実行されます。
goto **ip;
このオペコードが cntxt_item である場合、ip 変数は次のオペコードまで iskip バイト分前進し、オペコードの解釈を続けます。これは opcodes.h の 369 行目で行われます。
ip += iskip;
しかし、この ip が、解釈対象メモリ(プログラム変数)として割り当てられたメモリ領域内にとどまるようチェックする仕組みがありません。**ip にジャンプした際に領域外メモリ参照が起こるよう iskip の値を設定した悪意のあるフォントを作成することができるのです。このメモリは事前にデータ用に割り当てられたメモリです。領域外メモリ参照により、攻撃者は TTF 仮想マシンにコードを実行させることができてしまうため、これは重大な脆弱性といえます。ブラウザのような環境では、JavaScript を使ってメモリに任意のバイトコードを書き込むことができます。昨年の Pwn2Own では、Joshua “jduck” Drake が、Java を悪用することで、TTF 仮想マシンを使ってジェネリックなメモリ読み取り/書き込み用プリミティブ型を作成できることを示しています。
ヒープ オーバーフローの詳細
悪意のあるフォントを使用して、コンテキスト アイテムの処理時に任意長のバッファ オーバーフローを発生させることができてしまいます。
Code.cpp の 158 ~ 161 行目で、_code と _data にメモリが割り当てられます。このメモリは、バイトコードから読み込まれるコードおよびデータの予想最大容量となるよう設定されています。しかし、ファイルから読み込まれた実際の容量が、予想を超えてしまう場合があります。
158: _code = static_cast(malloc((bytecode_end - bytecode_begin)
159: * sizeof(instr)));
160: _data = static_cast(malloc((bytecode_end - bytecode_begin)
161: * sizeof(byte)));
181 行目で、dec.load() 関数が呼び出されます。ポインタはバイトコードの始まりと終わりを指しています。この関数はその後、230 ~ 240 行目でバイトコードからオペコードを読み込みます。これはバイトコードの終わりに到達するまで行われます。さらに 238 行目では、現在処理しているバイトコードの値を指定して emitopcode() を呼び出します。
230: while (bc < bcend)
231: {
232: const opcode opc = fetchopcode(bc++);
233: if (opc == vm::MAXOPCODE)
234: return false;
235:
236: analyse_opcode(opc, reinterpret_cast<const int8 *>(bc));
237:
238: if (!emit_opcode(opc, bc))
239: return false;
240: }
emitopcode で処理されているオペコードがコンテキスト アイテム(CNTXTITEM)を参照している場合、500 行目で load 関数が再帰的に呼び出されます。ここではバイトコードが instrskip で指定した、新しい bytecodeend が渡されます。
if (load(bc, bc + instr_skip))
548 行目にある validateopcode 関数で、バッファ外からデータが読み込まれないようサイズをチェックします。この関数は新たに読み込まれるオペコードが _max.bytecode(bytecodeend の値を含む)よりも小さいことを確認します。
しかし、複数の CNTXTITEM を再帰的に処理する場合、データは、_data にコピーされている間、ロード用の再帰的呼び出し関数が指定する新しい bytecodeend を超えて読み込まれることも可能となってしまいます。そうなった時点でループが閉じられるようにはなっていますが、その前に instrskip で指定された値を超えてデータを読み込むことができてしまいます。validateopcode が呼び出された時点でのバイトコードの値が、バイトコードの終わりの値よりも低くなるからです。これを何度も繰り返すと、同じメモリが何度も処理されることになり、その結果データがバイトコードよりも大きくなって、領域外メモリへの書き込みが起こってしまいます。コード バッファでも同様のことが起こりえます。修正案としては、CNTXTITEM から戻る際に、バイトコードの現在値とバイトコードの開始値の差が、データサイズと instrcount 以上であることを確認することが考えられます。210 行目と 211 行目では、同様のチェックが assert を使って行われています。
サービス拒否の詳細
サービス拒否の問題のうち、1 つは Null ポインタ参照が原因で起こります。フォントが grmakefile_face 経由でロードされると、FeatureMap.cpp ファイルの 190 行目にある SillMap::readFace 関数から readFeats 関数が呼び出されます。
if (!m_FeatureMap.readFeats(face)) return false;
110 行目の readFeats 関数で、mnumFeats の値として 0 が割り当てられます。これにより、115 行目で readFeats より値が戻されます。しかし、戻り値は true となるため、フォントが何のエラーもなくロードされるにもかかわらず、mFeatureMap のいずれの変数も初期化されません。このフォントでその後 grmakeseg 関数が呼び出されると、SillMap::cloneFeatures の呼び出しは 241 行目で失敗します。0 が設定された mFeatureMap.mdefaultFeatures を参照してしまうためです。
return new Features (*m_FeatureMap.m_defaultFeatures);
Table のコンストラクタが失敗した場合は、同様のエラーが 103 行目でも発生します。
サービス拒否はさらに、領域外メモリ参照が原因でも起こります。領域外メモリ参照はサービス拒否以外に情報漏えいも引き起こす可能性があります。loca テーブルのサイズが 0 に設定されている不正なフォントを読み込もうとすると、領域外メモリ参照が発生します。
GlyphCache.cpp の 187 行目で Loader 関数が定義されています。この関数はフォントからいくつかのテーブルを読み込みますが、その中に loca テーブルも含まれています。その後 206 行目で、字形の数、loca テーブルおよび loca テーブルのサイズを引数に、TtfUtil:LocaLookup を呼び出します。
if (TtfUtil::LocaLookup(numglyphsgraphics-1, _loca, _loca.size(), _head) == sizet(-1))
さらに 1164 行目で(ファイルは TtfUtil.cpp)、loca テーブルにアクセスして字形の数を渡します。
return be::peek(pLongTable + nGlyphId);
1161 行目でサイズ チェックを実行しますが、サイズが 0 に設定されているので、チェックは必ず通ります。
if (nGlyphId < (lLocaSize >> 2) - 1)
このため、テーブルの、任意の 16 バイト数分領域外のメモリが参照され、これによりサービス拒否、さらには情報漏えいが発生するおそれがあります。
既知の脆弱性バージョン
Libgraphite 2-1.2.4
Firefox 31 ~ 42
まとめ
ゼロデイ脆弱性を発見し、開示することは、日常的に使用されるデバイスやソフトウェアの全体的なセキュリティの向上に向けた、責任ある行為です。Talos では、攻撃者に悪用され得る問題や弱点を特定するためのプログラム的手法を開発することで、こうした取り組みに貢献しています。このようなアプローチにより、お客様がプラットフォームやソフトウェアを安全に使用できるようになります。また、シスコとしても、よりよい製品を開発するためのプロセス改善に向けた知見が得られます。
また、お客様の保護のため、Talos はこうした脆弱性を悪用しようとする行為を検出するルールをリリースいたしました。今後、脆弱性に関する新たな情報が追加されるまでの間は、ルールが追加されたり、現行のルールが変更されたりする場合がありますのでご注意ください。最新のルールの詳細については、防御センター、FireSIGHT Management Center、または Snort.org を参照してください。
Snort ルール:36216、36213、36217、36225 ~ 36228、36385 ~ 36388
その他のゼロデイ レポートおよび脆弱性レポート、情報については、こちらをご覧ください。
http://talosintel.com/vulnerability-reports/