Speculative Execution Side Channels에 대한 C++ 개발자 지침

이 문서에는 C++ 소프트웨어의 투기적 실행 쪽 채널 하드웨어 취약성을 식별하고 완화하는 데 도움이 되는 개발자를 위한 지침이 포함되어 있습니다. 이러한 취약성은 신뢰 경계를 넘어 중요한 정보를 공개할 수 있으며, 명령의 순서가 잘못된 투기적 실행을 지원하는 프로세서에서 실행되는 소프트웨어에 영향을 줄 수 있습니다. 이 취약성 클래스는 2018년 1월에 처음 설명되었으며 추가 배경 및 지침은 Microsoft의 보안 권고에서 찾을 수 있습니다.

이 문서에서 제공하는 지침은 다음으로 표시되는 취약성 클래스와 관련이 있습니다.

  1. CVE-2017-5753, 또한 유령 변형으로 알려진 1. 이 하드웨어 취약성 클래스는 조건부 분기 잘못된 예측의 결과로 발생하는 투기적 실행으로 인해 발생할 수 있는 측면 채널과 관련이 있습니다. Visual Studio 2017의 Microsoft C++ 컴파일러(버전 15.5.5부터 시작)에는 CVE-2017-5753과 관련된 잠재적으로 취약한 코딩 패턴의 제한된 집합에 대한 컴파일 시간 완화를 제공하는 스위치 지원이 포함되어 /Qspectre 있습니다. 이 /Qspectre 스위치는 Visual Studio 2015 업데이트 3에서 KB 4338871 사용할 수도 있습니다. 플래그에 대한 /Qspectre 설명서는 해당 효과 및 사용에 대한 자세한 정보를 제공합니다.

  2. 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 참조되는 메모리의 공유 영역으로 인덱싱하는 shared_bufferbuffer_size사용됩니다.

아키텍처 관점에서 이 코드 시퀀스는 항상 보다 buffer_size작을 수 있으므로 untrusted_index 완벽하게 안전합니다. 그러나 투기적 실행이 있는 경우 CPU가 조건부 분기를 잘못 예측하고 if 문의 본문이 크거나 같은 경우에도 untrusted_index 실행할 수 buffer_size있습니다. 이로 인해 CPU는 (비밀일 수 있음) 범위를 buffer 벗어나는 바이트를 추측적으로 읽은 다음, 해당 바이트 값을 사용하여 후속 로드 shared_buffer의 주소를 계산할 수 있습니다.

CPU는 결국 이러한 잘못된 설명을 감지하지만, CPU 캐시에 잔여 부작용이 남아 있을 수 있습니다. 이 캐시는 범위에서 벗어난 바이트 값에 대한 정보를 표시합니다 buffer. 이러한 부작용은 시스템에서 실행되는 권한이 적은 컨텍스트에서 각 캐시 줄 shared_buffer 에 액세스하는 빈도를 검색하여 검색할 수 있습니다. 이 작업을 수행하기 위해 수행할 수 있는 단계는 다음과 같습니다.

  1. 보다 ReadBytebuffer_size게 여러 번 untrusted_index 호출합니다. 공격 컨텍스트는 피해자 컨텍스트가 호출 ReadByte (예: RPC를 통해)하여 분기 예측 변수가 보다 buffer_size작게 수행 untrusted_index 되지 않도록 학습되도록 할 수 있습니다.

  2. 에서 모든 캐시 줄을 플러시합니다 shared_buffer. 공격 컨텍스트는 참조되는 메모리 shared_buffer의 공유 영역에 있는 모든 캐시 줄을 플러시해야 합니다. 메모리 영역이 공유되므로 이는 간단하며 다음과 같은 _mm_clflush내장 함수를 사용하여 수행할 수 있습니다.

  3. ReadByte 보다 큼buffer_size으로 untrusted_index 호출합니다. 공격 컨텍스트로 인해 피해자 컨텍스트가 호출 ReadByte 되어 분기가 수행되지 않을 것이라고 잘못 예측합니다. 이로 인해 프로세서는 if buffer_size블록 untrusted_index 의 본문을 보다 크게 실행하여 범위를 벗어난 읽기buffer로 이어집니다. 따라서 shared_buffer 범위를 벗어나 읽은 잠재적으로 비밀 값을 사용하여 인덱싱되므로 해당 캐시 줄이 CPU에 의해 로드됩니다.

  4. 각 캐시 줄을 읽어 shared_buffer 가장 빠르게 액세스되는 캐시 줄을 확인합니다. 공격 컨텍스트는 각 캐시 줄을 읽고 다른 캐시 줄 shared_buffer 보다 훨씬 빠르게 로드되는 캐시 라인을 검색할 수 있습니다. 3단계에서 가져온 캐시 라인입니다. 이 예제에서는 바이트 값과 캐시 줄 간에 1:1 관계가 있으므로 공격자가 바깥에서 읽은 바이트의 실제 값을 유추할 수 있습니다.

위의 단계는 CVE-2017-5753 인스턴스를 악용하는 것과 함께 FLUSH+RELOAD라는 기술을 사용하는 예제를 제공합니다.

영향을 받을 수 있는 소프트웨어 시나리오는 무엇인가요?

SDL(보안 개발 수명 주기)과 같은 프로세스를 사용하여 보안 소프트웨어를 개발하려면 일반적으로 개발자가 애플리케이션에 존재하는 신뢰 경계를 식별해야 합니다. 신뢰 경계는 애플리케이션이 커널 모드 디바이스 드라이버의 경우 시스템의 다른 프로세스 또는 비관리 사용자 모드 프로세스와 같이 신뢰도가 낮은 컨텍스트에서 제공하는 데이터와 상호 작용할 수 있는 위치에 존재합니다. 투기적 실행 쪽 채널과 관련된 새로운 취약성 클래스는 디바이스의 코드와 데이터를 격리하는 기존 소프트웨어 보안 모델의 많은 신뢰 경계와 관련이 있습니다.

다음 표에서는 개발자가 이러한 취약성이 발생하는 것을 염려해야 할 수 있는 소프트웨어 보안 모델에 대한 요약을 제공합니다.

트러스트 경계 설명
가상 머신 경계 다른 가상 머신에서 신뢰할 수 없는 데이터를 수신하는 별도의 가상 머신에서 워크로드를 격리하는 애플리케이션은 위험에 처할 수 있습니다.
커널 경계 비관리 사용자 모드 프로세스에서 신뢰할 수 없는 데이터를 수신하는 커널 모드 디바이스 드라이버는 위험에 처할 수 있습니다.
프로세스 경계 RPC(원격 프로시저 호출), 공유 메모리 또는 기타 IPC(프로세스 간 통신) 메커니즘을 통해 로컬 시스템에서 실행되는 다른 프로세스에서 신뢰할 수 없는 데이터를 수신하는 애플리케이션은 위험에 처할 수 있습니다.
Enclave 경계 Enclave 외부에서 신뢰할 수 없는 데이터를 수신하는 보안 Enclave(예: Intel SGX) 내에서 실행되는 애플리케이션은 위험에 처할 수 있습니다.
언어 경계 더 높은 수준의 언어로 작성된 신뢰할 수 없는 코드를 해석하거나 JIT(Just-In-Time)를 실행하는 애플리케이션은 위험에 처할 수 있습니다.

위의 신뢰 경계에 노출되는 공격 노출 영역이 있는 애플리케이션은 공격 표면의 코드를 검토하여 투기적 실행 쪽 채널 취약성의 가능한 인스턴스를 식별하고 완화해야 합니다. 원격 네트워크 프로토콜과 같은 원격 공격 표면에 노출되는 신뢰 경계는 투기적 실행 쪽 채널 취약성의 위험에 노출된 것으로 입증되지 않았다는 점에 유의해야 합니다.

잠재적으로 취약한 코딩 패턴

여러 코딩 패턴의 결과로 투기적 실행 쪽 채널 취약성이 발생할 수 있습니다. 이 섹션에서는 잠재적으로 취약한 코딩 패턴을 설명하고 각각에 대한 예제를 제공하지만 이러한 테마의 변형이 존재할 수 있음을 인식해야 합니다. 따라서 개발자는 이러한 패턴을 잠재적으로 취약한 모든 코딩 패턴의 전체 목록이 아니라 예제로 사용하는 것이 좋습니다. 오늘날 소프트웨어에 존재할 수 있는 동일한 종류의 메모리 안전 취약성은 버퍼 오버런, 범위를 벗어난 배열 액세스, 초기화되지 않은 메모리 사용, 형식 혼동 등을 포함하되 이에 국한되지 않는 예측 및 순서가 잘못된 실행 경로를 따라 존재할 수도 있습니다. 공격자가 아키텍처 경로를 따라 메모리 안전 취약성을 악용하는 데 사용할 수 있는 동일한 기본 형식이 투기 경로에도 적용될 수 있습니다.

일반적으로 조건부 분기 잘못된 예측과 관련된 투기적 실행 쪽 채널은 조건식이 덜 신뢰할 수 있는 컨텍스트에 의해 제어되거나 영향을 받을 수 있는 데이터에 대해 작동할 때 발생할 수 있습니다. 예를 들어 , , whileswitch또는 3개 문에 iffor사용되는 조건식을 포함할 수 있습니다. 이러한 각 문에 대해 컴파일러는 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 루프의 for 본문을 잘못 예측하고 추측적으로 실행하여 buffer_size투기적 아웃오브바운드 로드를 초래합니다.

// 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에 신뢰할 수 없는 메시지 식별자가 제공됩니다. 보다 MAX_MESSAGE_ID작으면 untrusted_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 통해 전달됩니다. 배열의 pointers 요소 수(256개 요소)보다 작은 경우 untrusted_index 제공된 포인터 값 ptr 이 배열에 pointers 기록됩니다. 이 코드는 아키텍처적으로 안전하지만 CPU가 조건부 분기를 잘못 예측하는 경우 스택 할당 pointers 배열의 범위를 넘어 서술될 수 있습니다ptr. 이로 인해 .에 대한 반환 주소가 투기적으로 손상될 수 있습니다 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 일부로 보안 쿠키에 인접하게 배치되도록 지역 변수를 다시 정렬하는 등 스택 할당 간접 분기 대상을 추측적으로 수정하기 어렵게 만드는 단계를 이미 수행합니다.

투기적 형식 혼동

이 범주는 추측 형식 혼동을 야기할 수 있는 코딩 패턴을 다룹니다. 이는 투기적 실행 중에 비 아키텍처 경로를 따라 잘못된 형식을 사용하여 메모리에 액세스할 때 발생합니다. 조건부 분기 잘못된 예측과 투기적 저장소 바이패스 모두 잠재적으로 투기적 형식 혼동으로 이어질 수 있습니다.

투기적 저장소 바이패스의 경우 컴파일러가 여러 형식의 변수에 스택 위치를 다시 사용하는 시나리오에서 발생할 수 있습니다. 이는 형식 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 (필드가 같음type)을 사용하여 여러 번 실행 ProcessType 되도록 Type1할 수 있습니다. 이렇게 하면 첫 번째 if 문에 대한 조건부 분기를 학습하여 수행되지 않을 것으로 예측합니다. 그러면 공격 컨텍스트로 인해 피해자 컨텍스트가 형식CType2의 개체로 실행 ProcessType 될 수 있습니다. 이렇게 하면 첫 번째 if 문의 조건부 분기가 문 본문을 if 잘못 예측하고 실행하여 형식 개체를 캐스팅하는 CType1경우 추측 형식 CType2 혼동이 발생할 수 있습니다. 보다 CType1작기 때문에 CType2 메모리 액세스로 CType1::field2 인해 비밀일 수 있는 데이터의 범위를 벗어난 추측 로드가 발생합니다. 이 값은 앞에서 설명한 배열 바깥쪽 예제와 마찬가지로 관찰 가능한 부작용을 만들 수 있는 로드 shared_buffer 에 사용됩니다.

간접 분기로 이어지는 투기적 형식 혼동

이 코딩 패턴에는 투기적 형식 혼동으로 인해 투기적 실행 중에 안전하지 않은 간접 분기가 발생할 수 있는 경우가 포함됩니다. 이 예제에서 공격 컨텍스트는 피해자 컨텍스트가 형식 CType2 (필드가 같음type)을 사용하여 여러 번 실행 ProcessType 되도록 Type2할 수 있습니다. 이렇게 하면 첫 번째 if 문이 수행되고 문이 수행되지 않도록 조건부 분기를 else if 학습하는 효과가 있습니다. 그러면 공격 컨텍스트로 인해 피해자 컨텍스트가 형식CType1의 개체로 실행 ProcessType 될 수 있습니다. 이렇게 하면 첫 번째 if 문의 조건부 분기가 수행될 것으로 예측하고 else if 문이 수행되지 않을 것으로 예측하여 형식의 본문을 else if 실행하고 형식 개체를 캐스팅하는 CType2경우 추측 형식 혼동이 발생할 수 있습니다CType1. 필드가 CType2::dispatch_routine 배열CType1::field1char 겹치므로 의도하지 않은 분기 대상에 대한 투기적 간접 분기가 발생할 수 있습니다. 공격 컨텍스트가 배열의 바이트 값을 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];
}

간접 분기로 이어지는 초기화되지 않은 투기적 사용

초기화되지 않은 투기적 사용은 잠재적으로 공격자에 의해 분기 대상이 제어되는 간접 분기로 이어질 수 있습니다. 아래 routine 예제에서는 값modeDefaultMessageRoutine1DefaultMessageRoutine 따라 할당됩니다. 아키텍처 경로에서 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()
ARM 현재 사용할 수 없음 __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의 강력한 크기에 할당할 수 있는 경우 간단한 마스크를 도입할 수 있습니다. 아래 샘플에서는 2의 힘에 맞춰 정렬된 것으로 가정 buffer_size 합니다. 이렇게 하면 조건부 분기 잘못된 예측이 발생하고 untrusted_index 값이 untrusted_index 1보다 크거나 같은 값으로 전달되더라도 항상 보다 작buffer_size습니다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];
    }
}

메모리에서 중요한 정보 제거

투기적 실행 쪽 채널 취약성을 완화하는 데 사용할 수 있는 또 다른 기술은 메모리에서 중요한 정보를 제거하는 것입니다. 소프트웨어 개발자는 투기적 실행 중에 중요한 정보에 액세스할 수 없도록 애플리케이션을 리팩터링할 기회를 찾을 수 있습니다. 이 작업은 애플리케이션의 디자인을 리팩터링하여 중요한 정보를 별도의 프로세스로 격리하여 수행할 수 있습니다. 예를 들어 웹 브라우저 애플리케이션은 각 웹 원본과 연결된 데이터를 별도의 프로세스로 격리하여 한 프로세스가 투기적 실행을 통해 원본 간 데이터에 액세스할 수 없도록 할 수 있습니다.

참고 항목

투기적 실행 사이드 채널 취약성을 완화하기 위한 지침
투기적 실행 쪽 채널 하드웨어 취약성 완화