理論式執行側邊通道的 C + + 開發人員指導
本文包含開發人員的指引,可協助識別和緩解 C++ 軟體中的推測性執行端通道硬體弱點。 這些弱點可以揭露跨信任界限的敏感性資訊,而且可能會影響在支援推測性、順序錯亂執行指令的處理器上執行的軟體。 此類別的弱點是在 2018 年 1 月首次說明,您可以在 Microsoft 的安全性諮詢 中找到 額外的背景和指引。
本文提供的指引與下列所代表弱點類別有關:
CVE-2017-5753,也稱為 Spectre 變體 1。 此硬體弱點類別與因條件分支錯誤取代而發生的推測性執行而可能發生的側邊通道相關。 Visual Studio 2017 中的 Microsoft C++ 編譯器(從 15.5.5 版開始)包含參數的支援
/Qspectre
,可為與 CVE-2017-5753 相關的一組可能易受攻擊的編碼模式提供編譯時間風險降低。 此參數/Qspectre
也可在 Visual Studio 2015 Update 3 到 KB 4338871 中使用。 旗標的檔/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 可能會誤判條件式分支,並執行 if 語句的主體,即使 untrusted_index
大於或等於 buffer_size
。 因此,CPU 可能會推測性地讀取超出界限的 buffer
位元組(可能是秘密),然後使用該位元組值透過 計算後續負載 shared_buffer
的位址。
雖然 CPU 最終會偵測到此錯誤,但剩餘副作用可能會留在 CPU 快取中,以顯示從 buffer
讀取超出界限的位元組值相關資訊。 藉由探查 中每個快取行 shared_buffer
存取的速度,可以透過在系統上執行的較不具特殊許可權內容來偵測這些副作用。 若要達成此目的,可以採取的步驟如下:
使用 小於
buffer_size
的多次叫ReadByte
用untrusted_index
。 攻擊內容可能會導致犧牲者內容叫ReadByte
用 (例如透過 RPC),讓分支預測器定型為不視為untrusted_index
小於buffer_size
。清除 中的所有
shared_buffer
快取行。 攻擊內容必須清除 所參考shared_buffer
之記憶體共用區域中的所有快取行。 由於共用記憶體區域,這很簡單,而且可以使用 內建來完成,例如_mm_clflush
。使用
ReadByte
大於buffer_size
叫用untrusted_index
。 攻擊內容會導致犧牲者內容叫ReadByte
用,使其不正確地預測不會採取分支。 這會導致處理器以推測方式執行 if 區塊的untrusted_index
主體,且大於buffer_size
,因而導致讀取buffer
超出界限。 因此,shared_buffer
會使用可能讀取超出界限的秘密值來編制索引,因而導致 CPU 載入個別快取行。讀取 中的每個
shared_buffer
快取行,以查看哪一個快取行最快速 存取。 攻擊內容可以讀取 中的每個shared_buffer
快取行,並偵測快取行的載入速度遠高於其他快取行。 這是可能已由步驟 3 引進的快取行。 由於此範例中的位元組值與快取行之間有 1:1 關聯性,這可讓攻擊者推斷讀取超出界限的位元組實際值。
上述步驟提供使用稱為 FLUSH+RELOAD 的技術搭配利用 CVE-2017-5753 實例的範例。
哪些軟體案例可能會受到影響?
使用安全性開發週期 (SDL) 等 程式開發安全軟體,通常需要開發人員識別其應用程式中存在的信任界限。 信任界限存在於應用程式可能會與較不受信任內容所提供的資料互動的地方,例如系統上的另一個進程,或在核心模式裝置驅動程式的情況下,非系統管理使用者模式進程。 涉及推測性執行端通道的新弱點類別,與現有軟體安全性模型中的許多信任界限相關,可隔離裝置上的程式碼和資料。
下表提供軟體安全性模型的摘要,開發人員可能需要擔心這些弱點:
信任界限 | 描述 |
---|---|
虛擬機器界限 | 隔離不同虛擬機器中接收不受信任資料之不同虛擬機器工作負載的應用程式可能會面臨風險。 |
核心界限 | 從非系統管理使用者模式進程接收不受信任的資料的核心模式裝置驅動程式可能會面臨風險。 |
進程界限 | 從本機系統上執行的另一個進程接收不受信任的資料的應用程式,例如透過遠端程序呼叫(RPC)、共用記憶體或其他處理序間通訊 (IPC) 機制可能會面臨風險。 |
記憶體保護區界限 | 在安全記憶體保護區內執行的應用程式,例如 Intel SGX,從記憶體保護區外部接收不受信任的資料可能會面臨風險。 |
語言界限 | 解譯或 Just-In-Time (JIT) 的應用程式會編譯和執行以較高層級語言撰寫的不受信任的程式碼,可能會面臨風險。 |
暴露在上述任何信任界限的攻擊面的應用程式,應該檢閱受攻擊面上的程式碼,以識別並減輕可能的推測性執行端通道弱點實例。 請注意,暴露在遠端攻擊介面(例如遠端網路協定)的信任界限尚未證明有投機性執行端通道弱點的風險。
可能易受攻擊的編碼模式
推測性執行端通道弱點可能會因為多個編碼模式而產生。 本節描述可能易受攻擊的編碼模式,並提供每個模式的範例,但應該辨識這些主題上的變化可能存在。 因此,建議開發人員將這些模式視為範例,而不是所有可能易受攻擊編碼模式的詳盡清單。 目前存在於軟體中的相同記憶體安全性弱點類別也可能存在於推測性與順序錯亂的執行路徑上,包括但不限於緩衝區滿溢、超出界限的陣列存取、未初始化的記憶體使用、類型混淆等等。 攻擊者可用來利用架構路徑上的記憶體安全性弱點的相同基本類型,也可能套用至推測路徑。
一般而言,當條件運算式對可受到較不信任內容控制或影響的資料運作時,可能會引發與條件式分支誤判相關的推測性執行端通道。 例如,這可以包含 、、 for
、 while
switch
或 三元語句中使用的 if
條件運算式。 針對上述每個語句,編譯器可能會產生一個條件式分支,讓 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) {
// SPECULATION BARRIER
unsigned char value = buffer[untrusted_index];
return shared_buffer[value * 4096];
}
}
同樣地,陣列超出界限的負載可能會與因錯誤取代而超過其終止條件的迴圈一起發生。 在此範例中,與 x < buffer_size
運算式相關聯的條件式分支在大於或等於 buffer_size
時 x
,可能會誤判並推測性地執行迴圈主體 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 錯誤預測條件式分支,則當 DispatchTable
untrusted_message_id
其值大於或等於 MAX_MESSAGE_ID
時,可能會導致索引超出界限的存取。 這可能會導致衍生自超出陣列界限的分支目標位址進行推測性執行,這可能會導致資訊洩漏,視以推測方式執行的程式碼而定。
#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
時,與安全性 Cookie 相鄰。
推測類型混淆
此類別會處理可能會導致推測類型混淆的編碼模式。 當在推測執行期間,在非架構路徑上使用不正確的類型來存取記憶體時,就會發生這種情況。 條件式分支誤判和投機性存放區旁路都可能導致投機類型混淆。
針對推測性存放區略過,在編譯器針對多個類型的變數重複使用堆疊位置的情況下,可能會發生這種情況。 這是因為可能會略過 型 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;
}
}
導致超出範圍負載的推測類型混淆
此程式碼撰寫模式牽涉到推測型別混淆可能會導致超出界限或類型混淆的欄位存取,而載入的值會饋送後續載入位址。 這類似于陣列超出界限的程式碼撰寫模式,但它會透過替代編碼順序來表示,如上所示。 在此範例中,攻擊內容可能會導致受害者內容使用 類型的物件執行 ProcessType
多次( type
欄位等於 Type1
)。 CType1
這會對第一個 if
語句進行條件式分支定型,以預測未採取的效果。 接著,攻擊內容可能會讓受害者內容以 型 CType2
別 的物件執行 ProcessType
。 如果第一 if
個語句的條件分支錯誤並執行 語句的 if
主體,因而將 類型的 CType2
CType1
物件轉換成 ,這可能會導致推測型別混淆。 由於 CType2
小於 CType1
,因此 的記憶體存取 CType1::field2
會導致推測性超出界限的資料負載,而這些資料可能是秘密。 接著,這個值會用於負載 shared_buffer
中,從中建立可觀察的副作用,如同先前所述的陣列超出範圍範例一樣。
導致間接分支的推測類型混淆
此程式碼撰寫模式牽涉到推測類型混淆可能會導致推測性執行期間不安全的間接分支的情況。 在此範例中,攻擊內容可能會導致受害者內容使用 類型的物件執行 ProcessType
多次( type
欄位等於 Type2
)。 CType2
這會產生將條件式分支定型的效果,以便取得第一個 if
語句,而且 else if
不會採用 語句。 接著,攻擊內容可能會讓受害者內容以 型 CType1
別 的物件執行 ProcessType
。 如果第一個 if
語句的條件分支預測,而且語句預測未採用,因此執行 的主體 else if
,並將 else if
型別的物件轉換成 ,這可能會導致推測型 CType1
CType2
別混淆。 CType2::dispatch_routine
由於欄位與 char
陣列 CType1::field1
重迭,這可能會導致推測性間接分支到非預期的分支目標。 如果攻擊內容可以控制陣列中的 CType1::field1
位元組值,他們或許可以控制分支目標位址。
推測性未初始化的使用
此類別的編碼模式牽涉到推測執行可能會存取未初始化記憶體的案例,並用它來饋送後續的負載或間接分支。 若要利用這些程式碼撰寫模式,攻擊者必須能夠控制或有意義地影響所使用的記憶體內容,而不需由其所使用的內容初始化。
推測性未初始化的使用導致超出界限的負載
推測性未初始化的使用可能會導致使用攻擊者控制的值超出界限負載。 在下列範例中,會在所有架構路徑上指派 trusted_index
的值 index
,並 trusted_index
假設 小於或等於 buffer_size
。 不過,根據編譯器所產生的程式碼,可能會發生推測性存放區旁路,讓載入和 buffer[index]
相依運算式在指派 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
會根據 的值 mode
指派給 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() |
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 版開始)包含參數的支援 /Qspectre
,該參數會自動插入一組與 CVE-2017-5753 相關的有限潛在易受攻擊編碼模式的猜測屏障。 旗標的檔 /Qspectre
提供有關其效果和使用方式的詳細資訊。 請務必注意,此旗標並未涵蓋所有可能易受攻擊的程式碼撰寫模式,因此開發人員不應依賴它作為此類別弱點的完整風險降低。
遮罩陣列索引
在可能發生推測性超出界限負載的情況下,可以藉由新增邏輯來明確系結陣列索引,在架構和非架構路徑上強式系結陣列索引。 例如,如果陣列可以配置成對齊兩個乘冪的大小,則可以引進簡單的遮罩。 下面範例會說明這一點,假設該 buffer_size
範例會對齊兩個乘冪。 這可確保 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];
}
}
從記憶體中移除敏感性資訊
另一種可用來減輕推測性執行端通道弱點的技術是從記憶體中移除敏感性資訊。 軟體發展人員可以尋找重構其應用程式的機會,讓敏感性資訊無法在推測執行期間存取。 這可藉由重構應用程式的設計,將敏感性資訊隔離到不同的進程來完成。 例如,網頁瀏覽器應用程式可以嘗試將每個 Web 來源相關聯的資料隔離成不同的進程,從而防止一個進程能夠透過推測性執行存取跨原始資料。