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

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

開發(fā) 前端
我們將為讀者介紹調(diào)試寄存器以及進入內(nèi)核的不同方法。

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

[[345622]]

堆棧段(%ss)

寄存器%ss應該是我們在進入內(nèi)核的指令之前設(shè)置的最后一個寄存器,這樣我們就可以確??吹饺魏窝舆t陷阱或異常的影響。我們可以使用與上面%ds相同的代碼;我們不使用popw %ss的原因是,我們可能已經(jīng)將%rsp設(shè)置為指向一個“奇怪”的位置,所以此時堆??赡軣o法使用。

32位兼容模式(%cs)

有趣的是:你實際上可以在執(zhí)行過程中把你的64位進程改變成32位進程,甚至不需要告訴內(nèi)核。CPU包含了一種機制,在ring 3模式下是允許的:遠跳轉(zhuǎn)指令

特別是,我們要使用的指令是“絕對間接遠跳轉(zhuǎn)指令,地址由m16:32給出”。由于要弄清楚具體的語法和字節(jié)可能有點麻煩,所以下面將借助于一個完整的匯編例子進行解釋。

  1.   .global main 
  2. main: 
  3.     ljmpl *target 
  4.   
  5. 1: 
  6.     .code32 
  7.     movl $1, %eax # __NR_exit == 1 from asm/unistd_32.h 
  8.     movl $2, %ebx # status == 0 
  9.     sysenter 
  10.     ret 
  11.   
  12.     .data 
  13. target: 
  14.     .long 1b # address (32 bits) 
  15.     .word 0x23 # segment selector (16 bits) 

這里,ljmpl指令使用target標簽處的內(nèi)存,該標簽是一個32位指令指針,后跟一個16位段選擇器(這里指向用戶空間的32位代碼段0x23)。這里的目標地址1b不是十六進制值,它實際上是對標簽1的引用;b代表“向后”。這個標簽處的代碼是32位的,這就是為什么我們使用sysenter,而不是以前使用的syscall。調(diào)用約定也不同,實際上,我們需要使用32位ABI中的系統(tǒng)調(diào)用號(SYS_exit在64位系統(tǒng)上是60,但這里是1)。另一個有趣的事情是,如果你嘗試在strace下運行這段代碼,將會看到如下所示的結(jié)果:

  1. [...] 
  2. write(1, "\366\242[\204\374\177\0\0\0\0\0\0\0\0\0\0\376\242[\204\374\177\0\0\t\243[\204\374\177\0\0"..., 140722529079224 
  3. +++ exited with 0 +++ 

strace顯然認為我們?nèi)匀皇且粋€64位進程,并認為我們調(diào)用了write(),而實際上我們是在調(diào)用exit()(最后一行就證明了這一點,它清楚地告訴我們進程退出了)。

由于ljmp的內(nèi)存操作數(shù)和目標地址都是32位的,我們需要確保它們都位于高32位都為0的地址中,最好的方法是使用mmap()和MAP_32BIT標志來分配內(nèi)存。

  1. struct ljmp_target { 
  2.     uint32_t rip; 
  3.     uint16_t cs; 
  4. } __attribute__((packed)); 
  5.   
  6. struct data { 
  7.     struct ljmp_target ljmp; 
  8. }; 
  9.   
  10. static struct data *data; 
  11.   
  12. int main(...) 
  13.     ... 
  14.   
  15.     void *addr = mmap(NULL, PAGE_SIZE, 
  16.         PROT_READ | PROT_WRITE, 
  17.         MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT, 
  18.         -1, 0); 
  19.     if (addr == MAP_FAILED) 
  20.         error(EXIT_FAILURE, errno, "mmap()"); 
  21.   
  22.     data = (struct data *) addr; 
  23.   
  24.     ... 
  25.   
  26. void emit_code() 
  27.     ... 
  28.   
  29.     // ljmp *target 
  30.     *out++ = 0xff; 
  31.     *out++ = 0x2c; 
  32.     *out++ = 0x25; 
  33.     for (unsigned int i = 0; i < 4; ++i) 
  34.         *out++ = ((uint64_t) &data->ljmp) >> (8 * i); 
  35.   
  36.     // cs:rip (jump target; in our case, the next instruction) 
  37.     data->ljmp.cs = 0x23; 
  38.     data->ljmp.rip = (uint64_t) out
  39.   
  40.     ... 

 

這里有幾件事需要注意:

這將改變CPU模式,這意味著后續(xù)指令必須在32位中有效(否則,您可能會得到一般保護故障或無效操作碼異常)。

上面我們用來加載段寄存器的指令序列(例如movw ..., %ax; movw %ax, %ss)在32位和64位上有完全相同的編碼,所以我們可以在切換到32位代碼段后毫不費力地執(zhí)行它——這對于確保我們在進入內(nèi)核之前仍然可以加載%ss特別有用。

我們可以選擇是否始終更改為段4(段選擇器0x23),或者嘗試更改為隨機段選擇器(例如使用get_random_segment_selector())。如果我們選擇一個隨機的,我們甚至可能不知道我們是仍然在32位還是64位模式下執(zhí)行。

我們可能希望在從內(nèi)核返回后嘗試跳回我們的正常代碼段(段6,段選擇器0x33),如果我們沒有退出、崩潰或被殺死的話。對于不同的段選擇器,該過程完全相同。

調(diào)試寄存器(%dr0等)

x86上的調(diào)試寄存器用于設(shè)置代碼斷點和數(shù)據(jù)觀察點。寄存器%dr0到%dr3用于設(shè)置實際的斷點/觀察點地址,寄存器%dr7用于控制這四個地址的使用方式(是斷點還是觀察點等)。

設(shè)置調(diào)試寄存器比我們目前看到的要棘手一些,因為你不能直接在用戶空間加載它們。就像修改LDT一樣,內(nèi)核要確保我們不會在內(nèi)核地址上設(shè)置斷點或觀察點,但更重要的是,CPU本身不允許ring 3直接修改這些寄存器。我所知道的設(shè)置調(diào)試寄存器的唯一方法就是使用ptrace()

ptrace()是一個非常難用的API。有很多隱含的狀態(tài)需要跟蹤器手動跟蹤,還有很多圍繞信號處理的邊緣情況。幸運的是,在這種情況下,我們只需要附加到子進程,設(shè)置調(diào)試寄存器,然后脫離即可;即使在我們停止跟蹤之后,調(diào)試寄存器的變化也會持續(xù)存在。

  1. #include 
  2. #include 
  3.   
  4. #include 
  5. #include 
  6.   
  7. int main(...) 
  8.     pid_t child = fork(); 
  9.     if (child == -1) 
  10.         error(EXIT_FAILURE, errno, "fork()"); 
  11.   
  12.     if (child == 0) { 
  13.         // make us a tracee of the parent 
  14.         if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) 
  15.             error(EXIT_FAILURE, errno, "ptrace(PTRACE_TRACEME)"); 
  16.   
  17.         // give the parent control 
  18.         raise(SIGTRAP); 
  19.   
  20.         ... 
  21.   
  22.         exit(EXIT_SUCCESS); 
  23.     } 
  24.   
  25.     // parent; wait for child to stop 
  26.     while (1) { 
  27.         int status; 
  28.         if (waitpid(child, &status, 0) == -1) { 
  29.             if (errno == EINTR) 
  30.                 continue
  31.   
  32.             error(EXIT_FAILURE, errno, "waitpid()"); 
  33.         } 
  34.   
  35.         if (WIFEXITED(status)) 
  36.             exit(WEXITSTATUS(status)); 
  37.         if (WIFSIGNALED(status)) 
  38.             exit(EXIT_FAILURE); 
  39.   
  40.         if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) 
  41.             break; 
  42.   
  43.         continue
  44.     } 
  45.   
  46.     // set debug registers and stop tracing 
  47.     if (ptrace(PTRACE_POKEUSER, child, offsetof(struct user, u_debugreg[0]), ...) == -1) 
  48.         error(EXIT_FAILURE, errno, "ptrace(PTRACE_POKEUSER)"); 
  49.     if (ptrace(PTRACE_POKEUSER, child, offsetof(struct user, u_debugreg[7]), ...) == -1) 
  50.         error(EXIT_FAILURE, errno, "ptrace(PTRACE_POKEUSER)"); 
  51.     if (ptrace(PTRACE_DETACH, child, 0, 0) == -1) 
  52.         error(EXIT_FAILURE, errno, "ptrace(PTRACE_DETACH)"); 
  53.   
  54.     ... 

即使在這個小例子中,等待子程序停止也是有點麻煩的。waitpid()總是有可能在子程序到達raise(SIGTRAP)之前返回,例如,如果它被某個外部進程殺死了。我們對這些情況的處理方式也是簡單的退出。

由于設(shè)置調(diào)試寄存器需要跟蹤,處理信號被進行多次上下文切換(這些都很慢),我建議對每個子進程只做一次,然后讓子進程連續(xù)多次嘗試進入內(nèi)核。

設(shè)置任何一個調(diào)試寄存器都可能失敗,所以在實際的fuzzer中,我們可能希望忽略所有錯誤,每次將%dr7設(shè)置為一個斷點,例如:

  1. // stddef.h offsetof() doesn't always allow non-const array indices, 
  2. // so precompute them here. 
  3. const unsigned int debugreg_offsets[] = { 
  4.     offsetof(struct user, u_debugreg[0]), 
  5.     offsetof(struct user, u_debugreg[1]), 
  6.     offsetof(struct user, u_debugreg[2]), 
  7.     offsetof(struct user, u_debugreg[3]), 
  8. }; 
  9.   
  10. for (unsigned int i = 0; i < 4; ++i) { 
  11.     // try random addresses until we succeed 
  12.     while (true) { 
  13.         unsigned long addr = get_random_address(); 
  14.         if (ptrace(PTRACE_POKEUSER, child, debugreg_offsets[i], addr) != -1) 
  15.             break; 
  16.     } 
  17.   
  18.     // Condition: 
  19.     // 0 - execution 
  20.     // 1 - write 
  21.     // 2 - (unused) 
  22.     // 3 - read or write 
  23.     unsigned int condition = std::uniform_int_distribution 
  24.     if (condition == 2) 
  25.         condition = 3; 
  26.   
  27.     // Size 
  28.     // 0 - 1 byte 
  29.     // 1 - 2 bytes 
  30.     // 2 - 8 bytes 
  31.     // 3 - 4 bytes 
  32.     unsigned int size = std::uniform_int_distribution 
  33.   
  34.     unsigned long dr7 = ptrace(PTRACE_PEEKUSER, child, offsetof(struct user, u_debugreg[7]), 0); 
  35.     dr7 &= ~((1 | (3 << 16) | (3 << 18)) << i); 
  36.     dr7 |= (1 | (condition << 16) | (size << 18)) << i; 
  37.     ptrace(PTRACE_POKEUSER, child, offsetof(struct user, u_debugreg[7]), dr7); 

進入內(nèi)核

在本系列的第一篇文章中,我們已經(jīng)看到了如何進行系統(tǒng)調(diào)用的代碼;在這里,我們使用相同的基本方法,但也考慮到所有其他進入內(nèi)核的方式。正如我前面提到的,syscall指令不是進入64位內(nèi)核的唯一方法,甚至不是進行系統(tǒng)調(diào)用的唯一方法。對于系統(tǒng)調(diào)用,我們有以下選項:

  1. int $0x80 
  2. sysenter 
  3. syscall 

實際上,查看硬件生成的異常表也很有用。其中許多異常的處理方式與系統(tǒng)調(diào)用和常規(guī)中斷略有不同;例如,當您試圖加載一個帶有無效段選擇器的段寄存器時,CPU會將一個錯誤代碼壓入(內(nèi)核)堆棧上。

我們可以觸發(fā)許多異常,但不是所有的異常。例如,通過簡單地執(zhí)行除零來生成除零異常是非常簡單的,但是我們不能輕松地按需生成NMI。(也就是說,我們可以做一些事情來使NMI更有可能發(fā)生,盡管是以一種不可控制的方式:如果我們在VM中測試內(nèi)核,我們可以從主機注入NMI,或者我們可以啟用內(nèi)核NMI watchdog功能。)

  1. enum entry_type { 
  2.     // system calls + software interrupts 
  3.     ENTRY_SYSCALL, 
  4.     ENTRY_SYSENTER, 
  5.     ENTRY_INT, 
  6.     ENTRY_INT_80, 
  7.     ENTRY_INT3, 
  8.   
  9.     // exceptions 
  10.     ENTRY_DE, // Divide error 
  11.     ENTRY_OF, // Overflow 
  12.     ENTRY_BR, // Bound range exceeded 
  13.     ENTRY_UD, // Undefined opcode 
  14.     ENTRY_SS, // Stack segment fault 
  15.     ENTRY_GP, // General protection fault 
  16.     ENTRY_PF, // Page fault 
  17.     ENTRY_MF, // x87 floating-point exception 
  18.     ENTRY_AC, // Alignment check 
  19.   
  20.     NR_ENTRY_TYPES, 
  21. }; 
  22.   
  23. enum entry_type type = (enum entry_type) std::uniform_int_distribution 
  24.   
  25. // Some entry types require a setup/preamble; do that here 
  26. switch (type) { 
  27. case ENTRY_DE: 
  28.     // xor %eax, %eax 
  29.     *out++ = 0x31; 
  30.     *out++ = 0xc0; 
  31.     break; 
  32. case ENTRY_MF: 
  33.     // pxor %xmm0, %xmm0 
  34.     *out++ = 0x66; 
  35.     *out++ = 0x0f; 
  36.     *out++ = 0xef; 
  37.     *out++ = 0xc0; 
  38.     break; 
  39. case ENTRY_BR: 
  40.     // xor %eax, %eax 
  41.     *out++ = 0x31; 
  42.     *out++ = 0xc0; 
  43.     break; 
  44. case ENTRY_SS: 
  45.     { 
  46.         uint16_t sel = get_random_segment_selector(); 
  47.   
  48.         // movw $imm, %bx 
  49.         *out++ = 0x66; 
  50.         *out++ = 0xbb; 
  51.         *out++ = sel; 
  52.         *out++ = sel >> 8; 
  53.     } 
  54.     break; 
  55. default
  56.     // do nothing 
  57.     break; 
  58.   
  59. ... 
  60.   
  61. switch (type) { 
  62.     // system calls + software interrupts 
  63.   
  64. case ENTRY_SYSCALL: 
  65.     // syscall 
  66.     *out++ = 0x0f; 
  67.     *out++ = 0x05; 
  68.     break; 
  69. case ENTRY_SYSENTER: 
  70.     // sysenter 
  71.     *out++ = 0x0f; 
  72.     *out++ = 0x34; 
  73.     break; 
  74. case ENTRY_INT: 
  75.     // int $x 
  76.     *out++ = 0xcd; 
  77.     *out++ = std::uniform_int_distribution 
  78.     break; 
  79. case ENTRY_INT_80: 
  80.     // int $0x80 
  81.     *out++ = 0xcd; 
  82.     *out++ = 0x80; 
  83.     break; 
  84. case ENTRY_INT3: 
  85.     // int3 
  86.     *out++ = 0xcc; 
  87.     break; 
  88.   
  89.     // exceptions 
  90.   
  91. case ENTRY_DE: 
  92.     // div %eax 
  93.     *out++ = 0xf7; 
  94.     *out++ = 0xf0; 
  95.     break; 
  96. case ENTRY_OF: 
  97.     // into (32-bit only!) 
  98.     *out++ = 0xce; 
  99.     break; 
  100. case ENTRY_BR: 
  101.     // bound %eax, data 
  102.     *out++ = 0x62; 
  103.     *out++ = 0x05; 
  104.     *out++ = 0x09; 
  105.     for (unsigned int i = 0; i < 4; ++i) 
  106.         *out++ = ((uint64_t) &data->bound) >> (8 * i); 
  107.     break; 
  108. case ENTRY_UD: 
  109.     // ud2 
  110.     *out++ = 0x0f; 
  111.     *out++ = 0x0b; 
  112.     break; 
  113. case ENTRY_SS: 
  114.     // Load %ss again, with a random segment selector (this is not 
  115.     // guaranteed to raise #SS, but most likely it will). The reason 
  116.     // we don't just rely on the load above to do it is that it could 
  117.     // be interesting to trigger #SS with a "weird" %ss too. 
  118.   
  119.     // movw %bx, %ss 
  120.     *out++ = 0x8e; 
  121.     *out++ = 0xd3; 
  122.     break; 
  123. case ENTRY_GP: 
  124.     // wrmsr 
  125.     *out++ = 0x0f; 
  126.     *out++ = 0x30; 
  127.     break; 
  128. case ENTRY_PF: 
  129.     // testl %eax, (xxxxxxxx) 
  130.     *out++ = 0x85; 
  131.     *out++ = 0x04; 
  132.     *out++ = 0x25; 
  133.     for (int i = 0; i < 4; ++i) 
  134.         *out++ = ((uint64_t) page_not_present) >> (8 * i); 
  135.     break; 
  136. case ENTRY_MF: 
  137.     // divss %xmm0, %xmm0 
  138.     *out++ = 0xf3; 
  139.     *out++ = 0x0f; 
  140.     *out++ = 0x5e; 
  141.     *out++ = 0xc0; 
  142.     break; 
  143. case ENTRY_AC: 
  144.     // testl %eax, (page_not_writable + 1) 
  145.     *out++ = 0x85; 
  146.     *out++ = 0x04; 
  147.     *out++ = 0x25; 
  148.     for (int i = 0; i < 4; ++i) 
  149.         *out++ = ((uint64_t) page_not_writable + 1) >> (8 * i); 
  150.     break; 

小結(jié)

我們現(xiàn)在幾乎擁有了所有的東西,我們需要真正開始進行模糊測試了!不過還有幾件事要做……

如果你運行目前的代碼,很快就會遇到一些問題。首先,我們使用的許多指令可能會導致崩潰(而且是故意的),這使得fuzzer速度很慢。通過為一些常見的終止信號(SIGBUS、SIGSEGV等)安裝信號處理程序,我們可以跳過故障指令,(希望)在同一個子進程內(nèi)繼續(xù)執(zhí)行。

其次,我們進行的一些系統(tǒng)調(diào)用可能會產(chǎn)生意想不到的副作用。特別是,我們并不希望在I/O上進行阻塞,因為這將使fuzzer停止運行。一種解決方法是安裝一個間隔定時器報警,以檢測子進程何時掛起。另一種方法可以是過濾掉某些已知會阻塞的系統(tǒng)調(diào)用(如read()、select()、sleep()等)。其他“不幸”的系統(tǒng)調(diào)用可能是fork()、exit()和kill()。fuzzer刪除文件或以其他方式擾亂系統(tǒng)的可能性較小,但我們?nèi)孕枰褂媚撤N形式的沙盒(如setuid(65534))。

如果你只是想看看最終的結(jié)果,這里有一個代碼的鏈接:

https://github.com/oracle/linux-blog-sample-code/tree/fuzzing-the-linux-kernel-x86-entry-code

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

【編輯推薦】

 

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

2020-10-10 10:14:42

Linux內(nèi)核

2020-09-23 12:42:08

Linux

2020-10-13 10:51:10

Linux內(nèi)核

2021-08-20 11:22:05

X86架構(gòu)NFV虛擬化

2011-04-19 09:17:36

2012-04-28 09:07:48

甲骨文x86

2019-07-15 13:11:57

Power

2011-12-01 11:09:48

AMDx86服務(wù)器英特爾

2021-09-14 10:07:09

英特爾初始代碼用戶中斷

2020-09-15 06:15:23

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

2021-06-07 15:20:22

Linux X861MB內(nèi)存BIOS

2011-12-19 10:55:58

云計算中國電信

2011-02-20 22:23:43

X86虛擬化XenServer

2010-04-06 14:20:33

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

2011-04-25 14:51:59

Linux任務(wù)切換TSS

2011-11-10 09:26:48

Solaris 11

2009-08-28 14:38:33

2013-06-24 14:16:50

云計算

2013-08-07 09:55:05

IBMGoogleNVIDIA

2010-04-29 17:50:15

點贊
收藏

51CTO技術(shù)棧公眾號