태그: Intel

현대 CPU의 구조적 결함 – 멜트다운(Meltdown)과 스펙터(Spectre) 그리고 KAISER

 

지난 1월 2일, 미국의 인터넷 커뮤니티인 Reddit의 컴퓨터 서브레딧에 리눅스 커널에서 수상한 수정 작업이 이루어지고 있다는 소식이 올라왔습니다.

리눅스 커널에서 KASLR 관련 패치라는 코멘트를 달고 대규모의 수정과 백포팅이 이루어지고 있으며, 해당 패치를 적용할 시 시스템 콜 성능이 30%~70%까지 저하된다는 이야기였습니다. 이는 곧바로 이슈가 되었고, 그 다음날인 3일, 사태의 전모가 밝혀지게 되었습니다. 바로 멜트다운과 스펙터입니다.

여기서는 상기 링크에 게시된 멜트다운과 스펙터에 대한 논문과 멜트다운의 대응책으로 제시된 KAISER에 대한 논문을 참고하여, 공격 방법과 영향 범위에 대해 이야기해 볼 것입니다.

 

1. 멜트다운 공격

멜트다운과 스펙터는 모두 현대 CPU의 중요 구성요소 중 하나인 분기 예측기(Branch Predictor)의 결함에 근간을 두고 있습니다. 구글 프로젝트 제로(Project Zero)의 연구팀들은 이 결함을 총 3가지의 종류로 분류하였으며, 각각 경계값 검사 우회(CVE-2017-5753), 분기 목표 인젝션(CVE-2017-5715), 악의적인 데이터 캐시 로드(CVE-2017-5754)라는 이름을 붙였습니다. 이 중 앞의 두 가지는 Spectre 공격과 관련이 있고, 마지막 하나는 Meltdown 공격과 관련이 있습니다.

멜트다운 공격은 마이크로옵 단위에서의 레이스 컨디션 공격입니다. 현대 CPU는 길다란 명령어 파이프라인을 가지고 있고, 프론트엔드에서 해석된 명령어는 작은 단위의 명령어(μop)로 쪼개진 뒤 재정렬 버퍼(Re-Order Buffer)로 들어갑니다. 이를 통해 ALU가 데이터 병목 없이 최대한의 병렬성을 가질 수 있도록 합니다. 그리고 명령어 실행이 종료된 뒤, 실행 결과는 다시 정렬됩니다. 이러한 일련의 동작을 비순차적 실행(Out of Order Execution)이라고 합니다. 멜트다운은 인텔 CPU가 가지고 있는 분기 예측기의 결함과, Out-of-Order Execution 매커니즘의 결함을 악용하여 공격을 수행합니다.

멜트다운 공격은 다음과 같은 순서로 이루어집니다.

1. 공격자가 사용자 영역에서 접근이 불가능한 영역의 메모리(Ex. 커널 영역 메모리)를 참조하여 예외(Execption)를 일으킵니다.
2. 예외가 발생하였으므로, 코드의 실행 흐름은 예외 핸들러로 넘어가고, 분기 예측기의 투기적 실행과 비순차적 실행 매커니즘에 따라 로드된 메모리 값은 폐기됩니다.
2-1. 투기적 실행이 실패하였으므로, 파이프라인은 클리어되고 결과는 커밋되지 않습니다. 하지만 CPU의 캐시에는 해당 주소에서 불러온 값이 들어 있습니다.
3. 예외 핸들러가 코드의 실행 흐름을 물고 있는 동안, 다른 프로세스로 Flush+Reload 공격을 가해 캐시 데이터를 추측합니다.
4. 데이터 추출이 완료된 뒤 예외를 적절하게 처리하거나, 인텔의 TSX 명령어를 이용한 예외 억제(Exception suppression)를 통해 공격 속도를 증가시킬 수 있습니다.

결국, 멜트다운 공격은 CPU의 분기 예측기가 투기적인 명령어 실행을 수행할 때, 해당 메모리에 대한 접근 권한을 확인하지 않기에 발생하는 문제입니다. AMD는 이 문제에 대하여 ‘AMD 아키텍처의 분기 예측기 구조는 인텔의 것과는 다르기 때문에 안전하다’는 내용의 공식 성명을 내놓았습니다.

이 문제를 해결하기 위한 방법으로, 구글은 지금 즉시 모든 커널에 KAISER를 적용할 것을 권고하였습니다. KAISER는 본래 캐시 메모리 부채널 공격에 의해 KASLR이 무력화되는 것을 방지하기 위해 제안되었으나, 멜트다운 공격에도 역시 유효하다는 것이 확인되었습니다.

KAISER는 연속적으로 할당되어 있는 커널 영역 메모리와 유저 영역 메모리를 서로 분리하여 참조를 통해 접근하도록 수정하는 것을 골지로 하고 있습니다. 이러한 수정은 커널 영역 메모리에 접근할 때 오버헤드를 가져오게 되는데, CR3 레지스터의 PML4값을 변경하는 것과, TLB(Transaction Lookaside Buffer)의 초기화(Flush)가 그것입니다.

 

첫번째 문제를 해결하기 위해 KAISER는 Shadow Address Space라는 트릭을 제안합니다. 유저와 커널 메모리는 분리하지만, 물리 주소는 특정 값(12번째 비트)을 차로 가지도록 할당한다는 이야기입니다.
논문에서 제안된 대로 12번째 비트를 변경하여 메모리 페이지를 할당하게 된다면, 컨텍스트 스위칭이 발생할 때마다 전체 PML4 값을 갱신하는 것이 아니라, 1비트의 비트 연산을 수행하는 것 만으로 올바른 주소를 계산할 수 있게 됩니다.

다시 말해, CR3 레지스터는 변경되지만 변경에 필요한 연산과 클럭은 최소화될 수 있습니다.

두번째로 TLB에 대해 이야기하려면 먼저 IA-32e 아키텍처에서의 메모리 구조를 알아야 합니다. IA-32e 아키텍처에는 총 5단계에 걸친 메모리 페이지가 존재하고, 이러한 페이지 참조를 빠르게 하기 위해 CPU는 일종의 캐시인 TLB(Transaction Lookaside Buffer)를 가지고 있습니다.

TLB에는 페이지 번호와 페이지 프레임의 주소가 저장되고, TLB를 참조하게 되면 연산 유닛은 페이지 오프셋만을 가지고 정확한 물리 주소를 찾을 수 있게 됩니다. 이러한 이점을 최대화하기 위하여 현대 OS들은 TLB Flush가 최대한 발생하지 않도록 최적화가 이루어져 있습니다. 만약 KAISER를 적용한다면, 컨텍스트 스위칭마다 PML4 값이 변경되므로 TLB Flush가 필연적으로 발생하게 될 것입니다.

하지만, 최근의 연구에서 아키텍처 설명과는 달리 내부적으로 TLB에 대한 최적화가 이루어져서 CR3 레지스터의 변경이 TLB를 Flush하지 않는다는 사실이 밝혀졌습니다. 해당 논문은 이 사실에 근거하여 테스트를 진행, KAISER를 적용할 시의 성능 저하는 0.08%~0.68%에 불과하다고 결론짓습니다.

KAISER는 요청한 커널 페이지가 캐시에 로드되고, 두번째로 페이지 폴트가 발생할 때의 명령어 수행 시간의 차이를 탐지하여 올바른 커널 메모리 주소를 찾아내는 Double Page Fault 공격에 대한 대응책으로 제시되었지만, 결과적으로 커널 메모리 영역에 대한 시간차 기반 캐시 부채널 공격을 차단함으로써, 멜트다운 공격을 방어하는 것에도 역시 유효합니다. 하지만 유저 영역과 유저 영역에 남아있는 일부 커널 메모리 영역은 여전히 멜트다운의 영향을 받습니다.

 

2. 스펙터 공격

 

Intel Developer Manual – Virtual Machine Extensions(VMX) 소개

 

CPU는 VMX operation이라 불리는 동작을 통해 가상화를 지원한다. VMX operation에는 두 가지의 동작이 있다. 하나는 VMX root이고, 다른 하나는 VMX non-root이다.

VMX root : VMX가 작동하지 않을 때의 동작과 거의 유사하다. 주요한 차이점은 VMX 명령어를 지원한다는 것과, 특정 제어 레지스터로부터 값을 읽어들이는 것이 제한된다는 것이다.

VMX non-root: 몇 가지의 명령어가 제한되며, 가상화에 적합하도록 수정되었다. 그리고 특정 명령어(VMCALL 명령어 등)은 VM exits를 발생, 제어권을 VMM에게 돌려준다. 이때, 소프트웨어적으로 VMX non-root의 구동을 확인할 수 있는 방법은 없다. 그렇기 때문에 VMM은 게스트 소프트웨어가 자신이 가상 환경에서 실행되고 있다는 것을 알아차리지 못하게 할 수 있을 것이다. 실제로 소프트웨어가 특권 레벨 0에서 동작하고 있더라도, VMX는 게스트 소프트웨어가 원래의 특권 레벨에서 동작하도록 제한할 수 있다. 이것은 VMM의 개발을 쉽게 만들어 준다.

 

VMX 초기화

VMM이 VMX 모드로 진입하기 전에, CR4.VMXE[bit 13] = 1를 통해 VMX를 활성화해야 한다. 만약 CR4.VMXE = 0으로 VMXON을 실행하려 하면 Invaild-opcode exception을 발생시킨다.

VMX에 진입하면, CR4.VMXE를 초기화하는 것은 불가능하다. VMM은 VMX를 빠져나간 뒤 VMXOFF 명령어로 VMXE를 초기화 할 수 있다.

 

VMM 소프트웨어의 동작

  • VMXON 명령어를 통해 VMX 동작 모드로 진입한다.
  • VM Entry를 사용하여 VMM은 VM안의 게스트로 진입할 수 있다(한번에 하나씩)
  • VMM은 VMLAUNCH와 VMRESUME 명령어를 통해 VM entry를 발생시킨다. 그리고 VM exits를 사용하여 제어권을 다시 가져올 수 있다.
  • VM Exits는 VMM에 의해 지정된 엔트리 포인트로 제어권을 이동한다. VMM은 VM exits를 발생시켜 적절한 처리(non-root에서 수행할 수 없는 명령)을 하고, VM entry로 VM에 돌아온다.
  • VMM이 작동을 정지하고, VMX 동작에서 빠져나오려면 VMXOFF 명령어를 실행한다.

 

Virtual-Machine Control Structure(VMCS)

VMX non-root 동작과 VMX 전환은 VMCS라는 자료 구조체에 의하여 제어된다. 이것은 VMM이 메모리에 할당하는 4KB 크기의 구조체로, VM에 대한 정보들을 가지고 있다.

VMCS는 VMCS PointerVMPTR를 통해 접근할 수 있으며, 각 논리 프로세서에 하나씩 할당된다. VMPTR은 VMCS의 64비트 주소를 가리키고 있고, VMPTRST와 VMPTRLD명령을 통해 읽고 쓸 수 있다. VMM은 VMCS를 VMREAD, VMWRITE, VMCLEAR 명령어를 통해 제어한다. 이 때 VMPTR은 4KB Boundary에 맞게 정렬되어야 한다. 즉, 0~11비트의 값을 0으로 두어야 한다. 또한, 이 비트값들은 프로세서의 물리 주소값을 넘어서는 안 된다. 이 물리 주소값의 크기는 cpuid를 통해 구할 수 있다.

물리 주소 범위의 비트값이 EAX의 0~7비트를 통해 리턴된다.

VMM은 각각의 VM마다 별도의 VMCS를 사용할 수 있다. 만약 VM이 다중 프로세서를 가지고 있다면, 각 vCPU마다 하나의 VMCS를 설정할 수 있다.

 

VMCS는 크게 Current VMCS와 non-Current VMCS로 나뉜다. 각각의 VMCS들은 Active이거나 Inactive 상태일 수 있으며, Active 상태의 VMCS중 하나는 Current VMCS이어야 한다.

  • VMCS의 메모리 주소를 오퍼랜드로 하여 VMPTRLD를 실행할 경우, 해당 VMCS는 Active/Current VMCS가 된다. 다른 VMCS는 Active로 남으나, non-Current VMCS가 된다.
  • VMCS의 메모리 주소를 오퍼랜드로 하여 VMCLEAR을 실행할 경우, 해당 VMCS는 non-Active/non-Current/Clear VMCS 가 된다.
  • Active/Current/Clear VMCS의 주소를 오퍼랜드로 하여 VMLAUNCH를 실행할 수 있다.

즉, VMCLEAR -> VMPTRLD -> VMLAUNCH 순서로 VM을 활성화하게 된다.

 

VMX 동작 시의 제한

VMX 동작에서 프로세서는 CR0과 CR4의 몇몇 비트들을 고정시킨다. VMXON은 이 비트들에 다른 값이 들어갈 경우 동작하지 않는다.

그리고, IA32_FEATURE_CONTROL MSR (MSR address 3AH)또한 VMX 동작을 제어한다.

IA32_FEATURE_CONTROL MSR의 구조는 다음과 같다.

IA32_FEATURE_CONTROL

첫 번째 비트(Bit 0)는 논리 프로세서가 초기화 될 때 0으로 설정된다. VMX를 사용하기 위해서는 두번째(Bit 1)혹은 세번째(Bit 2) 비트를 1로 설정해 주어야 한다.

Bit 1은 SMX 동작시의 VMX 활성화를, Bit 2는 SMX 비활성화 시 VMX 활성화 여부를 제어한다. 이 비트들을 0으로 설정하고 VMXON을 실행할 경우 일반 보호 예외를 발생시킨다.

 

VMX non-Root Instructions – VM Entries

 

VMLAUNCH와 VMRESUME 명령어를 통해 VMX non-Root 모드로 진입할 수 있다.