戸田 洋三(JPCERTコーディネーションセンター) [著]
本連載では、脆弱性を含むサンプルコードを題材に、修正方法の例を解説していきます。今回取り上げるコードは、Windowsからアクセスできるファイルサーバーを構築するためのオープンソースソフトウェア「Samba」です。
今回はSambaを取り上げます。Sambaは、Unix系マシンをWindowsファイル共有サーバーにするためのソフトウェアとして有名です。市販のNAS(ネットワーク接続ストレージ)の多くも、LinuxとSambaの組み合わせでその機能を実現しています。ここでは、昨年2009年に発見・修正された問題を取り上げてみましょう。
サンプルコード
Sambaでは、種々のサーバープログラムと共に、サーバーに接続するためのクライアントプログラムも提供しています。今回は、そのクライアントプログラムのソースコードに含まれているcmd_get()という関数に注目しましょう。それほど長い関数ではありませんが、他の部分で定義されている関数をいろいろ呼び出しているので、それらのプロトタイプ宣言とコメント部分を一緒に並べてあります。
■samba-3.2.12/source/client/client.c 他より抜粋/* Get us the current top of the talloc stack. */ TALLOC_CTX *talloc_tos(void); /* strdup with a talloc */ char *talloc_strdup(const void *t, const char *p); /* Get the next token from a string, return false if none found. */ bool next_token_talloc(TALLOC_CTX *ctx, const char **ptr, char **pp_buff, const char *sep); /* print intenal strings in the "display charset" */ int d_printf(const char *format, ...); /* Realloc s to append the formatted result of fmt and return s, which may have moved. Good for gradually accumulating output into a string buffer. */ char *talloc_asprintf_append(char *s, const char *fmt, ...); /* Reduce a file name, removing .. elements. */ char *clean_name(TALLOC_CTX *ctx, const char *s); /* Get a file from rname to lname */ static int do_get(const char *rname, const char *lname_in, bool reget); /**************************************************************************** Get a file. ****************************************************************************/ static int cmd_get(void) { TALLOC_CTX *ctx = talloc_tos(); char *lname = NULL; char *rname = NULL; char *fname = NULL; rname = talloc_strdup(ctx, client_get_cur_dir()); if (!rname) { return 1; } if (!next_token_talloc(ctx, &cmd_ptr,&fname,NULL)) { d_printf("get[localname]\n"); return 1; } rname = talloc_asprintf_append(rname, fname); if (!rname) { return 1; } rname = clean_name(ctx, rname); if (!rname) { return 1; } next_token_talloc(ctx, &cmd_ptr,&lname,NULL); if (!lname) { lname = fname; } return do_get(rname, lname, false); }
コードの説明
Sambaのクライアントプログラムsmbclientは、コマンドラインインターフェイスのプログラムです。ftpコマンドと同様なもの、と言えば大体想像のつく方も多いのではないでしょうか。サービス名を引数に指定して起動するとそのサービスを提供するサーバーに接続し、プロンプトが出てきます。それに対してコマンドを入力していくことで、ファイルのアップロードやダウンロード、ディレクトリ操作などを行うものです。
各コマンドに対応してその処理を行う関数が定義されており、getコマンドに対応する関数がcmd_get()です。ユーザーがプロンプトに対して「get filename」と入力すると、サーバー上の「filename」というファイルをダウンロードしますが、実際の処理はこのcmd_get()関数のなかで行われています。
cmd_get()の処理を上から順番に見ていきましょう。
- talloc_strdup()でサーバ側のカレントディレクトリを取得
- next_token_talloc()でgetコマンドの引数に指定されたファイル名を取り出す
- talloc_asprintf_append()を使ってディレクトリ名とファイル名を連結したパスを表す文字列を生成
- clean_name()で、パス名のなかに".."などが含まれているときはそれを含まない形に直す
- next_token_talloc()でダウンロードしてきたファイルを保存するときに使うローカル側のファイル名を取り出す(指定されていなければリモート側でのファイル名をそのまま使う)
- 最後にdo_get()で、ファイルをダウンロードして保存して終了
「talloc」という文字列を含む関数がいくつか出てきますが、これはSambaで使われている動的メモリ管理ライブラリtallocのなかの関数です。Sambaではパス名やサービス名などの文字列の操作が頻繁に行われますが、それらを動的メモリ領域に置いて管理しています。malloc()やfree()などのC標準ライブラリ関数を直接使うとメモリ管理が繁雑になるため、不要になったメモリ領域をある程度自動的に解放できる仕組みをtallocライブラリ(trivial alloc)として実装して使っているのです。今ではこのtallocライブラリはSambaとは独立に公開されていますので、興味のある方はどのように実装されているか調べてみてください。
今回のコードでは、tallocライブラリの詳細まで知っている必要はありません。使われている関数の働きとその型情報を知っていれば、アヤシイ部分に気付くことができます。いかがでしょうか。どこに問題があるか見つけられましたか?
コードに潜む脆弱性の解説
今回のコードに潜む脆弱性は書式指定文字列に関する脆弱性です。
talloc_asprintf_append()の使い方に問題があります。この関数の型を確認してみましょう。char *talloc_asprintf_append(char *s, const char *fmt, ...);
「...」という部分は何かを省略しているわけではなく、実際にこのように記述します。そう、printf()を代表とする可変引数関数です。プロトタイプ宣言と一緒に引用したコメントにも書いてあるように、talloc_asprintf_append()は、第2引数の書式指定文字列を使って第3引数以降で指定されるデータを整形した文字列を、第1引数のsが示す文字列の後ろに連結した文字列を生成します。
talloc_asprintf_append()の呼び出しでは、第2引数のfnameはユーザーが入力した文字列から取ってきています。ここにはユーザーが指定したファイル名が入っているわけですが、talloc_asprintf_append()では、第2引数は書式指定文字列だと想定しています。
では、ユーザーが「get aa%sbb」のような入力を行って書式指定子「%s」を含むファイル名を与えたらどうなるでしょうか? talloc_asprintf_append()はその後ろに「%s」にはめこむべき文字列引数が置いてあるものとしてアクセスしにいくでしょう。しかし、実際の呼び出しは第2引数までしかありません。このコードでは、スタックに残されていたデータを勝手に「%s」の部分にはめこんで処理することになります。
この問題は「書式指定文字列の脆弱性」として知られています。一般にプログラムで使われている書式指定文字列を自由に操作できる場合、スタックに積まれている値を調べることが可能です。また、メモリアドレスを指定してその値を調べたり、さらには任意のメモリアドレスに指定した値を書き込んだり、入力に潜ませたコードを実行させることが可能になることもあります。
今回紹介したコードでは、smbclientプログラムへの入力を処理する部分の問題であり、コマンドラインからsmbclientを起動して使う通常の使用では問題にならないと思われます。しかし、自動化スクリプトやWebアプリケーションのなかからsmbclientを呼び出しているようなケースの場合、外部からの攻撃に悪用される危険性は十分に考えられます。
Sambaでは3.2.13でこの問題を修正しています。修正はいたって簡単。書式指定文字列を明示的に与えるだけです。
■samba-3.2.13 における修正- remote_name = talloc_asprintf_append(remote_name, fname); + remote_name = talloc_asprintf_append(remote_name, "%s", fname);
この修正により、書式指定文字列として解釈されるのは「%s」となり、fnameはそこにはめこまれるためにだけ使われるようになります。
なお、cmd_get()から呼び出される関数のなかで、d_printf()もtalloc_asprintf_append()と同様の可変引数関数として定義されており、書式指定文字列を引数に取ります。
int d_printf(const char *format, ...);
しかし、実際の呼び出しでは、外部入力に左右されない文字列リテラルを書式指定文字列として引数に渡しているため、悪用される心配はありません。
終わりに
書式指定文字列の問題は、1999年ころから広く知られるようになり、printf()やsyslog()関数を使う多くのアプリケーションがこぞって修正を行いました。
今回のような問題を作りこまないようにするには、プログラム中で使われる書式指定文字列を外部からの入力で操作されないようにすることが必要です。セキュアコーディングスタンダードでは、「FIO30-C.ユーザーからの入力を使って書式指定文字列を組み立てない」というルールがあります。こちらも一度眺めてみてください。