もろず blog

もろちゃんがITに関しての様々なトピックを解説します

bash の脆弱性 "Shell Shock" のめっちゃ細かい話 (CVE-2014-6271)


f:id:chanmoro999:20140927195501p:plain

※(2014/10/1 追記) 脆弱性の番号を誤って CVE-2014-6721 と表記してしまっていました
正しくは "CVE-2014-6271" です 失礼致しました

※(2014/10/7 追記) 2014/10/7 14:00時点で Shell Shock への修正パッチは6個 公開されています
既に対応済みのシステムでもパッチの漏れがないか注意してください


シェルに脆弱性が見つかったらしいです

このコマンドを実行すると脆弱性があるバージョンかのチェックができるようです

$ env x='() { :;}; echo vulnerable' bash -c "echo this is a test"

以下のように表示されたらアウトです

vulnerable
this is a test


どうやら、このコマンドが正常に実行できるというのがこの脆弱性の正体らしく、

echo vulnerable

の部分には任意のコマンドを入れることができます


どこまでのコマンドが実行可能かどうかは 実行ユーザーの権限によりますが、
場合によっては完全にサーバーを乗っ取られる可能性があります

脆弱性についての危険性や対処方法は既にたくさんの記事が書かれているため、
詳しくは以下のサイトをご確認ください

日本語での有用な情報をまとめてくれています

bashの脆弱性(CVE-2014-6271) #ShellShock の関連リンクをまとめてみた - piyolog


今回の記事では bash がどんな実装になっていたのが問題で、
どういった修正がされたのかを紐解いていきます

シェル自体のソースを見る機会なんてまずないので、いい機会だと思って一緒に勉強しましょう


自宅の Macbash を見ると version 3.2 だったので、bash 3.2 のソースを元に調査します


この記事では
1. SehllShock (CVE-2014-6271) の概要
2. ソースコードの入手とパッチの適用
3. 修正箇所について
4. コードが実行されるまでの詳細について
5. まとめ
について説明します


1. SehllShock (CVE-2014-6271) の概要

環境変数にある特殊な構文の bash コードがセットされていると、bash の起動時にそのコードが勝手に実行してしまう、というものです

そのため、外部から何でもいいのでどれかの環境変数に書き込むことができるようになっていると、ShellShock の脆弱性をついた攻撃が可能になってしまいます

これにより OSコマンドインジェクションという種類の攻撃が可能になります


bash は起動時に その時設定されている環境変数を読み込んでから処理を開始します

通常は環境変数の値にコードが入っていても、明示的に実行しない限りは文字列として扱われる仕様になっています

ですが、この部分の処理にバグがあり、bash 起動時の環境変数を読み込むタイミングでこの文字列がコードとして実行されてしまう、というのが今回明らかになった問題点です


ソースを調査した結果、脆弱性を含んだ bash のバージョンでは以下のような処理になっていることが分かりました

f:id:chanmoro999:20140927190200p:plain
ここからは調査したソースの解説をしていきます


記事の先頭に戻る


2. ソースコードの入手とパッチの適用

パッチの適用前後のソースを確認したいので bashソースコードを入手します

以下の記事を参考にさせていただき、
ソースのダウンロードと手動でのパッチ適用を行いました
CVE-2014-6271のbashの脆弱性に対応する方法


今回の脆弱性への修正パッチのバージョンは 3.2.52 とのことなので、
bash-3.2.51 と bash-3.2.52 のソースを用意します

$ curl https://opensource.apple.com/tarballs/bash/bash-92.tar.gz | tar zxf -
$ mv bash-92 bash-3.2.51
$ cp -pr bash-3.2.51 bash-3.2.52
$ cd bash-3.2.52/bash-3.2
$ curl https://ftp.gnu.org/pub/gnu/bash/bash-3.2-patches/bash32-052 | patch -p0


今回はあくまでソースの確認が目的なので、本家の GNU からダウンロードしてきてもいいです
Index of /pub/gnu/bash

opensource.apple.com なんてものを初めて知ったのですが、appleオープンソースのソフトウェアを公開しているサイトらしいです
Open Source - Releases


記事の先頭に戻る


3. 修正箇所について

GNU が公開しているパッチファイルと、
先ほどダウンロードしてきたソースの差分をそれぞれ確認します

GNU で公開されているパッチファイル

http://ftp.gnu.org/pub/gnu/bash/bash-3.2-patches/bash32-052

先ほど入手したソースの差分
$ diff -rq bash-3.2.52 bash-3.2.51
Files bash-3.2.52/bash-3.2/builtins/common.h and bash-3.2.51/bash-3.2/builtins/common.h differ
Files bash-3.2.52/bash-3.2/builtins/evalstring.c and bash-3.2.51/bash-3.2/builtins/evalstring.c differ
Files bash-3.2.52/bash-3.2/patchlevel.h and bash-3.2.51/bash-3.2/patchlevel.h differ
Files bash-3.2.52/bash-3.2/variables.c and bash-3.2.51/bash-3.2/variables.c differ


差分を確認すると、以下の4ファイルに修正が入っているようです

  • bash-3.2/builtins/evalstring.c
  • bash-3.2/builtins/common.h
  • bash-3.2/patchlevel.h
  • bash-3.2/variables.c

1つづつ内容を見て行きましょう

bash-3.2/builtins/evalstring.c parse_and_execute ()

エラーを吐いて処理を停止させる感じのコードが追加されています
恐らく今回の脆弱性に対応したコアな部分でしょう

$ diff bash-3.2.52/bash-3.2/builtins/evalstring.c bash-3.2.51/bash-3.2/builtins/evalstring.c
237,244d236
< 	      if ((flags & SEVAL_FUNCDEF) && command->type != cm_function_def)
< 		{
< 		  internal_warning ("%s: ignoring function definition attempt", from_file);
< 		  should_jump_to_top_level = 0;
< 		  last_result = last_command_exit_value = EX_BADUSAGE;
< 		  break;
< 		}
< 
302,304d293
< 
< 	      if (flags & SEVAL_ONECMD)
< 		break;
bash-3.2/variables.c initialize_shell_variables ()

parse_and_execute () を呼び出す際の引数が変更されています
また、削除されている箇所もありましたが、ソース上のコメントからは 下位互換性を保つためのコードのようです

$ diff bash-3.2.52/bash-3.2/variables.c bash-3.2.51/bash-3.2/variables.c
321,324c321,326
< 	  /* Don't import function names that are invalid identifiers from the
< 	     environment. */
< 	  if (legal_identifier (name))
< 	    parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);
---
> 	  parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST);
> 
> 	  /* Ancient backwards compatibility.  Old versions of bash exported
> 	     functions like name()=() {...} */
> 	  if (name[char_index - 1] == ')' && name[char_index - 2] == '(')
> 	    name[char_index - 2] = '\0';
332a335,338
> 
> 	  /* ( */
> 	  if (name[char_index - 1] == ')' && name[char_index - 2] == '\0')
> 	    name[char_index - 2] = '(';		/* ) */
bash-3.2/builtins/common.h

SEVAL_FUNCDEF、SEVAL_ONECMD の2つの定数が追加されています
この定数は上の2つで追加された処理内で利用されているものです

$ diff bash-3.2.52/bash-3.2/builtins/common.h bash-3.2.51/bash-3.2/builtins/common.h
36,37d35
< #define SEVAL_FUNCDEF	0x080		/* only allow function definitions */
< #define SEVAL_ONECMD	0x100		/* only allow a single command */
bash-3.2/patchlevel.h

これはパッチ番号を表す定数っぽいですね

$ diff bash-3.2.52/bash-3.2/patchlevel.h bash-3.2.51/bash-3.2/patchlevel.h
28c28
< #define PATCHLEVEL 52
---
> #define PATCHLEVEL 51

記事の先頭に戻る


4. コードが実行されるまでの詳細について

bash が起動されてからの処理を追うと、以下の順番で処理が実行されていました

1.bash-3.2/shell.c main ()

2.bash-3.2/shell.c shell_initialize ()

3.bash-3.2/variables.c initialize_shell_variables ()

4.bash-3.2/builtinsevalstring.c parse_and_execute ()

5.bash-3.2/builtinsevalstring.c execute_command_internal ()


パッチを適用する前の initialize_shell_variables () を細かく見てみましょう


引数で渡される env には環境変数の一覧が格納されています

variables.c initialize_shell_variables ()
/* Initialize the shell variables from the current environment.
   If PRIVMODE is nonzero, don't import functions from ENV or
   parse $SHELLOPTS. */
void
initialize_shell_variables (env, privmode)
     char **env;
     int privmode;
{

env で渡された内容が1つ1つチェックされます
"() {" から始まっている文字列は処理が分岐され、今回の脆弱性で問題となった parse_and_execute () に環境変数の値を渡して入って行きます

variables.c initialize_shell_variables ()
      /* If exported function, define it now.  Don't import functions from
	 the environment in privileged mode. */
      if (privmode == 0 && read_but_dont_execute == 0 && STREQN ("() {", string, 4))
	{
	  string_length = strlen (string);
	  temp_string = (char *)xmalloc (3 + string_length + char_index);

	  strcpy (temp_string, name);
	  temp_string[char_index] = ' ';
	  strcpy (temp_string + char_index + 1, string);

	  parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST);

parse_and_execute () という関数の名前からは、渡された文字列のコマンドをパースして実行する処理だということが想像できます
すごく怪しそうな感じがします

evalstring.c parse_and_execute ()
int
parse_and_execute (string, from_file, flags)
     char *string;
     const char *from_file;
     int flags;
{

この後の parse_command () 内の処理にて渡された文字列がパースされます
構文エラーが無かった場合には parse_command () から戻り値 0 が返されます

evalstring.c parse_and_execute ()
      if (parse_command () == 0)
	{
	  if (interactive_shell == 0 && read_but_dont_execute)
	    {
	      last_result = EXECUTION_SUCCESS;
	      dispose_command (global_command);
	      global_command = (COMMAND *)NULL;
	    }
	  else if (command = global_command)
	    {

// -------- 省略 --------

		}
	      else
		last_result = execute_command_internal
				(command, 0, NO_PIPE, NO_PIPE, bitmap);

parse_command () の処理では 与えられた文字列がどんな構文のコマンドなのかをチェックし、判定されたコマンドの種類が command->type にセットされます
また、パースした結果構文エラーが無ければ、global_command には環境変数の文字列がセットされます

そして、execute_command_internal () 内で構文チェックを終えたコマンドに関しての処理が実行されます

execute_command_internal という名前からして、
ここに入るとどんな処理でも実行されてしまいそうに見えます


execute_command_internal () の内部ではパースで判定された command->type の値によって処理が分岐されます

文字列の内容が関数の定義として認識された場合には command->type に cm_function_defがセットされるため、execute_command_internal () 内では以下の処理が実行されます

execute_cmd.c execute_command_internal ()
    case cm_function_def:
      exec_result = execute_intern_function (command->value.Function_def->name,
					     command->value.Function_def->command);
      break;

この時実行されている execute_intern_function () はコマンドの文字列を関数としてバインドするだけの処理で、関数を実行する処理にはなっていませんでした

なので正しい?構文で関数定義が指定されている場合には、環境変数に関数としてバインドされるだけで関数そのものの処理が実行されることはないことが分かりました


さて、問題は

x='() { :;}; echo vulnerable'

という文字列がなぜ環境変数の読み込み時に実行されてしまうかです


細かい所を見て行くと、上記の文字列をパースした結果は cm_function_def として判定されておらず、以下のような構造の cm_connection という種類に判定されていました

cm_function_def ; cm_simple

cm_connection は "&" 、";"、"|" などでコマンドが連結されている構造を示すものです

x=() { :;} の部分が cm_function_def として解釈され、
echo vulnerable の部分が cm_simple として解釈されていました


cm_connection の場合の execute_intern_function () の処理は以下になっており、; で繋がれた2つのコマンドは左側から1つづつ execute_intern_function () が実行されます

これはワンライナーのように、コマンドを ; で区切って記述した場合の処理なので、特別なものではありません
当然、この処理自体がバグを含んでいるわけではありません


処理の詳細を見てみましょう
まず、cm_connection のコマンドは execute_connection () が実行されます

execute_cmd.c execute_intern_function ()
    case cm_connection:
      exec_result = execute_connection (command, asynchronous,
					pipe_in, pipe_out, fds_to_close);
      break;

execute_connection () の中身です
連結している文字が ";" の場合は左側から1つづつ実行されていることが分かります
右側のコマンドはさらに 連結されたものである可能性があるので execute_command_internal () で実行されているようです

execute_cmd.c execute_connection ()
    /* Just call execute command on both sides. */
    case ';':
      if (ignore_return)
	{
	  if (command->value.Connection->first)
	    command->value.Connection->first->flags |= CMD_IGNORE_RETURN;
	  if (command->value.Connection->second)
	    command->value.Connection->second->flags |= CMD_IGNORE_RETURN;
	}
      QUIT;
      execute_command (command->value.Connection->first);
      QUIT;
      exec_result = execute_command_internal (command->value.Connection->second,
				      asynchronous, pipe_in, pipe_out,
				      fds_to_close);
      break;

cm_simple の場合は単純にコマンドが実行される処理になっていました

execute_cmd.c execute_intern_function ()
    case cm_simple:
      {
	save_line_number = line_number;

// -------- 省略 --------

	exec_result =
	  execute_simple_command (command->value.Simple, pipe_in, pipe_out,
				  asynchronous, fds_to_close);

5. まとめ

今までの話をまとめると、環境変数のセットアップを行う処理の中で関数のバインドを行うだけだと思っていた処理が、実は関数定義の後に続くコマンドが勝手に実行されてしまう実装になっていたわけです

これに対しての修正内容は、bash 起動時の環境変数セットの時だけチェック処理を追加しています
環境変数の "() {" から始まっている文字列がcm_function_def と判定されれば通常のバインド処理を実行し、
それ以外の場合はエラーとなるように修正されていました

この差分がまさにそのチェック処理の部分です
bash 起動時の環境変数セットの時だけ、flags に SEVAL_FUNCDEF、SEVAL_ONECMD のビットがセットされるようになっています

$ diff bash-3.2.52/bash-3.2/builtins/evalstring.c bash-3.2.51/bash-3.2/builtins/evalstring.c
237,244d236
< 	      if ((flags & SEVAL_FUNCDEF) && command->type != cm_function_def)
< 		{
< 		  internal_warning ("%s: ignoring function definition attempt", from_file);
< 		  should_jump_to_top_level = 0;
< 		  last_result = last_command_exit_value = EX_BADUSAGE;
< 		  break;
< 		}
< 
302,304d293
< 
< 	      if (flags & SEVAL_ONECMD)
< 		break;


なんだか、ホントにその修正で大丈夫なの?という感じがとてもしますが、超緊急の暫定対応というところなんでしょうか

関数のバインドとコマンドの実行という全く別の性質を持つ処理が parse_and_execute () という関数にまとめられてしまっていることが、混乱を招く原因になってしまっているように思えます
ただ、そこを改修するのは相当大変そうなのが分かるので、とりあえずの対応としてこういった修正になっているんでしょう


実際、この修正を適用した後もまだ実行できるコードがあるよ!という報告があり、CVE-2014-7169 として対応されています
かなりトリッキーなことをしていて興味深かったので、これについてはまた別途記事を書こうと思います