自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Linux內(nèi)核(x86)入口代碼模糊測試指南Part 2 (上篇)

開發(fā)
在本文中,我們將為讀者更進一步介紹標志寄存器、堆棧指針、段寄存器、調(diào)試寄存器以及進入內(nèi)核的不同方法。

在本系列的第一篇文章中,我們介紹了Linux內(nèi)核入口代碼的作用,以及如何進行JIT匯編和調(diào)用系統(tǒng)調(diào)用。在本文中,我們將為讀者更進一步介紹標志寄存器、堆棧指針、段寄存器、調(diào)試寄存器以及進入內(nèi)核的不同方法。

    

更多標志(%rflags)

方向標志只是我們眾多感興趣的標志之一。維基百科上關于%rflags的文章列出了我們感興趣的其他一些標志:

· bit 8:陷阱標志(用于單步調(diào)試)

· bit 18:對齊檢查

大多數(shù)與算術相關的標志(進位標志等)并不是我們感興趣的對象,因為它們在普通代碼的正常運行過程中變化較大,這意味著內(nèi)核對這些標志的處理很可能已經(jīng)過了充分的測試。而另外一些標志(如中斷啟用標志)可能無法被用戶空間修改,所以即使嘗試也沒什么用。

我們需要重點關注陷阱標志,因為設置該標志后,CPU在每條指令后都會傳遞一個調(diào)試異常,自然也會干擾輸入代碼的正常運行。

對齊檢查標志也應當重點關注,因為當一個錯誤對齊的指針被解除引用時,它會使CPU傳遞一個對齊檢查異常。雖然CPU在0環(huán)中執(zhí)行時不應該執(zhí)行對齊檢查,但是檢查是否存在因為對齊檢查異常而進入內(nèi)核的相關漏洞還是很有意思的(我們稍后再談)。

維基百科的文章給出了修改這些標志的程序,但我們可以做得更好一點。

  1. 0:   9c                              pushfq 
  2. 1:   48 81 34 24 00 01 00 00         xorq   $0x100,(%rsp) 
  3. 9:   48 81 34 24 00 04 00 00         xorq   $0x400,(%rsp) 
  4. 11:   48 81 34 24 00 00 04 00         xorq   $0x40000,(%rsp) 
  5. 19:   9d                              popfq 

這段代碼將%rflags的內(nèi)容壓入堆棧上,然后直接修改堆棧上的標志值,再將該值彈出到%rflags中。實際上,我們在這里可以選擇使用orq或者xorq指令;我選擇xorq,因為它可以切換寄存器中的任何值。這樣一來,如果我們連續(xù)進行多次系統(tǒng)調(diào)用(或內(nèi)核入口),我們可以隨機切換標志,而不必關心現(xiàn)有的值是什么。

既然我們無論如何都要修改%rflags寄存器,那么我們不妨把方向標志的修改納入進去,把三個標志的修改合并到一條指令中。雖然這是一個很小的優(yōu)化,但沒有理由不這么做,最后的結(jié)果如下所示:

  1. // pushfq 
  2. *out++ = 0x9c; 
  3.   
  4. uint32_t mask = 0; 
  5.   
  6. // trap flag 
  7. mask |= std::uniform_int_distribution 
  8.   
  9. // direction flag 
  10. mask |= std::uniform_int_distribution 
  11.   
  12. // alignment check 
  13. mask |= std::uniform_int_distribution 
  14.   
  15. // xorq $mask, 0(%rsp) 
  16. *out++ = 0x48; 
  17. *out++ = 0x81; 
  18. *out++ = 0x34; 
  19. *out++ = 0x24; 
  20. *out++ = mask; 
  21. *out++ = mask >> 8; 
  22. *out++ = mask >> 16; 
  23. *out++ = mask >> 24; 
  24.   
  25. // popfq 
  26. *out++ = 0x9d; 

如果我們不希望進程在設置陷阱標志時立即被SIGTRAP殺死,我們需要注冊一個信號處理程序來有效地忽略這個信號(顯然使用SIG_IGN是不夠的):

  1. static void handle_child_sigtrap(int signum, siginfo_t *siginfo, void *ucontext) 
  2. {   
  3.     // this gets called when TF is set in %rflags; do nothing 
  4. }   
  5.      
  6. ... 
  7.      
  8. struct sigaction sigtrap_act = {}; 
  9. sigtrap_act.sa_sigaction = &handle_child_sigtrap; 
  10. sigtrap_act.sa_flags = SA_SIGINFO | SA_ONSTACK; 
  11. if (sigaction(SIGTRAP, &sigtrap_act, NULL) == -1) 
  12.     error(EXIT_FAILURE, errno, "sigaction(SIGTRAP)"); 

關于上面的SA_ONSTACK標志,我們將在下一節(jié)討論。

堆棧指針(%rsp)

在修改%rflags之后,我們其實就不需使用堆棧了,這意味著我們可以在不影響程序執(zhí)行的情況下,自由地更改棧指針。不過我們?yōu)槭裁匆薷臈V羔樐兀績?nèi)核又不會用我們的用戶空間棧來做任何事情,對吧?事實上,它可能會。

像ftrace和perf這樣的調(diào)試工具偶爾會在系統(tǒng)調(diào)用跟蹤期間取消對用戶空間堆棧的引用。事實上,我在這方面至少發(fā)現(xiàn)了兩個不同的漏洞:

· report 1 (July 16, 2019),

· report 2 (May 10, 2020).

當向用戶空間傳遞信號時,信號處理程序的堆棧幀由內(nèi)核創(chuàng)建,通常位于被中斷線程的當前堆棧指針的上方。

如果由于某些錯誤,%rsp會被內(nèi)核直接訪問,那么在正常操作期間可能不會被注意到,因為堆棧指針通??偸侵赶蛞粋€有效地址。要捕捉這種漏洞,我們可以簡單地將其指向一個非映射地址(甚至是內(nèi)核地址?。?。

為了幫助我們測試堆棧指針的各種可能感興趣的值,我們可以定義一個helper:

  1. static void *page_not_present; 
  2. static void *page_not_writable; 
  3. static void *page_not_executable; 
  4.      
  5. static uint64_t get_random_address() 
  6. {       
  7.     // very occasionally hand out a non-canonical address 
  8.     if (std::uniform_int_distribution 
  9.         return 1UL << 63; 
  10.      
  11.     uint64_t value = 0; 
  12.   
  13.     switch (std::uniform_int_distribution 
  14.     case 0: 
  15.         break; 
  16.     case 1: 
  17.         value = (uint64_t) page_not_present; 
  18.         break; 
  19.     case 2: 
  20.         value = (uint64_t) page_not_writable; 
  21.         break; 
  22.     case 3: 
  23.         value = (uint64_t) page_not_executable; 
  24.         break; 
  25.     case 4: 
  26.         static const uint64_t kernel_pointers[] = { 
  27.             0xffffffff81000000UL, 
  28.             0xffffffff82016000UL, 
  29.             0xffffffffc0002000UL, 
  30.             0xffffffffc2000000UL, 
  31.         }; 
  32.   
  33.         value = kernel_pointers[std::uniform_int_distribution 
  34.   
  35.         // random ~2MiB offset 
  36.         value += PAGE_SIZE * std::uniform_int_distribution 
  37.         break; 
  38.     } 
  39.   
  40.     // occasionally intentionally misalign it 
  41.     if (std::uniform_int_distribution 
  42.         value += std::uniform_int_distribution 
  43.   
  44.     return value; 
  45.   
  46. int main(...) 
  47.     page_not_present = mmap(NULL, PAGE_SIZE, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT, -1, 0); 
  48.     page_not_writable = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT, -1, 0); 
  49.     page_not_executable = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT, -1, 0); 
  50.     ... 

在這里,我使用了自己機器上的/proc/kallsyms中找到的一些內(nèi)核指針。它們不一定是很好的選擇,只是用于演示。正如我前面所提到的,我們需要找到一個平衡點,既要選擇那些瘋狂到?jīng)]有人想過要處理它們的值(我們畢竟在這里試圖尋找的是邊緣案例),又要不迷失在巨大的非目標值的海洋中;我們可以統(tǒng)一選擇隨機的64位值,但這很難帶來任何有效的指針(其中大部分可能是非規(guī)范的地址)。模糊測試的部分藝術是通過對哪些有可能和哪些不可能的關系進行有根據(jù)的猜測來抽出相關的邊緣案例。

現(xiàn)在只是設置值的問題,幸運的是,我們可以直接將64位的值加載到%rsp中:

  1. movq $0x12345678aabbccdd, %rsp 

可以使用下列代碼:

  1. uint64_t rsp = get_random_address(); 
  2.   
  3. // movq $imm, %rsp 
  4. *out++ = 0x48; 
  5. *out++ = 0xbc; 
  6. for (int i = 0; i < 8; ++i) 
  7.     *out++ = rsp >> (8 * i); 

但是,對于上面提到的%rflags來說,有一點需要引起我們的高度注意:一旦我們在%rflags中啟用了單步標志,CPU就會在隨后執(zhí)行的每條指令中傳遞一個調(diào)試異常。內(nèi)核將通過向進程傳遞一個SIGTRAP信號來處理調(diào)試異常。默認情況下,這個信號是通過堆棧傳遞的,而堆棧上的值就是%rsp的值……如果%rsp無效,內(nèi)核會用一個不可觸發(fā)的SIGSEGV來殺死進程。

為了處理這樣的情況,內(nèi)核提供了一個函數(shù),以便在傳遞信號時將%rsp設置為一個已知的有效值:sigaltstack()。我們要做的就是像下面這樣來調(diào)用它:

  1. stack_t ss = {}; 
  2.   
  3. ss.ss_sp = malloc(SIGSTKSZ); 
  4. if (!ss.ss_sp) 
  5.     error(EXIT_FAILURE, errno, "malloc()"); 
  6.   
  7. ss.ss_size = SIGSTKSZ; 
  8. ss.ss_flags = 0; 
  9. if (sigaltstack(&ss, NULL) == -1) 
  10.     error(EXIT_FAILURE, errno, "sigaltstack()"); 

然后,將SA_ONSTACK傳遞給處理SIGTRAP的sigaction()調(diào)用的sa_flags變量中。

段寄存器

說到段寄存器,你會經(jīng)??吹竭@樣的說法:其實在64位上已經(jīng)不太有用了。然而,這并不是全部的事實。的確,你不能改變基地址或段大小,但幾乎所有其他的東西都還是相關的。特別是一些與我們相關的東西,例如:

· %cs、%ds、%es和%ss必須含有有效的16位段選擇器,指向GDT(全局描述符表)或LDT(局部描述符表)中的有效條目。

· %cs不能使用mov指令加載,但我們可以使用ljmp(遠/長跳轉(zhuǎn))指令。

· %cs的CPL(當前權限級別)字段是CPU正在執(zhí)行的權限級別。通常情況下,64位用戶空間進程運行的%cs為0x33,即GDT的索引6,特權級別為3,內(nèi)核運行的%cs為0x10,即GDT的索引2,特權級別為0(因此稱為ring 0)。

· 實際上我們可以使用modify_ldt()系統(tǒng)調(diào)用在LDT中安裝條目,但要注意的是,內(nèi)核會對條目進行消毒,所以我們不能創(chuàng)建一個指向DPL 0的段的調(diào)用門。

· %fs和%gs的基地址是由MSRs指定的。這些寄存器通常分別用于用戶空間進程和內(nèi)核的TLS(線程本地存儲)和per-CPU數(shù)據(jù)。我們可以使用arch_prctl()系統(tǒng)調(diào)用來改變這些寄存器的值。在某些CPU/內(nèi)核上,我們可以使用wrfsbase和wrgsbase指令。

· 使用mov或pop指令設置%ss會使CPU在mov或pop指令之后的一條指令中屏蔽中斷、NMI、斷點和單步陷阱。如果下一條指令導致進入內(nèi)核,這些中斷、NMI、斷點或單步陷阱將在CPU開始在內(nèi)核空間執(zhí)行后生效。這就是CVE-2018-8897的來源,內(nèi)核沒有正確處理這種情況。

LDT

由于我們可能會從LDT中加載段寄存器,所以不妨從設置LDT開始入手。由于modify_ldt()沒有glibc封裝器,所以我們必須使用syscall()函數(shù)來調(diào)用它:

  1. #include 
  2. #include 
  3. #include 
  4. #include 
  5.   
  6. for (unsigned int i = 0; i < 4; ++i) { 
  7.     struct user_desc desc = {}; 
  8.     desc.entry_number = i; 
  9.     desc.base_addr = std::uniform_int_distribution 
  10.     desc.limit = std::uniform_int_distribution 
  11.     desc.seg_32bit = std::uniform_int_distribution 
  12.     desc.contents = std::uniform_int_distribution 
  13.     desc.read_exec_only = std::uniform_int_distribution 
  14.     desc.limit_in_pages = std::uniform_int_distribution 
  15.     desc.seg_not_present = std::uniform_int_distribution 
  16.     desc.useable = std::uniform_int_distribution 
  17.   
  18.     syscall(SYS_modify_ldt, 1, &desc, sizeof(desc)); 

我們可能要檢查這里的返回值;我們不應該生成無效的LDT條目,所以知道我們是否存在這種條目是很有用的。

  1. static uint16_t get_random_segment_selector() 
  2.     unsigned int index
  3.   
  4.     switch (std::uniform_int_distribution 
  5.     case 0: 
  6.         // The LDT is small, so favour smaller indices 
  7.         index = std::uniform_int_distribution 
  8.         break; 
  9.     case 1: 
  10.         // Linux defines 32 GDT entries by default 
  11.         index = std::uniform_int_distribution 
  12.         break; 
  13.     case 2: 
  14.         // Max table size 
  15.         index = std::uniform_int_distribution 
  16.         break; 
  17.     } 
  18.     unsigned int ti = std::uniform_int_distribution 
  19.     unsigned int rpl = std::uniform_int_distribution 
  20.   
  21.     return (index << 3) | (ti << 2) | rpl; 

數(shù)據(jù)段(%ds)

下面展示如何使用數(shù)據(jù)段:

  1. if (std::uniform_int_distribution 
  2.     uint16_t sel = get_random_segment_selector(); 
  3.   
  4.     // movw $imm, %ax 
  5.     *out++ = 0x66; 
  6.     *out++ = 0xb8; 
  7.     *out++ = sel; 
  8.     *out++ = sel >> 8; 
  9.   
  10.     // movw %ax, %ds 
  11.     *out++ = 0x8e; 
  12.     *out++ = 0xd8; 

%fs與 %gs

對于%fs和%gs,我們需要使用系統(tǒng)調(diào)用arch_prctl()。在普通(非JIT匯編)代碼中,可以這樣使用:

  1. #include 
  2. #include 
  3.   
  4. ... 
  5.   
  6. syscall(SYS_arch_prctl, ARCH_SET_FS, get_random_address()); 
  7. syscall(SYS_arch_prctl, ARCH_SET_GS, get_random_address()); 

不幸的是,這樣做很有可能導致glibc/libstdc++在任何使用線程本地存儲的代碼上崩潰(甚至在第二次get_random_address()調(diào)用時就可能發(fā)生)。如果我們想生成系統(tǒng)調(diào)用來做這件事,我們可以通過支持代碼進行協(xié)助:

  1. enum machine_register { 
  2.     // 0 
  3.     RAX, 
  4.     RCX, 
  5.     RDX, 
  6.     RBX, 
  7.     RSP, 
  8.     RBP, 
  9.     RSI, 
  10.     RDI, 
  11.     // 8 
  12.     R8, 
  13.     R9, 
  14.     R10, 
  15.     R11, 
  16.     R12, 
  17.     R13, 
  18.     R14, 
  19.     R15, 
  20. }; 
  21.   
  22. const unsigned int REX = 0x40; 
  23. const unsigned int REX_B = 0x01; 
  24. const unsigned int REX_W = 0x08; 
  25.   
  26. static uint8_t *emit_mov_imm64_reg(uint8_t *out, uint64_t imm, machine_register reg) 
  27.     *out++ = REX | REX_W | (REX_B * (reg >= 8)); 
  28.     *out++ = 0xb8 | (reg & 7); 
  29.     for (int i = 0; i < 8; ++i) 
  30.         *out++ = imm >> (8 * i); 
  31.   
  32.     return out
  33.   
  34. static uint8_t *emit_call_arch_prctl(uint8_t *outint code, unsigned long addr) 
  35.     // int arch_prctl(int code, unsigned long addr); 
  36.     out = emit_mov_imm64_reg(out, SYS_arch_prctl, RAX); 
  37.     out = emit_mov_imm64_reg(out, code, RDI); 
  38.     out = emit_mov_imm64_reg(out, addr, RSI); 
  39.   
  40.     // syscall 
  41.     *out++ = 0x0f; 
  42.     *out++ = 0x05; 
  43.   
  44.     return out

需要注意的是,除了需要一些寄存器來執(zhí)行系統(tǒng)調(diào)用本身之外,syscall指令還用返回地址(即syscall指令后的指令地址)覆蓋%rcx,所以我們可能要在做其他事情之前進行這些調(diào)用。

小結(jié)

在本文中,我們?yōu)樽x者更進一步介紹了各種標志寄存器、堆棧指針以及部分段寄存器,在下一篇文章中,我們將為讀者介紹調(diào)試寄存器以及進入內(nèi)核的不同方法。

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

 

責任編輯:姜華 來源: 嘶吼網(wǎng)
相關推薦

2020-10-12 10:22:16

Linux內(nèi)核

2020-10-13 10:51:10

Linux內(nèi)核

2020-09-23 12:42:08

Linux

2021-08-20 11:12:31

NFVX86架構(gòu)地址

2011-04-19 09:17:36

2019-07-15 13:11:57

Power

2012-04-28 09:07:48

甲骨文x86

2011-12-01 11:09:48

AMDx86服務器英特爾

2020-09-15 06:15:23

滲透測試風險評估網(wǎng)絡安全

2021-09-14 10:07:09

英特爾初始代碼用戶中斷

2021-06-07 15:20:22

Linux X861MB內(nèi)存BIOS

2011-11-10 09:26:48

Solaris 11

2009-08-28 14:38:33

2011-12-19 10:55:58

云計算中國電信

2011-02-20 22:23:43

X86虛擬化XenServer

2010-04-06 14:20:33

數(shù)據(jù)庫服務器

2011-04-25 14:51:59

Linux任務切換TSS

2017-03-30 07:56:30

測試前端代碼

2021-06-21 09:54:24

Windows 11CPU操作系統(tǒng)

2010-03-20 11:03:13

VMControl虛擬化管理
點贊
收藏

51CTO技術棧公眾號