[컴] Meltdown







요즘 CPU의 심각한 보안 구멍(security hole) 이 있다고 여기저기서 난리다. 그래서 내용을 좀 파보기로 했다.

일단 이 문제에 대해 자세한 설명을 해주는 page 는 아래와 같다.

크게 2개의 bug 가 보고 되었는데, meltdown 과 spectre 라 불린다.
  1. Meltdown
  2. Spectre
여기서는 Meltdown 리포트에 있는 글 가운데 몇개의 내용을 번역하고, 정리해봤다.


Meltdown

Out-of-order execution

Out-of-order execution 은 최적화 기술의 하나이다. 이 기술은 CPU core 의 모든 실행 단위(execution units) 에 대한 사용을 최대화 하기 위한 기술이다.(즉, execution unit 이 놀지않고 쉬지않고 일하도록 한단 이야기다.)

CPU 가 instruction들을 "순차적인 프로그램 순서(sequential program order)"로만 처리하는 것 대신에, 모든 필요한 resource(자원)가 갖춰지자마자 실행하게 된다. 순서를 벗어나서 실행을 하기 때문에 이름이 out-of-order execution 이다.

이러면 현재 동작에 의해 execution unit이 점유되어 있는 동안에도, 다른 execution unit 들은 다른 것들을 실행할 수 있다. 그래서 instruction들은 그들의 결과가 architectural definition을 따르는 만큼 병렬로 실행될 수 있다.

좀 더 쉽게 이야기하면, 원래 프로그램은 프로그램 내에서도 분리될 수 있는 부분이 있다. 예를 들면, 아래와 같은 코드가 있다면,
(1) c = a+b
(2) f = d+e
(3) h = c+f
(1) 과 (2) 는 아무런 연관성이 없기 때문에 병렬로 진행해도 된다. 만약 이것을 sequential program order 로 수행한다면, (1) 이후에 (2)가 진행되고, 그 후에 (3)이 진행될 것이다.

실제로,  out-of-order  execution을 지원하는 CPU 들은 추측해서 operation 을 수행하는 기능을 제공한다. 이 기능은 CPU가 instruction 이 필요해지고 commit되는 것인지 아닌지 대해 확실해 지기 전(즉, 결과를 확정짓기전에), 프로세서의 out-of-order logic 이 instruction들을 처리할 때까지는 수행된다.
In  practice,  CPUs  supporting  out-of-order  execution support  running  operations
speculatively to  the  extent that the processor’s out-of-order logic processes instruc-
tions before the CPU is certain whether the instruction will be needed and committed.

모든 이전의 instruction 들의 결과를 commit 하기전에 프로세서가 operation 을 수행하는 것을 여기서는 out-of-order execution 이라고 하자.


branch prediction units

branch prediction unit 들은 어떤 instruction 이 다음에 수행될 지에 대한 학습된 추측(educated guess) 을 얻게 된다. Branch predictor 들은 조건이 실제로 정해지기전에 어떤 branch 의 방향으로 갈 것인지를 정하기 위해 노력한다.

그래서 정해진 branch 에 있는 dependency 가 없는 instruction 들은 미리 수행되고, 만약 예측이 맞는다면 그대로 그 결과를 사용하게 된다.
만약 예측이 틀리면, reorder buffer 를 clearing 하고 unified reservation station 을 다시 초기화를 한다.

branch prediction 종류

  • static branch prediction
  • Dynamic branch prediction
  • One-level branch prediction 
  • neural branch prediction


주소 공간 Address Spaces


translation table

각 프로세스들을 독립적으로 하기위해서 CPU들은 가상메모리 공간(virtual address spaces)을 제공한다. virtual address 들은 물리적인 memory 주소로 번역(translate)된다.

virtual address spaces 들은 page 들의 집합(set)들로 나눠진다. "multi-level page translation table"을 통해서 이 page set들은 은 물리적인 메모리에 mapping 된다.

translation table 들은 가상주소에서 물리적주소로의 mapping 을 정의한다. 또한 readable, writeable, executable, user-accessible 같은 권한 체크(privilege check)를 수행할 때 사용되는 protection property 들을 정의한다.
  • virtual address --> physical address
  • protection property
특별한 CPU register 에 최근에 사용된 translation table 이 저장되어 있다. process 마다 virtual address space 를 구현하기 위해서 context switch 마다 OS 는 next process의 translation table address리 이 register 로 가져온다.

이 덕에 각 process는 그들의 virtual address space 에 속해있는 data 에만 접근할 수 있다. 이것은 해당하는 translation table들의 user-accessible property 를 disable 하는 방법을 통해서 OS 에 의해 수행된다.


kernel address space

kernel address space 는 kernel 의 사용만을 위한 memory mapped 만을 가지고 있는 것이 아니라 user page 들에 대한 operation 들을 수행한다. 즉, data 를 그곳에 write 할 수 있다. 결과적으로, 모든 물리적 memory 는 일반적으로 kernel 에 mapped 된다.

리눅스OS X 에서 이것은 direct-physical map을 통해서 이뤄진다. 즉, 모든 물리적인 메모리는 직접적으로 이미 정의된 virtual address 에 mapped 된다.

윈도우에서는 direct-physical map 대신에 paged pools, non-paged pools, system cache 라고 불리는 것들을 이용한다. 이 pool 들은 kernel memory space 에 있는 virtual memory 영역들이다. 이 virtual memory 영역들은 물리적인 페이지들을 virtual address 들에 mapping 해준다. virtual address 들은 memory 에 남거나 (non-paged pool) 또는 copy 가 이미 disk 에 저장됐기 때문에 memory 에서 지워질 수 있다.(paged pool) system cache 는 모든 file-backed page들의 mapping들을 포함한다. 이 memory pool들은 일반적으로 물리적 메모리의 많은 부분(large fraction) 을 모든 process의 kernel address space로 map 한다.


ASLR

memroy corruption 버그들은 종종 특정 data 의 주소들의 지식을 요구한다. memory corruption bug 들을 이용한 공격들을 지연시키기 위해서 non-executable stacks 와 stack canaries 가 와 함께 address space layout randomization(ASLR) 이 소개됐다.

  • non-executable stacks
  • stack canaries
  • address space layout randomization(ASLR)

kernel 을 보호하기 위해 KASLR 은 모든 boot 에서 드라이버가 위치하는 곳에 대한 offset들을 randomize 한다. kernel data 구조의 위치를 추측하게 만들기 때문에 결과적으로 공격을 하기 어렵게 한다.

side-channel attack

그러나 side-channel attack 들은 kernel data 구조들의 특정 위치를 알 수 있게 해주거나 자바스크립트에서 ASLR 을 무력화 시킨다. 소프트웨어의 버그와 이 주소들의 지식은 높은 권한의 코드 수행을 가능하게 해준다.


Cache attack

보통 CPU 들은 cache 를 가지고 있다. 우리가 거의 상식적으로 알고 있듯이 cache 는 자주쓰는 내용을 저장해 놓고, 빠르게 불러오는 용도의 저장소라고 할 수 있다.
Address space translation tables 들도 memory 에 저장되면서 동시에 regular cache 들에 cached 된다.

Cache side-channel attacks 은 cache 들로 인해 생기는 시간차(timing differences)를 이용한다.

예제, toy example

아래의 코드를 보자.
raise_exception();
// the line below is never reached
access(probe_array[data * 4096]);

이 코드에서 access(probe_array[data * 4096]); 부분은 실제로 수행될 수 없다. 일단은 앞에 raise_exception() 에서 프로그램이 종료되기 때문이기도 하고, raise_exception() 이 없다고 해도 이부분은 OS 에서 exception 으로 처리해서 이론적으로는 접근할 수 없다.

그런데, out-of-order exception 때문에 CPU 는 아마 이미 위의 코드에 해당하는 모든 instruction 들을 수행했을 것이다. 왜냐하면, exception 코드와 access(probe_array[data * 4096]); 코드는 dependency 가 없기 때문이다.

이것은 register 나 memory 로 load 되지 않았기 때문에 크게 문제되지 않지만, 미세구조적인 부작용(microarchitectural side effect)이 있다.

out-of-order execution 를 수행하는 동안에 참조되어지는 memoryregister 에 fetch 되고, 또한 cache 에 저장된다. 만약 out-of-order execution 이 버려져야 하면, register 와 memory 에 있는 내용들은 commit 되지 않는다. 그럼에도, cached 된 내용은 cache 에 그냥 남아 있게 된다.

Flush+Reload 로 "특정 memory 위치가 cache 됐는지 여부"를 알아낼 수 있는데, 이 방법을 통해 이 microarchitectural side-channel attack 의 효과를 크게 할 수 있다.

access(probe_array[data * 4096]); 는 probe_array를 data 값에 따라 4096 byte(4kB) 간격으로 접근하게 된다. 그래서 data 의 값으로 부터 memory page 까지의 injective mapping 이 있다. 즉 2개의 값은 같은 page 로 접근이 되지 않는다. 결과적으로 만약 page의 cache line 이 cached 되면 우리는 data 의 값을 알 수 있다.
(역자: 이것에 대한 좀 더 이해하기 쉬운 설명은 ref. 3을 확인하자. ref. 3 의 설명은 data 의 값은 cache된다 하더라도 user 가 접근할 수 없기에, data 을 주소를 가리키는 index 로서 사용해서 data 가 array 의 어떤 부분을 가리키는지를 확인하므로써 data 의 값을 판단한다고 한다.)

prefetcher는 page 범위들을 넘어서 data 를 접근할 수 없기 때문에, 다른 페이지들로 펼치는 것은 prefetcher 에 의한 false positive (틀렸는데 맞다고 하는 경우)들을 없애준다.




Meltdown


Meltdown(멜트다운) 은 2개의 빌딩 블럭들을 합쳐 놓은 것이다.
먼저 공격자는 CPU 가 transient instruction sequence(일시적으로 머무르는 명령어 순서) 를 실행하게 한다. 이 transient instruction sequence는 물리적인 메모리 어딘가에 저장해 놓은 "접근할 수 없는 비밀 값(inaccessible secret value)" 을 사용한다.

transient instruction sequence 은 은신처의 송신기처럼 동작해서 결국에 공격자에게 이 비밀값을 넘겨주게 된다.

Meltdown 은 3가지 단계로 구성된다.

Step 1

공격자가 접근할 수 없는 특정 메모리 위치의 내용이 register 로 load 된다.

원래 user application 의 virtual address 에도 kernel memory 의 내용이 mapping 된다. 그러나 user 의 권한으로 이 부분을 접근하면, OS 는 exception 을 발생시킨다.
여기서 meltdown 은 out-of-order execution 취약점을 이용한다.

Step 2

transient  instruction 은 register 의 비밀내용을 바탕으로 cache line 에 접근한다.

Step 3

공격자는 Flush와 Reload 를 이용해서 자신이 접근할 cache line을 정할 수 있게 된다. 그래서 결국 선택된 메모리 위치에 저장된 비밀을 접근할 수 있게 된다.

여러 메모리 위치(memory location) 에 대해 이 방법을 반복해서 사용하므로써 공격자는 물리적인 메모리 전체와 kernel memory 를 dump 할 수 있다.



See Also


  1. CPU.fail

References

  1. 인텔 CPU 보안 무엇이 문제?...패치하면 성능 저하도, 동아사이언스
  2. Meltdown and Spectre
  3. https://pgr21.com/pb/pb.php?id=freedom&no=75348

댓글 없음:

댓글 쓰기