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);
name
がlegal_identifier
なら、parse_and_execute
でtemp_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_FUNCDEF
とSEVAL_ONECMD
というものが増えている。恐らくtemp_string
に"関数名
+
+ () { ... }
"という文字列がそのままparse_and_execute
関数に渡されて実行されているのが不味い感じだったようだ。
common.h
SEVAL_FUNCDEF
とSEVAL_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] = '('; /* ) */
これがあると何が不味いのか。
このパッチだけではまずいらしい
The bash patch seems incomplete to me, function parsing is still brittle. e.g. $ env X='() { (a)=>\' sh -c "echo date"; cat echo
— Tavis Ormandy (@taviso) 2014, 9月 24
まあ、なんかパーズには他にも問題があるみたい。