本稿は 2016年1月27日に Talos Group のブログに投稿された「Bypassing MiniUPnP Stack Smashing Protection」の抄訳です。
この記事は、Aleksandar Nikolic、Warren Mercer、Jaeson Schultz が執筆しました。
概要
MiniUPnP は一般に、それぞれが NAT ファイアウォールの内側にある 2 つのデバイスが、各ファイアウォールの接続を開く「ホール パンチング」によっての相互通信できるようにするために使用されます。この技術は多くのソフトウェアに実装されており、Tor や暗号通貨マイナー、ウォレットなど、さまざまなピアツーピアのソフトウェア アプリケーションのネットワーク上での動作を可能にしています。
2015 年に、Talos は、よく使用される MiniUPnP ライブラリのクライアント側コードにバッファ オーバーフローの脆弱性を確認し、報告しました。この脆弱性は、ベンダーによってすぐに修正され、TALOS-CAN-0035 および CVE 2015-6031 として登録されました。後に、Martin Zeiser と Aleksandar Nikolic は PacSec 2015 で、UPnP が攻撃される可能性のあるクライアント側の箇所について講演し(「Universal Pwn n Play」)、その中でこの脆弱性についても触れています。
Talos は、このライブラリを利用した Bitcoin-qt ウォレットに対するワーキング エクスプロイトを開発しました。このエクスプロイトでは、スタック破壊保護対策(SSP:Stack Smashing Protection) を回避します。この記事では、その詳細について説明します。
脆弱性の詳細
今回悪用する脆弱性は、MiniUPnP ライブラリの IGDstartelt 関数内にある、XML パーサー コードに存在します。
バッファ オーバーフローは、memcpy 関数が呼び出された際に発生します。この関数のパラメータ「l」は、長さがチェックされません。datas->cureltname は IGDdatas 構造体内の固定サイズ バッファであるため、渡された長さの値が大きすぎると、スタックでバッファ オーバーフローが起こります。上記のコードの場合、解析対象の XML 要素から直接取得された name 文字列の実際の長さが渡されます。
攻撃者は、書き込み先のバッファ(サイズは MINIUPNPC_URL_MAXSIZE で定義される)にコピーされる memcpy のソース引数の長さと内容を任意に決定できてしまいます。
攻撃対象
ほとんどのピアツーピア アプリケーションは、NAT デバイスの内側にある場合、ユニバーサル プラグ アンド プレイ(UPnP)を使用して、ポート フォワーディングのネゴシエートを試行します。アプリケーションは、起動時に UDP ポート 1900 経由で特定のブロードキャスト アドレスに対して M-SEARCH 要求を送信し、ネットワーク検出を行います。
アプリケーションは、ローカル ネットワーク上のすべての UPnP インターネット ゲートウェイ デバイス(IGD)に関する情報を検出するために、ブロードキャストを実行します。IGD は、この要求に対して HTTP 応答を返します。この応答には説明 URL が含まれています。
上図のように、パケットの LOCATION HTTP ヘッダーに rootDesc.xml ファイルの場所が記述されています。このファイルには当該の IGD の機能について記載されています。クライアント アプリケーションは、指定された XML を取得します。
rootDesc.xml ファイルの取得後、MiniUPnP ライブラリがファイルの解析を開始します。このときに今回の脆弱性が悪用可能となります。
Bitcoin-qt ウォレットに対する攻撃
Bitcoin-qt は、デフォルトのビットコイン クライアントであり、リファレンス実装です。この脆弱性がいかに悪用されうるかを示す上で興味深い攻撃対象として選択しました。Bitcoin-qt もピアツーピア アプリケーションであるため、前述の UPnP メカニズムを使用します。攻撃対象の LAN 上に偽の UPnP サーバをセットアップし、そこから過度に長い要素名を含む XML 記述ファイル(rootDesc.xml)を配信することで、脆弱性のトリガーとします。
Bitcoin-qt のようなアプリケーションではセキュリティが重要な要件となるため、公式バイナリには複数のエクスプロイト緩和策が盛り込まれています。今回は、バイナリ上にある SSP に注目します。SSP では、脆弱である可能性があるスタック上のバッファをスタック カナリア(スタック クッキー)で保護します。これは MiniUPNP の脆弱性をトリガーしてみるとわかります。
[user@localhost bin]$ ./bitcoin-qt
*** stack smashing detected ***: ./bitcoin-qt terminated
Segmentation fault (core dumped)
この緩和策は、脆弱性を悪用する上で障害となります。
さらなる悪用
MiniUPnP ライブラリに存在する脆弱性の悪用を試みましたが、Bitcoin-qt 内の SSP によって阻止されてしまいました。そこで、逆にこの仕組みを積極的に悪用し、SSP を回避できないかを検討することにしました。
SSP の概要
SSP はコンパイル時の悪用に対する緩和策で、最近のコンパイラであれば使用可能です。SSP に含まれる対策の 1 つとして、スタック カナリアがあります。脆弱である可能性のあるスタック バッファを、スタック カナリアと呼ばれる乱数を使って確実に保護する方法です。従来型のスタック バッファ オーバーフロー攻撃では、任意のコード実行を可能にするには、リターン アドレスを上書きする必要があります。リターン アドレスを上書きすると、スタック カナリアも上書きされ、後者の上書きは関数が返る際に検出できます。スタック カナリアが上書きされたことが検出されると、SSP が機能してプロセスを中止させます。SSP の実装の詳細についてはこの記事では取り上げませんが、興味がある場合は Adam Zabrocki の記事をご覧ください。
リターン アドレスを上書きすると、スタック カナリアも上書きされ、後者の上書きは関数が返る際に検出できます。スタック カナリアが上書きされたことが検出されると、SSP が機能してプロセスを中止させます。
上のアセンブリ リストが示すとおり、スタック クッキーのチェックが通らないと、__stack_chk_fail が呼び出されます。
興味深いのは、スタック破壊が検出されてもプロセスがただちには中止されないことです。その前に、ユーザへの通知とクラッシュについてのロギングのために、かなりの量のコードが実行されます。これは以前にも悪用され、情報漏えいやコード実行を許してしまったことがあります。Dan Rosenberg の記事には、このメッセージングを悪用し、またプロセスの引数配列を上書きすることで、情報漏えいを実現する方法が記載されています。Joshua Drake も、無制限のオーバーフローの悪用例についての記事を投稿しています。その例では、ヒープ メタデータを上書きすることで SSP を回避し、コード実行を可能にしています。
今回のエクスプロイトでも、SSP の検出後の挙動を利用するという同様のアプローチで、Bitcoin-qt の MiniUPnP の脆弱性を悪用します。
システム コールと ELF 補助ベクトル
スタック破壊が検出されると、__stack_chk_fail が画面にメッセージを出力しようとします。__stack_chk_fail は、呼び出した残りのコードをスキップし、write システム コールを呼び出すことで、画面出力を行います。最新の Linux や libc 実装では、パフォーマンス上の理由から、システム コールは __kernel_vsyscall 関数によって呼び出されます。これは、write システム コールを逆アセンブリしたものを見てもわかります。
上図より、*%gs:0x10 にある関数が呼び出されていることがわかります。セグメント レジスタ「gs」がスレッド制御ブロック(TCB)の場所です。TCB は tcbhead_t 型として typedef された構造体です。
オフセット 0x10 には sysinfo ポインタがあります。プロセスの実行中、このポインタは __kernel_vsyscall 関数の場所を指しています。この関数は sysenter の仕組みを使って syscall を呼び出します。
TCB の sysinfo ポインタは libc によって設定され、ELF ローダを介してカーネルより提供されます。これを可能にしているのが ELF の補助ベクトル(AT_SYSINFO)です。
ELF 補助ベクトルは、ローダが特定の情報をカーネルからプロセスに渡す際に使用されます。ローダはスタック上で ELF 補助ベクトルを環境変数のすぐ後に配置します。ELF 補助ベクトルは binfmt_elf.c で定義されています。AT_SYSINFO 補助ベクトルは __kernel_vsyscall 関数へのポインタを持ちます。大きなバッファを利用したスタック オーバーフローで SSP を回避するには、argc および argv 配列や環境変数ポインタまで、つまり補助ベクトルの手前までスタックを上書きし、AT_SYSINFO をこちらで用意した任意のコードのアドレスで上書きすることが正攻法と考えられます。こうすることで、プロセスがシステム コールを実行しようとした際、__kernel_vsyscall を呼び出す代わりに、こちらが指定したアドレスにジャンプさせることができます。
ただし、今回の場合はこの方法ではうまくいきません。__stack_chk_fail は最初に環境変数を解析し、システム コール実行の前にメッセージを出力する方法を決定するからです。ジャンプ先のアドレスを指定するには、AT_SYSINFO を上書きする必要があり、これは必然的に環境ポインタの上書きを伴います。これでは目的を達成できる前にクラッシュしてしまいます。クラッシュすることは、簡単なバッファ オーバーフローで示すことができます。
Starting program: /home/user/tests/bof `perl -e 'print "A"x300'`
Program received signal SIGSEGV, Segmentation fault.
__GI_getenv (name=0xb7f5e5fd "BC_FATAL_STDERR_") at getenv.c:85
85 if (name_start == ep_start && !strncmp (*ep + 2, name, len)
(gdb)
上記のプログラムは、環境ポインタがオーバーフローで上書きされるためクラッシュしてしまいます。環境解析コードがクラッシュしない方法で、オーバーフローで環境ポインタを上書きできないか試みるというアプローチも考えられますが、Bitcoin-qt の場合はまた別のやり方があります。
POSIX スレッドと最終版のエクスプロイト
Bitcoin-qt は Pthreads を使用する GUI アプリケーションです。スレッドの初期化コードをたどると、各スレッドに AT_SYSINFO ポインタのコピーがあることがわかります。このスレッドローカルなポインタを上書きして、Bitcoin-qt アプリケーションでの最初のコード実行を可能にします。このポインタは大規模なバッファ オーバーフローを起こすことでアクセスできます。次に、syscall を呼び出そうとする SSP のエラー レポート メカニズムを妨害し、こちらが指定した場所のコードを実行するようリダイレクトさせます。
この上書きには、いくつかの問題があります。そのうち最も重要なのは、__kernel_vsyscall 関数ポインタが無効になり、ROP チェーンやシェルコードが実行しようとするシステム コールが結局上書きされたアドレスにジャンプし、失敗してしまうということです。これは、エクスプロイトに悪影響をおよぼすため、修正する必要があります。
まず必要なのは、上書きされたポインタを修復することです。NX 保護機能が有効になっているので、修復には ROP ガジェットを使う必要があります。初めに、制御可能な場所にスタック ポインタを返します。これは、以下のガジェットで行います。
ガジェットはスタック ポインタに 0x13ec を付加することで、オーバーフローしたバッファのどこかに、しかも、スタック由来の 4 つの値を ebx、esi、edi、ebp にポップしてから返るようにします。esi と edi の操作が可能になったので、今度は次のガジェットを使用して __kernel_vsyscall への元のポインタを適切な場所に書き戻すことができます。
注:上のガジェットでは、esp+0x14 と esp+0x18 の 2 つのアドレスが読み取り可能である必要があります。
__kernel_vsyscall が復元されることで、エクスプロイトは動作を継続できるようになります。mprotect() を使用して、シェルコードを含むメモリを実行可能にし、そこにジャンプします。
エクスプロイトの全容については、こちらをご覧ください。
注:このエクスプロイトは ASLR の問題に対応していないため、ハードコードされたアドレスがいくつか含まれています。
まとめ
この記事では、SSP を Pthreads と組み合わせることで生じる興味深い副作用について記述しました。本件は、一見悪用しにくそうな問題が、今日のプロセス実行チェーンが内包する複雑性がもたらす「予期せぬ結果」のため、悪用できてしまう場合があることを示す例といえます。