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してもいいか(互換性がアレだけど)。
ちなみに
OpenBSDや
sudo(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;