이 문서에는 C++ 소프트웨어의 투기적 실행 쪽 채널 하드웨어 취약성을 식별하고 완화하는 데 도움이 되는 개발자를 위한 지침이 포함되어 있습니다. 이러한 취약성은 신뢰 경계를 넘어 중요한 정보를 공개할 수 있으며, 명령의 순서가 잘못된 투기적 실행을 지원하는 프로세서에서 실행되는 소프트웨어에 영향을 줄 수 있습니다. 이 취약성 클래스는 2018년 1월에 처음 설명되었으며 추가 배경 및 지침은 Microsoft의 보안 권고에서 찾을 수 있습니다.
이 문서에서 제공하는 지침은 다음으로 표시되는 취약성 클래스와 관련이 있습니다.
CVE-2017-5753, Spectre 변종 1로도 알려져 있습니다. 이 하드웨어 취약성 종류는 조건부 분기 예측이 잘못된 경우, 그 결과로 발생하는 투기적 실행에 의해 나타날 수 있는 사이드 채널과 관련이 있습니다. Visual Studio 2017의 Microsoft C++ 컴파일러(버전 15.5.5부터 시작)에는 CVE-2017-5753과 관련된 잠재적으로 취약한 코딩 패턴의 제한된 집합에 대한 컴파일 시간 완화를 제공하는 스위치 지원이 포함되어
/Qspectre있습니다. 이/Qspectre스위치는 KB 4338871을 통해 Visual Studio 2015 업데이트 3에서 사용할 수도 있습니다. 플래그에 대한/Qspectre설명서는 해당 효과 및 사용에 대한 자세한 정보를 제공합니다.CVE-2018-3639, 투기 저장소 바이패스 (SSB)라고도 합니다. 이 하드웨어 취약성 클래스는 메모리 액세스 잘못된 예측의 결과로 종속 저장소보다 먼저 부하가 투기적으로 실행되어 발생할 수 있는 측면 채널과 관련이 있습니다.
투기적 실행 사이드 채널 취약성에 대한 이해하기 쉬운 소개는 이러한 문제를 발견한 연구 팀 중 하나가 스펙터와 멜트다운의 사례라는 프레젠테이션에서 찾을 수 있습니다.
투기적 실행 사이드 채널 하드웨어 취약성이란?
최신 CPU는 명령의 추측 및 잘못된 순서 실행을 사용하여 더 높은 수준의 성능을 제공합니다. 예를 들어 이는 종종 분기의 대상(조건부 및 간접)을 예측하여 CPU가 예측된 분기 대상에서 지침을 추측적으로 실행하기 시작하므로 실제 분기 대상이 해결될 때까지 중단을 방지합니다. 나중에 CPU에서 잘못된 예측이 발생했음을 발견할 경우 추측적으로 계산된 모든 컴퓨터 상태가 삭제됩니다. 이렇게 하면 잘못 예측된 추측의 아키텍처상 가시적인 영향이 없습니다.
투기적 실행은 아키텍처적으로 가시적인 상태에 영향을 주지 않지만 CPU에서 사용되는 다양한 캐시와 같은 비 아키텍처 상태에 잔여 흔적을 남길 수 있습니다. 사이드 채널 취약성을 야기할 수 있는 것은 투기적 실행의 잔여 추적입니다. 이를 더 잘 이해하려면 CVE-2017-5753(경계 검사 바이패스)의 예를 제공하는 다음 코드 조각을 고려하세요.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
if (untrusted_index < buffer_size) {
unsigned char value = buffer[untrusted_index];
return shared_buffer[value * 4096];
}
}
이 예제에서는 버퍼, ReadByte 버퍼 크기 및 인덱스가 해당 버퍼에 제공됩니다. 인덱스 매개 변수는 관리자가 아닌 프로세스와 같이 권한이 낮은 컨텍스트에서 제공됩니다 untrusted_index.
untrusted_index가 buffer_size보다 작으면, 해당 인덱스의 문자를 buffer에서 읽어와 shared_buffer로 참조되는 메모리의 공유 영역에 인덱싱하는 데 사용됩니다.
아키텍처적 관점에서, 이 코드 시퀀스는 untrusted_index이 항상 buffer_size보다 작다는 것이 보장되기 때문에 완벽하게 안전합니다. 그러나 투기적 실행이 있는 경우, CPU가 조건부 분기를 잘못 예측하여 untrusted_index이 buffer_size보다 크거나 같은 경우에도 if 문의 본문을 실행할 수 있습니다. 이로 인해 CPU는 (비밀일 수 있음) 범위를 buffer 벗어나는 바이트를 추측적으로 읽은 다음, 해당 바이트 값을 사용하여 후속 로드 shared_buffer의 주소를 계산할 수 있습니다.
CPU는 결국 이러한 잘못된 예측을 감지하지만, CPU 캐시에 잔여 부작용이 남아 있을 수 있습니다. 해당 캐시에는 범위에서 벗어난 바이트 값에 대한 정보가 노출될 수 있습니다 buffer. 이러한 부작용은 시스템에서 실행 중인 권한이 적은 컨텍스트가 각 캐시 줄 shared_buffer 에 얼마나 빨리 액세스되는지를 조사하여 확인할 수 있습니다. 이 작업을 수행하기 위해 수행할 수 있는 단계는 다음과 같습니다.
untrusted_index가buffer_size보다 작을 때ReadByte를 여러 번 호출합니다. 공격 컨텍스트는 피해자 컨텍스트가ReadByte를 호출하게 하여(예: RPC를 통해),untrusted_index가buffer_size보다 작은 경우 분기 예측기가 '실행 안 함'으로 학습되도록 설정할 수 있습니다.shared_buffer의 모든 캐시 라인을 플러시합니다. 공격 컨텍스트는 참조되는 메모리shared_buffer의 공유 영역에 있는 모든 캐시 줄을 플러시해야 합니다. 메모리 영역이 공유되므로 이는 간단하며 다음과 같은_mm_clflush내장 함수를 사용하여 수행할 수 있습니다.untrusted_index가buffer_size보다 큰 상태로ReadByte을 호출합니다. 공격 컨텍스트는 피해자 컨텍스트가ReadByte를 호출하면서 분기가 수행되지 않으리라 잘못된 예측을 하게 만듭니다. 이로 인해 프로세서는untrusted_index이(가)buffer_size보다 큰 경우에 if 블록의 본문을 추측적으로 실행하게 되어,buffer의 범위를 벗어난 읽기로 이어집니다. 따라서shared_buffer는 범위를 벗어난 상태에서 읽어낸 비밀 가능성이 있는 값을 사용하여 인덱싱되었으며, 이로 인해 해당 캐시 라인이 CPU에 의해 로드됩니다.각 캐시 줄을 읽어
shared_buffer가장 빠르게 액세스되는 캐시 줄을 확인합니다. 공격 컨텍스트는 각 캐시 줄을 읽고 다른 캐시 줄shared_buffer보다 훨씬 빠르게 로드되는 캐시 라인을 검색할 수 있습니다. 3단계에서 가져왔을 가능성이 있는 캐시 라인입니다. 이 예제에서는 바이트 값과 캐시 줄 간에 1:1 관계가 있으므로 공격자가 바깥에서 읽은 바이트의 실제 값을 유추할 수 있습니다.
위의 단계는 CVE-2017-5753 인스턴스를 악용하는 것과 함께 FLUSH+RELOAD라는 기술을 사용하는 예제를 제공합니다.
영향을 받을 수 있는 소프트웨어 시나리오는 무엇인가요?
SDL(보안 개발 수명 주기)과 같은 프로세스를 사용하여 보안 소프트웨어를 개발하려면 일반적으로 개발자가 애플리케이션에 존재하는 신뢰 경계를 식별해야 합니다. 신뢰 경계는 애플리케이션이 커널 모드 디바이스 드라이버의 경우 시스템의 다른 프로세스 또는 비관리 사용자 모드 프로세스와 같이 신뢰도가 낮은 컨텍스트에서 제공하는 데이터와 상호 작용할 수 있는 위치에 존재합니다. 투기적 실행 쪽 채널과 관련된 새로운 취약성 클래스는 디바이스의 코드와 데이터를 격리하는 기존 소프트웨어 보안 모델의 많은 신뢰 경계와 관련이 있습니다.
다음 표에서는 개발자가 이러한 취약성이 발생하는 것을 염려해야 할 수 있는 소프트웨어 보안 모델에 대한 요약을 제공합니다.
| 신뢰 경계 | 설명 |
|---|---|
| 가상 머신 경계 | 다른 가상 머신에서 신뢰할 수 없는 데이터를 수신하는 별도의 가상 머신에서 워크로드를 격리하는 애플리케이션은 위험에 처할 수 있습니다. |
| 커널 경계 | 비관리 사용자 모드 프로세스에서 신뢰할 수 없는 데이터를 수신하는 커널 모드 디바이스 드라이버는 위험에 처할 수 있습니다. |
| 프로세스 경계 | RPC(원격 프로시저 호출), 공유 메모리 또는 기타 IPC(프로세스 간 통신) 메커니즘을 통해 로컬 시스템에서 실행되는 다른 프로세스에서 신뢰할 수 없는 데이터를 수신하는 애플리케이션은 위험에 처할 수 있습니다. |
| 엔클레이브 경계 | Enclave 외부에서 신뢰할 수 없는 데이터를 수신하는 보안 Enclave(예: Intel SGX) 내에서 실행되는 애플리케이션은 위험에 처할 수 있습니다. |
| 언어 경계 | 더 높은 수준의 언어로 작성된 신뢰할 수 없는 코드를 해석하거나 JIT(Just-In-Time)를 실행하는 애플리케이션은 위험에 처할 수 있습니다. |
위의 신뢰 경계에 노출되는 공격 노출 영역이 있는 애플리케이션은 공격 표면의 코드를 검토하여 투기적 실행 쪽 채널 취약성의 가능한 인스턴스를 식별하고 완화해야 합니다. 원격 네트워크 프로토콜과 같은 원격 공격 표면에 노출되는 신뢰 경계는 투기적 실행 쪽 채널 취약성의 위험에 노출된 것으로 입증되지 않았다는 점에 유의해야 합니다.
잠재적으로 취약한 코딩 패턴
여러 코딩 패턴의 결과로 인해 투기적 실행 부채널 취약성이 발생할 수 있습니다. 이 섹션에서는 잠재적으로 취약한 코딩 패턴을 설명하고 각각에 대한 예제를 제공하지만 이러한 테마의 변형이 존재할 수 있음을 인식해야 합니다. 따라서 개발자는 이러한 패턴을 잠재적으로 취약한 모든 코딩 패턴의 전체 목록이 아니라 예제로 사용하는 것이 좋습니다. 오늘날 소프트웨어에 존재할 수 있는 동일한 종류의 메모리 안전 취약성은 버퍼 오버런, 범위를 벗어난 배열 액세스, 초기화되지 않은 메모리 사용, 형식 혼동 등을 포함하되 이에 국한되지 않는 예측 및 순서가 잘못된 실행 경로를 따라 존재할 수도 있습니다. 공격자가 아키텍처 경로를 따라 메모리 안전 취약성을 악용하는 데 사용할 수 있는 동일한 기본 형식이 투기 경로에도 적용될 수 있습니다.
일반적으로 조건부 분기의 잘못된 예측과 관련된 투기적 실행의 사이드 채널은 조건식이 신뢰도가 낮은 컨텍스트에 의해 제어되거나 영향을 받을 수 있는 데이터에 대해 작동할 때 발생할 수 있습니다. 예를 들어, if, for, while, switch 또는 삼항 연산자에 사용되는 조건식을 포함할 수 있습니다. 이러한 각 문에 대해 컴파일러는 CPU가 런타임에 분기 대상을 예측할 수 있는 조건부 분기를 생성할 수 있습니다.
각 예제에 대해 개발자가 장벽을 완화로 도입할 수 있는 "SPECULATION BARRIER"라는 문구가 포함된 주석이 삽입됩니다. 이 내용은 완화에 대한 섹션에서 자세히 설명합니다.
추측적 경계 초과 로드
코딩 패턴의 이 범주에는 투기적 아웃오브바운드 메모리 액세스로 이어지는 조건부 분기 잘못된 예측이 포함됩니다.
부하를 유발하는 배열 경계 초과 접근
이 코딩 패턴은 원래 CVE-2017-5753(경계 검사 바이패스)에 대해 설명된 취약한 코딩 패턴입니다. 이 문서의 배경 섹션에서는 이 패턴을 자세히 설명합니다.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
if (untrusted_index < buffer_size) {
// SPECULATION BARRIER
unsigned char value = buffer[untrusted_index];
return shared_buffer[value * 4096];
}
}
마찬가지로 배열의 범위를 벗어난 로드는 잘못된 예측으로 인해 종료 조건을 초과하는 루프와 함께 발생할 수 있습니다. 이 예제에서 x < buffer_size 표현식과 연결된 조건부 분기는 x가 buffer_size보다 크거나 같을 때 for 루프의 본문을 잘못 예측하여 추론적으로 실행함으로써, 결과적으로 투기적 범위 초과 로드가 발생할 수 있습니다.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadBytes(unsigned char *buffer, unsigned int buffer_size) {
for (unsigned int x = 0; x < buffer_size; x++) {
// SPECULATION BARRIER
unsigned char value = buffer[x];
return shared_buffer[value * 4096];
}
}
간접 분기를 공급하는 배열 범위를 벗어난 부하
이 코딩 패턴은 조건부 분기 잘못 해석으로 인해 함수 포인터 배열에 대한 범위를 벗어난 액세스로 이어질 수 있는 경우를 포함하며, 이로 인해 범위를 벗어난 대상 주소에 대한 간접 분기가 발생합니다. 다음 코드 조각은 이를 보여 주는 예제를 제공합니다.
이 예제에서는 매개 변수를 통해 untrusted_message_id DispatchMessage에 신뢰할 수 없는 메시지 식별자가 제공됩니다.
untrusted_message_id가 MAX_MESSAGE_ID보다 작으면, 이는 함수 포인터 배열을 인덱싱하여 해당 분기 지점으로 분기하는 데 사용합니다. 이 코드는 아키텍처적으로 안전하지만 CPU가 조건부 분기를 잘못 예측하면, MAX_MESSAGE_ID값이 이상일 때 untrusted_message_id이 DispatchTable을(를) 인덱싱하여 범위를 벗어난 액세스를 유발할 수 있습니다. 이로 인해 배열의 범위를 벗어나 파생된 분기 대상 주소에서 투기적 실행이 발생할 수 있으며, 이로 인해 투기적으로 실행되는 코드에 따라 정보가 공개될 수 있습니다.
#define MAX_MESSAGE_ID 16
typedef void (*MESSAGE_ROUTINE)(unsigned char *buffer, unsigned int buffer_size);
const MESSAGE_ROUTINE DispatchTable[MAX_MESSAGE_ID];
void DispatchMessage(unsigned int untrusted_message_id, unsigned char *buffer, unsigned int buffer_size) {
if (untrusted_message_id < MAX_MESSAGE_ID) {
// SPECULATION BARRIER
DispatchTable[untrusted_message_id](buffer, buffer_size);
}
}
다른 부하를 공급하는 배열의 범위를 벗어난 부하의 경우와 마찬가지로 이 조건은 잘못된 예측으로 인해 종료 조건을 초과하는 루프와 함께 발생할 수도 있습니다.
배열 경계를 벗어난 저장소에서 간접 분기로 데이터를 공급하기
이전 예제에서는 투기적 아웃 오브 바운드 로드가 간접 분기 대상에 영향을 줄 수 있는 방법을 보여 주지만, 범위를 벗어난 저장소가 함수 포인터 또는 반환 주소와 같은 간접 분기 대상을 수정할 수도 있습니다. 이로 인해 공격자가 지정한 주소에서 투기적 실행이 발생할 수 있습니다.
이 예제에서는 신뢰할 수 없는 인덱스가 매개 변수를 untrusted_index 통해 전달됩니다.
untrusted_index가 pointers 배열의 요소 수(256개 요소)보다 작으면, ptr에 제공된 포인터 값이 pointers 배열에 기록됩니다. 이 코드는 아키텍처적으로 안전하지만, CPU가 조건부 분기를 잘못 예측할 경우, ptr가 스택에 할당된 pointers 배열의 범위를 넘어 추측적으로 기록될 수 있습니다. 이로 인해 WriteSlot에 대한 반환 주소가 예상치 못하게 손상될 수 있습니다. 공격자가 값 ptr을 제어할 수 있는 경우, WriteSlot가 투기 경로를 통해 반환될 때 임의의 주소에서 투기적 실행을 유발할 수 있습니다.
unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
void *pointers[256];
if (untrusted_index < 256) {
// SPECULATION BARRIER
pointers[untrusted_index] = ptr;
}
}
마찬가지로, 명명 func 된 함수 포인터 지역 변수가 스택에 할당된 경우 조건부 분기 잘못된 예측이 발생할 때 참조하는 func 주소를 추측적으로 수정할 수 있습니다. 이로 인해 함수 포인터가 호출될 때 임의의 주소에서 투기적 실행이 발생할 수 있습니다.
unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
void *pointers[256];
void (*func)() = &callback;
if (untrusted_index < 256) {
// SPECULATION BARRIER
pointers[untrusted_index] = ptr;
}
func();
}
이 두 가지 예시는 모두 스택에 할당된 간접 분기 포인터의 투기적 수정을 수반한다는 점에 유의해야 합니다. 일부 CPU에서 전역 변수, 힙 할당 메모리 및 읽기 전용 메모리에 대해서도 투기적 수정이 발생할 수 있습니다. 스택 할당 메모리의 경우 Microsoft C++ 컴파일러는 버퍼가 컴파일러 보안 기능의 /GS 일부로 보안 쿠키에 인접하게 배치되도록 지역 변수를 다시 정렬하는 등 스택 할당 간접 분기 대상을 추측적으로 수정하기 어렵게 만드는 단계를 이미 수행합니다.
투기적 형식 혼란
이 범주는 추측 형식 혼동을 야기할 수 있는 코딩 패턴을 다룹니다. 이는 투기적 실행 중에 비 아키텍처 경로를 따라 잘못된 형식을 사용하여 메모리에 액세스할 때 발생합니다. 조건부 분기 예측 오류와 투기적 저장소 우회는 잠재적으로 투기적 타입 혼동으로 이어질 수 있습니다.
투기적 저장소 바이패스(speculative store bypass)의 경우 컴파일러가 여러 형식의 변수에 스택 위치를 재사용하는 상황에서 발생할 수 있습니다. 이는 형식 A 변수의 아키텍처 저장소를 바이패스할 수 있으므로 변수가 할당되기 전에 형식 A 의 로드를 추측적으로 실행할 수 있기 때문입니다. 이전에 저장된 변수가 다른 형식인 경우 추측 형식 혼동에 대한 조건을 만들 수 있습니다.
조건부 분기 잘못된 예측의 경우 다음 코드 조각은 추측 형식 혼동으로 인해 발생할 수 있는 다양한 조건을 설명하는 데 사용됩니다.
enum TypeName {
Type1,
Type2
};
class CBaseType {
public:
CBaseType(TypeName type) : type(type) {}
TypeName type;
};
class CType1 : public CBaseType {
public:
CType1() : CBaseType(Type1) {}
char field1[256];
unsigned char field2;
};
class CType2 : public CBaseType {
public:
CType2() : CBaseType(Type2) {}
void (*dispatch_routine)();
unsigned char field2;
};
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ProcessType(CBaseType *obj)
{
if (obj->type == Type1) {
// SPECULATION BARRIER
CType1 *obj1 = static_cast<CType1 *>(obj);
unsigned char value = obj1->field2;
return shared_buffer[value * 4096];
}
else if (obj->type == Type2) {
// SPECULATION BARRIER
CType2 *obj2 = static_cast<CType2 *>(obj);
obj2->dispatch_routine();
return obj2->field2;
}
}
범위를 초과한 로드로 이어지는 투기적 유형 혼동
이 코딩 패턴에는 투기적 형식 혼동으로 인해 로드된 값이 후속 부하 주소를 공급하는 범위를 벗어나거나 형식이 혼동되는 필드 액세스가 발생할 수 있는 경우가 포함됩니다. 이는 배열 아웃 오브 바운드 코딩 패턴과 유사하지만 위와 같이 대체 코딩 시퀀스를 통해 나타납니다. 이 예제에서는 공격 컨텍스트가 피해자 컨텍스트가 형식 CType1의 객체를 사용하여 ProcessType를 여러 번 실행하도록 유도할 수 있으며, 이때 type 필드가 Type1와 같습니다. 이렇게 하면 첫 번째 if 문에 대한 조건부 분기를 학습하여 수행되지 않을 것으로 예측합니다. 그러면 공격 컨텍스트로 인해 피해자 컨텍스트가 형식ProcessType의 개체로 실행 CType2 될 수 있습니다. 첫 번째 if 문의 조건부 분기가 잘못 예측되어 if 문의 본문을 실행하면서, CType2 형식의 개체를 CType1 형식으로 캐스팅해 추측에 의한 형식 혼동이 발생할 수 있습니다.
CType2보다 CType1 작기 때문에, CType1::field2에 대한 메모리 액세스는 비밀일 수 있는 데이터의 경계 밖에서 예측적 추측 로드가 발생하게 됩니다. 이 값은 앞에서 설명한 배열 바깥쪽 예제와 마찬가지로 관찰 가능한 부작용을 만들 수 있는 로드 shared_buffer 에 사용됩니다.
간접 분기로 이어지는 추측적 형식 오류
이 코딩 패턴에는 투기적 형식 혼동으로 인해 투기적 실행 중에 안전하지 않은 간접 분기가 발생할 수 있는 경우가 포함됩니다. 이 예제에서 공격 컨텍스트는 피해자 컨텍스트가 객체 형식 CType2 (type 필드는 Type2과 같음)을 사용하여 ProcessType을(를) 여러 번 실행하도록 유도할 수 있습니다. 이렇게 하면 첫 번째 if 문은 선택되고 else if 문은 선택되지 않도록 조건부 분기를 학습하는 효과가 있습니다. 그러면 피해자 컨텍스트가 공격 컨텍스트로 인해 ProcessType 형식의 개체와 함께 CType1을 실행하게 됩니다. 이렇게 하면 첫 번째 if 문의 조건부 분기가 수행될 것으로 예측하고 else if 문이 수행되지 않을 것으로 예측하여 형식의 본문을 else if 실행하고 형식 개체를 캐스팅하는 CType1경우 추측 형식 혼동이 발생할 수 있습니다CType2. 필드가 CType2::dispatch_routine 배열char과 CType1::field1 겹치므로 의도하지 않은 분기 대상에 대한 투기적 간접 분기가 발생할 수 있습니다. 공격 컨텍스트가 배열의 바이트 값을 CType1::field1 제어할 수 있는 경우 분기 대상 주소를 제어할 수 있습니다.
초기화되지 않은 예측적 사용
이 코딩 패턴 범주에는 추측 실행이 초기화되지 않은 메모리에 액세스하여 후속 로드 또는 간접 분기를 공급하는 데 사용할 수 있는 시나리오가 포함됩니다. 이러한 코딩 패턴을 악용하려면 공격자가 사용 중인 컨텍스트에 의해 초기화되지 않고 사용되는 메모리의 콘텐츠를 제어하거나 의미 있게 영향을 줄 수 있어야 합니다.
초기화되지 않은 투기적 사용으로 인해 범위를 벗어난 로드가 발생합니다.
투기적 초기화되지 않은 사용 사례는 공격자가 제어하는 값을 사용하여 범위를 벗어난 데이터 로딩을 유발할 수 있습니다. 아래 예제에서 값 index 은 모든 아키텍처 경로에 할당 trusted_index 되며 trusted_index 보다 작거나 같은 것으로 buffer_size간주됩니다. 그러나 컴파일러에서 생성한 코드에 따라 index에의 할당보다 먼저 buffer[index] 및 종속 식을 로드하고 실행할 수 있는 투기적 저장 바이패스가 발생할 수 있습니다. 이 경우 index의 초기화되지 않은 값이 buffer에 대한 오프셋으로 사용될 수 있어, 공격자가 범위 밖의 중요한 정보를 읽고 shared_buffer의 종속적 로드를 통해 측면 채널로 이를 전달할 수 있습니다.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
void InitializeIndex(unsigned int trusted_index, unsigned int *index) {
*index = trusted_index;
}
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int trusted_index) {
unsigned int index;
InitializeIndex(trusted_index, &index); // not inlined
// SPECULATION BARRIER
unsigned char value = buffer[index];
return shared_buffer[value * 4096];
}
간접 분기로 이어지는 초기화되지 않은 투기적 사용
초기화되지 않은 예측적 사용은 잠재적으로 공격자에 의해 분기 대상이 제어되는 간접 분기로 이어질 수 있습니다.
mode의 값에 따라 아래의 예제에서 routine는 DefaultMessageRoutine1 또는 DefaultMessageRoutine에 할당됩니다. 아키텍처 경로에서 routine 이렇게 하면 항상 간접 분기보다 먼저 초기화됩니다. 그러나 컴파일러가 생성한 코드에 따라 routine을 통한 간접 분기가 routine에 대한 할당 전에 투기적으로 실행되도록 하는 투기적 저장소 바이패스가 발생할 수 있습니다. 이 경우 공격자가 초기화되지 않은 값 routine에 영향을 주거나 제어할 수 있다고 가정하여 임의의 주소에서 투기적으로 실행할 수 있습니다.
#define MAX_MESSAGE_ID 16
typedef void (*MESSAGE_ROUTINE)(unsigned char *buffer, unsigned int buffer_size);
const MESSAGE_ROUTINE DispatchTable[MAX_MESSAGE_ID];
extern unsigned int mode;
void InitializeRoutine(MESSAGE_ROUTINE *routine) {
if (mode == 1) {
*routine = &DefaultMessageRoutine1;
}
else {
*routine = &DefaultMessageRoutine;
}
}
void DispatchMessage(unsigned int untrusted_message_id, unsigned char *buffer, unsigned int buffer_size) {
MESSAGE_ROUTINE routine;
InitializeRoutine(&routine); // not inlined
// SPECULATION BARRIER
routine(buffer, buffer_size);
}
완화 옵션
소스 코드를 변경하여 투기적 실행 사이드 채널 취약성을 완화할 수 있습니다. 이러한 변경 사항에는 취약성의 특정 인스턴스를 완화하기 위해 투기 실행 방지를 추가하거나, 애플리케이션 설계를 변경하여 중요한 정보가 투기 실행에 노출되지 않도록 하는 작업이 포함될 수 있습니다.
수동 계측을 통한 추측 장벽
투기적 실행이 비 아키텍처 경로를 따라 진행되지 않도록 개발자가 추측 장벽을 수동으로 삽입할 수 있습니다. 예를 들어 개발자는 조건부 블록의 본문에서 위험한 코딩 패턴 앞에 블록의 시작 부분(조건부 분기 뒤) 또는 중요한 첫 번째 로드 앞에 투기 장벽을 삽입할 수 있습니다. 이렇게 하면 조건부 분기 잘못된 예측이 실행을 직렬화하여 비 아키텍처 경로에서 위험한 코드를 실행하지 못하게 됩니다. 추측 장벽 시퀀스는 다음 표에 설명된 대로 하드웨어 아키텍처에 따라 다릅니다.
| 아키텍처 | CVE-2017-5753에 대한 투기 장벽 내장 함수 | CVE-2018-3639에 대한 고유의 투기 장벽 |
|---|---|---|
| x86/x64 | _mm_lfence() | _mm_lfence() |
| 팔 | 현재 사용할 수 없음 | __dsb(0) |
| ARM64 | 현재 사용할 수 없음 | __dsb(0) |
예를 들어 아래와 같이 내장 함수를 사용하여 _mm_lfence 다음 코드 패턴을 완화할 수 있습니다.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
if (untrusted_index < buffer_size) {
_mm_lfence();
unsigned char value = buffer[untrusted_index];
return shared_buffer[value * 4096];
}
}
컴파일러 시간 계측을 통한 추측 장벽
Visual Studio 2017의 Microsoft C++ 컴파일러(버전 15.5.5부터 시작)에는 CVE-2017-5753과 관련된 잠재적으로 취약한 코딩 패턴의 제한된 집합에 대한 추측 장벽을 자동으로 삽입하는 스위치 지원이 /Qspectre 포함되어 있습니다. 플래그에 대한 /Qspectre 설명서는 해당 효과 및 사용에 대한 자세한 정보를 제공합니다. 이 플래그는 잠재적으로 취약한 코딩 패턴을 모두 다루지 않으므로 개발자는 이 취약성 클래스에 대한 포괄적인 완화로 의존해서는 안 됩니다.
배열 인덱스 마스킹
투기적 아웃 오브 바운드 로드가 발생할 수 있는 경우 배열 인덱스를 명시적으로 바인딩하는 논리를 추가하여 아키텍처 및 비 아키텍처 경로 모두에서 배열 인덱스를 강력하게 바인딩할 수 있습니다. 예를 들어, 배열을 2의 제곱수 크기로 할당할 수 있는 경우, 간단한 마스크를 도입할 수 있습니다. 아래 샘플에서는 buffer_size가 2의 거듭제곱에 맞춰 정렬된 것으로 가정합니다. 조건부 분기 예측이 잘못되었고 untrusted_index이(가) buffer_size보다 크거나 같은 값으로 전달되더라도, untrusted_index이(가) 항상 buffer_size보다 작도록 보장합니다.
여기서 수행된 인덱스 마스킹은 컴파일러에서 생성된 코드에 따라 추측 저장소 바이패스가 발생할 수 있습니다.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
if (untrusted_index < buffer_size) {
untrusted_index &= (buffer_size - 1);
unsigned char value = buffer[untrusted_index];
return shared_buffer[value * 4096];
}
}
메모리에서 중요한 정보 제거
투기적 실행 부채널 취약성을 완화하기 위해 사용할 수 있는 또 다른 기술은 메모리에서 민감한 정보를 제거하는 것입니다. 소프트웨어 개발자는 투기적 실행 중에 중요한 정보에 액세스할 수 없도록 애플리케이션을 리팩터링할 기회를 찾을 수 있습니다. 이 작업은 애플리케이션의 디자인을 리팩터링하여 중요한 정보를 별도의 프로세스로 격리하여 수행할 수 있습니다. 예를 들어 웹 브라우저 애플리케이션은 각 웹 원본과 연결된 데이터를 별도의 프로세스로 격리하여 한 프로세스가 투기적 실행을 통해 원본 간 데이터에 액세스할 수 없도록 할 수 있습니다.