LoginSignup
17
17

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-09-25

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

脆弱性の概要

seclistsの投稿によると、

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 を書きかえている部分がこうなっている。

*** ../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 が怪しいようなのでソースを引っ張ってきて周辺を見る。318行目から、どうやらパッチを当てているのは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;
{

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

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

  create_variable_tables ();

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

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

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

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

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

      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にする。

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

謎の変数temp_varを初期化。

      temp_var = (SHELL_VAR *)NULL;

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

      /* 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"を格納。

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

namelegal_identifierなら、parse_and_executetemp_stringをパースして実行。

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

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

      /* 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_FUNCDEFSEVAL_ONECMDというものが増えている。恐らくtemp_stringに"関数名 + + () { ... }"という文字列がそのままparse_and_execute関数に渡されて実行されているのが不味い感じだったようだ。

common.h

SEVAL_FUNCDEFSEVAL_ONECMDを追加したらしい。

*** ../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関数の定義があり、これで引数の意味がなんとなく分かる。

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

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

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

*** ../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(これはコマンドをパースした結果か?)が関数ではなかったら警告を出すという感じか。

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

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

subst.c

*** ../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行目のこの辺が削除されている。

      /* 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] = '(';     /* ) */

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

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

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

17
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
17