av激情亚洲男人的天堂国语,日韩欧美精品一中文字幕,无码av一区二区三区无码,国产又色又爽又刺激的a片,国产又色又爽又刺激的a片

Linux內(nèi)核(x86)入口代碼模糊測(cè)試指南Part1

在本系列文章中,我們將為讀者分享關(guān)于內(nèi)核代碼模糊測(cè)試方面的見(jiàn)解。

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

簡(jiǎn)介

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

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

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

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

· entry_64.S:64位進(jìn)程的入口代碼。

· entry_64_compat.S:32位進(jìn)程的入口代碼。

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

memset()示例

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

在x86平臺(tái)上,memset()通常是由rep stos指令實(shí)現(xiàn)的,因?yàn)樵谶B續(xù)的字節(jié)范圍內(nèi)進(jìn)行寫操作方面,該指令已經(jīng)被CPU/微碼進(jìn)行了高度的優(yōu)化。從概念上講,這是一個(gè)硬件循環(huán),它重復(fù)(rep)一個(gè)存儲(chǔ)操作(stos)若干次;目標(biāo)地址由%RDI寄存器指定,迭代次數(shù)由%RCX寄存器給出。例如,您可以使用內(nèi)聯(lián)匯編實(shí)現(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 

對(duì)于上述內(nèi)聯(lián)匯編代碼來(lái)說(shuō),其作用就是告訴GCC:

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

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

3. 將變量value保存到%eax寄存器中(無(wú)論我們將其放入%rax、%eax、%ax還是%al寄存器中,這都是無(wú)關(guān)緊要的,因?yàn)閞ep stosb指令只使用與%al寄存器中的值相對(duì)應(yīng)的低位字節(jié));

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

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

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

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

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

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

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

fuzzer

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

· 標(biāo)志寄存器 (%rflags)

· 堆棧指針 (%rsp)

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

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

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

· int指令

· sysenter指令

· syscall指令

· INT3/INTO/INT1指令

· 被零除

· 調(diào)試異常

· 斷點(diǎn)異常

· 溢出異常

· 操作碼無(wú)效

· 一般保護(hù)故障

· 頁(yè)面錯(cuò)誤

· 浮點(diǎn)異常

· 外部硬件中斷

· 不可屏蔽中斷

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

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

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

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

高層架構(gòu)

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

下面,我們從一個(gè)基本的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; 

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

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

如果您仔細(xì)閱讀代碼,很可能會(huì)感到奇怪:為什么要使用MAP_32BIT標(biāo)志創(chuàng)建映射呢?這是因?yàn)槲覀兿M鹒uzzer在32位兼容模式下運(yùn)行時(shí)進(jìn)入內(nèi)核,所以,首先需要能在有效的32位地址下運(yùn)行。

進(jìn)行系統(tǒng)調(diào)用

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

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

· Anatomy of a system call, part 1

· Anatomy of a system call, part 2

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

 
 
 
 
  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 

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

我們可以使用gcc read.S命令來(lái)構(gòu)建上述代碼(假設(shè)上述匯編代碼保存在名為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 +++ 

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

 
 
 
 
  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 

要將這個(gè)字節(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()和方向標(biāo)志位

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

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

既然我們要寫一個(gè)fuzzer,那么,自然需要給這個(gè)標(biāo)志位隨機(jī)賦值。如果我們使用的編程語(yǔ)言是C++的話,可以通過(guò)如下所示的代碼來(lái)初始化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.     ... 

然后,我們可以在進(jìn)行系統(tǒng)調(diào)用之前,使用類似下面的方式來(lái)設(shè)置(或清除)該標(biāo)志位:

 
 
 
 
  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é)只是用于手工拼裝一個(gè)短測(cè)試程序,然后查看objdump的輸出結(jié)果。

注意:在子進(jìn)程中生成隨機(jī)數(shù)的時(shí)候,我們要格外小心;因?yàn)槲覀儾幌M械淖舆M(jìn)程都生成相同的數(shù)字!這就是為什么我們實(shí)際上在父進(jìn)程中生成代碼,并在子進(jìn)程中簡(jiǎn)單地執(zhí)行它們的原因。

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


分享文章:Linux內(nèi)核(x86)入口代碼模糊測(cè)試指南Part1
URL網(wǎng)址:http://uogjgqi.cn/article/dpehodo.html
掃二維碼與項(xiàng)目經(jīng)理溝通

我們?cè)谖⑿派?4小時(shí)期待你的聲音

解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流