Vagrant 소개
2020, Apr 28  
인텔리전스팀

Vagrant(베이그런트)는 가상 시스템 환경을 관리하기 위한 도구입니다. 가상 환경 셋팅 시간을 줄이고 개발성과 생산성을 높일 수 있도록 개발 환경 이나 테스트 환경을 자동으로 설정하도록 도와줍니다. 본 글에서는 베이그런트의 사용법과 주식회사 쏘마의 공격 시뮬레이터 ARESAPT3 공격 시나리오를 재현할 때 Vagrant를 어떻게 활용하고 있는지 설명하겠습니다.

Vagrant를 상세히 살펴보기에 앞서, 이해를 돕기위해 우리에게 좀 더 친숙한 Docker(도커)와 간단히 비교해보겠습니다.

비교 사항 Docker Vagrant
가상화 타입 Container Virtual Machine
지원하는 OS 플랫폼 Linux Linux, Unix, Windows
시작하기 위해 필요한 시간 초 단위 분 단위
가상 시스템 분리 수준 부분 전체
  • Docker란?
    도커는 2013년 3월 Pycon Conference에서 The future of Linux Containers 세션에서 소개 된 Container 기반 오픈 소스 가상화 플랫폼입니다. 리눅스에서 컨터이너를 실행하고 관리할 수 있도록 도와줍니다.

  • Container란?
    격리된 공간에서 프로세스가 동작하는 기술입니다. Container는 프로세스를 격리하기 때문에 가볍고 빠르게 동작할 뿐만 아니라 CPU나 메모리를 프레세스가 필요한 만큼만 사용하기 때문에 성능적으로도 뛰어납니다. 또한 여러 개의 Container를 실행하면 서로 영향을 미치지 않고 독립적으로 실행됩니다.


1. Vagrant 관련 용어


Boxes

Vagrant는 이미 만들어진 VM 이미지를 사용하여 복제합니다. 이 이미지를 Vagrant에서 Boxes라고 합니다. 사용 할 Boxes를 지정하여 Vagrantfile을 만드는것이 Vagrant 시작입니다.

Vagrantfile

Vagrantfile은 프로젝트에 필요한 설정 파일입니다. 프로젝트가 Provisioning 할 때 이 파일을 참고합니다.

Provisioning

Provisioning는 시스템에 소프트웨어를 자동으로 설치하고 구성을 변경하는 등 준비 작업을 의미합니다.


2. Boxes


2.1 Boxes 제작

2.1.1 Packer를 이용한 제작

Packer는 다양한 플랫폼에서 사용 가능한 이미지를 동적으로 생성할 수 있게 도와주는 툴 입니다. Packer를 이용해 Boxes를 제작할 때 Template(템플릿)을 설정해야 합니다. Packer는 아래와 같은 구조를 가집니다.

Packer/
      |---Template.json
      |---http/
      |       |--- preseed.cfg    // preconfiguration file
      |                           // Packer 를 이용해 .iso 에서 운영 체제를 설치하는 경우 
      |                           // 이 설정 파일이 필요함
      |---scripts/
              |--- init.sh

Template은 Json 형식으로 작성되며 Builder와 Provisioner의 설정을 담고 있습니다. Builder는 AWS, VirtualBox, Docker 등 박스를 생성할 플랫폼을 지정합니다. Provisioner는 박스를 생성할 때 사용할 빌드 도구를 의미합니다.

{
  "builders": [
    {
      "type": "virtualbox-iso"
      ...
    }
  ],
  "provisioners": [
    {
      "type": "shell"
      ...
    }
  ],
  "post-processors": [
    {
      "type": "vagrant"
      ...
    }
  ]
}

2.1.2 Vagrant를 이용한 제작

VM을 박스로 제작할 때 설정 해야하는 요소가 있습니다. 이를 설정하지 않으면 Vagrant 명령어를 통해 제어를 할 수 없습니다. 환경 설정 방법은 공식 홈페이지에서 확인 가능합니다.

설정이 완료된 후 VM 이미지가 있는 경로에서 아래와 같이 명령어를 입력하면 박스를 제작하고 실행 가능합니다.

// Box 제작
> vagrant package --output [Box_이름].box --base [VM 이미지]

// Vagrantfile을 포함한 Box 이미지 제작
> vagrant package --output [Box_이름].box --base [VM 이미지] --vagrantfile [Vagrantfile 경로]

2.2 Boxes 검색

위처럼 Box를 직접 제작하지 않고, 다른 사람이 제작하여 공유한 Box를 이용할 수도 있습니다. Box는 Vagrant 클라우드에서 공유가 가능합니다.

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/trusty64"       // Box 이름
  config.vm.box_version = "20190514.0.0"  // Box의 특정 버전 입력 [선택 옵션]
end


3. Vagrantfile


Vagrantfile은 프로젝트에 필요한 설정 파일입니다. 프로젝트가 Provisioning 할 때, 즉 실제 실행 될 때 이 파일을 참고합니다.

3.1 Vagrantfile을 찾는 순서

Vagrantfile을 파일을 참조하기 위해 명령어를 실행한 작업 디렉터리에서 시작해 한 단계씩 위로 올라가면서 Vagrantfile을 찾습니다. 예를 들어 /home/ares/work/win10 디렉토리에서 vagrant 명령어를 실행할 경우 파일을 찾는 순서는 다음과 같습니다.

/home/ares/work/win10/Vagrantfile
/home/ares/work/Vagrantfile 
/home/ares/Vagrantfile
/home/Vagrantfile
/Vagrantfile

VAGRANT_CWD 환경 변수를 수정하여 Vagrantfile 탐색 시작 경로를 변경할 수 있습니다.

3.2 환경 설정 적용 순서

Vagrant 명령어를 통해서 VM을 로딩하면 다음 순서에 따라서 환경 설정을 합니다. 이 설정 순서를 진행하면서 앞 단계에서 설정한 속성이더라도 뒤에서 새로운 속성으로 설정하면 새로운 값으로 속성이 수정됩니다.

1. Box를 제작할 때 함께 패키징 된 Vagrantfile.
2. Vagrant 홈 디렉토리(~/.vagrant.d)의 Vagrantfile에서 설정한 값.
3. 사용자 디렉토리에 있는 Vagrantfile에서 설정한 값. 이 Vagrantfile은 사용자가 직접 수정할 수 있는 파일.
4. 사용자 디렉토리에 있는 Vagrantfile 설정 값 중 vm.define 블록에서 설정한 값.
5. 사용자 디렉토리에 있는 Vagrantfile 설정 값 중 Provider 에서 설정한 값.

3.3 Vagrantfile 버전

Vagrantfile은 버전에 따라 작성 방법 다릅니다.

- Vagrantfile 1.0.x 버전
Vagrant::Config.run
...
end

- Vagrantfile 1.1 이상 버전
Vagrant.configure("1")
...
end

- Vagrantfile 2.x 버전
Vagrant.configure("2")
...
end

Vagrant에서 현재 Vagrantfile 2.x 버전 형식을 사용을 권장합니다.

3.4 Multi Machine

vm.define 을 이용하여 블록을 만들고 내부에서 box를 선택하고 환경 설정을 합니다.

Vagrant.configure("2") do |config|
  config.vm.define "win-server" do |cfg|
    # box 정보
    cfg.vm.box = "somma/windows-server"
    ...
  end

  ...

  config.vm.define "win-10" do |cfg|
    # box 정보 
    cfg.vm.box = "somma/windows-10"
    ...
  end
end

3.5 파일 공유

vm.synced_folder 를 사용하여 공유 폴더를 설정합니다. 사용자가 synced_folder를 설정하지 않으면 기본 옵션이 사용함으로 설정되어 있습니다. 첫번째 파라미터는 host의 디렉토리입니다. 상대 경로면 Vagrantfile 를 기준으로 설정합니다. 두번째 파라미터는 guest의 디렉토리입니다. 이 파라미터는 guest 내부에 존재하는 절대 경로이여야 합니다. 공유 폴더를 사용하지 않으려면 disable: true 옵션을 설정합니다.

cfg.vm.synced_folder ".", "/vagrant", disabled: true
...
cfg.vm.synced_folder "./shared", "/vagrant"
synced_folder 옵션 설명
create true인 경우 host 디렉토리 경로가 없으면 생성합니다. 기본값은 false 입니다.
disabled true인 경우 공유 폴더가 비활성화 됩니다. 기본값은 false 입니다.
group 공유 폴더의 그룹을 설정합니다. 기본값은 ssh 사용자로 설정되어 있습니다.
owner 공유 폴더의 소유자를 설정합니다. 기본값은 ssh 사용자로 설정되어 있습니다.
type 공유 폴더의 유형을 설정합니다.

3.6 Network 설정

vm.network 를 사용하여 네트워크를 설정합니다.

3.6.1 Port Forwarding

guest의 port를 host의 port로 맵핑을 하는 예제입니다. 만약 host의 port가 이미 사용 중이라면 자동으로 사용 중이지 않는 다른 포트로 변경해주는 auto_correct 옵션이 있습니다.

cfg.vm.network "forwarded_port", guest: 5985, host: 55985, auto_correct: true

vm.usable_port_range 속성을 사용하면 port가 충돌이 발생할 때 vagrant가 자동으로 할당하는 포트 범위를 설정 할 수 있습니다. 다음 예제는 8000 ~ 8999 포트 범위를 지정합니다.

config.vm.usable_port_range = 8000..8999
forwarded_port 옵션 설명
auto_correct true 인 경우 host port가 사용 중이면 host port를 사용하지 않는 포트 중 하나로 자동으로 변경합니다. 기본값은 false 입니다.
guest port forwarding하는 guest의 port 입니다.
guest_ip port forwarding하는 guest의 ip 입니다.
host 실제 접속하는 port 입니다.
host_ip 실제 접속하는 ip입니다.
protocol udp 또는 tcp 입니다. 기본값은 tcp 입니다.

3.6.2 Public Network

공용 ip 네트워크를 설정합니다. VirtualBox 경우 Bridge로 설정됩니다.

cfg.vm.network "public_network", 
  ip: "192.168.0.221", 
  gateway: "192.168.0.1", 
  dns: "8.8.8.8"
network 옵션 설명
ip IP 를 설정합니다.
gateway Gateway 를 설정합니다.
dns DNS 를 설정합니다.
netmask Subnetmask를 설정합니다.

3.6.3 Private Network

사설 IP 네트워크를 설정합니다. VirtualBox 경우 NAT으로 설정됩니다.

cfg.vm.network "private_network", ip: "192.168.209.128"

Vagrant에서 ssh, 공유 폴더 등을 위해서 기본적으로 네트워크 어댑터 1번에 NAT을 설정합니다. 이 설정을 제거하면 Vagrant를 이용하여 제어할 수 없습니다. 이 ip를 설정하기 위해서 VirtualBox 네트워크 설정을 참고하여 VirtualBox에 직접 명령을 하여 수정했습니다.

cfg.vm.provider "virtualbox" do |vb|
  ...  
  vb.customize ["modifyvm", :id, "--natnet1", "10.0.3/24"]
end

설정 전 NAT IP : 10.0.2.15
설정 후 NAT IP : 10.0.1.15

3.7 provider

provider는 가상 환경을 제공해주는 공급자입니다. Provider로 선택할 수 있는 건 VirtualBox, VMware, Docker, Hyper-V 가 있습니다. 아래 스크립트는 VirtualBox를 사용합니다.

# VirtualBox 설정
cfg.vm.provider "virtualbox" do |vb|
  vb.name = "win10_agent"    
  vb.memory = 4096
  vb.cpus = 2
  vb.customize ["modifyvm", :id, "--natnet1", "10.0.3/24"]
end

3.8 provision

provision은 Vagrantfile의 vm.provision에 정의되어있는 명령을 실행합니다. provision은 vagrant 명령어 중 다음 3가지 중 한 개가 입력됐을 때 실행됩니다.

> vagrant up
또는
> vagrant reload --provision
또는 
> vagrant provision

vagrant up 명령어를 사용할 때 provision을 실행하고 싶지 않으면, --no-provision 플래그를 입력하면 provision이 실행이 안 됩니다.

> vagrant up --no-provision

provision은 여러 가지 방법으로 작성 할 수 있습니다.

cfg.vm.provision "shell" do |s|
  s.path = ".\\shared\\test.bat"
  s.args = "'hello'"
end

cfg.vm.provision "shell", privileged: true, :inline => "
    reg.exe add 'HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows Defender' /v DisableAntiSpyware /t REG_DWORD /d 1 /f
    reg.exe add 'HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows Defender' /v DisableRoutinelyTakingAction /t REG_DWORD /d 1 /f
    ...
  "

cfg.vm.provision "reboot_system", type: "shell",  reboot: true
provision 옵션 설명
name provision의 이름입니다. type 옵션 이외의 값으로 설정될 때는 type 옵션이 반드시 필요합니다.
type Provisioner 타입은 shell 또는 file 입니다.
before :each, :all 또는 특정 provision 이름 값을 가질 수 있습니다. 값에 따라 모든(:all), 각각(:each) 또는 특정 provision이 실행되기 전에 해당 provision이 실행되도록 설정합니다.
after :each, :all 또는 특정 provision 이름 값을 가질 수 있습니다. 값에 따라 모든(:all), 각각(:each) 또는 특정 provision이 실행된 후에 해당 provision이 실행되도록 설정합니다.

3.8.1 Provisioner 타입 : file

File Provisioner 을 사용하면 host에서 guest로 파일 또는 디렉토리를 업로드할 수 있습니다. 예를 들어 host의 A 폴더를 guest의 B 폴더로 업로드하고 싶으면 아래 명령어 줄을 추가합니다.

config.vm.provision "file", source: "C:\\A", destination: "C:\\B"
file 옵션 설명
soruce host에서 업로드할 파일 또는 폴더의 경로입니다.
destination host에서 보낸 파일 또는 폴더를 받을 guest의 경로

3.8.2 Provisioner 타입 : Shell

Shell Provisioner를 사용하면 guest에 스크립트를 업로드하고 실행할 수 있습니다. linux의 경우 ssh로 스크립트를 실행하며, winrm을 사용하는 windows의 경우 powershell 또는 batch 스크립트를 실행합니다.

shell 옵션 설명
inline guest에서 실행할 shell 명령어입니다.
path 업로드 및 실행할 스크립트 경로입니다. 이 스크립트는 파워쉘 스크립트나 sh 등입니다.
args 스크립트에 문자열 또는 배열 형식으로 파라미터로 전달합니다.
powershell_args powershell의 파라미터로 전달합니다.
powershell_elevated_interactive 관리자 권한으로 대화식 모드를 실행합니다. 기본값은 false이며, 사용하려면 windows에 자동 로그인을 활성화 해야합니다.
privileged 스크립트를 관리자 권한으로 실행할지 여부를 지정합니다. 기본값은 true 입니다. windows guest 경우 스케줄 작업을 통해 WinRM 제한 없이 관리자로 실행합니다.
reboot guest를 재부팅합니다.
binary windows 줄 끝을 uinx 줄 끝으로 자동으로 변경합니다. 기본값은 false 입니다.
env 환경 변수로 스크립트에 전달한 키-값 입니다.
keep_color vagrant 에서 stdout, stderr으로 나오는 것을 각각 녹색색과 빨간색으로 출력합니다.
name shell에 이름을 설정합니다.
md5 다운로드한 스크립트를 md5 체크섬을 이용해 유효성 검사를 진행합니다.
sha1 다운로드한 스크립트를 sha1 체크섬을 이용해 유효성 검사를 진행합니다.
sha256 다운로드한 스크립트를 sha256 체크섬을 이용해 유효성 검사를 진행합니다.
sha384 다운로드한 스크립트를 sha384 체크섬을 이용해 유효성 검사를 진행합니다.
sha512 다운로드한 스크립트를 sha512 체크섬을 이용해 유효성 검사를 진행합니다.
upload_path 스크립트가 업로드 되는 guest의 경로입니다. 기본 값은 linux에서는 /tmp/vagrant-shell 윈도우에서는 c:\tmp\vagrant-shell 입니다.


4. ARES에 적용한 Vagrantfile 예


이제 실제 주식회사 쏘마의 공격 시뮬레이터 ARES에서 APT3 공격 그룹을 시뮬레이션 하기 위해 vagrant를 어떻게 적용했는지, Vagrantfile 스크립트를 예제로 살펴보겠습니다. 다음은 APT3 시뮬레이션 시스템 환경입니다. 이 환경에서 Vagrant는 Victim #1, Victime #2을 자동으로 실행하기 위해 이용합니다.

그림

Vagrant 실행 명령어는 아래와 같습니다

> vagrant snapshot restore win-10 apt3_ready
> vagrant up win-10

> vagrant snapshot restore win-server apt3_ready
> vagrant up win-server

Vagrantfile은 아래와 같이 구성되며, 스크립트가 실행되면 Victim #1, Victime #2 가상 머신이 부팅됩니다. 그리고 테스트를 위해 Victime #1에서는 윈도우 디펜더를 내려주고, 아레스 에이전트가 부팅과 동시에 실행되도록 추가로 설정한 것입니다.

Vagrant.configure("2") do |config|

  config.vm.define "win-server" do |cfg|
    # box 정보
    cfg.vm.box = "somma/windows-server"
    cfg.vm.define "winserver"

    # 공유 폴더
    cfg.vm.synced_folder ".", "/vagrant", disabled: true

    # guest boxes에 연결하는 communicator
    cfg.vm.communicator = "winrm"

    # 네트워크 설정
    cfg.vm.network "public_network", ip: "192.168.0.221", gatway: "192.168.0.1", dns: "8.8.8.8"
    
    # VirtualBox 설정
    cfg.vm.provider "virtualbox" do |vb|
        vb.name = "win2019_target"    
        vb.customize ["modifyvm", :id, "--natnet1", "10.0.4/24"]
    end    
  end


  config.vm.define "win-10" do |cfg|
    # box 정보
    cfg.vm.box = "somma/windows-10"
    cfg.vm.define "win10"

    # 공유 폴더
    cfg.vm.synced_folder "./shared", "/vagrant"

    # guest boxes에 연결하는 communicator
    cfg.vm.communicator = "winrm"

    # 네트워크 설정
    cfg.vm.network "public_network", ip: "192.168.0.222", gateway: "192.168.0.1", dns: "8.8.8.8"

    # VirtualBox 설정
    cfg.vm.provider "virtualbox" do |vb|
        vb.name = "win10_agent"    
        vb.memory = 4096
        vb.cpus = 2
        vb.customize ["modifyvm", :id, "--natnet1", "10.0.3/24"]
    end

    # provision 설정
    cfg.vm.provision "shell", privileged: true, :inline => "
        reg.exe add 'HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows Defender' /v DisableAntiSpyware /t REG_DWORD /d 1 /f
        reg.exe add 'HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows Defender' /v DisableRoutinelyTakingAction /t REG_DWORD /d 1 /f

        reg.exe add 'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon' /v AutoAdminLogon /t REG_SZ /d 1 /f
        reg.exe add 'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon' /v DefaultUserName /t REG_SZ /d 'vagrant' /f
        reg.exe add 'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon' /v DefaultPassword /t REG_SZ /d 'vagrant' /f

        reg.exe add 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' /v ARES /t REG_EXPAND_SZ /d 'C:\\ares\\ares_run.bat' /f    

        cp -r c:\\vagrant\\ARESagent c:\\ares
    
        Expand-Archive -Path c:\\ares\\python.zip -DestinationPath c:\\ares\\python
    "

    cfg.vm.provision "reboot_system", type: "shell",  reboot: true
  end
end