検索
連載

10年以上前からあるLinux Kernelの懸念点が脆弱性として発見された「Stack Clash」――意図的に公開が遅らされた理由OSS脆弱性ウォッチ(2)(2/2 ページ)

連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェアの脆弱性に関する情報を取り上げ、解説していく。今回は、2017年9月14〜15日に開催された「Linux Security Summit」でも話題になっていた「Stack Clash」の詳しい説明と情報をまとめる。

PC用表示 関連情報
Share
Tweet
LINE
Hatena
前のページへ |       

具体的な攻撃の手順

 今回Qualysが公開した脆弱性情報とPoCでは、前章で説明した概略に沿って攻略する際に多くのパターンとOSが含まれており、Linuxのものだけでも下記などが存在します。

  • eximを用いて攻略する
  • sudoを用いて攻略する
  • suを用いて攻略する
  • ld.soとSUIDされた任意のバイナリを用いて攻略する
  • ld.soとSUIDされたPIEを用いて攻略する

 今回は、この中で「ld.soとSUIDされた任意のバイナリを用いて攻略する」という方法に関して(特にamd64:64bitの場合を例に)詳しく見ていきます。

 この方法では、前述した一連の脆弱性の中から下記の3つを使用して、スタックガードページにアクセスすることなく、飛び越してしまうことが可能になっています。

  • Linux Kernelのスタックガードページの脆弱性(CVE-2017-1000364)
  • glibcでLD_LIBRARY_PATHの値に細工をすることでヒープ/スタックの値を操作できる脆弱性(CVE-2017-1000366)
  • Linux Kernelでld.soやヒープと、スタックアドレス間の距離が最小で128MBになっているという脆弱性(CVE-2017-1000379)

 以下、前章と同じ【ステップ1〜4】に沿って追っていきます。

【ステップ1】衝突(スタックを他のメモリ領域と衝突させる)

 スタックの開始アドレスをAnonymous mmap()に到達させるために、以下のことを行います。

  1. RLIMIT_STACKをRLIM_INFINITYにして、mmap()をボトムアップレイアウトにする
  2. Mmap-Stack間(32bitだと2GB、64bitだと後述)の半分の1GBを、引数/環境変数で埋め尽くす
  3. さらに1GBを、Anonymous mmap()でLD_LIBRARY_PATHなどの環境変数で埋め尽くす(CVE-2017-1000366を利用)(図5)。

図5

【ステップ2】実行(スタックポインタをスタックの開始アドレスに移動する)

 最初の128KBのスタック拡張を消費させるために、128KBのargv[]とenvp[]ポインタをexecve()に引き渡します(図6)。


図6

【ステップ3】ジャンプ(スタックガードページを飛び越えて他のメモリ領域まで到達させる)

 ld.soは、LD_LIBRARY_PATH環境変数のスタックベースのコピーであるllp_tmpを割り当てます。このllp_tmpは128KB(MAX_ARG_STRLEN)より大きく、スタックガードページを飛び越して直接マップされたメモリ領域を突き破ることができます(図7)。


図7

【ステップ4】破壊(スタックや他のメモリ領域の破壊)

 スタックガードページを飛び越してしまい、重なったメモリ領域を好きなように書き換えてコードを実行します。この箇所は実際に攻撃につながってしまう可能性があるため詳細は書きませんが、ld.so中のhwcap(hwcap-pointersとhwcap-strings)とLD_LIBRARY_PATHを用います。これとSUID/SGIDが設定されたプログラムを用いて、root権限の取得が可能になります。

【ステップ1'】(64bitの場合)

 64bitの場合は、対象となるメモリ領域が大きいため、【ステップ1】のようにmmap-stack間が2GBどころではなく、とんでもない広さになる可能性があります。

 しかし、CVE-2017-1000379によって、mmap-stack間の距離は最小で128MBになることが分かります(※計算参照)。

※(計算)

 mmapによってマッピングされるAnonymous mmapなどの終わりの位置(mmap_end)は、Linux Kernelのarch/x86/mm/mmap.c中のarch_pick_mmap_layout()の実装により、下記のような式で求められます。

mmap_end = TASK_SIZE - MIN_GAP - arch_mmap_rnd()
  • TASK_SIZE:ユーザー空間の最上位アドレス(0x7ffffffff000)
  • MIN_GAP=128MB+stack_maxrandom_size()=128MB+0〜16GB
  • arch_mmap_rnd()=0〜1TBの乱数

 スタックの上端(stack_end)は、fs/binfmt_elf.c中のrandomize_stack_top()により、下記のような式になります。

stack_end = TASK_SIZE - [0〜stack_maxrandom_size()]

 従って、mmap_endとstack_end間の距離は下記のようになります。

stack_end - mmap_end = MIN_GAP + arch_mmap_rnd() - "stack_rand"
= 128MB + stack_maxrandom_size() - "stack_rand" + arch_mmap_rnd()
= 128MB + StackRand + MmapRand
  • StackRand:[0B, 16Bの乱数]
  • mapRand:[0B, 1TBの乱数]

 さらに、全てのカーネルでPIEやld.soがスタックの直下にマッピングされる確率は(計算は省略しますが)、約1万7331分の1になります。つまり、PIEやld.soのread-writeセグメントがスタックガードページの開始アドレスと同じになるわけです。

 これらの条件により、ld.soとSUIDされた任意のバイナリを用いて攻略した場合には、平均5時間程度で攻撃のPoCが成功するといわれています。

 筆者も実際にPoCを行いましたが、条件がなかなか厳しいので(実際にはmmap-stack間も最小で128MBなので実際にはもっと広い)、成功までにかなりの時間がかかってしまいました。しかし、だいたい3000回ぐらいの試行を数十回繰り返して行い、攻撃が成功しています。また、この脆弱性の特徴上、SELinuxが有効(Enforcing)になっていても攻撃は成功しています(図8)。


図8

修正内容

 今回の一連の脆弱性を受けて、glibc、カーネルなどに修正が加えられています。ディストリビューションのカーネルでは、緩和策としてスタックガードページのサイズを1MiB(1024KB)に増やして、突破される可能性を低くするようにしました。

 しかし、根本的な修正になっていないことがやや懸念されます。スタックガードページのサイズを例えば1MiBにしたとして、今回のStack Clashの脆弱性は防げるかもしれませんが、将来にわたって、その1MiBで安全だとは言い切れないからです。

 そのため、Linux Kernel 4.11.7からは、スタックガードページのサイズを、ブート時に「stack_guard_gap=[MM]」で引き渡すことにより、ユーザーがサイズを変更できるようにしました。

意図的に公開を遅らせた根が深い問題

 今回の件は、10以上年前から懸念されていたことが実際に発覚したということと、根本的には確率を下げる以外にないという方法でしか修正ができないため、根が深い問題になっています。今回の問題も64bitに関しては発生確率は低いため、深刻度もそれほどではないと思われるかもしれません。しかし、現実に特権を取れる脆弱性であるため、なるべく早い対処が望まれます。

 また今回の件は、関係するパッケージが多く、かつ根が深い問題だったため、脆弱性の公開自体も異例に遅れました。意図的に公開を遅らせたことについては、OSS-Security MLのSolar Designer氏が謝罪しており、今後の対応を早くするために各ディストリビューションのセキュリティ担当と脆弱性に関して緊密に連携を取るようにしています(参考)。

 今後も、脆弱性情報については最新のものをウォッチして対処していく姿勢が必要となるでしょう。

筆者紹介

面和毅

略歴:OSSのセキュリティ専門家として20年近くの経験があり、主にOS系のセキュリティに関しての執筆や講演を行う。大手ベンダーや外資系、ユーザー企業などでさまざまな立場を経験。2015年からサイオステクノロジーのOSS/セキュリティエバンジェリストとして活躍し、同社でSIOSセキュリティブログを連載中。

CISSP:#366942

近著:『Linuxセキュリティ標準教科書』(LPI-Japan)」


Copyright © ITmedia, Inc. All Rights Reserved.

前のページへ |       
ページトップに戻る