連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェアの脆弱性に関する情報を取り上げ、解説していく。今回は、2017年9月14〜15日に開催された「Linux Security Summit」でも話題になっていた「Stack Clash」の詳しい説明と情報をまとめる。
「OSSセキュリティ技術の会」の面和毅です。本連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェア(OSS)の脆弱(ぜいじゃく)性に関する情報を取り上げ、解説していきます。
2017年6月20日に、「Stack Clash」と呼ばれる一連の脆弱性が報告されました。少し時間がたってしまっていますが、2017年9月14〜15日に開催された「Linux Security Summit」でも話題になっていたこともあり、今回はStack Clashの詳しい説明と情報をまとめます。
このStack Clashは、glibcやカーネルなどに関わる問題で、かつ“スタックガードページ”の実装方法に直接関係することが多いため、2017年6月20日まで公開されず、各Linuxディストリビューションや主要OSSのみに報告されて修正が行われていたものです。
今回のStack Clashは、以下のような一連の脆弱性から構成されています。
この脆弱性は、「user-spaceのプロセスのスタックやヒープが衝突(Clash)した場合にどうなるか」という長期間(実際、12年間にわたっています)の疑問の延長にある問題です(そういう意味では、決して新しいものではなく、古くからある脆弱性といえます)。
この「スタックやヒープの衝突」という問題を理解するために、まずはプロセスを起動した際のメモリ領域の情報を見てみましょう。例として挙げるのは、sleep 3000を実行した際の/proc/[pid]/mapsです(図1)。
このように出力されたスタックとヒープを増大させたり、ld.soやAnonymousmmapを増大させたりすることで、これらの領域を衝突させて好きなように書き換えることはできないかというものです。
このようなスタックやヒープの衝突の例は、既に幾つかありました。
しかし、user-spaceでは、先の2005年のものと2010年のものだけになっています。それは、Linux上でStack Clashを防ぐための保護機能(スタックの下に“スタックガードページ”を設けるもの)が設けられたためです。スタックガードページはCVE-2010-2240の問題を回避する際に導入されました。
2017年6月20日にQualysから公開された脆弱性のアドバイザリによると、幾つかの“スタックガードページ”の実装方法に問題があり、これをuser-spaceで破る方法があるということです。
ユーザープロセスは起動時にスタックなどをメモリ領域にマッピングします。このスタックの位置はスタックポインタ(i386のespレジスタ)で表されますが、これはスタックに積むものが大きくなるにつれて下側に下がってきます。そして、スタックの下限(スタートアドレス)に到達した際には、ページフォルト例外が発生し、スタック開始アドレスが下側に拡張されます。これはスタックのサイズがRLIMIT_STACK(ulimit -sコマンドで表示。通常のデフォルトは8192KB)になるまで続けられます。そして、RLIMIT_STACKまで達してこれ以上拡張できなくなると、SIGSEGVなどが発生してプロセスが終了します(図2)。
しかし、この拡張だと、スタックのすぐ下に他のメモリ領域などが来ていた際にオーバーラップして書き換えられてしまいます(CVE-2010-2240など)。これを防ぐため、スタックガードページが設けられました。これは数KB(1Page:i686の場合4096KB)をスタック開始アドレスの下側にあらかじめ付けておき、このガードページにアクセスがあった場合には「ページフォルト例外」を発生させ、プロセスに対してSIGSEGVを発生させて終了させます(図3、図4)。
これにより、スタックが拡張していくときに、他のメモリ領域(他のプロセスのヒープ領域など)がスタックと重ならないようになります。
一般的に、メモリを増加させるのは“連続的な変化”であるため、このスタックガードページは有効です。しかし、今回の「Linux Kernelのスタックガードページの脆弱性(CVE-2017-1000364)」においては、数KB程度のスタックガードページでは、スタックガードページにアクセスすることなく、“飛び越してしまう”抜け道が発覚しています。これを緩和するには、スタックガードページのサイズを増加させることが有効になります。
このStack Clashは、大まかに以下の手順で実施できます。
スタックの開始アドレスが他のメモリ領域の上限に当たるまで、または他のメモリ領域の上限がスタック開始アドレスに達するまでメモリを割り当てます。他のメモリ領域の例は下記です。
このステップではメモリを割り当てる際に下記を利用します。
なお「ヒープとAnonymous mmap()」は、LinuxではLD_AUDITのメモリリーク(glibc: CVE-2017-1000366)を利用します。
スタックを割り当てに使う場合は、コマンドラインの引数と環境変数を利用します。LinuxではRLIMIT_STACKの4分の1です(man execve参照)。しかし、「Linux KernelでRLIMIT_STACK/RLIM_INFINITYの制限を、引数や環境変数が迂回できてしまう脆弱性(CVE-2017-1000365)」を利用することで、アプリケーションに依存せずにメモリの割り当てが可能になっています。
スタックポインタは通常スタック開始アドレスの数KB上にあります。Linuxでは、ポインタのargv[]とenvp[]配列を利用して、最初のスタック拡張の128KBを消費できます。
スタックポインタをスタックガードページに触れさせずに、スタックから【ステップ1】で衝突させた範囲まで移動させます。この【ステップ3】では、下記のような条件を満たす、大きなスタックベースのバッファー、alloca()、VLA(可変長配列)のいずれかが必要になります。
「Qualys Security Advisory」では、Linux上で、スタックガードページを飛び越えさせる一般的な方法として、a「LANGUAGE環境変数を用いる」、b「ld.soのLD_LIBRARY_PATH環境変数のコピーであるllp_tmpを用いる」など幾つか考えられています。
【ステップ4】は、前述の【ステップ3】におけるa、bから2通りに分けられます。
4a:スタック(espがまだポイントしている状態)以下のメモリ領域に書き込みます。この方法は、exim、sudo、suを用いた攻略時に使用されます。
4b:スタックに書き込んで衝突しているメモリ領域を破壊します。これはld.soを用いた攻略時に使用されます。この場合は、アプリケーション固有のものになります。
Copyright © ITmedia, Inc. All Rights Reserved.