[side-channel] Meltdown: Reading Kernel Memory from User Space
Loading content...
Abstract
컴퓨터 보안의 가장 기본적인 전제는 메모리 분리이다. 유저 프로세스와 커널 메모리는 주소 공간이 분리되어 있다. 커널 주소의 범위는 non-accessible로 표시되며 유저 모드에서 접근 시 즉시 fault 처리 된다. 즉 유저 프로그램은 커널 메모리를 절대 읽을 수 없다는 것이 모든 OS 보안의 출발점이다.
Meltdown은 out-of-order(OOO)의 부작용을 이용해서 유저 프로세스가 임의의 커널 메모리 주소를 읽을 수 있게 한다. 그 안에는 개인정보, password, key, token 등을 포함한다. out-of-order execution은 cpu 성능의 필수 요소이기에 대부분의 cpu에 존재한다.
이 공격은 OS와 무관하고 소프트웨어 버그가 아니며 하드웨어(마이크로 아키텍처) 취약점이다. Meltdown은 주소 공간 격리(user/kernel), 파라가상화(paravirtualization) 환경, 그 위에 쌓인 모든 보안 메커니즘(프로세스 격리, VM 격리, 컨테이너 격리)을 위협한다.
공격자는 권한 없이도 다른 프로세스의 메모리, 다른 VM의 메모리(클라우드)를 읽을 수 있다.
커널 페이지를 유저 페이지 테이블에서 제거하는 KAISER라는 기존 방어 기법이 의도치 않게 Meltdown을 막고 있어서 즉시 도입해야한다.
1. Introduction
메모리 분리
사용자 프로그램들이 서로의 메모리나 커널 메모리에 접근하지 못하도록 OS가 보장한다.
Supervisor bit(U/S bit)
현대 CPU는 페이지 테이블(가상주소 → 물리주소 변환표 + 권한 정보)의 각 메모리 페이지(메모리를 일정한 크기로 쪼갠 조각)마다 권한 비트를 둔다.
supervisor → 커널 모드만 접근 가능
user → 유저 프로세스도 접근 가능
커널 메모리 페이지는 Supervisor-only로 설정된다.
OS는 프로세스 주소 공간에 항상 매핑 된다
시스템 콜, 인터럽트, 예외처리 등은 자주 발생하기에 성능상의 이유가 있다.
만약 유저 → 커널 전환 시 마다 페이지 테이블을 완전히 바꾼다면 엄청난 overhead 발생한다.
유저 → 커널 전환 시에도 페이지 테이블은 안바뀐다.
CPU는 권한 체크를 메모리 접근 전에 반드시 수행한다
유저 코드가 커널 주소를 load하면 → CPU가 즉시 permission fault를 내고 → 데이터는 절대 읽히지 않는다
1.1 Meltdown
주장
소프트웨어 버그 없이, OS 상관 없이, CPU의 microarchitecture 문제로 사용자 프로세스가 커널 메모리를 읽는 공격이 가능하다.
Out-Of-Order execution 문제
CPU는 성능을 위해 명령어를 권한 체크 전에 실행할 수 있다(sepculative).
이후에 rollback 가능하다.
하지만 캐시 상태는 되돌리지 않는다.
Timing side channel 에서는 정상 실행, 비정상(추측) 실행 모두 정보 유출이 가능하다.
sequential : 전통적 캐시 공격 가능
out of order : meltdown 공격 가능
문제점
유저 모드 코드가 커널 주소를 load 하고자 하면 논리적으로는 불가능 해야하지만
→ out of order CPU는 권한 체크가 끝나기 전에 load를 일단 실행한다.
→ 그 결과 커널 메모리 값이 임시 레지스터에 들어가게 된다.
→ “임시” 레지스터란 : architectural register 가 아니며, 나중에 폐기 가능한 상태임을 나타낸다.
단순히
1load kernel_data
에서 끝이 아니라,
1value = *(kernel_addr);2array[value * 4096]+=1;
같은 후속 연산까지 실행되어, value가 커널 메모리에서 온 비밀 값인데 그 값으로 배열을 인덱싱 → 캐시 상태가 변하게 되는 정보 유출의 트리거가 된다.
CPU는 나중에 권한 위반을 감지하면
→ 레지스터 결과 폐기 + 프로그램에 fault 발생시킨다.
→ 최종 결과만 보면 커널 데이터는 유저에게 안보이고 프로그램은 올바르게 중단된 것 처럼 보인다.
architectural level에서는 문제가 없고 명세 위반이 아니지만,
microarchitectural level에서는 캐시, TLB, 분기 예측기, 실행 포트 사용 흔적 등이 rollback 대상이 아니기에 문제가 된다.
CPU는 결과를 버리면 안전하다고 생각했지만, 결과를 계산하는 동안 남긴 흔적을 버리지 못했다.
파이프라인
out-of-order 중에 실행된 권한 없는 메모리 접근(load)이 캐시 상태를 바꾼다.
캐시는 측정 가능하다.(Flush + Reload / Prime + Probe)
커널 데이터가 캐시 패턴으로 변환된다.
결과적으로, 커널 주소 공간이 모든 프로세스 주소 공간에 매핑되어 있고 entire kernel memory dump가 가능하다.
covert channel 수신을 완료하면 레지스터 값이 복구 가능하다. (레지스터 → 캐시 → time → 레지스터 값 복원)
1.2 KAISER
소개
KASLR 깨는 부채널 공격을 막기 위해 고안되었지만 Meltdown에 대응한다.
원리
Meltdown : 커널 메모리가 항상 매핑되어 있고 권한 체크만으로 접근을 차단한다.
KAISER : 유저 모드의 경우 커널 주소 공간 자체가 없고, 커널 모드일 경우 커널 페이지 테이블로 전환한다.
1.3 Spectre
차이점
Spectre는 victim의 소프트웨어 구조에 맞게 공격을 조정해야 하지만, 더 넓은 CPU에 적용되며 KAISER로는 막을 수 없다.
항목
Meltdown
Spectre
공격 대상
커널 메모리
다른 프로세스 / 동일 프로세스
공격 방식
권한 체크 우회
분기 예측 오염
공격 난이도
비교적 단순
환경 맞춤 필요
영향 CPU
주로 Intel
거의 모든 CPU
KAISER로 차단
YES
NO
1.4 Contribution
기여
1.5 Outline
문제 제기 → 예시 → building blocks of Meltdown → 공격 → 성능/한계 → 방어책 → 관련 연구 → 결론
2. Background
out-of-order execution과 address translation, cache attack의 background에 대해 살펴 볼 예정이다.
2.1 Out-of-order execution
특징
OOO는 한 코어 안의 모든 execution unit을 최대한 바쁘게 쓰기 위한 최적화 작업이다.
순서대로가 아니라 준비된 것부터 실행한다.
speculative : 추측 실행 → e.g. branch에서 결과가 나오기 전에 예측 실행한다.
cpu가 그 결과를 확정(커밋)하기 전에도 실행할 수 있다.
알고리즘
1967 Tomasulo : 다이나믹 스케줄링으로 OOO 기법을 구현하였다.
reservation station을 사용해서 결과를 레지스터에 쓰고 다시 읽는 대신 계산되자마자 바로 다음 연산에 전달한다.
Front-end : 메모리에서 x86 명령어 fetch → 여러개의 µOP(micro-operation) 로 분해한다.
Reorder buffer(ROB) : 레지스터 할당, 레지스터 리네이밍, 결과 커밋(retire), 예외처리 & 롤백하는 일을 담당함. 이때 커밋은 반드시 프로그램 순서대로 해야 한다.
Unified Reservation Station(scheduler) : µOP들을 실행 유닛 종류별로 대기시키고 필요한 피연산자가 준비되면 즉시 실행 유닛으로 전송한다.
Execution units(실행 유닛) : ALU(정수 연산), AES-NI(암호 연산), AGU(주소 연산), Load/Store(메모리 접근)
이 중 AGU + Load/Store는 memory subsystem과 직접 연결된다.
Branch prediction & speculation
CPU 효율을 위해 예측하고 실행한다.
분기 예측 종류
Static prediction : 명령어 자체를 보고 예측하는 것, 단순하고 정확도가 낮다.
Dynamic prediction : 실행 중 통계를 수집한다.
One-level(1-bit / 2-bit) : 지난 번 결과를 기억한다.
Two-level adaptive : 최근 n번 히스토리 기반, 반복 패턴에 강하다.
Neural branch prediction : 퍼셉트론 기반이고, 최근 CPU에서 실제로 사용한다.
분기 예측 실패 시 :
ROB flush, reservation station 초기화, 파이프라인 재시작을 하지만, 캐시 / TLB 등 미세구조 상태는 되돌아가지 않는다.
2.2 address spaces
프로세스를 서로 격리하기 위해서 cpu는 가상 주소 공간을 지원한다.
가상 주소 공간을 페이지 단위(4KB)로 나누어서 각 페이지를 어느 물리 페이지로 매핑할지 정한다. ← multi-level translation table
주소 공간
cpu 레지스터가 현재 사용하는 페이지 테이블을 가리킨다. → context switch가 발생할 때, os가 이 레지스터를 업데이트 한다. → 그래서 프로세스마다 서로 다른 가상 주소 공간을 얻게 된다. → 그래서 결국 각 프로세스는 “자기 주소 공간”만 참조 가능하다.
각 가상 주소 공간은 그 자체로 유저공간과 커널 영역으로 나뉜다.
유저 영역은 응용에서 접근 가능하지만, 커널 영역은 CPU가 특권 모드에 있을 때만 접근 가능하다.
유저 버퍼에 데이터 채우기, 파일 읽어서 유저 메모리에 복사 등 커널은 유저 페이지도 만지고 커널 페이지도 만진다.
즉, 커널은 보통 전체 물리 메모리를 매핑한다.
Linux/ OS X : direct-physical map(직접 물리 매핑)
전체 물리 메모리가 미리 정해진 가상 주소에 대해 직접 매핑 되어있다.
커널은 PA를 알면 KVA를 알기 쉽다.
Windows : pool + system cache
paged pools : 필요하면 디스크로 내릴 수 있는 커널 메모리
non-paged pools : 절대 디스크로 내려가면 안되는 커널 메모리(항상 RAM에 있어야 한다)
system cache : file-backed 페이지(파일 내용이 RAM에 캐시된 것들)의 매핑
이것들이 합쳐지면 커널 주소 공간에 물리 메모리의 큰 부분이 매핑된다.
ASLR/KASLR : Address space layout randomization / kernel address space layout randomization
기존 메모리 취약점(버퍼 오버플로우 등)을 악용하려면 주소를 알아야 하는 경우가 많다.
→ 그 대안으로 등장한 게 ASLR : 주소 랜덤화, stack canary : 스택 변조 탐지, NX : 실행 불가 스택, KASLR : 커널 주소 랜덤화
하지만 사이드 채널이 KASLR도 무력화 한다.
2.3 Cache attacks
캐시란
메모리 접근 속도를 높이거나 주소 변환 속도를 높이기 위해 CPU는 캐시라는 메모리 버퍼를 가진다.
주소 공간 테이블도 메모리에 저장되고 따라서 캐시된다.
캐시 공격
Evict + Time
Prime + Probe
Flush + Reload
사이드 채널 공격
covert channel : 공격자가 정보를 보내는 쪽과 받는 쪽 모두를 통제하는 비밀 통신 채널
attacker ↔ attacker의 독특한 구조이다.
멜트 다운에서의 도메인
송신자 : 커널 주소에서 읽힌 값
수신자 : 유저 공간 공격 코드
아키텍처 레벨을 우회한다
아키텍처가 정의하지 않은 통로(covert channel) 이용
3. Toy example
3.1 아키텍처 관점
1// (1) 일부러 예외를 발생시킴 (예: invalid memory access / divide by zero 등)2raise_exception();34// (2) 예외 이후에 배열 접근 (원래라면 절대 실행되면 안 됨)5data =...; // 어떤 값 (예: 84)6probe_array[data * 4096]+=1;
아키텍처적으로 예외가 발생하면 CPU는 코드 진행하면 안되는것으로 진단하고 처리 방식에 따라 프로세스가 죽거나, 커널의 예외 핸들러로 점프한다.
그런데 CPU는 성능 때문에 out-of-order 실행을 한다. 명령을 순서대로만 실행하는 것이 아니라, 의존성이 없으면 앞질러 실행할 수 있다. 다만 이것은 retire(커밋)되지 않아서, 프로그램이 읽는 레지스터 값이나 메모리 내용은 변하지 않는다. 그러므로 architectural한 영향은 없다. 그럼에도 그들은 microarchitectural 한 사이드 이펙트가 있다.
architecture 상태 : 레지스터, 메모리 : 변화 없음
micro architecture 상태 : 캐시, 예측기, 버퍼 등 : 변화 남음 → leak
이후에 Flush + Reload, Prime + Probe 를 통해 캐시로부터 정보를 얻는다.
3.2 4096
data * 4096(=4KB) 로 페이지 단위를 벌려놓는 이유가 중요하다.
대부분 시스템에서 페이지 크기 = 4096 bytes 이다.
probe_array[data * 4096] 는 data 가 0 이면 page 0 쪽, data가 84면 84번째 페이지의 시작 근처 등 값 1개 ↔ 페이지 1개로 1:1 매핑 된다. (injective mapping 단사) 이로써 서로 다른 data 값이 같은 페이지를 건드리는 일이 없게 만든다.
CPU에는 prefetcher가 있어서 연속적으로 접근하는 것 같으면 다음 주소도 미리 가져오는 최적화를 한다.
만약 인덱스가 촘촘하면, data = 84 하나만 접근해도 prefetcher가 주변 인데스도 캐시에 올려 “여러 군데가 hit로 판정되는 false positive”가 생길 수 있다. 그런데 페이지 경계(4KB)를 건너뛰면 진짜 접근한 페이지만 hit되게 할 수 있는, 즉 측정 정확도를 높일 수 있게 된다.
4. Building blocks of the attack
커널 데이터, 다른 프로세스의 데이터, 레지스터 값(컨텍스트 스위치 시 스택/메모리에 저장됨)과 같은 비밀(secret)은 “물리 메모리”에 있다.
일반적인 OS에서 각 프로세스의 가상 주소 공간에는 사용자 공간, 커널 공간 이 둘이 모두 매핑되어있다. 이때 커널 공간은 privileged mode(커널 모드)에서만 접근 가능하지만 CPU가 권한 체크를 완료하기 전에, OOO로 메모리 값을 읽어버리게 만들고 그 값을 캐시 상태로 외부에 새게 만드는 meltdown 공격이 가능하다.
결과적으로 공격자는 사용자 권한으로 다른 프로세스의 물리 메모리, 커널 내부 구조체, 비밀번호, 키, 데이터 같은 커널 메모리 전체를 읽는 효과를 가진다.
Spectre
Kocher et al.
orthogonal 한 approach이다.
읽는 대상 : victim process가 원래 접근 권한은 가진 데이터
방법 : 분기 예측, speculative execution을 속임
특징 : 권한 상승은 없음, victim code에 의존
방어 : KAISER로는 안막힘, 광범위한 CPU에서 영향
Building block
빌딩 블록 1
transient instruction
원래는 절대 실행되면 안되는 명령이지만 OOO 때문에 잠깐 실행된다. 예외/권한 실패 때문에 retire는 안된다.
transient instruction sequence
이런 transient instruction을 하나 이상 포함한 명령 시퀀스이다.
meltdown에서는 권한 없는 메모리 읽기(load)가 transient instruction이 된다.
중요한 조건 : secret = *(kernel_address); 같은 형태로 비밀값이 실제로 데이터 흐름에 들어가야 한다.
빌딩 블록 2
transfer micro architectural side effect of the transient instruction sequence to an architectural state to further process the leaked secrete
micro architectural → architectural 전달한다.
transient 실행의 결과는 남지 않아 볼 수 없어서 → 캐시, TLB, 분기 예측기 같은 마이크로아키텍처 흔적을 관측 가능하게 변환해야 한다.
방법 : covert channel
결론
4.1 Executing transient instructions
트랜전트 명령어
transient 실행은 캐시 같은 마이크로 아키텍처 상황을 바꿀 수 있다.
항상 exploitable side channel이 되는 것은 아니다.
transient하게 실행된 코드가 secret 값에 따라 다른 주소를 접근한다든지, 다른 캐시 라인을 데운다든지, 다른 분기/테이블을 건드린다든지 처럼 secret에 의해 동작이 달라지면, 그 차이가 흔적으로 남아 leak로 이어진다.
공격 대상
사용자 프로세스의 가상 주소 공간에 커널 주소 범위도 같이 매핑되어 있는 경우가 많고 다만 권한 비트로 “user 모드 접근 금지”가 걸려 있었다.
그래서 사용자 프로그램이 커널 주소를 읽으면 일반적으로는 예외(page fault)가 나야 한다.
막혀있는 kernel page에서 transient하게 값이 읽히는 순간이 생기면 → 그 값을 캐시로 인코딩해서 leak한다는 것이다.
물리 메모리 탈취
리눅스 커널은 관리를 위해 시스템의 전체 물리 메모리(RAM)를 커널 주소 공간의 특정 부분에 1:1로 매핑해 둔다.
공격자가 자기 프로세스 안에 매핑된 커널 메모리만 읽을 수 있게 되면, 결과적으로 그 안에 들어있는 다른 모든 프로세스의 메모리(비밀번호, 암호키 등)와 물리 RAM 전체를 읽을 수 있다는 뜻이다.
현실적인 문제
커널 페이지와 같은 사용자 접근이 불가한 페이지들은 접근 시 예외처리로 프로그램이 종료된다.
대응 전략
Exception handling(예외 처리) : 예외는 어차피 나는데, 대신 난 다음에 컨트롤을 내가 하겠다 !
Exception suppression(예외 억제/회피) : 예외 자체가 아키텍처적으로는 발생하지 않게 만들어서, 흐름이 끊기지 않게 하겠다 !
Exception handling
예외가 난 뒤에 프로그램이 계속 동작하도록 만든다.
Exception suppression
예외가 아키텍쳐적으로는 안나게 만든다.
4.2 Building a covert channel
Meltdown의 두번째 building block은 microarchitectural state의 전송이다.
microarchitectural state는 transient instruction sequence에 영향 받은 상태를 말한다.
Covert channel(은닉 채널)
수신 측은 microarchitectural state를 수신하고, secret을 deduce 한다.
수신자는 transient instruction sequence의 부분이 아니고 다른 스레드나 프로세스일 수 있다.
1-bit channel
sender가 캐시에 올리면 1, 안올리면 0을 의미한다.
transient sequence가 접근 가능한(유저가 접근 가능한) 주소 하나를 평범하게 읽고, 그 결과 해당 주소가 캐시에 올라간다. receiver는 그 주소를 reload 해보고 hit/miss 로 1/0을 판정한다.
한번에 1byte(256가지)도 가능하지만, 왜 1 비트를 사용하는가?
Flush + Reload 쪽에서 병목이 발생하기 때문에, 1 비트씩 전송하는게 효율적이다.
secret 값을 shift + mask 해서, 지금 보내고 싶은 비트만 남긴 뒤 그 비트에 따라 라인 접근/미접근으로 인코딩 한다.
covert channel의 다른 예
캐시 말고도 관측 가능한 마이크로 아키텍처 상태라면 뭐든 가능하다.
sender가 어떤 실행 포트(e.g. ALU 포트)를 바쁘게 점유하는 instruction sequence를 실행해서 1을 보내고, receiver는 같은 포트를 쓰는 연산을 실행해 보고 지연이 크면 1, 작으면 0으로 해석하는 등의 방법도 있다.
Flush + reload의 장점
노이즈에 강하고, 전송률이 높다.
더욱이 다른 CPU 코어에서도 관측 가능해서, 스케줄링/리스케줄링에서도 채널이 크게 깨지지 않는다.
5. Meltdown
attack setting
공격자는 해당 머신에서 일반 유저 권한으로 임의 코드를 실행할 수 있다.
공격자는 물리적 접근을 하지 않는다.
ASLR, KASLR 같은 소프트웨어 방어와, SMAP, SMEP, NX, PXN 같은 CPU 기능이 켜져있다고 가정한다.
OS는 bug free로 가정해 소프트웨어 취약점이 존재하지 않는다고 가정한다.
나중에 다시 확인해볼 것.
5.1 attack description
Meltdown consists of 3-steps :
Step 1 : Reading the secret
메모리에서 레지스터로 로드
메인 메모리의 값을 레지스터로 가져오려면, CPU는 가장 먼저 가상 주소(VA)를 가지고 접근한다. 이때 CPU 내부에서는 두 가지가 병렬로 진행된다.
이러한 permission bit를 통한 HW based isolation(user/kernel 격리)은 안전하다고 여겨졌고, 현대 OS는 항상 커널을 유저 프로세스의 VA 공간에 매핑한다.
CPU는 권한 없는 메모리 접근이 일어났을때, 예외 처리가 일어나는 그 시간 틈새를 이용해 비밀을 읽는다.
line 4 :
mov → copy. 한개의 코어에 fetch되고, 여러개의 µOPs로 decode 된 후, ROB로 보내진다.
[ ] → 그 주소에 직접 가서 안에 들어있는 값을 가져와라
rcx → 메모리 주소가 담긴 상자, 커널의 주소가 들어있다(일반 사용자가 들여다 보면 안되는)
al → RAX의 일부, cpu 안의 작은 임시 저장 공간이다
즉, RCX가 가리키는 커널 주소에 가서 데이터를 1바이트 읽어와서 AL에 담아 라는 명령!
CPU 내부 파이프라인
커널 데이터가 버스에 보이는 순간(CDB + coherence)
line 4가 데이터를 가져오는 동안, 뒤 µOP들이 reservation station에서 기다리다가
data가 common data bus(CDB) 에서 관측되는 순간, 그 µOP들이 실행을 시작할 수 있다.
interconnect와 cache coherence protocol이 최신 값을 읽도록 보장하는데, 즉 커널 값이 L1,L2,L3,메모리,다른 코어 캐시 어디에 저장되어 있든 가장 최신 값이 전달된다.
Pipeline Flush
결과가 아키텍처적으로 남지 않는 이유는, µOP들이 실행을 끝내도 커밋은 in-order로만 진행된다. 이 커밋 시점에 인터럽트/예외가 처리된다. 따라서 line 4의 MOV 가 커밋되는 순간 예외가 등록되고 파이프라인은 flush가 되어 out-of-order로 실행된 후속 명령들의 아키텍처 결과가 제거된다.
즉 레지스터/메모리 같은 architectural state에는 흔적이 남지 않아 안전하다고 평가했다.
Race condition
예외가 raise/처리되어 flush되기 전에 step 2에서 캐시 인코딩이 끝날 수 있다.
Prefetch
step 1은 결국 예외가 나기 전 데이터가 빨리 도착하면 도착할수록 레이스를 이기기 좋다. prefetch로 타깃 주변이 캐시에 더 가까워지면, line 4의 데이터가 CDB에 빨리 나타날 확률이 올라가고, 그러면 µOP들이 실행할 시간을 확보하기 쉬워진다.
Step 2 : Transmitting the secret
Instructions
Step 1에서 out-of-order로 실행될 명령열은 transient instruction sequence가 되도록 골라야 한다.
MOV 가 retire(커밋)되기 전에 transient sequence가 실행되고, 그 안에서 secret으로 계산을 했다면, secret을 전송할 수 있다.
CPU cache
Step 2는 secret → 관측 가능한 상태로 바꾸고자 한다.
관측 가능한 상태로 측정이 빠르고 노이즈가 적은 캐시를 선택한다. 그래서 step 2는 secret에 따라 특정 캐시 라인을 캐싱하는 형태로 설계한다.
Protocol
공격자가 미리 큰 배열(probe array)을 만들고 step 2를 실행하기 전에 clflush 등으로 비워 놓는다.
Secret으로 계산한 주소에 대해 간접 메모리 접근(indirect access)을 넣는다. 간접 접근은 base + f(secret)의 형태이다.
간접 접근을 하는 이유는 secret이 0~255 중 무엇인지에 따라 서로 다른 주소를 만지도록 해야 하기 때문이다.
line 5 : shl rax, 0xc
0xc = rax *= 4096
→ secret 값 v 가 있으면 접근 주소가 base + v * 4096 이 된다.
secret을 페이지 크기(4kb)로 곱한다.
곱셈은 배열 접근 간 거리를 크게 만들어 prefetcher가 옆까지 가져오는 것을 막는다.
한번에 1 바이트 씩 읽는다. 따라서 probe array 는 256*4096 byte 이다.
→ secret 이 1 byte이면 가능한 값이 256개(0~255), 각 값마다 4KB 페이지 하나를 대응시키니 총 크기 = 256 페이지 = 256*4096 byte.
→ 값 후보(256개) 마다 전용 페이지 하나씩 배정한다는 느낌이다.
Noise bias
OOO 실행에는 레지스터 값이 0으로 읽히는 noise bias 현상이 있다.
그래서 transient sequence 안에 retry-logic을 넣는다.
retry logic
‘0’을 읽으면 Step 1을 다시 시도한다.
line 6의 jz retry 에 해당한다.
Encoding
Line 7 : mov rbx, qword [rbx + rax] → 인코딩 동작이다.
probe_base + secret * 4096을 읽으면, 그 주소가 포함된 캐시 라인이 L1에 올라간다. 그러면 step 3에서 reload 타이밍이 빨라져서 secret을 복구할 수 있게 된다.
주소는 L1에 올라가고, inclusive 하기 때문에 L3에도 올라가서 다른 코어에서도 읽힐 수 있다.
결과적으로, transient sequence가 secret 값에 따라 캐시 상태를 바꾼다.
Step 1에서 얻은 secret이 어느 페이지를 캐싱할지 결정하고, 캐시는 secret에 대해 표식(인코딩된 상태)가 된다.
성능 향상
Step 2는 예외 raise와 race 중이므로 step 2 런타임을 줄이면 성능이 좋아진다.
Step 2가 빠르면 flush가 오기 전에 인코딩이 끝나 성공률이 증가한다.
e.g. probe array 주소 변환이 TLB(translation lookaside buffer)에 캐시되게 하면 성능이 향상된다.
probe array에 접근할 때도 주소 변환(VA → PA)가 필요하다.
TBL 미스가 나면 step 2가 길어진다.
probe array 관련 번역이 TLB에 올라가 있으면 step 2가 빨라져 성공률이 올라간다.
Step 3 : Receiving the secret
receiving end
캐시는 마이크로 아키텍쳐 상태라 프로그램 변수로 직접 읽을 수 없다.
그래서 cache state → 측정 시간으로 변환하는 것이 receiving end의 역할이다.
Flush + Reload로 해당 작업을 진행한다.
Step 2의 Transient instruction sequence가 한 번 실행될 때, probe array에서 하나의 캐시라인만 캐시된다.
캐시된 위치는 Step 1에서 읽은 secret에만 의존한다. 따라서 hit가 나온 위치 = secret이라는 1 : 1 매핑이 성립한다.
공격자는 probe array의 256개 페이지를 전부 순회하며 캐시라인 접근 시간을 잰다.
dumping the entire memory
대부분의 OS는 커널이 물리 메모리에 빠르게 접근하려고 direct-physical map(물리 메모리 전체를 커널 가상주소에 연속적으로 매핑) 같은 것을 둔다.
커널 주소공간이 유저 프로세스에도 매핑 되어있는 경우가 많아서, meltdown은 커널 가상 주소를 통해 결국 물리 메모리 전체 영역까지 순회하며 읽어낼 수 있다.
5.2 optimizations and limitations
Inherent bias towards 0
CPU는 OOO load 에서 값이 아직 없으면 stall 한다.
하지만 성능 최적화를 이유로 CPU는 값을 가정하고 OOO 실행을 계속할 수도 있다.
Meltdown의 불법 load가 ‘0’을 반환하는 것을 관찰했다.
mov 대신 add 명령어를 사용하면 이 현상이 더 자세히 보인다.
‘0’ 편향 이유
race condition
‘0’ bias는 race condition에서 이기면 진짜 값이 되고 지면 ‘0’이 되는 결과로 나타난다.
이 편향은 머신/구성/구현에 따라 다르다. 최적화하지 않으면 ‘0’ 오류가 높다.
meltdown 구현에서 재시도는 트레이드오프이자 최적화 파라미터라고 한다.
실측
Intel i5-6200U에서 Exception handling을 이용했을 때, ‘0’은 평균 5.25%, retry 시 0.67%로 감소했다.
Intel i7-8700K에서 ‘0’ 은 평균 1.78%, TSX(exception suppression)를 이용한 후 0.008% 까지 감소시켰다.
Optimizing the case of 0
내재 편향 때문에 캐시 라인 ‘0’에 대한 캐시 히트는 정보가 되지 않는다.
따라서 ‘0’ 캐시 히트는 무시하고, 다른 캐시 히트가 없을 경우 ‘0’으로 간주한다.
‘0’ 이 나올 경우 루프를 돌리는데, non-zero 값이 나오거나, 예외가 raise 되면 끝난다.
invalid access 이후의 루프는 exception handling, exception suppression가 제어권을 돌려주는 시간에 영향을 거의 안준다(독립적이다).
single bit transmission
5.1 장에서는 8비트(1바이트)를 비밀 채널로 전송해 256개의 Flush+Reload로 복구했다.
하지만 transient sequence 실행 횟수와 Flush+Reload 측정 횟수 사이에 트레이드오프가 있다.
mov 로 더 많은 데이터를 읽거나 비트에 마스킹을 해 원하는 비트만 전송하는 등의 가공 작업을 할 수도 있다.
병목 현상의 원인은 Flush+Reload에 있다.
그래서 1개의 비트만 보내서 cache line 1만 측정한다.
1 비트 전송의 단점은, ‘0’ 편향이 있는데 1비트 전송을 하게 되면 불리하다.
그래서 한번에 몇 비트를 보낼 지는 “오류율”과 “측정 비용”의 트레이드오프다.
어쨌든 오류율이 둘 다 작아서, 논문은 1 비트 방식으로 진행한다.
Exception Suppression using Intel TSX
invalid access 예외를 안나오게 할 수도 있다. TSX로 완전히 suppress 가능하다.
인텔 TSX를 활용하면, 다수의 명령어를 트랜잭션으로 묶을 수 있고, 이렇게 되면 atomic operation 처럼 보이게 된다.
트랜잭션 내부에서 하나가 실패하면, 이미 실행된 결과는 되돌리지만 예외는 raise 되지 않는다.
listing 2 코드를 TSX로 감싸면 예외는 억제되고 캐시는 남는다. 결과적으로 커널 트랩해서 예외 처리 하는 것보다 TSX 억제가 빨라 채널 용량(throughput)이 커진다.
dealing with KASLR
kernel address space layout randomization 기법은 리눅스 커널의 주소를 부팅 시에 랜덤화하는 것이다.
KASLR이 켜지면 direct-physical map도 랜덤화되어, meltdown 전에 그 오프셋을 알아야 한다.
랜덤화는 40비트로 제한되기 때문에, 8GB 램 머신 기준 128번의 테스트면 40비트 공간을 커버한다.
6. Evaluation
6.1.1 Linux
2.632 to 4.13.0
위 버전에서 유저 프로세스 주소 공간 안에 커널 주소 공간도 매핑해 두었다.
물론 페이지 테이블의 U/S 비트 같은 권한 설정 때문에 유저 모드에선 접근이 막힌다.
meltdown은 커널 베이스 주소만 알면 커널 메모리를 읽을 수 있다.
KASLR이 존재해도, at most 128번의 location만 검사하면 되기 때문에, 꺼져 있거나 사전 단계에서 오프셋을 알아냈다고 두고 진행한다.
6.1.2 Linux with KAISER patch
KAISER patch by Gruss et al.
커널 공간과 유저 공간을 강하게 분리한다.
유저 주소 공간에 커널 메모리를 아예 매핑하지 않는다.
x86 구조상 꼭 필요한 일부(인터럽트 핸들러 등)만 예외적으로 남긴다.
direct-physical map과 같은 유저 공간에서 커널 메모리/물리 메모리의 직접적인 매핑은 없다. 해당 주소를 주어도 페이지 테이블 변환이 성립되지 않는다. 그래서 Meltdown은 대부분의 커널/물리 메모리를 못 읽고 남겨둔 몇개의 예외 매핑만 잠재적으로 남는다.
남은 예외 매핑은 KASLR로 랜덤화까지 되어있으면, 완전히 무해한가에 대해서는 7.2절에서 추후 논의한다.
6.1.3 Microsoft Windows
windows
윈도우 환경에서도 meltdown 실증 성공했다.
리눅스와 반대로, 윈도우는 identity mapping(물리 메모리 선형 매핑)이 없다.
대신에 windows는 커널이 쓰는 메모리 관리 구조(paged pool / non-paged pool / system cache)에 물리 페이지들이 매핑되어 있고, meltdown은 커널 주소 공간에 매핑되어 있는 것을 읽을 수 있으니 보안 취약점이 된다.
윈도우도 커널을 모든 앱의 주소 공간에 매핑한다.
따라서 swap-out 안된 커널 부분, paged/non-paged pool, system cache에 매핑된 페이지들을 읽을 수 있다.
6.1.4 Android
Exynos 8 octa 8890
(ARM Cortex-A53 CPU) 갤럭시 s7도 실증했다. ARM의 cpu는 공격이 성공하지 못했으나, 삼성의 커스텀 코어는 공격에 성공했다.
6.1.5 Containers
containers sharing a kernel
Docker, LXC, OpenVZ 같은 ‘커널 공유 컨테이너’에서 제한 없이 공격이 가능하다.
컨테이너 안에서 돌리면 호스트 커널 뿐만 아니라 같은 물리 호스트의 다른 컨테이너 정보도 유출된다.
대부분의 컨테이너는 같은 커널을 사용한다.
따라서 커널이 가진 direct-physical map도 공유되고, 결과적으로 전체 물리 메모리에 대한 유효 매핑이 컨테이너에도 존재하게 된다.
Meltdown은 메모리 access만 쓰므로 컨테이너에서 차단하기 어렵다.
6.1.6 uncached and uncacheable memory
L1 cache
타겟 데이터가 공격자의 L1 캐시에 없으면 meltdown이 안되는지에 관한 실험을 진행한다.
두 프로세스를 서로 다른 물리 코어에 pin 한다.
clflush로 값을 날리고 다른 코어에서만 reload 해서 공격자 코어의 L1에는 없는 상황을 만든다.
그래도 leak 가능하다.
→ 공격자 코어의 L1에 데이터가 존재하지 않아도 유출이 가능하다.
meltdown이 uncached memory를 leak할 수 있는건 meltdown이 명시적으로 데이터를 캐싱하기 때문이다.
uncacheable
페이지를 uncacheable로 마킹하고 leak 시도한다.
캐시 불가능 상태이면 읽기/쓰기마다 메인 메모리로 바로 가서 캐시를 쓰지 않으니 meltdown 공격은 불가능해 보인다.
공격자가 정상적인(load가 합법적으로 일어나는) 접근을 먼저 유발할 수 있으면, uncacheable 페이지도 leak 가능하다.
Line Fill Buffer(LFB)에서 값을 읽는 것으로 추정하고, LFB는 같은 코어의 스레드 간 공유라 가능해 보인다.
→ future work
MMIO(memory-mapped I/O) 영역에서는 동작하지 않는다.
MMIO 접근은 항상 architectural effect가 있기 때문이다.
6.2 Meltdown performance
known values from kernel memory
성능을 측정하기 위해 커널 메모리에서 알려진 값을 사용해 속도와 에러율을 측정한다.
(알려진 값은 시스템 콜을 이용해 데이터를 심고 캐싱을 유도하는 과정일 듯)
race condition이 존재하지만 조건만 맞추면 레이스를 이길 수 있다.
특히 L1 cache와 같이 코어에 가까울수록 load가 빨라서 레이스를 이길 가능성이 높다.
reading rates
error rates
비고
i7-8700K
582KB/s
0.003%
L1
i7-6700K
569KB/s
0.002%
L1
Xeon E5-1630
491KB/s / 137KB/s
10.7% / 0%
L1
i7-6700K
12.4KB/s
0.02%
L3, exception suppression
10B/s
uncached
3.2KB/s
uncached - optmizations
improving reading rates
6.3 Limitation on ARM and AMD
not affected
ARM Cortex-A75 만 영향 있었다고 알려져 있다.
커널 메모리 읽기 형태 말고, 시스템 레지스터를 타겟으로 하는 변형 공격은 여러 ARM에서 가능하다고 언급됐다.
AMD 는 영향 없음
AMD/ARM 도 out-of-order 자체는 하고, 불법 접근 뒤의 명령이 transient하게 실행되는 현상 자체는 관측된다.
다만 커널 값을 가져오는 load가 permission check보다 먼저 유의미하게 진행되느냐와 같은 세부 구현 차이 때문에 Meltdown이 안되었을 수도 있는 것이다.
7. Countermeasures
7.1 Hardware
hardware mitigation
Meltdown은 커널/유저 분리와 같은 하드웨어가 강제하는 격리를 우회한다.
따라서 7.2절의 KAISER와 같은 소프트웨어 패치로는 완벽한 보안이 힘들다. 하드웨어 재설계나 microcode update에도 수정될지에 대한 확신은 없다.
OOO
사소한 해결책으로 out-of-order 기능을 제거하면 될 것 같지만, 성능 하락이 치명적일 것으로 예상된다.
Meltdown
Meltdown은 메모리 주소 fetch와 권한 체크의 race condition이다.
권한 체크와 레지스터 fetch의 연속화 작업이 대응책이 될 수 있다.
그러나 이 작업도 메모리 fetch 마다 권한체크를 stall해야 하므로 overhead이다.
split
유저/커널 공간의 강한 분리가 실용적인 해결책이다.
CPU control register의 CR4에 새로운 hard-split bit 옵션을 넣자.
hard split이 켜지면 커널은 가상 주소 상단의 절반, 유저는 가상 주소 하단의 절반에 위치한다.
CPU가 어떤 주소를 load 하려 할 때, 주소의 위치에 따라(상단/하단) 페이지 테이블 조회 같은 추가 확인 없이 주소만 보고 즉시 판단할 수 있을 것이다.
bit는 기본적으로 off, 커널이 지원할 때만 on, 따라서 기존 SW는 그대로 동작하고 serialize보다 overhead가 적을 것 같다고 예상한다.
이 방법은 스펙터에 적용되지 않고, 스펙터의 대응책도 meltdown에 적용되지 않는다. 둘 다 방어해야 한다.
7.2 KAISER
필요성
하드웨어 취약점이기 때문에 모든 cpu를 바꿀 수 없으니 소프트웨어적 방어법도 제시한다.
KAISER는 kernel modification이다.
user space 에 kernel map 을 없애는 것이다.
유저 공간 페이지 테이블에서 커널 매핑을 제거하는 커널 패치이다.
KAISER는 본래 KASLR(커널 주소 랜덤화) 공격을 대응하기 위해 고안되었다.
비슷한 개념의 LAZARUS 도 있다.
limitations
x86 아키텍처 설계 때문에, 인터럽트 핸들러 등 일부는 유저 페이지 테이블에도 일부 privileged 메모리를 남겨야 한다. 이 부분은 여전히 meltdown에 취약하다.
남은 메모리에 비밀이 없더라도, 커널 포인터(주소)가 들어있을 수 있다.
포인터 하나만 새도, KASLR 랜덤 오프셋을 역산해서 커널 주소 배치를 알아낼 수 있다.
trampoline ??
KAISER를 보완하기 위해 trampoline 아이디어를 제시한다.
인터럽트 핸들러같은 유저 공간에도 매핑되어야 하는 코드가 커널 내부를 직접 호출하지 않고 트램폴린 함수를 거쳐 커널로 들어가게 하는 것이다.
트램폴린은 커널에만 매핑되어야 한다.
트램폴린은 나머지 커널과는 다른 랜덤 offset으로 배치한다.
공격자는 트램폴린 주소만 알 뿐 커널 본체의 랜덤 오프셋은 모른다.
대응
Linux : KPTI(kernel page table isolation) 기술 적용 완료했다.
MS : KVA shadow로 KAISER 발전했지만, KASLR을 공격하는 부채널 공격까지는 방어하지 못한다.
Mac : -no-shared-cr3 boot option이 있어도 “커널모드에서 유저를 언맵”하는 과정이라 “유저모드에서 커널이 매핑되어 생기는 문제”인 meltdown을 해결하지 못한다.
추후 Double Map 으로 대응했다.
8. discussion
meltdown
CPU 성능 최적화(OOO, cache 등)가 성능용 내부 구현이 아니라 보안 허점을 일으킬 수 있는 관점을 보여준다.
최적화의 보안 취약점은 필요악으로 여겨졌고, 오히려 암호 구현이 부채널 공격에 취약하면 상수시간으로 방어했어야 한다는 얘기까지 나왔지만, meltdown의 메모리 덤핑 이후로는 얘기가 바꼈다.
KAISER
기존의 커널 주소 매핑해두고 권한으로 막기 전략 → 최소한의 매핑 + 페이지 테이블 지우기 전략으로의 운영체제 설계 철학의 변화가 일어났다.
모든 OS가 표준화된 가상메모리 레이아웃(user/kernel hard split)을 따르게 강제하는 방향도 생각해 봐야 한다.
cloud providers
게스트가 fully 가상화가 안된 경우 meltdown 위협이 있다.
성능상의 이유로 container를 쓰는 경우 커널을 공유하기 때문에 같은 호스트의 다른 사용자 데이터까지 노출될 수 있기에 특히 위협적이다.
VM으로 인프라를 바꾸거나 KAISER 같은 우회책을 써야 한다.
9. conclusion
결론
OOO execution과 사이드채널을 이용해 유저 권한으로 커널 메모리를 읽을 수 있다.
OS 취약점이 필요 없고 OS에 의존하지 않는다.
다른 프로세스나 다른 VM 데이터도 노출될 수 있다.
KAISER는 본래 KASLR 대응책이었지만, MELTDOWN에 효과가 있다.
10. 기타
10.1 register renaming
정의
소프트웨어상의 가상 레지스터(논리 레지스터)를 CPU 내부의 실제 더 많은 레지스터(물리 레지스터)에 동적으로 할당하는 기술을 의미한다.
하는 이유
CPU가 out-of-order 실행을 할 때 데이터 의존성(data hazard) 때문에 문제가 발생한다.
의존성에는 크게 세 가지가 있는데 레지스터 리네이밍은 이중 가짜 의존성을 해결한다.
WAR(Write after read) : 뒤의 명령어가 앞의 명령어가 읽기도 전에 값을 써버리는 경우
WAW(Write after write) : 두 명령어가 같은 레지스터에 쓰려고 할 때 순서가 바뀌면 최종값이 잘못되는 경우
이 두 가지는 실제로 데이터가 흘러가야 하는 경로(RAW, Read After Write) 때문이 아니라, 사용할 수 있는 레지스터의 이름(개수)이 부족해서 발생하는 문제이다. 레지스터 리네이밍은 이 이름을 바꿔줌으로써 병렬 처리를 가능하게 한다.
10.2 memory
물리 메모리 (physical memory)
실체 : 컴퓨터 본체에 꽂혀있는 RAM 칩 그 자체를 의미한다.
특징 : CPU가 실제로 데이터를 읽고 쓰는 물리적인 저장 공간이다. 주소값이 0번부터 실제 용량(e.g. 16GB)까지 순차적으로 매겨져 있다.
커널 메모리 (kernel memory)
실체 : 운영체제(OS)의 핵심인 커널이 사용하기 위해 할당된 가상적인 영역이다.
특징 : 보안과 시스템 안정성을 위해 일반적인 사용자 프로그램(게임, 브라우저 등)이 접근하지 못하는 공간이다. 하드웨어 제어권, 시스템 설정, 프레서스 관리 정보 등 중요한 데이터가 담긴다.
Meltdown : CPU 설계 결함을 이용해 유저 프로그램이 커널 메모리가 위치한 물리 메모리의 실제 값을 몰래 훔쳐보는 것이다.
10.3 TSX
정의
Transactional Synchronization Extensions, 멀티스레드 환경에서 데이터 동기화 성능을 높이기 위해 도입된 하드웨어 트랜잭션 메모리(HTM) 기술이다.
락을 걸지 않고도 마치 락을 건 것처럼 원자성을 보장하며 병렬로 코드를 실행한다.
핵심 개념
Optimistic Execution : 낙관적 실행, 아무도 건드리지 않을 것이라 믿고 각자 실행한 뒤, 문제가 없으면 마지막에 한번에 커밋하는 전략을 사용한다.
Meltdown과의 연관
예외 억제(Exception Suppression) : 멜트다운 공격의 걸림돌은 권한 없는 메모리 접근 시 발생하는 예외(page fault)이다.
일반적인 상황에서 유저 프로그램이 커널 메모리를 읽으려 하면 CPU가 예외를 발생시키고 OS가 이를 감지해 프로그램을 강제종료(segmentation fault) 시킨다.
TSX의 트랜잭션 내부에서 발생한 오류는 OS에 보고되지 않고 하드웨어 수준에서 롤백된다.
공격자는 커널 메모리를 훔쳐보는 코드를 TSX로 감싼다.
CPU가 권한 체크를 하기 전(Retire 전) 데이터를 미리 가져오는 멜트다운의 특성을 이용해 데이터를 캐시에 남긴다.
이후 권한 위반으로 트랜잭션이 실패해도 프로그램은 죽지 않고 다음 시도를 반복할 수 있다.