The Man Who Fell From The Wrong Side Of The Sky:2009年12月分

2009/12/6(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/9(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);

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

2009/12/20(Sun)

今日

@

この記事、ニコニコ大百科で誰得wwwいいぞもっとやれwww

2009/12/26(Sat)

[C1X] JTC1/SC22/WG14ォチ

@

いつものJTC1/SC22/WG14を生温かく見守るネタ。

  • On The Removal of gets()
    libc界の悪夢ついに切除か(東スポ風味)。
    これまでC89(C90) -> C99ではシンボル増えることはあっても減ることは無かったので
    今回が初のケースなのだけど、__STDC_VERSION__ > 201X なソースではgetsという名前は自由に使っておk
    という訳にはいかないはず(ABI互換のためにlibc自体にはgets残るし)なのでどうすんだろね。
    getsをC1Xでは予約語扱いにするとか?
  • Multibyte C local
    何がうれしいのか知らんけど、C.UTF-8みたいなの。互換性考えるとcompletely uselessなんだけどねぇ。
    No interest in WG14 to pursue this path.
    
    ということで立ち消え、よかったよかった。
  • C Secure Coding Guidelines
    前回非公開だったやつ。
    strlen(3)SIZE_MAXを返すケースとか、パラノイア過ぎて吹いた。
  • Apple's Extensions to C
    みんな大好きクロージャとガベコレ for C1X、なんかC1Xって現状追認と
    C++0xの尻拭いしかやることないのかと思ってたら野心的ざますこと。

@

おれ専用しおり、過去のC1Xオチ記事。

2009/12/30(Wed)

[OpenBSD] Citrus patch for OpenBSD 4.6-current

@

おまたせ、本家リリースからこんなに遅れたのは初めてかな?
まぁ「ガイア(会社)が俺にもっと輝け(働け)」もそうなんだけど
実はVMWareのディスクイメージを破壊してしまって、リポジトリが消失したのねん orz

んで皆さんに朗報、今から4週間ほど前にOpenBSDではMB_LEN_MAXの定義をMD *1からMI *2に変更したのですが
そのついでにMB_LEN_MAXを1から4(RFC3629でのUTF-8のMB_CUR_MAXにあたる)に増やした模様。
よってこれまでCitrus patchを導入するのを躊躇わせるのに十分かもしれない、patchあてた後の
binary非互換回避のためのuserlandの再構築が不要になりました(説明めんどいのでINSTALLではmake build推奨だけど)

まぁOpenBSDはばんばんlibcのmajorが変わるのでuserlandなんか四六時中rebuildしてる気がするんだけど
binary packagesがそのまま使えるようになるのはうれしいよね(ただしcurrent生活者を除く)

細かいこというとja_JP.ISO2022-JPのようにMB_LEN_MAXが4じゃ足りないlocaleもあるのですが
どうせja_JP.eucJPかja_JP.UTF-8使えれば99.999999%の人の需要は満たせると思うので、MB_LEN_MAX=32化は
別のパッチとして分離します(後日リリースしまふ)、 LANG=ja_JP.ISO2022-JPで生活したい方
citrus.patchをあてたあとでrename.patchをあてて昔通りの作業やってちょ。

ちなみ^1にISO/IEC 2022のような冗長なエスケープを許容するstateful encodingの場合
MB_LEN_MAXは無限大です、この32という値はあくまで 実装上の都合です。

ちなみ^2にわれらが プロジェクトリーダー曰く「 MB_LEN_MAXは42」らしいですぞ。

ちなみ^3にNetBSD-current上のCitrusへの追従はこの一年トドが凍った状態、スマソ。

*1:Machine Dependent - src/sys/arch/*/include/limits.h 参照
*2:Machine Independent - src/sys/sys/limits.h 参照

2009/12/31(Thu)

[OpenBSD] 続 Citrus patch

@

昨日のやつ、mbrlen(3)のmanpageのCVS comflictを解消してなかったのでmake buildの最後で警告が出る件を修正しました、スマソ。

大晦日

@

2009年中もこんなチラシの裏を読んで頂いた皆様方の来年のますますのごハッテンと大貫憲章をお祈りいたします、ペコリ。