Not only is the Internet dead, it's starting to smell really bad.:2009年12月上旬

2009/12/06(Sun)

[FreeBSD] rtld(1) local exploit

いささか旬を過ぎたネタですが。

今回問題となったのはrtld(1)、つまりRuntime Link Editorの以下のコード。
rtld.c#rev1.145

359     trust = !issetugid();
360
361     ld_bind_now = getenv(LD_ "BIND_NOW");
362     /*
363      * If the process is tainted, then we un-set the dangerous environment
364      * variables.  The process will be marked as tainted until setuid(2)
365      * is called.  If any child process calls setuid(2) we do not want any
366      * future processes to honor the potentially un-safe variables.
367      */
368     if (!trust) {
369         unsetenv(LD_ "PRELOAD");
370         unsetenv(LD_ "LIBMAP");
371         unsetenv(LD_ "LIBRARY_PATH");
372         unsetenv(LD_ "LIBMAP_DISABLE");
373         unsetenv(LD_ "DEBUG");
374         unsetenv(LD_ "ELF_HINTS_PATH");
375     }

359行目でsetuid rootされたbinaryかどうかをチェックし、コメントにある通りLD_PRELOADなどの
危険が危なくこりゃ険しいな環境変数を消すという処理の部分。

unsetenv(3)の仕様によると

Upon successful completion, zero shall be returned. Otherwise, -1 shall be returned,
errno set to indicate the error, and the environment shall be unchanged.

失敗した場合は-1を返却し「環境変数は変更しない」とあります。
ですのでenvironを弄ってunsetenv(3)が失敗するような状況を作り出せば、簡単に攻撃できてしまうわけですな。

しかしですね、unsetenv(3)はVersion 7 AT&T UNIXで導入されたものですが、そもそも歴史的には
stdlib.h#rev1.165

@@ -161,7 +161,7 @@ void	 _Exit(int) __dead2;
 int	 posix_memalign(void **, size_t, size_t); /* (ADV) */
 int	 rand_r(unsigned *);			/* (TSF) */
 int	 setenv(const char *, const char *, int);
-void	 unsetenv(const char *);
+int	 unsetenv(const char *);
 #endif
 
 /*

ちう具合に戻り値はvoidのプロトタイプだったのですよな。
これはもうIEEE Std 1003.1-2001(=POSIX)でプロトタイプを変更したのが筋悪だったと思っちゃうのよね。
こういう場合は古い方の動作は変更せずに、新しい関数を導入して移行をさせるべきだったんじゃねと。
そうしときゃlibcで __warn_referencesなどを使ってリンク時に警告だしてやれば安全でない箇所を燻りだせたのにぃという。

かといってFreeBSDもなんだかなーという部分もあります。
getenv.c

349         /* Copy environ values and keep track of them. */
350         for (envNdx = envVarsTotal - 1; envNdx >= 0; envNdx--) {
351                 envVars[envNdx].putenv = false;
352                 envVars[envNdx].name =
353                     strdup(environ[envVarsTotal - envNdx - 1]);
354                 if (envVars[envNdx].name == NULL)
355                         goto Failure;
356                 envVars[envNdx].value = strchr(envVars[envNdx].name, '=');
357                 if (envVars[envNdx].value != NULL) {
358                         envVars[envNdx].value++;
359                         envVars[envNdx].valueSize =
360                             strlen(envVars[envNdx].value);
361                 } else {
362                         __env_warnx(CorruptEnvValueMsg, envVars[envNdx].name,
363                             strlen(envVars[envNdx].name));
364                         errno = EFAULT;
365                         goto Failure;
366                 }

350行目からはじまるループの中で、356行目でstrchr(3)でnameにセットされてる"FOO=bar"ちう文字列から
'='を探してvalueに"bar"をセットしようとしてるのですが、nameが"FOOLYOURSELF"のよに'='を含まない
文字列の場合、362行目からの処理の通りエラーメッセージを表示し、ループ処理を中断してしまいます、なにそれ。
全っ然気持ち伝わってこない!もっと死ぬ気で!!!腹の底から「unsetenv(3)するぞーっ」て!!!
はい今君root奪われた!今君root奪われたよ!!

NetBSDやOpenBSDの実装↓がやっとるように、単純に読み飛ばしてしまってたなら今回のexploitはなかったという。
getenv.c

123         for (p = environ; (c = *p) != NULL; ++p)
124                 if (strncmp(c, name, len) == 0 && c[len] == '=') {
125                         *offset = p - environ;
126                         return c + len + 1;
127                 }

エラーを返すという仕様はエラーを返さなきゃいけないわけじゃないし、そもそもPOSIXの改悪仕様書だって
'='が見つからなかったらエラーなんと一言も書いてないしね。
そもそもenviron変更されたら未定義動作だしいっそabortしてもいいか(互換性がアレだけど)。

ちなみに OpenBSDsudo(1)は、libcのunsetenv(3)なぞ一ミリたりと信用せずに
自前のunsetenv(3)をもっとりやす、まーsudo(1)は環境変数で散々やられてるからねぇ。

ま、FreeBSDのgetenv.cにもいいとこはあるのですがその話は次回に譲るちうことで。

あ、それとFreeBSDのgetenv.cはまだバグ持ってるね。

506                 /* Save name of name/value pair. */
507                 env = stpcpy(envVars[envNdx].name, name);
508                 if ((envVars[envNdx].name)[nameLen] != '=')
509                         env = stpcpy(env, "=");
510         }
511         else
512                 env = envVars[envNdx].value;
513
514         /* Save value of name/value pair. */
515         strcpy(env, value);

507行目のstpcpyできっちり"FOO=bar"までcopyされてるのに、更に515行目でvalueを再度copyしてるので
ある条件の場合"FOO=barbar"に化けるわメモリ破壊起こすわちう。
ちうかstpcpyとかportableでないしlength checkのない関数を使うとかもうね(ry

たぶんこんなかんじ、ちゃんとテストしてないけど。

--- getenv.c.orig       2009-12-07 13:49:05.000000000 +0900
+++ getenv.c    2009-12-07 13:49:59.000000000 +0900
@@ -504,9 +504,9 @@
                envVars[envNdx].valueSize = valueLen;

                /* Save name of name/value pair. */
-               env = stpcpy(envVars[envNdx].name, name);
-               if ((envVars[envNdx].name)[nameLen] != '=')
-                       env = stpcpy(env, "=");
+               memcpy(envVars[envNdx].name, name, nameLen);
+               env = envVars[envNdx].name + nameLen;
+               *env++ = '=';
        }
        else
                env = envVars[envNdx].value;

2009/12/09(Wed)

[FreeBSD][OpenBSD] dirname(3)

ちょっと以前に某所でdirname(3)の挙動の違いという話をしていた時に
なんとなく FreeBSDの実装を眺めてたのですが、 rev1.6

Reduce libc.so's memory footprint by lazily allocating memory used internally
by basename() and dirname().

というcommit logとともに

@@ -42,9 +42,15 @@ char *
 dirname(path)
 	const char *path;
 {
-	static char bname[MAXPATHLEN];
+	static char *bname = NULL;
 	const char *endp;
 
+	if (bname == NULL) {
+		bname = (char *)malloc(MAXPATHLEN);
+		if (bname == NULL)
+			return(NULL);
+	}
+
 	/* Empty or NULL string gets treated as "." */
 	if (path == NULL || *path == '\0') {
 		(void)strcpy(bname, ".");

ちう変更がはいっとるのですな、おいおいdirname(3)って
いつからNULL返してerrnoにENOMEMセットしていいことになったのよ。

いつもの如く 仕様書を紐解くと

 RETURN VALUE

    The dirname() function shall return a pointer to a string
    that is the parent directory of path. If path is a null pointer
    or points to an empty string, a pointer to a string "." is returned.

    The dirname() function may modify the string pointed to by path,
    and may return a pointer to static storage that may then be overwritten
    by subsequent calls to dirname().

ひとことも書いてない!
ちなみにbasename(3)も以下同文でございます。

ってもこのケースでmalloc(3)失敗する状態というのも以下略なので
こまけぇこたぁいいんだよ、ちゅう実装なのかもしれんけど。
あんまり気持ちのいいもんじゃないですな。

ま、これ元ネタの OpenBSDからして変で

	if (len >= sizeof(dname)) {
		errno = ENAMETOOLONG;
		return (NULL);
	}

MAXPATHLENを超える場合、NULLを返してENAMETOOLONGを返すというオレオレ仕様が入ってますな。
さすがパラノイア、伊達にsnprintf(3)何バイト書けたかまで姑チェックしてるわけじゃない。

それとさっきの仕様書にもある通り、glibc2なんかのdirname(3)は引数pathを破壊して戻り値とします。
一方で*BSDでは伝統的にstatic storageを返すのでreentrantでありません(なんでNetBSD以外は引数がconst付)。
POSIXなので移植性があるのだけど、そのじつどの実装でもちゃんと動くコード書こうとするとめんどくさい関数ですな。

#ifdef DIRNAME_CONST_PROTO
#define DIRNAME_CONST(s) ((const char *)s)
#else
#define DIRNAME_CONST(s) (s)
#endif
	char buf[PATH_MAX+1], *p, dir[PATH_MAX+1];
	const char *path = ...

	strlcpy(buf, path, sizeof(buf));
	p = dirname(DIRNAME_CONST(buf));
	if (p == NULL) /* XXX: for {Free,Open}BSD */
		abort();
	strlcpy(dir, p, sizof(dir));
	...

とかしないとならなんという、これはめんどくさい。

それとdynamic allocationを使ったコードを書く場合は

	char *p = dirname(strdup(path));
	free(p);

という手抜きコードを書いたりすると死亡フラグ。

ちゅうことで

	char *p, *q, *r;
	const char *path = ...

	p = strdup(path);
	if (p == NULL)
		abort();
	q = dirname(p);
	if (q == NULL)
		abort();
	r = strdup((const char *)q);
	if (r == NULL)
		abort();
	...
	free(p);
	free(r);

とまぁ、どっちみちめんどくさい罠。