OSSに空いたセキュリティーホールと「脆弱性のふさぎかた」 その対策方法を調べる

第4回PHPの脆弱性 ~スタックバッファオーバーフロー~

はじめに

いよいよ新年度が本格的に始まりましたね。新年度ということで初心に立ち返り、今回は基本的な脆弱性の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が異常終了しているのがわかります。

図1 PHPが異常終了する様子
$ php CVE-2011-1938.php
*** buffer overflow detected ***: php terminated Aborted (core dumped)

このプログラムの内部では、いったいどのような処理がなされているのか、読者としては気になるところだと思います。ですが、中身の解説を行う前に、その理解を促すためにも、まずは「そもそもスタックバッファオーバーフローとは何か」について説明します。

スタックバッファオーバーフローとは

まずバッファオーバーフローとは、⁠コンピュータのメモリ上で確保されたある領域(バッファ)に対して、その大きさ以上のデータを書き込めてしまうこと」に起因する脆弱性です。これは、開発者による実装ミスなどで作り込まれてしまいます。

メモリ領域にもさまざまな種類が存在し、関数内で利用する一時的なローカル変数などを保存する領域は「スタック領域」と呼ばれます。そして、スタック領域上で生じるバッファオーバーフローのことを「スタックバッファオーバーフロー」と呼びます。

ここで、⁠データを想定以上に書き込めてしまうことの、いったい何が問題なのか?」と疑問に思った方もいると思います。これは簡単に言えば、図2に示すように、もしプログラムの正常な続行に必要な「大事なデータ」がバッファの付近に存在した場合、上書きできてしまう[1]のが問題なのです。図2の場合は、バッファサイズを超える大量の「A」という文字を、バッファへの入力データとして与えた結果、大事なデータが上書きされてしまった様子です。

図2 スタックバッファオーバーフローの概念
図2

脆弱性を突く手法

脆弱性を悪用する際は、前述の挙動が利用されます。やり方はさまざまあるのですが、代表的な手法としては「関数の戻りアドレスを書き換える」というものがあります。

とある関数内でさらに別の関数を呼び出すことは、プログラミングをしたことのある方ならほぼ誰もが経験したことがあると思います。一般的に、このような関数呼び出しが行われる際、呼び出し元の関数に戻るための「戻りアドレス」がスタック上に保存されます。戻りアドレスがないと、呼び出し先関数の実行終了後、呼び出し元関数のどこから実行を再開していいのかわからなくなるからです。

スタックバッファオーバーフローの脆弱性があった場合、その戻りアドレスを上書きすることが可能になります。図3のように、たとえば戻りアドレスを「AAAA……」という文字列で上書きした場合、プログラムを異常終了させることができます。それだけではなく、もし攻撃者が指定したアドレスに書き換えることができればどうなるでしょうか。その指定したアドレスの先に、攻撃者が用意した悪意あるコードがあれば、それが実行されてしまうのです[2]。これが、いわゆる「脆弱性を悪用する」手法の一例です。

図3 スタックバッファオーバーフローを悪用して正常な実行を妨げる
図3

CVE-2011-1938.phpの中身

では、スタックバッファオーバーフローの概念について理解できたところで、いよいよPHPを異常終了させたプログラムであるCVE-2011-1938.phpの中身を見ていきます。⁠脆弱性を発現させるために、難しいプログラムを書いているのだろう」と思う方も多いと思いますが、実は高度なことをいっさい行っていません。

リスト1のとおり、CVE-2011-1938.phpは実質、たった3行のプログラムになります。では、このたった3行で何をやっているのでしょうか。理解を促すにあたり、まずは背景技術から説明します。

リスト1 脆弱性を発現させるPHPのプログラム(CVE-2011-1938.php)
<?php
$socket = socket_create(AF_UNIX, SOCK_STREAM, 1);
$address = str_repeat("A", 1000);
socket_connect($socket, $address);
?>

ソケット通信(UNIXドメインソケット)とは

そもそも、今回の脆弱性が存在するsocket_connect関数は、PHPでソケット通信を行う際に利用するものです。ネットワークプログラミングを行う人には馴染み深いものだと思いますが、ソケットとは、プログラムがネットワーク通信や、ほかのプログラムとの通信を行う際に用います。通信のための接続口だとイメージすればわかりやすいと思います。

ソケットを利用する際には、簡単に言えば接続先や、接続をする際に利用したいプロトコル(ドメイン)や通信方式などを指定する必要があり、そこに今回の脆弱性が潜んでいました。具体的にはUNIXドメインが指定されたソケット(UNIXドメインソケット)を利用し、かつその接続先(ソケット名)として、細工した文字列を指定した場合に発現するものでした。

「UNIXドメインソケット」自体聞き慣れない方もいると思います。これは簡単に言えば、ローカルでプロセス同士が通信するために利用されるものです図4⁠。通常、インターネット越しに通信を行う際、接続先(ソケット名)としては、IPアドレスなどを指定します。一方で、UNIXドメインソケットを利用してプロセス間通信を行う際は、⁠ソケットファイル」と呼ばれる特殊なファイルのパス名を指定します。

図4 UNIXドメインソケットの概要
図4

以上までわかったところで、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⁠。

図5 ソケット名を保存するためのバッファを上回る
図5

実際の脆弱性箇所について

脆弱性の概要が理解できたところで、実際の脆弱性箇所を見ていきます。脆弱性自体はsocket.cというファイルの中のリスト2の部分に存在していました。この部分は、socket_connect関数が呼び出された際、AF_UNIX(UNIXドメインソケット)が引数に指定されていた場合に対する処理を記した部分です。

リスト2 脆弱性箇所(socket.c)
case AF_UNIX:
    memset(&s_un, 0, sizeof(struct sockaddr_un));
    
    s_un.sun_family = AF_UNIX;
    memcpy(&s_un.sun_path, addr, addr_len);
    retval = connect(php_sock->bsd_socket, (struct sockaddr *) &s_un, (socklen_t) XtOffsetOf(struct sockaddr_un, sun_path) + addr_len);
    break;

今回、脆弱性の種類がスタックバッファオーバーフローとわかっているので、⁠どのバッファに対して、バッファを上回るサイズのデータが、どこで書き込まれてしまうのか?」を意識しながら解析していきます。

バッファオーバーフローが引き起こされる箇所

解析した結果、リスト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であることがわかります。

リスト3 sockaddr_un構造体の定義(Linuxのmanページより抜粋。ページ名は「unix」)
#define UNIX_PATH_MAX 108

struct sockaddr_un {
    sa_family_t sun_family;                /* AF_UNIX */
    char        sun_path[UNIX_PATH_MAX];   /* pathname */
};

まとめると、s_un.sun_pathはソケット名を格納するために用意されたchar型の配列であり、そのサイズは108バイトであるということです。また一般的に、関数内で宣言/利用されるローカルな変数は、スタック上にそのデータの保管領域が確保されますが、今回のs_un.sun_pathも同様です。そのため、108バイトを超える文字がソケット名として指定されると、図5のようなスタックバッファオーバーフローにつながってしまうのです。

脆弱性の修正方法

では、この脆弱性が実際にどのように修正されたかを見ていきます[3]リスト4が修正後のソースコードです。読んでみると、脆弱性箇所の直前に、新たにif文が追加されたのがわかります。

リスト4 修正後のソースコード
case AF_UNIX:
+    if (addr_len >= sizeof(s_un.sun_path)) {
+        php_error_docref(NULL TSRMLS_CC, E_WARNING, "Path too long", php_sock->type);
+        RETURN_FALSE;
+    }
+
    memset(&s_un, 0, sizeof(struct sockaddr_un));
    
    s_un.sun_family = AF_UNIX;
    memcpy(&s_un.sun_path, addr, addr_len);

このif文では、ユーザーから入力されたソケット名の長さaddr_lenが、ソケット名の最大長sizeof(s_un.sun_path)以上か否かを調べています。そして、最大長以上のソケット名が利用されていた場合、通常の処理を行わず、エラー処理を行ってそのまま処理を終了します。この処理を追加することにより、長大なソケット名が指定されていたとしても、バッファオーバーフローの発生を回避できます。このような値の検査を追加するような修正は、スタックバッファオーバーフローの修正としてはよくあることで、今回の修正もそれにならったものだと思われます。

最後になりましたが、この脆弱性はPHPのバージョン5.3.3から5.3.6に存在します[4]。該当するバージョンを利用している方は、最新版にアップデートすることをお勧めします。

さらに勉強したい人向け

さらに勉強したい方に向けて、関連する書籍などを紹介します。まず、スタックバッファオーバーフローの原理や対策についてさらに詳しく書いた書籍として、筆者の著書『サイバー攻撃』[5]があります。また、実際に脆弱性がどのように悪用されるかを技術的な観点で解説した書籍として『Hacking:美しき策謀 第2版』[6]があります。とくに後者は業界で20年近く読み親しまれてきた書籍であり、お勧めです。

それではみなさん、また次回お会いしましょう!

おすすめ記事

記事・ニュース一覧