日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關咨詢
選擇下列產品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關閉右側工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
Linux內核(x86)入口代碼模糊測試指南Part1

在本系列文章中,我們將為讀者分享關于內核代碼模糊測試方面的見解。

成都網(wǎng)站建設哪家好,找創(chuàng)新互聯(lián)建站!專注于網(wǎng)頁設計、成都網(wǎng)站建設、微信開發(fā)、成都小程序開發(fā)、集團成都企業(yè)網(wǎng)站定制等服務項目。核心團隊均擁有互聯(lián)網(wǎng)行業(yè)多年經驗,服務眾多知名企業(yè)客戶;涵蓋的客戶類型包括:廣告設計等眾多領域,積累了大量豐富的經驗,同時也獲得了客戶的一致贊賞!

簡介

對于長期關注Linux內核開發(fā)或系統(tǒng)調用模糊測試的讀者來說,很可能早就對trinity(地址:https://lwn.net/Articles/536173/)和syzkaller(地址:https://lwn.net/Articles/677764/)并不陌生了。近年來,安全研究人員已經利用這兩個工具發(fā)現(xiàn)了許多內核漏洞。實際上,它們的工作原理非常簡單:向內核隨機拋出一些系統(tǒng)調用,以期某些調用會導致內核崩潰,或觸發(fā)內核代碼中可檢測的漏洞(例如緩沖區(qū)溢出漏洞)。

盡管這些Fuzzer能夠對系統(tǒng)調用自身(以及通過系統(tǒng)調用可訪問的代碼)進行有效的模糊測試;但是,對于在用戶空間和內核之間的邊界上發(fā)生的事情,這兩款工具卻鞭長莫及。實際上,這個邊界處發(fā)生的事情比我們想象的更為復雜:這里的代碼是用匯編語言編寫的,在內核可以安全地開始執(zhí)行其C代碼之前,必須對各種體系結構狀態(tài)(CPU狀態(tài))進行安全檢查,或者說是“消毒”。

本文將同讀者一起,探索如何為x86平臺上的Linux內核入口代碼編寫Fuzzer工具。

在繼續(xù)之前,不妨先簡單了解一下64位內核涉及的主要兩個文件:

· entry_64.S:64位進程的入口代碼。

· entry_64_compat.S:32位進程的入口代碼。

總的來說,入口代碼大約有1700行匯編代碼(其中包括注釋),所以,閱讀這些代碼的工作量并不算小,同時,這也只是整個內核代碼中很小的一部分。

memset()示例

首先,我想給出一個從用戶空間進入內核時,內核需要進行驗證的CPU狀態(tài)的具體例子。

在x86平臺上,memset()通常是由rep stos指令實現(xiàn)的,因為在連續(xù)的字節(jié)范圍內進行寫操作方面,該指令已經被CPU/微碼進行了高度的優(yōu)化。從概念上講,這是一個硬件循環(huán),它重復(rep)一個存儲操作(stos)若干次;目標地址由%RDI寄存器指定,迭代次數(shù)由%RCX寄存器給出。例如,您可以使用內聯(lián)匯編實現(xiàn)memset(),具體如下所示:

 
 
 
 
  1. static inline void memset(void *dest, int value, size_t count) 
  2.     asm volatile ("rep stosb"       // 4 
  3.         : "+D" (dest), "+c" (count) // 1, 2 
  4.         : "a" (value)               // 3 
  5.         : "cc", "memory");          // 5 

對于上述內聯(lián)匯編代碼來說,其作用就是告訴GCC:

1. 將變量dest保存到%rdi寄存器中(+表示該值可能會被內聯(lián)匯編代碼所修改);

2. 將變量count保存到%rcx寄存器中;

3. 將變量value保存到%eax寄存器中(無論我們將其放入%rax、%eax、%ax還是%al寄存器中,這都是無關緊要的,因為rep stosb指令只使用與%al寄存器中的值相對應的低位字節(jié));

4. 將rep stosb指令插入到匯編代碼中;

5. 重載任何可能依賴于條件碼(“cc”,即x86平臺上的%rflags寄存器)或內存的值。

作為參考,你也可以考察一下memset()在x86平臺上的主流實現(xiàn)代碼。

重要的是,在%rflags寄存器中含有一個很少使用的位,叫做DF位(即方向標志位)。這個標志位決定了每寫入一個字節(jié)后,rep stos會令%rdi的值遞增或遞減。當DF位被設為0時,受影響的內存范圍是從%rdi到(%rdi + %rcx);而當DF位被設為1時,受影響的內存范圍是從(%rdi - %rcx)到%rdi!由于它對memset()的最終結果有重大的影響,所以,我們最好確保DF位總是被設為0。

實際上,按照x86_64 SysV ABI的要求,在進入函數(shù)以及從函數(shù)返回時,DF位必須始終為0(具體見第15頁):

“必須在進入函數(shù)以及從函數(shù)返回時清除%rFLAGS寄存器中的方向標志DF(將方向設置為“forward”)。其他用戶標志在標準調用序列中沒有指定的角色,并且在不同的調用中不予保留?!?/p>

實際上,這是內核在內部高度依賴的一種約定;如果在調用memset()時以某種方式將DF標志設置為1,它將錯誤地覆蓋某些內存。因此,內核進入代碼的任務之一,就是確保在進入任何內核C代碼之前,DF標志始終為0。我們可以用一條指令cld(即清除方向標志指令)來實現(xiàn)這一點,內核的許多入口路徑就是這么做的,具體請參考paranoid_entry()或error_entry()的實現(xiàn)代碼。

fuzzer

如您所見,哪怕是CPU狀態(tài)的一個標志位,都對內核有著巨大的影響。接下來,我們將枚舉入口代碼需要處理的所有CPU狀態(tài)變量:

· 標志寄存器 (%rflags)

· 堆棧指針 (%rsp)

· 段寄存器 (%cs, %fs, %gs)

· 調試寄存器 (%dr0到%dr3, %dr7)

到目前為止,我們一直回避的問題是,從用戶空間進入內核有許多不同的方式,而不僅僅是系統(tǒng)調用(也不僅僅是系統(tǒng)調用的一種機制)。這些方式包括:

· int指令

· sysenter指令

· syscall指令

· INT3/INTO/INT1指令

· 被零除

· 調試異常

· 斷點異常

· 溢出異常

· 操作碼無效

· 一般保護故障

· 頁面錯誤

· 浮點異常

· 外部硬件中斷

· 不可屏蔽中斷

Fuzzer的目標應該是測試CPU狀態(tài)和用戶空間/內核轉換的所有可能組合。在理想的情況下,我們會進行窮舉搜索,但是如果您考慮寄存器值和入口方法的所有可能組合,搜索空間就太大了。因此,我們將通過兩個主要的策略來提高我們發(fā)現(xiàn)bug的機會。

1. 關注那些我們懷疑更有可能導致有趣/不尋常事情發(fā)生的值/案例。為此,需要查看x86文檔(維基百科、英特爾手冊等)以及入口代碼本身。例如,入口代碼記錄了幾個處理器勘誤表案例,我們可以直接使用它們來確定已知的邊緣案例。

2. 壓縮我們認為沒有影響的那些類型的值。例如,在挑選要加載到寄存器的隨機值時,重要的是要嘗試不同類型的指針(例如,內核空間、用戶空間、非規(guī)范、映射、非映射等類型的指針),而不是嘗試所有可能的值。

值得一提的是,內核已經為x86代碼提供了一個優(yōu)秀回歸測試套件,它位于tools/testing/selftests/x86/目錄下,主要開發(fā)者為Andy Lutomirski。它提供了進入/離開內核的各種方法的測試用例,我們可以從中汲取靈感。

高層架構

我們這里要開發(fā)的fuzzer,實際上是一個供內核運行的用戶空間程序,用以完成相應的模糊測試工作。由于我們需要非常精確地控制一些用于觸發(fā)向內核過渡的指令,所以,我們實際上不會直接用C語言來編寫這些代碼;相反,我們將在運行時動態(tài)地生成x86機器代碼,然后執(zhí)行它。為了簡單起見,也為了避免在設置好所需的CPU狀態(tài)后恢復到一個干凈的狀態(tài)(如果可以的話),我們將在一個子進程中執(zhí)行生成的機器代碼,并且能夠在進入內核后將其丟棄。

下面,我們從一個基本的fork循環(huán)開始入手。

 
 
 
 
  1. #include 
  2. #include 
  3.   
  4. #include 
  5. #include 
  6. #include 
  7. #include 
  8. #include 
  9. #include 
  10.   
  11. static void *mem; 
  12.   
  13. static void emit_code(); 
  14.   
  15. typedef void (*generated_code_fn)(void); 
  16.   
  17. int main(int argc, char *argv[]) 
  18.     mem = mmap(NULL, PAGE_SIZE, 
  19.         // prot 
  20.         PROT_READ | PROT_WRITE | PROT_EXEC, 
  21.         // flags 
  22.         MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT, 
  23.         // fd, offset 
  24.         -1, 0); 
  25.     if (mem == MAP_FAILED) 
  26.         error(EXIT_FAILURE, errno, "mmap()"); 
  27.   
  28.     while (1) { 
  29.         emit_code(); 
  30.   
  31.         pid_t child = fork(); 
  32.         if (child == -1) 
  33.             error(EXIT_FAILURE, errno, "fork()"); 
  34.   
  35.         if (child == 0) { 
  36.             // we're the child; call our newly generated function 
  37.             ((generated_code_fn) mem)(); 
  38.             exit(EXIT_SUCCESS); 
  39.         } 
  40.   
  41.         // we're the parent; wait for the child to exit 
  42.         while (1) { 
  43.             int status; 
  44.             if (waitpid(child, &status, 0) == -1) { 
  45.                 if (errno == EINTR) 
  46.                     continue; 
  47.   
  48.                 error(EXIT_FAILURE, errno, "waitpid()"); 
  49.             } 
  50.   
  51.             break; 
  52.         } 
  53.     } 
  54.   
  55.     return 0; 

然后,我們還將實現(xiàn)一個非常簡單的emit_code(),到目前為止,只創(chuàng)建了一個包含單個retq指令的函數(shù):

 
 
 
 
  1. static void emit_code() 
  2.     uint8_t *out = (uint8_t *) mem; 
  3.   
  4.     // retq 
  5.     *out++ = 0xc3; 

如果您仔細閱讀代碼,很可能會感到奇怪:為什么要使用MAP_32BIT標志創(chuàng)建映射呢?這是因為我們希望fuzzer在32位兼容模式下運行時進入內核,所以,首先需要能在有效的32位地址下運行。

進行系統(tǒng)調用

在x86平臺上,系統(tǒng)調用的歷史有點混亂。首先,存在這樣一個事實,即系統(tǒng)調用最初是在32位系統(tǒng)上發(fā)展起來的,當時使用的是相對較慢的int指令。后來,英特爾和AMD公司分別開發(fā)了自己的快速系統(tǒng)調用機制(分別使用全新且互不兼容的sysenter和syscall指令)。更糟的是,64位系統(tǒng)需要同時處理32位進程(使用任何32位系統(tǒng)調用機制)、64位進程以及(可能的)第三種操作模式即x32,其中代碼像像通常那樣是64位的(并且可以訪問64位寄存器),然而,指針卻是32位的——之所以這么做,據(jù)說是為了節(jié)省內存。由于它們在進入內核模式時保存/修改的CPU狀態(tài)各不相同,因此,這些不同的系統(tǒng)調用機制中的大多數(shù)在內核的入口碼中采用的路徑也是各不相同的。這也是入口代碼很難理解的原因之一!

有關在x86上進行系統(tǒng)調用的更深入的介紹,可以參閱LWN網(wǎng)站上的優(yōu)秀文章,比如:

· Anatomy of a system call, part 1

· Anatomy of a system call, part 2

熟悉系統(tǒng)調用的一個好方法是,親自動手通過GNU匯編器來制作匯編代碼片段的原型,然后供fuzzer使用。例如,像下面那樣,對內核執(zhí)行一次read(STDIN_FILENO, NULL, 0)調用:

 
 
 
 
  1.      .text 
  2.         .global main 
  3. main: 
  4.         movl $0, %eax # SYS_read/__NR_read 
  5.         movl $0, %edi # fd = STDIN_FILENO 
  6.         movl $0, %esi # buf = NULL 
  7.         movl $0, %edx # count = 0 
  8.         syscall 
  9.   
  10.         movl $0, %eax 
  11.         retq 

從這段代碼中可以看到,當使用syscall指令時,系統(tǒng)調用號本身通過%rax寄存器傳遞,而參數(shù)則通過%rdi、%rsi、%rdx等寄存器進行傳遞。據(jù)我所知,Linux x86 SysCall ABI在入口代碼本身的entry_syscall_64()中是有“正式”記錄的(我們在這里使用的是%eXX寄存器,而不是%rXX寄存器,因為這里的機器代碼比較短;將%eXX設置為0時,將清除%rXX的高32位)。

我們可以使用gcc read.S命令來構建上述代碼(假設上述匯編代碼保存在名為read.S的文件中),并可以使用strace檢查它是否正確:

 
 
 
 
  1. $ strace ./a.out 
  2. execve("./a.out", ["./a.out"], [/* 53 vars */]) = 0 
  3. [...] 
  4. read(0, NULL, 0)                        = 0 
  5. exit_group(0)                           = ? 
  6. +++ exited with 0 +++ 

要獲得匯編后機器代碼的字節(jié)內容,我們可以先使用gcc-c read.s進行編譯,然后使用objdump -d read.o獲取相應的內容:

 
 
 
 
  1. 0000000000000000 
  2.    0:   b8 00 00 00 00          mov    $0x0,%eax 
  3.    5:   bf 00 00 00 00          mov    $0x0,%edi 
  4.    a:   be 00 00 00 00          mov    $0x0,%esi 
  5.    f:   ba 00 00 00 00          mov    $0x0,%edx 
  6.   14:   0f 05                   syscall 
  7.   16:   b8 00 00 00 00          mov    $0x0,%eax 
  8.   1b:   c3                      retq 

要將這個字節(jié)序列添加到我們的JIT匯編函數(shù)中,我們可以使用下列代碼:

 
 
 
 
  1. // mov $0, %eax 
  2. *out++ = 0xb8; 
  3. *out++ = 0x00; 
  4. *out++ = 0x00; 
  5. *out++ = 0x00; 
  6. *out++ = 0x00; 
  7.   
  8. [...] 
  9.   
  10. // syscall 
  11. *out++ = 0x0f; 
  12. *out++ = 0x05; 

重新回到memset()和方向標志位

現(xiàn)在,對于上面的memset()示例來說,編寫測試所需的大部分代碼都已經準備就緒了。為了設置df位,我們可以在進行系統(tǒng)調用之前執(zhí)行std指令(該指令用于設置方向標志):

 
 
 
 
  1. // std 
  2. *out++ = 0xfd; 

既然我們要寫一個fuzzer,那么,自然需要給這個標志位隨機賦值。如果我們使用的編程語言是C++的話,可以通過如下所示的代碼來初始化PRNG:

 
 
 
 
  1. #include 
  2.   
  3. static std::default_random_engine rnd; 
  4.   
  5. int main(...) 
  6.     std::random_device rdev; 
  7.     rnd = std::default_random_engine(rdev()); 
  8.   
  9.     ... 

然后,我們可以在進行系統(tǒng)調用之前,使用類似下面的方式來設置(或清除)該標志位:

 
 
 
 
  1. switch (std::uniform_int_distribution 
  2. case 0: 
  3.     // cld 
  4.     *out++ = 0xfc; 
  5.     break; 
  6. case 1: 
  7.     // std 
  8.     *out++ = 0xfd; 
  9.     break; 

同樣,這些字節(jié)只是用于手工拼裝一個短測試程序,然后查看objdump的輸出結果。

注意:在子進程中生成隨機數(shù)的時候,我們要格外小心;因為我們不希望所有的子進程都生成相同的數(shù)字!這就是為什么我們實際上在父進程中生成代碼,并在子進程中簡單地執(zhí)行它們的原因。

本文翻譯自:https://blogs.oracle.com/linux/fuzzing-the-linux-kernel-x86-entry-code%2C-part-1-of-3如若轉載,請注明原文地址:


網(wǎng)站題目:Linux內核(x86)入口代碼模糊測試指南Part1
文章來源:http://www.dlmjj.cn/article/dpehodo.html