Bashの脆弱性（CVE-2014-6271）のパッチをチラっとみた

http://ftp.gnu.org/pub/gnu/bash/bash-4.3-patches/bash43-025 を少しみてみた。あまりちゃんと読んでいないので、参考程度に。

# 脆弱性の概要

[seclistsの投稿](http://seclists.org/oss-sec/2014/q3/650)によると、

> Bash supports exporting not just shell variables, but also shell functions to other bash instances, via the process environment to
(indirect) child processes.  Current bash versions use an environment
variable named by the function name, and a function definition
starting with “() {” in the variable value to propagate function
definitions through the environment.  The vulnerability occurs because
bash does not stop after processing the function definition; it
continues to parse and execute shell commands following the function
definition.  For example, an environment variable setting of

どうも、環境変数に`() {`から始まる関数の定義があると、bashはその関数の定義だけで処理を停止せずにパーズしてしまい、関数定義に続くコマンドが実行されるということらしい。

# どうしてこうなったのだろうか

## variables.c

まず、パッチの _variables.c_ を書きかえている部分がこうなっている。

```diff
*** ../bash-4.3-patched/variables.c	2014-05-15 08:26:50.000000000 -0400
--- variables.c	2014-09-14 14:23:35.000000000 -0400
***************
*** 359,369 ****
  	  strcpy (temp_string + char_index + 1, string);
  
! 	  if (posixly_correct == 0 || legal_identifier (name))
! 	    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';
  
  	  if (temp_var = find_function (name))
--- 364,372 ----
  	  strcpy (temp_string + char_index + 1, string);
  
! 	  /* Don't import function names that are invalid identifiers from the
! 	     environment, though we still allow them to be defined as shell
! 	     variables. */
! 	  if (legal_identifier (name))
! 	    parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);
  
  	  if (temp_var = find_function (name))
```

どうも _variables.c_ が怪しいようなので[ソース](https://ftp.gnu.org/gnu/bash/bash-4.3.tar.gz)を引っ張ってきて周辺を見る。318行目から、どうやらパッチを当てているのは`initialize_shell_variables`関数のようだ。

```c
/* 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;
{
```

まず変数を宣言してよく分からない関数を実行。

```c
  char *name, *string, *temp_string;
  int c, char_index, string_index, string_length, ro;
  SHELL_VAR *temp_var;

  create_variable_tables ();
```

その後環境（`env`）から一つずつ環境変数と思われる文字列を取り出す。

```c
for (string_index = 0; string = env[string_index++]; )
    {
```

`char_index`を0で初期化した後、`name`に環境変数（`string`）のポインタをコピーして、`while`で`=`が出現するまで回す。`*string++`は変数`c`に代入した後にポインタを進めるので、この処理が終わると`string`は`=`の次の文字を指すようになる。

```c
      char_index = 0;
      name = string;
      while ((c = *string++) && c != '=')
	;
```

`=`がなかったりした場合はエラーになるようにする。エラーにならなかった場合は`char_index`に`=`の位置（数字）を入れる。

```c
      if (string[-1] == '=')
	char_index = string - name - 1;

      /* If there are weird things in the environment, like `=xxx' or a
	 string without an `=', just skip them. */
      if (char_index == 0)
	continue;
```

`name`の`=`を潰して`\0`にする。

```c
      /* ASSERT(name[char_index] == '=') */
      name[char_index] = '\0';
```

謎の変数`temp_var`を初期化。

```c
      temp_var = (SHELL_VAR *)NULL;
```

`string`の先頭から4バイトをチェックして、それが`() {`だったら関数定義の開始と判断して関数を処理する。

```c
      /* 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))
	{
```

変数`temp_string`に"`name` + ` ` + `string`"を格納。

```c
	  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);
```

`name`が`legal_identifier`なら、`parse_and_execute`で`temp_string`をパースして実行。

```c
	  if (posixly_correct == 0 || legal_identifier (name))
	    parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST);
```

ここにパッチが当てられて、次のように変更された。

```c
 	  /* Don't import function names that are invalid identifiers from the
 	     environment, though we still allow them to be defined as shell
 	     variables. */
 	  if (legal_identifier (name))
 	    parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);
```  

`SEVAL_FUNCDEF`と`SEVAL_ONECMD`というものが増えている。恐らく`temp_string`に"`関数名` + ` ` + `() { ... }`"という文字列がそのまま`parse_and_execute`関数に渡されて実行されているのが不味い感じだったようだ。

## common.h

`SEVAL_FUNCDEF`と`SEVAL_ONECMD`を追加したらしい。

```diff
*** ../bash-4.3-patched/builtins/common.h	2013-07-08 16:54:47.000000000 -0400
--- builtins/common.h	2014-09-12 14:25:47.000000000 -0400
***************
*** 34,37 ****
--- 49,54 ----
  #define SEVAL_PARSEONLY	0x020
  #define SEVAL_NOLONGJMP 0x040
+ #define SEVAL_FUNCDEF	0x080		/* only allow function definitions */
+ #define SEVAL_ONECMD	0x100		/* only allow a single command */
  
  /* Flags for describe_command, shared between type.def and command.def */
```

## builtins/evalstring.c

`parse_and_execute`関数を改造して、関数定義とかしかできないようにするフラグが指定された時の処理が追加されている。
189行目くらいに`parse_and_execute`関数の定義があり、これで引数の意味がなんとなく分かる。

```c
/* Parse and execute the commands in STRING.  Returns whatever
   execute_command () returns.  This frees STRING.  FLAGS is a
   flags word; look in common.h for the possible values.  Actions
   are:
   	(flags & SEVAL_NONINT) -> interactive = 0;
   	(flags & SEVAL_INTERACT) -> interactive = 1;
   	(flags & SEVAL_NOHIST) -> call bash_history_disable ()
   	(flags & SEVAL_NOFREE) -> don't free STRING when finished
   	(flags & SEVAL_RESETLINE) -> reset line_number to 1
*/

int
parse_and_execute (string, from_file, flags)
     char *string;
     const char *from_file;
     int flags;
{
```

どうやら第一引数を実行して、第三引数はフラグでどこまでの処理ができるのかを指定しているみたい。

パッチではここにも修正が入っている。

```diff
*** ../bash-4.3-patched/builtins/evalstring.c	2014-02-11 09:42:10.000000000 -0500
--- builtins/evalstring.c	2014-09-14 14:15:13.000000000 -0400
***************
*** 309,312 ****
--- 313,324 ----
  	      struct fd_bitmap *bitmap;
  
+ 	      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;
+ 		}
+ 
  	      bitmap = new_fd_bitmap (FD_BITMAP_SIZE);
  	      begin_unwind_frame ("pe_dispose");
***************
```

ここでは恐らく、フラグで関数の定義が期待されているにも関わらず、`command->type`（これはコマンドをパースした結果か？）が関数ではなかったら警告を出すという感じか。

```diff
*** 369,372 ****
--- 381,387 ----
  	      dispose_fd_bitmap (bitmap);
  	      discard_unwind_frame ("pe_dispose");
+ 
+ 	      if (flags & SEVAL_ONECMD)
+ 		break;
  	    }
  	}
```

`SEVAL_ONECMD`がフラグで指定されていたら、一度実行したら`break`でそれ以上処理しなくするとかだろう。

# 謎

## subst.c

```diff
*** ../bash-4.3-patched/subst.c	2014-08-11 11:16:35.000000000 -0400
--- subst.c	2014-09-12 15:31:04.000000000 -0400
***************
*** 8048,8052 ****
  	  goto return0;
  	}
!       else if (var = find_variable_last_nameref (temp1))
  	{
  	  temp = nameref_cell (var);
--- 8118,8124 ----
  	  goto return0;
  	}
!       else if (var && (invisible_p (var) || var_isset (var) == 0))
! 	temp = (char *)NULL;
!       else if ((var = find_variable_last_nameref (temp1)) && var_isset (var) && invisible_p (var) == 0)
  	{
  	  temp = nameref_cell (var);
```

よくわかりませんでした。

## Ancient backwards compatibility の消滅

パッチで _variables.c_ の364行目のこの辺が削除されている。

```c
	  /* 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';

....

	  /* ( */
	  if (name[char_index - 1] == ')' && name[char_index - 2] == '\0')
	    name[char_index - 2] = '(';		/* ) */
```

これがあると何が不味いのか。

# このパッチだけではまずいらしい

<blockquote class="twitter-tweet" lang="ja"><p>The bash patch seems incomplete to me, function parsing is still brittle. e.g. $ env X=&#39;() { (a)=&gt;\&#39; sh -c &quot;echo date&quot;; cat echo</p>&mdash; Tavis Ormandy (@taviso) <a href="https://twitter.com/taviso/status/514887394294652929">2014, 9月 24</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

まあ、なんかパーズには他にも問題があるみたい。