はじめに
いよいよ新年度が本格的に始まりましたね。新年度ということで初心に立ち返り、今回は基本的な脆弱性の1つである「スタックバッファオーバーフロー」を取り上げたいと思います。具体的には、2011年にPHPにて発見されたCVE-2011-1938を解説します。多くのWebアプリケーションなどで利用されているPHPに、いったいどのような脆弱性があったのか、さっそく見ていきましょう!
今回の脆弱性:CVE-2011-1938
CVE-2011-1938は、PHPに標準的に用意されているsocket_connect
関数の中に潜んでいた、スタックバッファオーバーフローの脆弱性です。
「PHPに潜む脆弱性って、なんだかイメージが湧きづらい……」と思った方もいると思います。これは、PHPのインタプリタ側に存在する脆弱性のことを指し、簡単に言えば不正なPHPのプログラムをインタプリタに解釈・実行させた場合にその脆弱性が発現します。今回の場合は、不正なデータをsocket_connect
関数の引数として渡して実行することで脆弱性が発現します。
実際にそのようなプログラム(CVE-2011-1938.php)を筆者のほうで書いて実行した様子が図1です(ファイルの中身は後ほど紹介)。「buffer overflow detected」と表示され、PHPが異常終了しているのがわかります。
このプログラムの内部では、いったいどのような処理がなされているのか、読者としては気になるところだと思います。ですが、中身の解説を行う前に、その理解を促すためにも、まずは「そもそもスタックバッファオーバーフローとは何か」について説明します。
スタックバッファオーバーフローとは
まずバッファオーバーフローとは、「コンピュータのメモリ上で確保されたある領域(バッファ)に対して、その大きさ以上のデータを書き込めてしまうこと」に起因する脆弱性です。これは、開発者による実装ミスなどで作り込まれてしまいます。
メモリ領域にもさまざまな種類が存在し、関数内で利用する一時的なローカル変数などを保存する領域は「スタック領域」と呼ばれます。そして、スタック領域上で生じるバッファオーバーフローのことを「スタックバッファオーバーフロー」と呼びます。
ここで、「データを想定以上に書き込めてしまうことの、いったい何が問題なのか?」と疑問に思った方もいると思います。これは簡単に言えば、図2に示すように、もしプログラムの正常な続行に必要な「大事なデータ」がバッファの付近に存在した場合、上書きできてしまう[1]のが問題なのです。図2の場合は、バッファサイズを超える大量の「A」という文字を、バッファへの入力データとして与えた結果、大事なデータが上書きされてしまった様子です。
脆弱性を突く手法
脆弱性を悪用する際は、前述の挙動が利用されます。やり方はさまざまあるのですが、代表的な手法としては「関数の戻りアドレスを書き換える」というものがあります。
とある関数内でさらに別の関数を呼び出すことは、プログラミングをしたことのある方ならほぼ誰もが経験したことがあると思います。一般的に、このような関数呼び出しが行われる際、呼び出し元の関数に戻るための「戻りアドレス」がスタック上に保存されます。戻りアドレスがないと、呼び出し先関数の実行終了後、呼び出し元関数のどこから実行を再開していいのかわからなくなるからです。
スタックバッファオーバーフローの脆弱性があった場合、その戻りアドレスを上書きすることが可能になります。図3のように、たとえば戻りアドレスを「AAAA……」という文字列で上書きした場合、プログラムを異常終了させることができます。それだけではなく、もし攻撃者が指定したアドレスに書き換えることができればどうなるでしょうか。その指定したアドレスの先に、攻撃者が用意した悪意あるコードがあれば、それが実行されてしまうのです[2]。これが、いわゆる「脆弱性を悪用する」手法の一例です。
CVE-2011-1938.phpの中身
では、スタックバッファオーバーフローの概念について理解できたところで、いよいよPHPを異常終了させたプログラムであるCVE-2011-1938.phpの中身を見ていきます。「脆弱性を発現させるために、難しいプログラムを書いているのだろう」と思う方も多いと思いますが、実は高度なことをいっさい行っていません。
リスト1のとおり、CVE-2011-1938.phpは実質、たった3行のプログラムになります。では、このたった3行で何をやっているのでしょうか。理解を促すにあたり、まずは背景技術から説明します。
ソケット通信(UNIXドメインソケット)とは
そもそも、今回の脆弱性が存在するsocket_connect
関数は、PHPでソケット通信を行う際に利用するものです。ネットワークプログラミングを行う人には馴染み深いものだと思いますが、ソケットとは、プログラムがネットワーク通信や、ほかのプログラムとの通信を行う際に用います。通信のための接続口だとイメージすればわかりやすいと思います。
ソケットを利用する際には、簡単に言えば接続先や、接続をする際に利用したいプロトコル(ドメイン)や通信方式などを指定する必要があり、そこに今回の脆弱性が潜んでいました。具体的にはUNIXドメインが指定されたソケット(UNIXドメインソケット)を利用し、かつその接続先(ソケット名)として、細工した文字列を指定した場合に発現するものでした。
「UNIXドメインソケット」自体聞き慣れない方もいると思います。これは簡単に言えば、ローカルでプロセス同士が通信するために利用されるものです( 図4 )。通常、インターネット越しに通信を行う際、接続先(ソケット名)としては、IPアドレスなどを指定します。一方で、UNIXドメインソケットを利用してプロセス間通信を行う際は、「ソケットファイル」と呼ばれる特殊なファイルのパス名を指定します。
以上までわかったところで、CVE-2011-1938.phpを1行ずつ解説していきます。
プログラムの解説
CVE-2011-1938.phpのプログラム(リスト1)は、表1に掲載されている3つの関数を利用して書かれています。具体的には、まずsocket_create
関数を利用してソケットを作成しています。その際、UNIXドメインソケットを利用したいため、第1引数にはAF_UNIX
と指定しています。また作成したソケットは、socket
という名の変数に格納しています。
表1 プログラム内で利用されている関数の概要
関数名 |
概要 |
第1引数 |
第2引数 |
第3引数 |
socket_create |
ソケットを作成する |
ソケットが利用するプロトコル(アドレス)ファミリ |
ソケットが利用する通信方式 |
ソケットが利用するプロトコル |
str_repeat |
文字列を反復する |
繰り返す文字列 |
繰り返す回数 |
|
socket_connect |
ソケット上の接続を初期化する |
socket_create で作成したソケット |
AF_UNIX が指定された場合は、ソケットファイルのパス名(例:/tmp/test.sock) |
(AF_UNIX の場合必要なし) |
次にAを1,000回繰り返した文字列を、str_repeat
関数を利用して生成し、address
という変数に保存します。最後にsocket_connect
関数にて、ソケット上の接続を初期化しています。その際ソケット名としては、address
に格納されている「AAAA……(千文字)」が指定されています。
何が問題なのか
このプログラムの何が問題かというと、ソケット名が入るべきsocket_connect
の引数に、大量のAという文字列を入れていることです。後ほど詳しく解説しますが、このソケット名を保存するために、スタック上にて固定サイズのバッファが確保されていました。開発者が想定したようなソケット名であれば、この仕様に何の問題もありません。ですが今回のように、そのバッファを上回るような非常に長いソケット名が指定された場合、バッファオーバーフローが発生してしまうのです( 図5 )。
実際の脆弱性箇所について
脆弱性の概要が理解できたところで、実際の脆弱性箇所を見ていきます。脆弱性自体はsocket.c
というファイルの中のリスト2の部分に存在していました。この部分は、socket_connect
関数が呼び出された際、AF_UNIX
(UNIXドメインソケット)が引数に指定されていた場合に対する処理を記した部分です。
今回、脆弱性の種類がスタックバッファオーバーフローとわかっているので、「どのバッファに対して、バッファを上回るサイズのデータが、どこで書き込まれてしまうのか?」を意識しながら解析していきます。
バッファオーバーフローが引き起こされる箇所
解析した結果、リスト2の中でもmemcpy
関数が呼ばれている次の箇所で、バッファオーバーフローが引き起こされていました。
memcpy(&s_un.sun_path, add, addr_len);
memcpy
自体は、指定バイト数分のメモリをコピーする、C言語を利用する方には馴染み深い関数で、表2のような3つの引数を受け取ります。
表2 memcpy
関数の引数について
引数 |
引数の定義 |
意味 |
今回の場合 |
第1引数 |
void *dest |
コピー先のメモリ領域 |
&s_un.sun_path |
第2引数 |
const void *src |
コピー元のメモリ領域 |
addr |
第3引数 |
size_t n |
コピーするデータのサイズ |
addr_len |
今回の場合、コピー先の領域としてはs_un.sun_path
が指定されており、これがソケット名を格納するためのバッファにあたります。
そしてコピー元の領域であるaddr
に、ユーザーが指定したソケット名が格納されています。最後にaddr_len
には、ユーザーが指定したソケット名の文字列の長さが格納されています。
今回の脆弱性は、ソケット名を格納するための領域(s_un.sun_path
)に、その領域を上回る文字列(AAAA……)が書き込まれてしまうことに起因していました。そこで次に、s_un.sun_path
を詳しく解析していきます。
オーバーフローするバッファ
脆弱性箇所から上にさかのぼってソースコードを読んでいくと、s_un
は、sockaddr_un
という構造体の変数であることが判明しました。
struct sockaddr_un s_un;
この構造体はいったい何者なのでしょうか。
調べてみると、これはUNIXドメインソケットを利用した場合に、ソケットの情報を格納するための構造体であることが判明しました。リスト3がこの構造体の定義です。構造体のメンバの1つとして、char型の配列sun_path
が定義されているのがわかります。そしてsun_path
は、配列サイズとしてはUNIX_PATH_MAX
が指定されています。では、このUNIX_PATH_MAX
は何かというと、ソケットファイル名の最大長を定義したものであり、Linuxでは108であることがわかります。
まとめると、s_un.sun_path
はソケット名を格納するために用意されたchar型の配列であり、そのサイズは108バイトであるということです。また一般的に、関数内で宣言/利用されるローカルな変数は、スタック上にそのデータの保管領域が確保されますが、今回のs_un.sun_path
も同様です。そのため、108バイトを超える文字がソケット名として指定されると、図5のようなスタックバッファオーバーフローにつながってしまうのです。
脆弱性の修正方法
では、この脆弱性が実際にどのように修正されたかを見ていきます[3]。リスト4が修正後のソースコードです。読んでみると、脆弱性箇所の直前に、新たにif文が追加されたのがわかります。
このif文では、ユーザーから入力されたソケット名の長さ(addr_len
)が、ソケット名の最大長(sizeof(s_un.sun_path)
)以上か否かを調べています。そして、最大長以上のソケット名が利用されていた場合、通常の処理を行わず、エラー処理を行ってそのまま処理を終了します。この処理を追加することにより、長大なソケット名が指定されていたとしても、バッファオーバーフローの発生を回避できます。このような値の検査を追加するような修正は、スタックバッファオーバーフローの修正としてはよくあることで、今回の修正もそれにならったものだと思われます。
最後になりましたが、この脆弱性はPHPのバージョン5.3.3から5.3.6に存在します[4]。該当するバージョンを利用している方は、最新版にアップデートすることをお勧めします。
さらに勉強したい人向け
さらに勉強したい方に向けて、関連する書籍などを紹介します。まず、スタックバッファオーバーフローの原理や対策についてさらに詳しく書いた書籍として、筆者の著書『サイバー攻撃』[5]があります。また、実際に脆弱性がどのように悪用されるかを技術的な観点で解説した書籍として『Hacking:美しき策謀 第2版』[6]があります。とくに後者は業界で20年近く読み親しまれてきた書籍であり、お勧めです。
それではみなさん、また次回お会いしましょう!