티스토리 뷰

원본 : https://googleprojectzero.blogspot.kr/2018/01/reading-privileged-memory-with-side.html


어려운 단어를 어렵게 작성하여서 너무 어렵다... 번역 개판과 오타는 나중에 수정해야지

---------------------------------------------------------------------------------------------------------------------------------

  CPU 데이터 캐시 타이밍으로 예측 오류 실행을 악용하여 임의의 가상 메모리로부터 취약점을 통해 정보를 누출하도록 유도할 수 있다는 사실을 발견할 수 있었다. 


  이 취약점은 Intel, AMD 및 ARM의 특정 프로세서를 포함하여 많은 최신 프로세서에 영향을 미치는 것으로 알려져 있다. Intel과 AMD CPU의 일부 모델의 경우, 실제 소프트웨어를 대상으로 동작하는 익스플로잇이  존재하고 이 문제를 Intel, AMD 및 ARM에 2017-06-01에 보고하였다.


  이 취약점에 대해서는 지금까지 3가지 유형이 존재하였다.

  • 유형 1 : bounds check bypass(CVE-2017-5753)
  • 유형 2 : branch target injection(CVE-2017-5715)
  • 유형 3 : rogue data cache load(CVE-2017-5754)

  이전에 Daniel Gruss, Moritz Lipp, Yuval Yarom, Paul Kocher, Daniel Genkin, Michael Schwarz, Mike Hamburg, Stefan Mangard, Thomas Prescher, Werner Haas도 이 취약점에 대해 공개한 적이 있다. 이들의 [writeups / blogposts / paper drafts]는 다음의 URL에 있다.

  • Spectre (유형 1 & 2)
  • Meltdown (유형 3)

  연구 과정에서 아래와 같은 PoC(proof of concept)를 제작하였다.

  1. 테스트한 Intel Haswell Xeon CPU, AMD FX CPU, AMD PRO CPU, ARM Cortex A57에서 유저 영역으로부터 유형 1의 기본 원리를 증명하는 PoC.
    - 이 PoC는 특정한 권한을 가지지 않고, 동일한 프로세스 내에서 예측 오류 실행 내에서 데이터를 읽을 수 있는지 테스트한다.
  2. Intel Haswell Xeon CPU의 최신 표준 분산형 Linux 커널에서 커널 가상 메모리 4GiB내 임의의 읽기를 수행할 일반 사용자 권한으로 실행되는 유형 1의 PoC.
    - 커널의 BPF JIT가 활성화(기본값 아님)된 경우, AMD PRO CPU에서도 작동하며, Intel Haswell Xeon CPU에서 4초 정도의 시작 시간 이후, 약 2000바이트의 속도로 커널 가상 메모리를 읽을 수 있다.
  3. Intel Haswell Xeon CPU에서 virt-manager를 사용하여 생성된 KVM 게스트 내의 특정 버전의 Debian 배포판 커널에서 루트 권한으로 실행되는 유형 2의 PoC.
    - 호스트 내에서 동작하고, 최적화가 된 상태에서 약 1500바이트의 속도로 호스트의 커널 메모리를 읽을 수 있다.
    - Host RAM의 크기에 따라 소요되는 시간이 조정된다. 64GiB RAM을 가진 시스템의 경우, 공격을 수행하려면 약 10 ~ 30분 정도의 초기화 작업을 수행해야 한다.(만약 2MB 크기의 큰 페이지를 게스트가 사용할 수 있다면, 초기화의 속도는 훨씬 빨라야 한다. 이에 대해서는 테스트하지 않았다.)
  4. Intel Haswell Xeon CPU에서 일부 전제 조건하에 커널 메모리를 읽을 수 있는 일반 사용자의 권한으로 실행되는 유형 3의 PoC.
    - 전제 조건 : 타겟인 커널 메모리가 L1D 캐시에 위치

  이 주제와 관련된 자료는 "Literature" 섹션을 참조하면 된다.


  이 blogpost의 프로세스 내부에 대한 설명과 관련된 경고 사항 : 이 블로그 게시물은 관찰된 내용을 기반으로 작성되었고, 하드웨어 내부에 대해 많은 추측이 포함되어 있다. 따라서 프로세서가 실제로 수행하는 작업과 일치하지 않을 수 있다.


  프로세서 벤더사보다 방어기법을 설계하고 평가하기 더 유리한 위치에 있다고 판단하였기 때문에 프로세서 벤더사에 가능한 방어기법과 약간의 아이디어를 제공하였다. 프로세서 벤더사에 영향을 줄 수 있기를 기대한다.


CPU 벤더사에 제출한 PoC 코드와 writeups은 다음과 같다:

https://bugs.chromium.org/p/project-zero/issues/detail?id=1272.



테스트한 프로세서

  • Intel(R) Xeon(R) CPU E5-1650 v3 @ 3.50GHz (해당 문서에서는 "Intel Haswell Xeon CPU"라고 표현)
  • AMD FX(tm)-8320 Eight-Core Processor(해당 문서에서는 "AMD FX CPU"라고 표현)
  • AMD PRO A8-9600 R7, 10 COMPUTE CORES 4C+6G(해당 문서에서는 "ADM PRO CPU"라고 표현)
  • Google Nexus 5x phone의 ARM Cortex A57 core(해당 문서에서는 "ARM Cortex A57"라고 표현)


용어


이탈(retire) : 명령어(Instruction)의 실행 결과(ex : 레지스터 쓰기 및 메모리 읽기)가 완료되어 시스템의 나머지 부분에 표시될 때, 명령어는 이탈된다. 명령어는 순서를 따르지 않고 실행될 수 있지만, 이탈은 항상 순서대로가 되어야 한다.


논리 프로세서 코어(logical processor core) : 논리 프로세서 코어는 운영체제가 프로세서 코어로써 보여주는 것이다. 하이퍼스레딩이 활성화된 상태에서, 논리 코어의 수는 실제 코어 수의 배수이다.


캐시된/캐시되지 않은 데이터(cached/uncached data) : 이 블로그 포스트에서 "캐시되지 않은" 데이터는 CPU의 어느 수준의 캐시가 아닌 주메모리에만 존재하는 데이터이다. 캐시되지 않은 데이터를 로드하는데 일반적으로 CPU 시간으로 100 사이클 이상이 소요된다. 


예측 실행(speculative execution) : 프로세서는 분기를 수행할 것인지나 대상이 어디에 있는지 모르는 상태로 분기를 지나 실행할 수도 있기 때문에 명령어의 실행 여부를 알기 전에 먼저 명령을 실행한다. 이 예측이 잘못된 것으로 판명되면 CPU는 아키텍처에 영향을 주지 않고 결과 상태를 버리기 때문에 올바른 실행 경로에서 실행을 계속할 수 있다. 명령어는 올바른 실행 경로에 있는 것을 알기 전까지는 이탈되지 않는다. 


예측 오류 창(mis-speculation window) : CPU가 예측으로 잘못된 코드를 실행하고 예측이 잘못된 것인지 인지하기 까지의 시간 창



유형 1 : Bounds check bypass


  이 섹션에서는 아래와 같은 구성으로 데비안 배포판 커널에서 커널 메모리의 4GiB 영역에 임의적으로 읽기를 수행할 수 있는 유형 1에 대한 PoC의 3가지 이론과 공동적인 이론에 대해서 설명한다.

  • Intel Haswell Xeon CPU, eBPF JIT는 끔(기본 상태)
  • Intel Haswell Xeon CPU, eBPF JIT는 켬(기본 상태 아님)
  • AMD PRO CPU, eBPF JIT는 켬(기본 상태 아님) 

  eBPF JIT의 상태는 net.core.bpf_jit_enable sysctl을 사용하여 전환할 수 있다.


이론적 설명


  Intel Optimization Reference Manual은 Sandy Bridge 및 마이크로 아키텍처 개정과 관련하여 2.3.2.3 섹션(분기 예측)에서 다음과 같이 설명하고 있다:

- 분기 에측은 분기의 목적지를 예측하고 실제 실행할 경로를 알기 전에 프로세서가 명령어 실행을 시작할 수 있게 한다.


2.3.5.2 섹션(L1 DCache)에서는 :

- 로드 가능:

  [...]

  · 앞에 있었던 분기가 해결되기 전에, 예측적으로 수행

  · 캐시가 순서대로 사용되지 않도록 하고, 겹쳐서 표시??


  Intel's Software Developter's Manual Volume 3A의 11.7 섹션(암시적 캐싱(Implicit Caching) (Pentium 4, Intel Xeon, and P6 family processors)):

- 암시적 캐싱은 메모리 요소를 잠재적으로 캐싱할 수 있을 때 발생하지만, 이 요소는 일반적인 폰 노이만 시퀀스에서는 절대 접근할 수 없을 것이다. 암시적 캐싱은 공격적인 프리 패치와 분기 예측 및 TLB 누락 처리로 인하여 P6과 이보다 최신의 프로세서 제품군에서 발생한다. 암시적 캐싱은 기존의 Intel386, Intel486, Pentium 프로세서의 제품군에서 실행되는 소프트웨어가 명령어 프리패치를 예측하여 결론을 지을 수 없기 때문에, 해당 프로세서시스템 동작을 확장한 것이다.


  아래의 코드 샘플은 고려해보자. 만약 arr1->length 이 캐싱되지 않는다면, 프로세서는 arr1->data[untrusted_offset_from_caller]로부터 data를 예측하여 로드할 수 있다. 이는 범위를 벗어난 읽기 작업이다. 분기가 실행될 때 프로세서가 실행 상태를 효과적으로 되돌리기 때문에 중요하지 않다. 예측적으로 실행된 명령어 중 어느 하나라도 이탈되지 않을 것이다.(예를 들자면, 레지스터 등이 영향을 받도록 만들거나...)


1
2
3
4
5
6
7
8
9
10
struct array {
    unsigned long length;
    unsigned char data[];
};
struct array *arr1 = ...;
unsigned long untrusted_offset_from_caller = ...;
if (untrusted_offset_from_caller < arr1->length) {
    unsigned char value = arr1->data[untrusted_offset_from_caller];
    ...
}
cs


  그러나 다음과 같은 코드 샘플에서는 문제가 된다. 만일 arr1->length, arr2->data[0x200], arr->data[0x300]이 캐싱되지 않았지만, 다른 모든 접근된 데이터와 분기 조건은 true로 예측된다. 따라서 프로세서는 이전에 arr1->length가 로드되고 실행이 재조정되기 전에 예측을 통해 다음을 수행할 수 있다.

  • value = arr1->data[untrusted_offset_from_caller] 로드
  • arr2->data의 데이터 종속 오프셋에서 로드를 시작하고, 해당 캐시 라인을 L1 캐시에 로드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct array {
    unsigned long length;
    unsigned char data[];
};
struct array *arr1 = ...; /* small array */
struct array *arr2 = ...; /* array of size 0x400 */
/* >0x400 (OUT OF BOUNDS!) */
unsigned long untrusted_offset_from_caller = ...;
if (untrusted_offset_from_caller < arr1->length) {
    unsigned char value = arr1->data[untrusted_offset_from_caller];
    unsigned long index2 = ((value&1)*0x100)+0x200;
    if (index2 < arr2->length) {
        unsigned char value2 = arr2->data[index2];
    }
}
cs

 

  프로세서는 untrusted_offset_from_callerarr1->length보다 크다는 것을 알기 때문에 실행이 예측되지 않은 경로로 반환된 후, arr2->data[index2]를 포함하고 있는 캐시 라인은 L1 캐시에 남는다. arr2->data[0x200]과 arr2->data[0x300]을 로드하는데 필요한 시간을 측정하면, 공격자는 예측 실행이 되는 동안, index2의 값이 0x200 또는 0x300(arr1->data[untrusted_offset_from_caller]&1이 0 또는 1인지를 나타냄)인지를 결정할 수 있다.


  실제로 이 방법을 공격에 사용하려면, 공격자는 out-of-bounds index를 가진 대상 콘텍스트에서 취약한 코드 패턴과 같은 실행할 수 있어야 한다. 이를 위해서는, 취약한 코드 패턴이 기존 코드에 이미 존재하거나, 취약한 코드 패턴을 생성하는데 사용할 수 있는 인터프리터 또는 JIT 엔진이 필요하다. 하지만 지금까지는 취약한 코드 패턴의 익스플로잇 가능한 인스턴스는 찾아볼 수 없었다. 유형 1을 사용하는 커널 메모리 누수에 대한 PoC는 커널에 내장되어 있고 일반 사용자가 접근할 수 있는 eBPF 인터프리터 또는 EBPF JIT 엔진을 사용한다.


  이 방법을 조금 변형하면 잘못 예측된 경로에서 실행 제어를 얻기 위한 함수 포인터를 out-of-bounds로 읽는 것으로 대신 사용가능하다. 이 유형에 대해서는 더 조사하지 않았다.



커널 공격


  이 섹션에서는 유형 1을 사용하여 eBPF 바이트코드 인터프리터와 JIT 엔진을 사용하여 Linux 커널 메모리 누수 방법에 대해 좀 더 자세히 설명한다. 유형 1 공격을 위한 많은 흥미로운 잠재적인 대상들이 있지만 다른 JIT보다 공격자에게 더 많은 제어권을 제공하는 리눅스 커널 내부의 eBPF JIT/인터프리터를 공격하기로 결정하였다.


  리눅스 커널은 3.18 버전 이 후부터 eBPF를 제공한다. 권한이 없는 유저영역의 코드는 커널이 검증한 바이트코드를 커널에 제공할 수 있다. 이 후 다음과 같이 수행된다:

  • 커널 내부 바이트코드 인터프리터에 의해 각각 해석됨
  • 또는 JIT 엔진을 사용하여 커널 컨텍스트에서 실행되는 native machine code(원래 사용 중인 기기의 코드)로 변환(추가적으로 최적화를 수행하지 않고 개별적인 바이트코드 명령어로 변환)

  바이트코드의 실행은 소켓에 eBPF 바이트코드를 첨부함으로써 실행할 수 있고, 소켓을 필터로 사용한 다음, 소켓의 다른 끝 부분을 통해 데이터를 보낸다.


  JIT 엔진을 활성화할지는 런타임 구성 세팅에 달려있다. 하지만 테스트한 Intel 프로세서에서는 공격이 세팅과 독립적으로 동작한다.


  기존의 BPF와 달리, eBPF는 eBPF 바이트코드가 색인할 수 있는 데이버 배열과 함수 포인터 배열과 같은 자료형을 가진다. 따라서 eBPF 바이트코드를 사용하여 커널에서 위에서 설명한 코드 패턴을 생성하는게 가능하다.


  eBPF의 데이터 배열은 eBPF의 함수 포인터 배열보다 효율적이지 않기 때문에, 가능하면 공격은 eBPF의 함수 포인터 배열을 사용할 것이다.


  테스트한 양 기기는 SMAP을 가지고 있지 않고, PoC는 여기에 의지한다.(그러나 원칙적으로 이것이 전제 조건이 되어서는 안 됨)


  또한 테스트한 Intel 기기에서는 코어간 수정된 캐시 라인을 바운싱(bouncing)하는 속도가 느린데, 이는 MESI 프로토콜이 캐시의 일관성을 위해 사용되기 때문이다. 하나의 물리 CPU 코어에서 eBPF 배열의 참조 카운터 변경은 해당 CPU 코어로 바운스(bounce)될 참조 카운터를 가진 캐시 라인을 발생시킨다. 왜냐하면 eBPF 배열의 참고 카운터와 길이는 동일한 캐시 라인에 저장되기 때문이며, 이는 또한 하나의 물리 CPU 코어에서 참조 카운터를 변경했을 때 다른 물리 CPU 코어에서 eBPF 배열의 길이를 읽는 속도가 느려짐을 의미한다.(의도적 false 공유)


  공격에서는 2개의 eBPF 프로그램을 사용한다. 첫 번째 프로그램은 구성 인덱스로 페이지 정렬된 eBPF 함수 포인터 배열인 prog_map을 통해 함수의 마지막 부분에 있는 함수를 호출(tail-calling)한다. 간단히 말하면, 이 프로그램은 prog_map에서 유저영역 주소으로 오프셋을 추측하고, 추측된 오프셋으로 prog_map을 통해 함수의 마지막 부분에 있는 함수를 호출(tail-calling)함으로써, prog_map의 주소를 정하는데 사용된다. 분기 예측이 prog_map의 길이보다 오프셋이 아래에 있다는 것을 예측시키려면, 중간에 in-bounds 인덱스에 대한 tail-call이 수행되면 된다. 예측 오류 창을 증가시키기 위해서, prog_map의 길이를 포함하고 있는 캐시 라인은 다른 코어로 바운스된다. 오프셋을 성공적으로 추측했는지 테스트하기 위해서는, 캐시 내부에 유저 영역의 주소가 로드되었는지를 테스트해보면 될 것이다.


  단순한 무작위 주소 추측같은 경우 느리기 때문에, 다음과 같은 최적화가 사용된다: 인접한 유저 영역 메모리 매핑 2^15개, 각각 구성된 페이지 2^4개는 유저 영역 주소 user_mapping_area에 의해 생성되고, 총 영역은 2^31 바이트이다. 각 매핑은 동일한 물리 페이지를 매핑하고, 모든 매핑은 페이지 테이블 내부에 위치한다.



  이렇게 하면 공격은 2^31 바이트 단위로 이루어진다. 각 단계를 위해서, prog_map을 통해 out-of-bounds 접근을 발생시킨 후, user_mapping_area의 첫 2^4 페이지에서 각각 오로지 하나의 캐시 라인만 캐시된 메모리에 테스트를 수행한다. L3 캐시는 물리적으로 색인되기 때문에, 물리적인 페이지에 매핑되는 가상 주소에 대한 임의적인 접근은 같은 물리적인 페이지에 매핑되는 다른 모든 가상 주소가 캐싱되도록 할 것이다.


  공격이 hit(캐시된 메모리 위치, 커널 주소의 상위 33비트는 알려져 있음(hit가 발생한 주소의 추측으로부터 얻는 것이 가능하기 때문), 또한 하위 16비트의 주소도 알려져 있음(hit가 발생된 user_mapping_area 내부 오프셋으로부터))를 발견한 경우, user_mapping_area의 중간의 나머지 비트만 남는다.



  중간의 나머지 비트는 나머지 주소 영역을 이등분하는 것으로 결정할 수 있다. 2개의 물리 페이지를 인접한 가상 주소 범위에 매핑하면, 각 가상 주소 범위는 나머지 검색된 영역의 크기 절반이 되고, 나머지 주소는 비트 단위로 결정한다.


  이 시점에서, 두 번째 eBPF 프로그램은 실제로 data를 leak하는데 사용될 수 있다. 이 프로그램은 pseudocode로 다음과 같이 나타낸다:

1
2
3
4
5
6
7
8
9
10
uint64_t bitmask = <runtime-configurable>;
uint64_t bitshift_selector = <runtime-configurable>;
uint64_t prog_array_base_offset = <runtime-configurable>;
uint64_t secret_data_offset = <runtime-configurable>;
// index will be bounds-checked by the runtime,
// but the bounds check will be bypassed speculatively
uint64_t secret_data = bpf_map_read(array=victim_array, index=secret_data_offset);
// select a single bit, move it to a specific position, and add the base offset
uint64_t progmap_index = (((secret_data & bitmask) >> bitshift_selector) << 7+ prog_array_base_offset;
bpf_tail_call(prog_map, progmap_index);
cs


  해당 프로그램은 eBPF 데이터 배열 "victim_map"에서 런타임 구성이 가능한 오프셋과 bitmask, bit-shift한 값으로 하나의 비트가 27 바이트만큼 떨어져 있는 2개의 값 중 하나에 매핑되도록 하여 8 바이트로 정렬된 64 bit 값을 읽을 수 있다(배열의 인덱스로 사용될 때, 동일하거나 인접한 캐시 라인에 위치시키는 것은 충분하지 않음). 마지막으로 이 값에 64 비트 오프셋을 더한 결과값을 tail call을 위해 prog_map 내부의 오프셋으로 사용한다.


  이 프로그램은 leak할 데이터를 지정하는 victim_map 내부의 out-of-bounds 오프셋과 prog_map + offset으로 유저 메모리 영역을 가리킬 수 있는 prog_map 내부의 out_of_bounds 오프셋으로 eBPF 프로그램을 반복적으로 호출함으로써 메모리를 leak하는데 사용할 수 있다. 분기 예측을 오해시키고 캐시 라인을 바운싱(bouncing)하는 것은 첫 번째 eBPF 프로그램과 동일한 방법으로 동작하고, 이 때를 제외하고는, victim_map의 길이를 유지하고 있는 캐시 라인은 또한 다른 코어에서 바운스되어야 한다. 



유형 2: Branch target injection


  이 섹션은 Intel Haswell Xeon CPU에서 virt-manager를 사용하여 생성된 KVM 게스트 내부에서 root 권한으로 실행할 경우, 특정 버전의 Debian 배포판 커널에서 약 1500 bytes/second의 속도로 호스트 커널 메모리를 읽을 수 있는 호스트에서 동작하는 유형 2에 대한 PoC의 이론을 설명한다.


기본


  이전 연구(끝에 있는 Literature 섹션 참고)는 각각의 보안 콘텍스트의 코드가 각각 다른 분기 예측에 영향을 미치는 것이 가능하다는 것을 보여주었다. 지금까지는 오로지 코드가 어디에 위치하고 있는지 정보를 추론하는데 사용되었다.(바꿔말하면, victim에서 공격자에게 영향을 미칠 수 있는 것); 그러나 이 공격 유형의 기본 가설은 또한 victim의 컨텍스트에서 코드를 간접 실행하도록 사용될 수 있다는 것이다.(바꿔말하면, 공격자가 victim에게 다른 방향으로 영향을 미칠 수 있는 것)



  이 공격의 기본 개념은 대상 간접 분기를 가지고 있는 victim 코드가 메모리로부터 대상 주소가 로드되고 메인 메모리 밖의 주소로 변경하도록 하는 것이다. 그런 다음, CPU가 간접 분기에 도달했을 때, CPU는 점프의 실제 목적지를 알 수 없고, 캐시 라인으로부터 CPU 내부에 다시 로드하기 위해 몇 백 사이클을 수행할 때 까지 CPU는 실제 목적지를 계산할 수 없다. 따라서 일반적으로 CPU가 분기 예측을 기반으로 예측적으로 명령어를 실행하는 100 사이클 이상의 지연 시간이 생긴다.


Haswell Branch prediction 내부


  Intel의 프로세서로 구현된 분기 예측의 내부 중 일부가 이미 발표되었었다. 그러나 이 공격이 정확히 동작하려면 추가적인 세부 사항을 결정하기 위해서 상당한 추가 실험이 필요했다.


  이 섹션에서는 Intel Haswell Xeon CPU에서 실험적으로 파생된 분기 예측의 내부를 중점적으로 볼 것이다.


Haswell은 매우 다른 방식으로 동작하는 다중 분기 예측 메커니즘을 가지고 있는 것으로 보인다:

  • 일반적인 분기 예측기 : 시작 주소 당 하나의 대상만을 저장할 수 있고, 절대 점프와 상대 점프와 같은 모든 종류의 점프문에 사용된다.
  • 특수한 간접 호출 예측기 : 시작 주소 당 여러 대상을 저장할 수 있고, 간접 호출에 사용된다.
  • (Intel의 optimization manual에 따르면, 특수한 반환 예측기가 있긴 하지만, 여기에 대해서는 자세히 분석하지 않았다. 만일 이 예측기가 VM 입력을 통해 call stack 중 일부를 안정적으로 덤프할 수 있다면 매우 흥미로울 것이다.)

일반적인 예측기


  이전 연구에서 문서화된 바와 같이, 일반적인 분기 예측기는 예측을 위해 시작 명령어의 마지막 바이트 주소의 하위 31비트만 사용한다. 예를 들어, 분기 대상 버퍼(BTB, branch target buffer) 엔트리가 0x4141.0004.1000에서 0x4141.0004.5123으로 점프하기 위해 존재하는 경우, 일반적인 예측기는 0x4242.0004.1000에서 점프하는 것을 예측하여 이를 사용할 것이다. 시작 주소의 최상위 비트가 이와 같이 다를 때, 예측된 목적지의 최상위 비트 이것과 함께 바뀐다(이 경우, 예측된 목적지 주소는 0x4242.0004.5123일 것임). 따라서 예측기는 절대 목적지 주소 전체를 저장하지는 않는다.


  상위 주소의 최하위 31비트가 BTB 엔트리를 찾는데 사용하기 전에, XOR을 사용하여 함께 포장된다. 특히 다음과 같은 비트들은 함께 포장된다:


 bit A

 bit B 

 0x40.0000

 0x2000 

 0x80.0000 

 0x4000 

 0x100.0000 

 0x8000 

 0x200.0000

 0x1.0000

 0x400.0000

 0x2.0000

 0x800.0000

 0x4.0000

 0x2000.0000

 0x10.0000

 0x4000.0000

 0x20.0000


  다시 말해서, 시작 주소가 이 테이블의 행에 있는 양 쪽의 숫자로 XOR된 경우, 분기 예측기는 검색을 수행할 때 원래 시작 주소로부터 결과 주소를  구별할 수 없을 것이다. 예를 들어, 분기 예측기는 시작 주소 0x100.0000과 0x180.0000을 구별할 수 있지만, 시작 주소 0x100.0000과 0x140.2000이나 시작주소 0x100.0000과 0x180.4000은 구별할 수 없다. 다음과 같이, 앨리어스된(aliased) 시작 주소로써 참조될 것이다.


  앨리어스된(aliased) 주소가 사용될 때, 분기 예측기는 앨리어스되지 않은(unaliased) 시작 주소와 관련하여 같은 대상을 여전히 예측할 것이다. 이는 분기 예측기는 검증되지 않은 불완전한 절대 목적지 주소를 저장한다는 점을 나타낸다.


  서로 다른 시작 주소들에 대해 관측된 최대 순·역방향 점프 거리를 기반으로, 대상 주소의 하위 32비트 중 반은 시작으로부터 대상까지 점프로 2^32의 경계를 가로지를 수 있는지 명시하는 부가적인 비트를 가진 32비트 값으로써 저장될 수 있다. 점프가 경계를 지르는 경우, 시작 주소의 비트 31은 instruction pointer의 상위 절반이 증가 또는 감소해야 할지를 결정한다.


간접 호출 예측기


해당 메커니즘을 위한 BTB 검색의 입력은 다음과 같다:

  • 시작 명령어의 주소 하위 12비트(첫 번째 바이트의 주소인지 마지막 바이트의 주소인지는 분명히 할 수 없음) 또는 이 중 일부
  • 분기 히스토리 버퍼 상태(branch history buffer state)

  간접 호출 예측기가 분기를 결정할 수 없는 경우, 대신 일반적인 예측기에 의해 결정된다. Intel's optimization manual는 이러한 동작에 대해 나타내고 있다: 간접 호출 및 점프. 단조(monotonic) 대상을 가지는 것으로 예측되거나 최근 프로그램의 동작에 따라 달라지는 대상을 가지는 것으로 예측될 수 있다.


  분기 히스토리 버퍼(BHB, branch history buffer)는 최근 29개의 분기에 대한 정보(기본적으로 최근 제어 흐름의 핑거프린트)를 저장하고, 여러 대상을 가질 수 있는 간접 호출의 예측을 보다 정확하도록 하는데 사용된다.


  BHB의 업데이트 기능은 다음과 같이 동작한다(pseudocode에서, src는 시작 명령어의 마지막 바이트 주소, dst는 목적지 주소):

1
2
3
4
5
6
7
8
9
10
11
12
void bhb_update(uint58_t *bhb_state, unsigned long src, unsigned long dst) {
    *bhb_state <<= 2;
    *bhb_state ^= (dst & 0x3f);
    *bhb_state ^= (src & 0xc0>> 6;
    *bhb_state ^= (src & 0xc00>> (10 - 2);
    *bhb_state ^= (src & 0xc000>> (14 - 4);
    *bhb_state ^= (src & 0x30<< (6 - 4);
    *bhb_state ^= (src & 0x300<< (8 - 8);
    *bhb_state ^= (src & 0x3000>> (12 - 10);
    *bhb_state ^= (src & 0x30000>> (16 - 12);
    *bhb_state ^= (src & 0xc0000>> (18 - 14);
}
cs


  BHB 상태의 비트 중 일부는 BTB 접근을 위해 사용될 때, XOR을 사용하여 함께 포장된 것처럼 보이지만, 아직 정확한 포장 기능에 대해서는 이해하지 못 하였다.


  BHB는 2가지 이유때문에 흥미롭다. 먼저, 간접 호출 예측기에서 정확하게 충돌을 일으킬 수 있는지를 위해 대략적인 동작에 대한 지식이 필요하다. 그러나 또한 공격자가 코드를 실행할 수 있는 일부 반복적인 프로그램 상태에서 BHB 상태를 덤프할 수 있다(예를 들어, hypercall 직후, hypervisor를 공격할 때). 덤프된 BHB 상태는 hypervisor에 핑거프린트로 사용될 수 있거나, 공격자가 hypervisor 바이너리에 접근하는 경우, hypervisor 로드 주소의 하위 20비트를 결정할 수 있다(KVM의 경우: kvm-intel.ko의 로드된 주소의 하위 20비트).


분기 예측기 내부 리버싱


  해당 하위 섹션에서는 어떻게 Haswell 분기 예측기의 내부를 리버싱했는지에 대해 설명한다. 이 중 일부는 리버싱 과정의 기록을 세세히 보관하지 않았기 때문에, 메모리로부터 작성되었다.


  이전 연구로부터의 지식을 이용해, 초기에 일반적인 예측기를 사용하여 커널 내부에 BTB Injection을 시도했다. 일반적인 예측기는 시작 주소의 최하위 절반만을 보고 부분적인 대상 주소만을 저장되었다. 그러나 Injection의 성공률은 1% 미만으로 매우 낮았다(방법으로는 Haswell에서 동작하는 수정된 hypervisor에 대해 방법 2에 대한 예비 PoC를 사용). 


  그래서 다른 상황에서 분기 예측기의 동작을 보다 쉽게 테스트할 수 있도록 유저 영역 테스트 케이스를 작성하기로 결정하였다.


  분기 예측기 상태가 hyperthreads 사이에서 공유된다는 가정을 기반으로, 2 개의 인스턴스가 특정 물리 코어에서 동작하여 2 개의 논리 프로세서 중 하나에 각각 고정되어, 한 개의 인스턴스가 branch injection 얼마나 자주 성공하는지 측정하는 동안, 또 한 개의 인스턴스는 branch injection을 수행하도록 프로그램을 작성하였다. 양 쪽의 인스턴스는 ASLR을 비활성화하고 동일한 주소에서 동일한 코드로 실행되었다. Injection 프로세스는 프로세스별로 테스트 변수에 접근하는 함수를 간접 호출하였다. 측정 프로세스는 타이밍을 기반으로, 프로세스별로 테스트 변수가 캐시되는지 테스트하는 함수를 간접 호출하였고, CLFLUSH를 사용하여 이를 제거하였다. 양 쪽의 간접 호출은 동일한 호출 사이트를 통해 수행되었다. 각 간전 호출 전에, 메모리에 저장된 함수 포인터는 예측 시간 창을 넓히기 위해 CLFLUSH를 사용하여 메인 메모리로 플러쉬(flushed out)되도록 하였다. 또한, Intel's optimization manual에서 "recent program behavior(최근의 프로그램 동작)"을 참고하였기 때문에, 항상 사용되는 일련의 조건부 분기가 간접 호출의 앞에 삽입되었다.


  해당 테스트에서, Injection 성공률은 99% 이상이었고, 앞으로의 실험을 위해 기본 설정으로 제공한다.



  그런 다음, 예측의 구조를 자세히 이해하려고 하였다. 예측 구조는 어떠한 종류의 전역 분기 히스토리 버퍼를 사용한다고 가정하였다.


  분기 정보가 히스토리 버퍼에 머무는 기간을 결정하기 위해, 2 개의 프로그램 인스턴스 중 하나만 오로지 사용하는 조건부 분기를 항상 사용하는 조건부 점프 구문의 앞에 삽입한 다음, 항상 사용하는 조건부 점프(N)의 수를 변경하였다. 그 결과 N=25의 경우, 프로세서는 분기를 1% 미만의 예측 오류률로 구분할 수 있었지만, N=26의 경우, 예측 오류률이 99%를 넘는데 실패하였다.

  따라서 분기 히스토리 버퍼는 최소한 최근 26개의 분기에 대한 정보를 저장할 수 있어야 했다.


  이 후, 2 개의 프로그램 인스턴스 중 하나의 코드는 메모리에서 이동되었다. 시작 및 대상 주소의 최하위 20비트만이 분기 히스토리 버퍼에 영향을 미친다는 것을 나타내었다.


  2 개의 프로그램 인스턴스에서의 다른 형태의 분기를 테스트는 정적 점프, 조건부 점프, 호출 및 반환이 동일한 방법으로 분기 히스토리 버퍼에 영향을 미칠 수 있다는 것을 나타내었다. 조건을 가지지 않는 점프는 여기에 영향을 미치지 않았다. 시작 명령어의 마지막 바이트 주소는 카운트에 사용된다. IRETQ는 히스토리 버퍼 상태에 영향을 주지 않는다(이는 히스토리 버퍼에 보이지 않는 프로그램 흐름을 생성하도록 허용하기 때문에 테스트하는데 유용함).


  메모리에서 여러 번의 간접 호출을 하기 전에 마지막 조건부 분기를 이동하면 분기 히스토리 버퍼의 내용은 마지막 조건부 분기 명령어의 많고 서로 다른 위치를 구별하는데 사용될 수 있다는 것을 보여준다. 이는 히스토리 버퍼가 작은 히스토리 값의 목록을 저장하지 않는 것을 나타내는 대신, 히스토리 데이터가 서로 섞여 더 큰 버퍼가 되는 것을 보여준다.


  그러나, 히스토리 버퍼는 분기 예측을 하는데 유용하도록 특정 수의 새로운 분기를 가진 후, 과거 분기에 대해 "망각"이 필요하다. 그렇게 되면 히스토리 버퍼에 새로운 데이터가 섞일 때, 히스토리 버퍼에 이미 존재하는 비트 정보가 아래 쪽으로 전파될 수 없고, 또한 위 쪽의 정보는 아마도 별로 유용하지 않을 것이다.

  또한 분기 예측이 매우 빨라야하기 때문에, 아마도 히스토리 버퍼의 업데이트 기능이 오래된 히스토리 버퍼를 왼쪽으로 쉬프트한 다음, 새로운 상태에서 XOR을 수행한다 결론지었다.



  이 추측이 맞다면, 히스토리 버퍼는 가장 최근의 분기에 대해서 많은 정보를 저장하지만, 어떠한 데이터를 저장하고 있는 마지막 분기에 대해서는 히스토리 버퍼를 업데이트할 때마다 쉬프트됨으로써 많은 정보 비트만을 가진다. 따라서, 점프의 시작과 목적지 주소들에서 다른 비트들을 반전한 다음 정적인 시작과 대상을 가지는 32개의 항상 사용되는 점프문을 사용하여 분기 예측이 간접 호출에 어떠한 차이를 보여주는지 테스트하였다.


  그 사이에 32개의 정적 점프가 있는 경우, 비트 플립이 영향을 미치지 않는 것처럼 보여, 차이를 관찰할 수 있을 때까지 정적인 점프의 수를 줄였다. 항상 사용되는 점프문의 갯수가 28개일 때의 결과에서 대상의 비트 0x1과 0x2와 시작의 비트 0x40과 0x80이 영향을 미쳤다. 그러나 대상의 0x1과 시작의 0x40 또는 대상의 0x2와 시작의 0x80 양 쪽의 비트를 반전하면 명확성을 확인할 수 없다. 이는 삽입당 히스토리 버퍼의 쉬프트는 2임을 보여주고, 히스토리 버퍼의 최하위에 어떤 데이터가 저장되는지를 보여준다. 그런 다음 나머지 비트에 저장되는 정보를 확인하기 위해서 비트가 반전된 점프 후 고정된 점프의 수를 감소시키기를 반복하였다.


KVM 게스트로부터 호스트 메모리 읽기


호스트 커널의 찾기


PoC는 여러 단계에서 호스트 커널을 찾는다. 다음 단계의 공격을 위해 결정된 필요한 정보는 다음과 같다:

  • kvm-intel.ko의 주소 하위 20비트
  • kvm.ko의 전체 주소
  • vmlinux의 전체 주소

  되돌아 보면, 불필요할 정도로 복잡하지만, 공격자가 사용할 수 있는 다양한 기법을 잘 설명하고 있다. 더 간단한 방법은 먼저 vmlinux의 주소ㅡㄹ 결정한 다음, kvm.ko와 kvm-intel.ko의 주소를 이등분하는 것이다.


  첫 번째 단계에서, kvm-intel.ko의 주소가 leak된다. 이를 위해 게스트 항목 이후 분기 히스토리 버퍼 상태가 덤프된다. 그런 다음, kvm-intel.ko의 로드된 주소의 비트 12..19의 모든 가능한 값에 대해서, 히스토리 버퍼의 예상되는 최하위 16비트가 로드된 주소 추측과 게스트 항목 이전 마지막 8개의 분기의 알려진 오프셋을 기반으로 계산된다. 이 결과는 leak된 히스토리 버퍼 상태의 최하위 16비트와 비교된다.


  분기 히스토리 버퍼 상태는 2개의 대상이 나타내는 간접 호출의 예측 오류률을 측정함으로써 2비트 단위로 leak된다. 간접 호출에 접근하는 한 가지 방법은 시작과 타겟 주소 비트가 전부 0인 N 개의 분기문에 의해 따라오는 vmcall 명령어로부터 접근하는 것이다. 간접 호출에 접근하는 2번째 방법은 분기 히스토리 버퍼 내에 임의의 값을 쓰는데 사용가능한 유저영역에서 제어된 분기문으로부터 접근하는 것이다.

  "분기 예측기 내부 리버싱" 섹션에서 예측오류률은 캐시 라인을 로드하는 한 개의 호출 인스턴스와 또 하나의 동일한 캐시 라인이 로드되었는지를 확인하는 인스턴스를 사용하여 측정되었다.



  N=29일 때, 모든 히스토리 버퍼 상태는 hypercall로부터 제거되기 때문에 예측오류는 제어된 분기 히스토리 버퍼의 값이 0인 경우에 높은 수치로 나타날 것이다. N=28일 때, 제어된 분기 히스토리 버퍼의 값이 0<<(28*2), 1<<(28*2), 2<<(28*2), 3<<(28*2) 중에서 하나인 경우에 예측 오류는 나타날 것인데, 이는 4가지 모든 가능성을 테스트해 봄으로써, 어느 것이 옳은지 탐지할 수 있다. 그런 다음, N의 값을 감소시키기 위한 4가지 가능성은 {0|1|2|3}<<(28*2) | (history_buffer_for(N+1)>>2) 이다. N을 위한 값을 감소시키기 위해 반복함으로써, N=0이 되는 동안 분기 히스토리 버퍼 값은 결정될 수 있다.



  이 시점에서, kvm-intel.ko의 하위 20비트는 알려져 있다. 다음 단계는 대략적으로 kvm.ko를 찾는 것이다.

  이를 위해, 모든 hypercall에서 발생하는 kvm.ko로부터 kvm-intel.ko로의 간접 호출에 의해 BTB 내에 삽입된 데이터를 사용하여, 일반적인 분기 예측기가 사용된다. 이는 간접 호출의 시작 주소가 BTB의 밖으로 leak되어야 한다는 것을 나타낸다.


  kvm.ko는 아마도 0xffffffffc0000000에서 0xfffffffffc4000000까지의 범위 내부 어딘가에 페이지 정렬(0x1000)이 적용된 상태로 위치하고 있을 것이다. 따라서 "일반적인 예측기" 섹션의 table의 처음 4개의 항목이 적용된다. 올바른 주소 1개를 위해 24-1=15개의 앨리어싱(aliasing) 주소들이 있을 것이다. 그러나 이것 또한 검색 공간을 0x4000에서 0x4000/24=1024로 줄여주기 때문에 이점이다.


  시작 또는 앨리어싱(aliasing) 주소들 중 하나에서 올바른 주소 하나를 찾아야 하는데, 특정 레지스터를 통해 데이터를 로드하는 코드가 모든 가능한 호출 대상(kvm-intel.ko의 leak된 하위 20비트 + 호출 대상의 내부 모듈 오프셋 + 220의 배수)에 위치하고 있고 간접 호출은 모든 가능한 호출의 시작에 위치한다. 그런 다음, 동작으로부터 특수한 예측을 방해하는 랜덤한 히스토리 버퍼 상태와 함께, hypercall이 수행되고 간접 호출은 다른 가능한 non-aliasing call sources을 통해 수행된다.


  다음으로, vmlinux로부터 kvm.ko로의 간접 호출을 사용하여, vmlinux의 로드된 주소를 비슷한 방법으로 결정할 수 있다. 운 좋게도, vmlinux의 로드된 주소에서 무작위인 모든 비트가 서로 포장되어 있었다. 그래서 kvm.ko를 찾을 때와 달리, 결과가 직접적이고 고유할 것이다. vmlinux는 2MiB의 정렬과 1GiB의 랜덤한 범위를 가지므로, 가능한 주소는 512개뿐이다. 단순한 hypercall은 vmlinux로부터 kvm.ko로의 간접 호출을 실제로 일으키지 않기 때문에, virt-manager로 생성된 가상 머신의 기본 구성에 위치한 에뮬레이트된 직렬 포트의 상태 레지스터에서 I/O 포트를 대신 사용한다.


  정보의 유일하게 남은 조각은 kvm.ko의 16개의 aliasing load address 중 어느 것이 실제로 올바른지 이다. kvm.ko에 대한 간접 호출의 시작 주소는 알려져 있기 때문에, 이부분은 이등분(bisection)을 사용하여 해결할 수 있다: 예측적으로 실행되는 코드의 인스턴스에 따라, 두 개의 캐시 라인 중 하나를 로드하고, 로드되어 얻는 캐시 라인 중 하나를 측정하도록 가능한한 다양한 대상에 코드를 배치시킨다.


캐시 세트 식별(번역 개판.....)


  이 PoC는 VM이 매우 큰 페이지에 접근할 수 없다고 가정한다. 4KiB 페이지 경계와 관련하여 특정한 정렬을 가지는 모든 L3 캐시 세트를 대상으로 축출 세트를 발견하기 위해, 이 PoC는 먼저 25600 메모리 페이지를 할당한다. 그런 다음, 루프에서, 남아있는 모든 정리되지 않은 페이지 중 랜덤하게 서브 세트를 서브 세트에 포함되는 축출 세트의 기대 갯수가 1이 되도록 선택하고, 캐시 라인에 반복적으로 접근함으로써 각 서브 세트를 축출 세트로 감소시켜, 캐시 라인이 항상 캐시되는지 테스트(이 경우, 아마 축출 세트의 일부가 아닐 것임)한다. 그리고 동일한 캐시 세트에 있는지 결정하여 남아있는 모든 정리되지 않은 캐시 라인에서 새로운 축출 세트를 축출하여 사용하도록 시도한다. 


게스트 페이지의 호스트 가상 주소 찾기


  이 공격은 데이터를 leak하기 위해서 FLUSH+RELOAD를 사용하기 때문에, 한 게스트 페이지의 호스트의 커널 가상 주소를 알 필요가 있다. PRIME+PROBE와 같은 대체 접근법은 요구사항 없이 동작해야 한다. 


  이 공격 단계를 위한 기본 개념은 공격자가 제어가능한 주소를 로드하는 hypervisor에 대해 branch target injection 공격을 사용하여, 게스트 소유의 페이지가 로드되었는지 테스트하는 것이다. 이를 위해, R8에 의해 지정된 메모리 위치로부터 간단하게 로드하는 가젯을 사용할 수 있다(커널 빌드에서 게스트 종료 후에 첫 번째 간접 호출에 도달하면 R8 ~ R11은 게스트가 제어가능한 값을 포함하고 있을 것임).


  공격자는 축출 세트를 이 시점에서 사용할지 또는 동시에 brute-force로 사용할지를 알아야 한다. 그러나, 실험적으로, 무작위한 축출 세트를 사용하는 것도 마찬가지이다. 이론으로써 관찰된 행동이 실제로 L1D와 L2 축출의 결과로 나타났고, 이는 예측 실행의 몇 가지 명령어 가치를 허용하기에 충분할 수 있다.


  호스트 커널은 KVM 게스트에 할당된 메모리를 포함하여, 물리 매핑(physmap) 영역의 모든(거의?) 물리 메모리를 매핑한다. 그러나 physmap의 위치는 크기가 128PiB인 영역에서 무작위(1GiB 정렬과 함께)로 결정된다. 따라서, 게스트 페이지의 호스트 가상 주소를 직접 무작위 대입 공격을 하는 것은 매우 긴 시간이 걸린다. 반드시 불가능한 것은 아니다. 야구장 크기로, 초당 12000건의 성공적인 Injection과 30개의 게스트 페이지를 병행해서 테스트한다고 가정하면, 1일 이내로 가능하다. 하지만 몇 분안에 하는 것보다 인상깊지 않다.


  최적화를 위해, 문제를 나눌 수 있다: 먼저, physmap 영역의 기본 주소를 무작위 공격의 대상으로 지정한 다음, 물리 주소로부터 로드할 수 있는 가젯을 사용하여 물리 주소를 무작위로 공격한다. 물리 주소는 128KiB보다 훨씬 낮다고 가정할 수 있기 때문에, 더욱 효과적으로 무작위 공격을 할 수 있고, 1GiB 정렬을 사용하는 주소 추측에 사용할 수 있기 때문에 physmap 영역의 기본 주소를 무작위 공격하는 것은 이후 더욱 쉬워진다.


물리 주소를 무차별 공격하려면 다음과 같은 가젯이 사용될 수 있다:

1
2
3
4
5
6
7
ffffffff810a9def:       4c 89 c0                mov    rax,r8
ffffffff810a9df2:       4d 63 f9                movsxd r15,r9d
ffffffff810a9df5:       4e 8b 04 fd c0 b3 a6    mov    r8,QWORD PTR [r15*8-0x7e594c40]
ffffffff810a9dfc:       81
ffffffff810a9dfd:       4a 8d 3c 00             lea    rdi,[rax+r8*1]
ffffffff810a9e01:       4d 8b a4 00 f8 00 00    mov    r12,QWORD PTR [r8+rax*1+0xf8]
ffffffff810a9e08:       00
cs


  이 가젯은 적절한 R9 세팅에 의해 커널의 텍스트 섹션 주위의 영역으로부터 8바이트의 정렬된 값을 로드하도록 허용한다. 특히 page_offet_base, physmap의 시작 주소 로드가 허용된다. 그런 다음, 원래의 R8에 있던 값(물리 주소는 -0xf8한 값)이 이전에 로드된 결과에 더해지고, 0xfa가 여기에 더해진 후, 이 결과는 역참조된다.


캐시 세트 선택


  올바른 L3 축출 세트를 선택하기 위해서, 다음의 섹션으로부터 공격이 동작할 때까지 다른 축출 세트를 사용하여 본격적으로 실행된다.


데이터 leak


  이 시점에서, 일반적으로 공격자가 제어한 위치로부터 읽어들임으로써 실제로 데이터를 leak하는데 사용될수 있는 호스트 커널 코드에 가젯을 위치시킬 필요가 있으며, 해당 결과를 적절하게 쉬프트와 마스크 연산을한 다음, 이 결과를 로드를 위해 공격자가 제어한 주소에 대한 오프셋으로 사용한다. 그러나 가젯을 결합하여 예측 컨텍스트에서 동작하는 것을 알아내는 것은 매우 힘들다. 그 대신, 호스트 커널에 내장된 eBPF 인터프리터를 사용하기로 결정하였다. VM 내부에서 코드를 정당한 방법으로 실행하는 방법은 없지만, 호스트 커널의 텍스트 섹션에서 코드가 존재한다면 일반적인 ROP 가젯같이 공격에 사용하기 충분하다.


eBPF 인터프리터 진입점은 다음과 같은 함수 서명을 가진다:


1
static unsigned int __bpf_prog_run(void *ctx, const struct bpf_insn *insn)
cs


  두 번째 매개 변수는 정적으로 사전에 검증된 실행 가능한 eBPF 명령어의 배열 포인터이다. 이는 __bpf_prog_run()는 타입 확인이나 범위 확인을 수행하지 않다는 것을 나타낸다. 첫 번째 매개 변수는 초기에 에뮬레이트된 레지스터 상태의 일부로써 단순히 저장되므로, 이 값은 중요하지 않다.


eBPF 인터프리터가 제공하는 다른 것들:

  • 다중 에뮬레이트된 64비트 레지스터
  • 에뮬레이트된 레지스터에 64비트 상수 기록
  • 에뮬레이트된 레지스터에 저장된 주소로부터 메모리 일기
  • 비트 연산(비트 시프트 연산 포함) 및 산술 연산

인터프리터 진입점을 호출하려면, 알려진 메모리 위치에서 RSI와 RIP 제어와 R8 ~ R11 제어와 제어된 데이터를 제공하는 가젯이 필요하다. 다음과 같은 가젯이 이 기능을 제공한다:


1
2
ffffffff81514edd:       4c 89 ce                mov    rsi,r9
ffffffff81514ee0:       41 ff 90 b0 00 00 00    call   QWORD PTR [r8+0xb0]
cs


이제 physmap에서 R8과 R9가 게스트가 소유한 페이지의 매핑을 가리킴으로써, 호스트 커널에서 예측적으로 임의의 검증되지 않은 eBPF 바이트코드를 실행할 수 있다. 그런 다음, 상대적으로 간단한 바이트코드를 사용하여 캐시로부터 데이터를 leak할 수 있다.



유형 3: Rogue data cache load


  기본적으로, Anders Fogh의 blogpost를 읽어야 함: https://cyber.wtf/2017/07/28/negative-result-reading-kernel-memory-from-user-mode/


  요약하면, 이 유형을 사용하는 공격은 커널 코드의 제어 흐름을 잘못 가리키도록 하지 않고, 유저영역으로부터 커널 메모리를 읽으려고 시도한다. 이전 유형을 위해 사용된 코드 패턴을 사용하여 동작하지만, 유저 영역이다. 근본적인 원인은 권한 검사가 중요한 성능 영향을 미칠 수 있는 메모리에서 레지스터로 데이터를 읽는 동안, 심각한 경로에 주소 접근과 관련된 권한 검사가 없다는 것이다. 대신, 메모리 읽기는 다음 명령어에서 즉시 사용할 수 있게 읽기의 결과를 만들 수 있고 오로지 비동기적으로 권한 검사를 수행하여, 권한 검사에 실패한 경우 예외를 발생시키는 재정렬 버퍼의 플래그를 세팅한다.


Anders Fogh의 blogpost에 추가할 사항이 몇 가지 있다: 

- "유저모드에서 실행된 다음 명령어를 상상해보라.

1
mov rax,[somekernelmodeaddress]
cs

이탈할 때, 인터럽트를 일으킬 것이다, [...]"


  또한 페이지 폴트를 피하기 위해 높은 대기 시간을 가진 예측 오류 분기 뒤의 명령을 이미 실행했을 가능성이 있다. 이는 커널 주소로부터의 읽기와 연관된 예외 전달 사이의 지연이 증가함으로써 예측 장을 넓힐 수도 있다.


- "먼저, 이 메모리와 접촉하는 syscall을 호출한다. 다음으로, L1에 로드된 주소를 가질 확률을 높일 수 있게 prefetcht0 명령어를 사용한다."


  syscall을 한 후에 프리패치 명령어를 사용한 경우, 이 공격은 이유는 알 수 없지만, 원하는데로 동작을 중단했다. 혹시 CPU는 마지막 접근에서 왜 접근이 거부되었는지를 어딘가에 저장하고 이런 경우, 동작으로부터 공격을 막을 수 있을까?


- "다행스럽게도 접근이 허용되지 않았을 때 Intel이 결과가 무의미하다고 제안하는 것을 읽을 수 없었다."

 

  충분히 캐싱되지 않았지만 적어도 읽기 시도를 반복한 후에, 페이지 테이블 항목에 있는 동안 메모리에 이것(커널 주소로부터 읽은 것은 모두 0을 반환)이 발생한 것으로 보인다. 매핑되지 않은 메모리를 대상으로, 커널 주소를 읽는 것은 결과를 전혀 반환하지 않는다.


추가 연구를 위한 생각


  이 연구가 아직 조사하지 않은 많은 나머지 연구 주제를 제공하고 있다고 생각하고, 다른 공공 연구자들이 이에 대해 조사해보는 것을 권장한다. 이 섹션에서는 이 블로그 포스트의 나머지 부분보다 더 많은 양의 예측을 포함하고 있다. 여기에는 별로 쓸모없는 테스트해보지 않은 아이디어도 포함한다.


leaking without data cache(데이터 캐시 타이밍을 필요로 하지 않는 leak)


  예측 실행에서 데이터를 추출하는데 사용될 수 있는 데이터 캐시 타이밍 측정 이외의 마이크로아키텍처 공격이 있는지 조사하는 것은 흥미로울 것이다.


다른 microarchitectures


  이 연구는 상대적으로 Haswell 중심으로 연구되었다. 다른 현대의 프로세서의 분기 예측이 어떻게 동작하고 공격을 받을 수 있는지에 대해 자세히 보면 흥미로울 것이다.


다른 JIT 엔진


  Linux 커널에 내장된 JIT 엔진에 대한 유형 1의 공격을 성공적으로 개발하였다. 시스템에 대한 제어가 덜한 보다 향상된 JIT 엔진에 대한 공격 또한 실용적인지 보는 것은 흥미로울 것이다(특히 자바 스크립트 엔진).


호스트 가상 주소와 캐시 세트를 대상으로 하는 보다 효과적인 스캔


  유형2에서, 게스트가 소유한 페이지의 호스트 가상 주소를 스캐닝하는 동안, L3 캐시를 먼저 확인하는 것이 좋다. physmap을 통해 축출 패턴을 사용하여 L3 축출을 수행함으로써, 축출이 게스트가 소유한 페이지에 영향을 미치는지 테스트를 할 수 있다.


  캐시 세트에서도 동일한 작업이 가능하다. 호스트 커널 컨텍스트에서 함수 포인터를 제거하는데 L1D+L2 축출 세트를 사용하고, 커널에서 물리 주소들을 사용해서 L3 세트를 제거하는 가젯을 사용한다. 그런 다음, 게스트가 소유한 축출 세트가 작성될 때까지 게스트 라인이 속한 캐시 세트를 식별하여 사용한다.


전체 BTB 상태 덤프


  일반적인 BTB 는 2^(31-8)이나 약간의 시작 주소들만을 구별할 수 있는 것처럼 보이며, 몇 시간 정도 소요되는 시간대를 가진 hypercall에 의해 생성된 전체 BYB 상태를 덤프하는 것이 가능해 보인다(점프 시작을 검색한 다음, 발견된 모든 점프 시작을 향해 점프 대상을 이등분화). 호스트 커널이 커스텀으로 빌드된 경우, 호스트 커널에서 함수의 위치를 잠재적으로 식별하는데 사용될 수 있다.


  시작 주소 앨리어싱(aliasing)은 어느정도 유용성을 감소시킬 수 있지만, 대상 주소들이 여기에 대해 영향을 받지 않기 때문에, 다른 KASLR 오프셋을 가지는 기기로부터 상관(시작, 대상)시키는 것이 가능하고, aliasing이 비트 연산을 하는 동안 KASLR을 기반으로 하는 후보 주소의 수를 감소시킬 수 있다.


  잠재적으로 공격자가 함수들 사이의 점프 오프셋 또는 거리를 기반으로 한 빌드하는데 사용된 호스트 커널 버전이나 컴파일러에 대해 추측하도록 허가할 수 있다.


유형 2: 보다 효과적인 가젯을 사용한 leak


  유형 2를 위해 출분히 효과적인 가젯이 사용된다면, L3캐시로부터 호스트 커널 함수 포인터를 전혀 제거할 필요가 없다. L1D와 L2로부터만 제거하는 것으로 충분할 수 있다.


다양한 속도 향상


  유형 2의 PoC는 여전히 느릴 것이다. 아마도 다음과 같은 부분이 문제이다:

  • 한 번에 1비트씩만 leak한다. 한 번에 보다 많은 비트를 누수할 수 있어야 한다.
  • 프로세서로부터 제어 흐름을 숨기기 위해 IRETQ를 많이 사용한다.

  유형 2를 사용하여 데이터 누수률을 어떻게 달성할 지 보는 것이 흥미로울 것이다.


반환 예측기를 통해 Leak 또는 Injection


  반환 예측기가 또한 권한 레벨 변경에서 상태를 잃지 않는다면, VM 내부로부터 호스트 커널을 찾거나(이 경우, 이분법은 호스트 커널의 전체 주소를 매우 빠르게 발견하는데 사용될 수 있음), 반환 대상을 삽입(특히 반환 주소가 공격자에 의해 flush될 수 있고 반환 명령어 전에 리로드될 수 없는 캐시 라인에 저장된 경우)하는데 유용할 것이다.


  그러나, 지금까지 최종 결과를 산출한 반환 예측기를 사용해서 어떠한 실험도 수행하지 않았다.


간접 호출 예측기 밖으로 데이터 leak


  간접 호출 예측기의 밖으로 대상 정보를 leak하려고 시도하였으나, 실패하였다.


벤더사 공식 성명


  Project Zero가 이 취약점을 벤더사에 공개하였을 때, 벤더사로부터 이 문제와 관련하여 다음과 같은 공식 성명을 제공받았다:


Intel


  Intel은 컴퓨터 시스템의 전반적인 보안을 향상시키기 위해 적용하였다. 이 방법은 여기에 의존하는 현대 마이크로 프로세서의 공통 속성을 설명하였다. 따라서, 이러한 방법에 대한 민감함은 Intel 프로세서에 국한되지 않으며, 프로세서가 의도된 기능 명시를 벗어나 동작하지 않는다는 것을 나타내는 것도 아니다. Intel은 동업자들과 함께 긴밀히 협력하여, 영향을 받는 다른 프로세서 실리콘 벤더사와 함께 이를 위한 소프트웨어와 하드웨어 양쪽의 방어기법을 설계하고 배포한다.  


보다 유용한 정보와 링크를 얻으려면, 다음 사이트를 방문하십시오:

- https://security-center.intel.com/advisory.aspx?intelid=INTEL-SA-00088&languageid=en-fr

- http://newsroom.intel.com/wp-content/uploads/sites/11/2018/01/Intel-Analysis-of-Speculative-Execution-Side-Channels.pdf


AMD


  AMD가 제공한 다음과 같은 링크 : http://www.amd.com/en/corporate/speculative-execution


ARM


  ARM은 많은 현대 고성능 프로세서의 예측 기능이, 예정된대로 동작함에도 불구하고, 이 블로그에서 설명된대로 정보를 leak하기 위해 캐시 동작의 타이밍이 사용될 수 있음을 인정하였다. 이에 따라 ARM은 배포를 권장하는 소프트웨어 방어기법을 개발하였다.


영향을 받는 프로세서와 방어기법에 관련하여 다음의 웹 사이트에서 자세히 명시하고 있음:

https://developer.arm.com/support/security-update


  ARM에는 특정 구현과 방어기법에 관련된 약간의 ARM의 아키텍처 파트너로부터의 정보가 담긴 링크와 상세한 기술 백서가 포함되어 있다.


Literature


  이 문서 중 일부(특히, Intel의 문서는)는 시간이 지남에 따라 변경되므로 따옴표 및 참조 부분이 Intel의 최신 버전 문서에 반영되지 않을 수 있다. 

  • https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf: Intel's optimization manual은 관련 마이크로 아키텍쳐의 동작을 나타내는 매우 흥미로운 최적화된 충고에 대한 정보가 들어있음, 예를 들어
    • "간접 분기 바로 다음에 데이터를 배치하면 성능 문제를 일으킬 수 있다. 데이터가 모두 0으로 구성된다면, 메모리 목적지에 대한 ADD의 긴 스트림처럼 보이게 되고, 자원 충돌과 분기 복구의 속도 저하를 일으킬 수 있다. 또한 간접 분기 바로 다음의 데이터는 다른 데이터 페이지를 실행하기 위해 분기할 수 있는 분기 예측 하드웨어에 분기되어 나타날 수 있다. 이로 인해 자제적인 코드 수정 문제가 발생할 수 있다."
    • "로드할 수 있음:[...]앞의 분기가 해결되기 전에, 예측을 수행하십시오"
    • "소프트웨어는 동일한 1-KByte 서브 페이지에서 실행 중인 코드 페이지에 쓰거나 동일한 2-KByte 서브 페이지에서 패치된 코드를 쓰는 것은 피해야 한다. 또한, 데이터 페이지로써 직접 또는 예측적을 실행된 코드를 포함한 페이지를 또 다른 프로세서와 함께 공유하면 기기의 전체 파이프라인과 추적 캐시를 지우는 SMC 조건이 발생할 수 있다. 이는 자체 수정 코드 조건이기 때문에 당연하다."
    • "WB나 WT로 매핑된 경우, 캐시에 데이터를 가져오기 위해서 예측 프로세서를 읽을 가능성이 있다."
    • "WC로 영역을 매핑 실패하는 것은 프로세서 캐시에 예측적으로 라인을 읽도록 허용할 수 있다.(잘못 예측된 분기의 잘못된 경로를 통해)"
  • https://software.intel.com/en-us/articles/intel-sdm: Intel's Software Developter Manuals
  • http://www.agner.org/optimize/microarchitecture.pdf: 리버싱된 프로세서 동작과 관련된 이론의 Agner Fog의 문서는 이 연구에 매우 도움이 많이 되었다.
  • http://www.cs.binghamton.edu/~dima/micro16.pdf 와 https://github.com/felixwilhelm/mario_baslr:
    Haswll 프로세의 분기 예측 분석을 위한 시작점으로써 사용된 주소 leak을 위해 분기 대상 버퍼 동작 남용한 Dmitry Evtyushkin, Dmitry Ponomarev와 Nael Abu-Ghazaleh에 의한 선행 연구. 이를 기반으로 한 Felix Wilhelm의 연구는 유형 2의 기본 개념을 제공하였다.
  • https://arxiv.org/pdf/1507.06955.pdf: 함수 포인터 제거를 위해 KVM PoC에서 재사용한 L3 캐시 축출 패턴에 대한 정보를 포함한 Daniel Gruss, Clementine Maurice와 Stefan Mangard에 의한 rowhammer.js 조사
  • https://xania.org/201602/bpu-part-one: Matt Godbolt는 인텔 프로세서에서 분기 예측의 구조를 리버싱하는 것에 대해 블로깅
  • https://www.sophia.re/thesis.pdf: Sophia A' Antoine는 이론적으로 hyperthreads 사이에서 데이터를 전송하는데 opcode 스케쥴링이 사용될 수 있다고 보여주는 논문을 작성하였다.
  • https://gruss.cc/files/kaiser.pdf: Daniel Gruss, Moritz Lipp, Michael Schwarz, Richard Fellner, Clemetine Maurice 및 Stefan Mangard는 유저 영역과 커널 사이에서 페이지 테이블 공유에 의해 일어날 수 있는 마이크로 아키텍처 문제의 대처 방법에 대해 논문을 작성하였다.
  • https://www.jilp.org/: 이 저널은 분기 예측에 대해 많은 기사를 포함하고 있다.
  • http://blog.stuffedcow.net/2013/01/ivb-cache-replacement/: Intel의 Ivy Bridge 아키텍처에 의해 사용되는 L3 캐시 교체 정책을 조사해서 Henry Wong에 의해 작성된 블로그 포스트

참조


  1. 초기 보고서에는 유형 3에 대한 정보가 포함되어 있지 않다. 커널 메모리로부터 직접 읽기가 수행될 수 있는지에 대해 논의하였지만, 가능성이 낮다고 생각하였고, 나중에 Anders Fogh의 연구가 발표되기 전에 유형 3을 테스트하고 보고하였다.
    https://cyber.wtf/2017/07/28/negative-result-reading-kernel-memory-from-user-mode/.
  2. 정확한 모델 이름은 "테스트된 프로세서" 섹션에 나와 있다. 이를 재현하는 코드는 bugtracker의 writeup_files.tar 아카이브의 userland_test_x86과 userland_test_aarch64 폴더에 있다.
  3. 공격자가 제어가능한 오프셋은 커널 힙 영역에서 4GiB 창에 접근가능한 주소를 제한하는 32비트 값인 PoC에 의해 배열에 out-of-bounds를 수행하기 위해 사용한다.
  4. 이 PoC는 SMAP을 지원하는 CPU에서는 동작하지 않는다. 그러나 이는 중요한 제한 사항이 아니다.
  5. linux-image-4.9.0-3-amd64 at version 4.9.30-2+deb9u2 (http://snapshot.debian.org/archive/debian/20170701T224614Z/pool/main/l/linux/linux-image-4.9.0-3-amd64_4.9.30-2%2Bdeb9u2_amd64.deb, sha256 5f950b26aa7746d75ecb8508cc7dab19b3381c9451ee044cd2edfd6f5efff1f8, Release.gpg, Release, Packages.xz에 의해 서명) 기계에 설치할 당시의 배포판 커널 버전이다. 변경없이 PoC가 다른 커널 버전에서 동작한다. 하드 코딩된 주소/오프셋이 많이 포함되어 있다.
  6. 2017년 5월부터 빌드되어 휴대폰에 Android로 동작하고 있다.
  7. https://software.intel.com/en-us/articles/intel-sdm
  8. https://software.intel.com/en-us/articles/avoiding-and-identifying-false-sharing-among-threads, "background" 섹션
  9. 2^15개보다 많은 매핑이 더 효율적이지만, 커널은 프로세스가 가질 수 있는 VMA의 개수인 2^16의 하드 캡을 지니고 있다.
  10. Intel's optimization manual에는 "HT 기술의 첫 번째 구현에서, 물리적 실행 자원은 공유되고 아키택처 상태가 각 논리 프로세서에 대해서 중복된다"라고 되어 있으며, 예측기의 상태를 공유하는 것이 그럴듯할 것이다. 예측기 상태가 논리 코어에 의해 태그될 수 있는 동안, 다중스레드 프로세스의 성능은 감소될 수 있으므로, 그렇게 보이지 않는다.
  11. 히스토리 버퍼가 측정된 것보다 큰 경우에 대해, 약간의 여유를 추가하였다. 특히 다른 실험에서 약간 다른 히스토리 버퍼 길이를 보았고 26이 라운드 수가 아니기 때문이다. 
  12. http://palms.ee.princeton.edu/system/files/SP_vfinal.pdf로부터 기본 개념을 가져옴. 섹션 4, 논문의 저자들은 여전히 거대한 페이지를 사용했다.


'System' 카테고리의 다른 글

GDT, LDT, IDT, Paging, Gate와 Descriptor에 대한 모든 것  (0) 2016.10.24
운영체제 - 가상메모리  (0) 2016.05.17
댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31