○[FreeBSD] environ(7)
先日のエントリで、FreeBSD の unsetenv(3)をあんまりいじめないで(バリバリ)!
という記事をそのうち書くと宣言したので、そもそもなんであんなに他の
*BSD とは異なる実装に書き直す必要があったのかというお話。
まず第壱に memory leak 問題すな、setenv(3) は環境変数の為に heap memory を割り当てますが
これは environ(7) の
仕様を見ていただくと判ると思いますが
extern char **environ;
と宣言する事でユーザがこれを改変してしまう事が可能なのですよね、ですので単純に
#include <stdlib.h>
extern char **environ;
int
main(void)
{
setenv("FOO", "bar", 1);
environ = malloc(1024 * sizeof(char *));
...
}
と上書きしただけで古い *BSD の libc 実装では、setenv(3) が内部で確保した heap がまんま
memory leak してしまうのです、なんというおおらかな時代の遺物。
この問題は
OpenBSDと
NetBSDでは、およそ5年前に対応がされていますが
FreeBSD ではその後もしばらくは放置されとりました。
千奈美に、環境変数をgetenv(3)を介さずに直接参照するには上の方法以外に
int
main(int argc, char *argv[], char **envp)
{
char **p;
for (p = envp; *p; ++p)
printf("%s\n", *p);
return 0;
}
と main 3番目の引数を使う方法があったりしますが、standard ではないことに注意。
話を戻して、沙良にもういっちょ setenv(3) で上書きを選択した場合、常に新規に heap を確保し直すので
setenv("FOO", "bar", 1);
setenv("FOO", "buzz", 1);
というようなコードを実行すると、environ[n] もさっくり leak する問題もあります。
なんでこれ realloc(3) しないの?という素朴な疑問が沸きますが、これは environ[n] が
本当に heap 上に存在し realloc(3) 可能かどうかは、environ(7) が自由に弄れてしまうという
仕様の都合上まったく保証できないんですよね、例えば
static char s[1024];
strlcpy(s, "FOO=bar", sizeof(s));
environ[0] = s;
というコードもたうぜんありうるわけでして。
こっちの leak 問題については、まだ何ら
NetBSDも
OpenBSDは対策してません。
82 if ((c = __findenv(name, &offset)) != NULL) {
83 if (!rewrite)
84 goto good;
85 if (strlen(c) >= l_value) /* old larger; copy over */
86 goto copy;
87 } else { /* create new slot */
...
106 if ((c = malloc(size + l_value + 2)) == NULL)
107 goto bad;
108 environ[offset] = c;
109 (void)memcpy(c, name, size);
110 c += size;
111 *c++ = '=';
112 copy:
113 (void)memcpy(c, value, l_value + 1);
82行目の __findenv() によるチェックで既に環境変数が存在する場合、106行目で新規に malloc(3) して置き換えます。
ちなみに上のコードは NetBSD の実装ですが、前回セットした値と長さが一致するか短い場合には
再利用しているのですが、でもこれ c がもし書込不可領域に存在したら?の
ケアレス航空ですな。
よって OpenBSD では85~86行目がコメントアウトされとりやす:D
第弐に SUSv3 への追従です、putenv(3) について
仕様では
the string pointed to by string shall become part of the environment,
so altering the string shall change the environment.
とあり、putenv(3) の引数として渡した文字列を後から変更した場合、getenv(3) の結果に反映されるとあります。
$ cat >putenv_test.c
#include <assert.h>
int
main(void)
{
static char s[1024];
strlcpy(s, "FOO=bar", sizeof(s));
putenv(s);
strlcpy(s, "FOO=buzz", sizeof(s));
assert(!strcmp("buzz", getenv("FOO")));
}
^D
うーむなんというデンジャラスな仕様…
すでに
CERT Secure Coding Standardの記事にもなってるけど、ここはもっと突っ込んで
使うなボケ
で良かったと思うんだけどな…しかも RATIONAL には
The standard developers noted that putenv() is the only function available to add
to the environment without permitting memory leaks.
と、leak しないのは putenv(3) だけ!とか使用を推奨しとるように読めるのがどうにもアレ。
TOG先生の次回作にご期待ください。
putenv(3) は HISTORY によると
4.3BSD-Renoよりとあるのですが、この実装では
単に setenv(3) の wrapper 関数で、渡された "FOO=bar" はコピーされた上で environ(7) にセットされます。
34 if (!(p = strdup(str)))
35 return(1);
...
41 rval = setenv(p, equal + 1, 1);
よってその実装を受け継ぐ NetBSD/OpenBSD 上では、先ほどのコード例は
$ make putenv_test
$ ./putenv_test
assertion "!strcmp("buzz", getenv("FOO"))" failed: file "test.c", line 10, function "main"
Abort (core dumped)
と SUSv3 通りには動いてくれません、ヒャッハー!標準化モヒカン狩りまくり。
しかしやっぱこれ SUSv3 がウンコだと思うんだよね、おそらく SysV 系の putenv(3)の実装に
*1
これに依存したアプリがあったとかで、
RPGじゃなくて XPG つまり X/Open Portability Guide を作る際
こんなキナ臭い仕様まで明文化してしまったんじゃねーのと。
先日の dirname(3) なんかでは SysV と *BSD の最大公約数を取ってたのに…
第参は「unsetenv(3)は本当にちゃんと環境変数を根こそぎお掃除してるのか?」問題すね。
以下の姑コードを実行してみましょう。
$ cat >unsetenv_test.c
#include <stdlib.h>
int
main() {
extern char **environ;
char *save;
setenv("FOO", "bar", 1);
save = environ[0];
environ[0] = NULL;
unsetenv("FOO");
environ[0] = save;
printf("%s\n", getenv("FOO"));
}
^D
こいつをNetBSD/OpenBSDそしてglibc2で実行すると
$ make unsetenv_test
$ ./unsetenv_test
bar
あらいやだ、きれいになってないじゃないの。ちょっとMIT子さん(以下ry
これは environ(7) の終端チェックを NULL か否かでしかやってないからです、NetBSD の実装だと
123 for (p = environ; (c = *p) != NULL; ++p)
124 if (strncmp(c, name, len) == 0 && c[len] == '=') {
んな感じ。まぁこれ烏賊にもヤバそうに見えますが、冷静に考えるとこれ攻撃には使い道ないと思うけどね。
これを悪用できる状況ならもっと別の攻撃した方が早いという良くあるお話。
でも OpenSolaris や、FreeBSD ではこんな細工をものともせず綺麗にお掃除してくれます。
$ make unsetenv_test
$ ./unsetenv_test
なんという吸引力、だいそんだいそん!
この
commit logを参照のこと。
ということで、この3つの問題に果敢に取り組んで地雷を踏んだらサヨウナラしちゃったのが
FreeBSD-SA-09:16.rtld ですな、おそらくOpenSolaris の libc 実装が同じようなことをやっとるので
インスパイアされちゃったんだろうけど…
要は「今動いてるものは(なるべく)触るな、なぜなら今動いてるのは奇跡だから」といういつものお話。
(SUSv3対応しないとはいってないけどもっと清朝にねというアヘン戦争!)
んで後日談、FreeBSD は今回の exploit で unsetenv(3) を一端は他の *BSD と同様に、environ(7) に
不正な値が入っていた場合でもスルーして処理を続行するように
変更したのを
なぜかまた
元に戻してるのよね、どうしてそうなるのかなかな。
これおそらく仕様を勘違いしてると思うんだよな。
The unsetenv() function shall fail if:
[EINVAL]
The name argument is a null pointer, points to an empty string,
or points to a string containing an '=' character.
こいつを
(引数として渡された) name が NULL とか空文字とか '=' を含む不正な文字の場合、EINVAL を返す
を
environ(7) で保持している環境変数の name が~
と読み間違えてるんじゃないの?疑惑、まぁNetBSDじゃないからいいけど(ぉ
*1:ググルさんでは当時の SVR4 のマヌアルがヒットしないので、アマゾンでポチりましたのでまた後日…