溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何測試Linux內(nèi)核入口代碼

發(fā)布時間:2021-10-26 15:28:50 來源:億速云 閱讀:141 作者:iii 欄目:web開發(fā)

本篇內(nèi)容介紹了“如何測試Linux內(nèi)核入口代碼”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠?qū)W有所成!

通用寄存器+delta狀態(tài)

到目前為止,我們尚未涉及的一件事是將其他通用寄存器也設(shè)置為隨機值。入口代碼在工作過程中確實會用到某些通用寄存器,如果我們真的在某個地方遇到了問題,那么它很可能因隨機值而崩潰。

我們可能還想找出更細微的漏洞——雖然這些漏洞不會使得內(nèi)核徹底崩潰,但可能會將內(nèi)核地址泄漏到用戶空間從未見過的某個寄存器中。一種檢查內(nèi)核是否正確,是否保存了我們的寄存器/標志等的方法是在從內(nèi)核模式返回后寫出寄存器的狀態(tài)。這并不難實現(xiàn),因為我們可以將所有(或者至少是大部分)寄存器的值存放到固定的地址中(例如,在我們已經(jīng)用于其他用途的數(shù)據(jù)頁中)。這里的難點在于如何將其與在一個子進程中運行多個進入嘗試(entry attempts)/系統(tǒng)調(diào)用結(jié)合起來,因為需要將健全性檢查與進入嘗試交織在一起,這可能會非常麻煩。

最大限度地降低崩潰的概率

我們在第二篇文章中已經(jīng)提到,令子進程崩潰的代價相當高,因為這意味著要啟動一個全新的子進程。因此,盡可能避免崩潰(并在同一個子進程中運行盡可能多的進入嘗試)可能是提高fuzzer性能的可行策略。這包括兩個主要部分:

· 保存/恢復行內(nèi)所需的狀態(tài),例如,你要保存和恢復%rsp,以便后續(xù)的pushf/popf指令能夠繼續(xù)工作。

· 從信號處理程序中恢復,例如通過安裝處理程序,可以將進程恢復到已知的良好狀態(tài)。

檢查生成的匯編代碼

雖然代碼很容易在生成匯編代碼的時候出錯,但人們卻很難注意到,因為程序都崩潰了,你也看不出你得到的是一個意外的結(jié)果。我曾經(jīng)遇到過類似的問題,但是在2年的時間里一直沒有覺察到:我在編碼ljmp操作數(shù)的地址時,不小心用錯了字節(jié)順序,所以在32位兼容模式下,它實際上從來沒有運行過任何東西!

一種檢查匯編代碼的簡便方法是使用像udis86這樣的反匯編庫,然后通過手動方式驗證生成的代碼。

#include   ...   ud_t u; ud_init(&u);   ud_set_vendor(&u, UD_VENDOR_INTEL); ud_set_mode(&u, 64); ud_set_pc(&u, (uint64_t) mem); ud_set_input_buffer(&u, (unsigned char *) mem, (char *) out - (char *) mem);   ud_set_syntax(&u, UD_SYN_ATT);   while (ud_disassemble(&u))     fprintf(stderr, "  %08lx %s\n", ud_insn_off(&u), ud_insn_asm(&u));   fprintf(stderr, "\n");

KVM/Xen/Intel/AMD的交互

在一個案例中,我們看到了與KVM的交互,其中啟動任何KVM實例都會破壞GDTR(GDT寄存器)的大小,并允許fuzzer通過使用超出GDT預期大小的段而導致崩潰。事實證明,這個漏洞是可利用的,并能獲得ring 0的執(zhí)行權(quán)限。在另一個案例中,我們看到了在硬件加速的嵌套式客戶機(客戶機中的客戶機)中運行時的交互。

通常,KVM需要模擬底層硬件的某些特性,這增加了相當多的復雜性。fuzzer很有可能在KVM或Xen等管理程序中發(fā)現(xiàn)漏洞,因此在不同的裸機CPU和多種管理程序下運行fuzzer是很有價值的。

要想以編程方式創(chuàng)建KVM實例,請參閱Serge Zaitsev撰寫的KVM host in a few lines of code一文。

一個相關(guān)的有趣實驗可能是為運行在x86上的Windows或其他操作系統(tǒng)編譯fuzzer,看看它們的效果如何。我在WSL(Windows Subsystem for Linux)上簡單地測試了Linux二進制文件,沒有發(fā)生什么不良情況。

配置/啟動選項

配置/啟動選項會影響入口代碼的具體操作。下面是我在最新的內(nèi)核中發(fā)現(xiàn)的相關(guān)選項:

$ grep -o 'CONFIG_[A-Z0-9_]*' arch/x86/entry/entry_64*.S | sort | uniq CONFIG_DEBUG_ENTRY CONFIG_IA32_EMULATION CONFIG_PARAVIRT CONFIG_RETPOLINE CONFIG_STACKPROTECTOR CONFIG_X86_5LEVEL CONFIG_X86_ESPFIX64 CONFIG_X86_L1_CACHE_SHIFT CONFIG_XEN_PV

其實,還有更多的選項,它們都隱藏在頭文件中。通過這些選項的不同組合來構(gòu)建多個內(nèi)核,可以幫助揭示那些被破壞的組合,也許只有在由fuzzer觸發(fā)的邊緣情況下才會出現(xiàn)。

通過查看Documentation/admin-guide/kernel-parameters.txt,你還可以找到一些可能影響入口代碼的選項。這里有一個Python腳本,它可以生成隨機的配置選項組合,這對于用KVM傳遞內(nèi)核命令行非常有用:

import random   flags = """nopti nospectre_v1 nospectre_v2 spectre_v2_user=off spec_store_bypass_disable=off l1tf=off mds=off tsx_async_abort=off kvm.nx_huge_pages=off noapic noclflush nosmap nosmep noexec32 nofxsr nohugeiomap nosmt nosmt noxsave noxsaveopt noxsaves intremap=off nolapic nomce nopat nopcid norandmaps noreplace-smp nordrand nosep nosmp nox2apic""".split()   print(' '.join(random.sample(flags, 5)), "nmi_watchdog=%u" % (random.randrange(2), ))

ftrace

Ftrace啟用時,會在入口代碼中插入一些代碼,例如用于系統(tǒng)調(diào)用和irqflags跟蹤。這可能也非常值得進行測試,所以我建議在運行fuzzer之前,不妨調(diào)整一下這些文件(位于/sys/kernel/tracing路徑下):

如何測試Linux內(nèi)核入口代碼

PTRACE_SYSCALL

我們已經(jīng)看到,ptrace改變了處理系統(tǒng)調(diào)用進入/退出的方式(因為需要停止進程并通知跟蹤器),所以最好在ptrace()下使用ptrace_syscall運行一部分進入嘗試。當被ptrace停止時,嘗試調(diào)整被跟蹤的進程的一些/所有寄存器也可能很有趣。要完全正確地完成這個任務(wù)是非常困難的,所以這里就不多介紹了。

mkinitrd.sh

當我在VM中進行測試時,我更喜歡將程序綁定在initrd中,并以init(pid1)的形式運行,這樣就不需要將其復制到文件系統(tǒng)映像上。您可以使用如下所示的腳本:

#! /bin/bash   set -e set -x   rm -rf initrd/ mkdir initrd/ g++ -static -Wall -std=c++14 -O2 -g -o initrd/init main.cc -lm   (cd initrd/ && (find | cpio -o -H newc)) \     | gzip -c \     > initrd.entry-fuzz.gz

如果你使用的是Qemu/KVM,只要傳入-initrd initrd.entry-fuzz.gz,它就會在開機后立即運行fuzzer。

污點檢查

如果fuzzer真的遇到了某種內(nèi)核崩潰或漏洞,那么確保我們不會遺漏它們是很有用的。我個人喜歡在內(nèi)核命令行中使用參數(shù)ops=panic panic_on_warn panic=-1,并將-no-reboot傳遞給Qemu/KVM;這將確保任何警告都會立即導致Qemu退出(將任何診斷程序留在終端上)。如果你正在使用專門的裸機運行fuzzer(例如,使用上面的initrd方法),可以令panic=0,這樣只會掛起機器。

如果你在普通的工作站上進行測試,并且不想讓整臺機器掛掉,則可以檢查內(nèi)核是否被污染(每當出現(xiàn)警告或漏洞時都會被污染),然后直接地退出:

int tainted_fd = open("/proc/sys/kernel/tainted", O_RDONLY); if (tainted_fd == -1)     error(EXIT_FAILURE, errno, "open()");   char tainted_orig_buf[16]; ssize_t tainted_orig_len = pread(tainted_fd, tainted_orig_buf, sizeof(tainted_orig_buf), 0); if (tainted_orig_len == -1)     error(EXIT_FAILURE, errno, "pread()");   while (1) {     // generate + run test case       ...       char tainted_buf[16];     ssize_t tainted_len = pread(tainted_fd, tainted_buf, sizeof(tainted_buf), 0);     if (tainted_len == -1)         error(EXIT_FAILURE, errno, "pread()");       if (tainted_len != tainted_orig_len || memcmp(tainted_buf, tainted_orig_buf, tainted_len)) {         fprintf(stderr, "Kernel became tainted, stopping.\n");         // TODO: dump hex bytes or disassembly         exit(EXIT_FAILURE);     } }

網(wǎng)絡(luò)日志

如果內(nèi)核崩潰了,并且不清楚問題出在哪里,那么將所有正在嘗試的內(nèi)容記錄到網(wǎng)絡(luò)中是非常有用的。我將給出一個UDP日志的簡單框架:

int main(...) {     int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);     if (udp_socket == -1)         error(EXIT_FAILURE, errno, "socket(AF_INET, SOCK_DGRAM, 0)");       struct sockaddr_in remote_addr = {};     remote_addr.sin_family = AF_INET;     remote_addr.sin_port = htons(21000);     inet_pton(AF_INET, "10.5.0.1", &remote_addr.sin_addr.s_addr);       if (connect(udp_socket, (const struct sockaddr *) &remote_addr, sizeof(remote_addr)) == -1)         error(EXIT_FAILURE, errno, "connect()");       ... }

然后,在生成了每個入口/出口的代碼之后,您可以簡單地將其轉(zhuǎn)儲到這個套接字上:

write(udp_socket, (char *) mem, out - (uint8_t *) mem);

我們希望日志服務(wù)器最后接收到的數(shù)據(jù)(這里是10.5.0.1:21000)會包含導致崩潰的匯編代碼。根據(jù)具體的用例,有時需要添加某種框架,以便可以輕松地判斷出測試用例的具體開始和結(jié)束位置。

檢查fuzzer是否能捕捉到已知的漏洞

多年來,人們已經(jīng)在入口代碼中找到了許多漏洞。因此,我們可以構(gòu)建一些舊的、有漏洞的內(nèi)核,并在它們上面運行fuzzer,以確保它確實能捕捉到這些已知的漏洞。我們也可以用尋找漏洞所花費的時間來衡量fuzzer的效率,但是,我們必須小心,不要過度優(yōu)化,以防止它們只找到這些漏洞。

代碼覆蓋率/插樁技術(shù)反饋

插樁技術(shù)

AFL和syzkaller這樣的fuzzer如此有效的原因之一是,它們使用代碼復蓋率來非常精確地衡量調(diào)整測試用例的各個二進制位的效果。這通常是通過使用一個特殊的編譯器標志編譯C代碼來實現(xiàn)的,該標志發(fā)出額外的代碼來收集覆蓋率數(shù)據(jù)。對于匯編代碼,尤其是入口代碼,這是一個非常棘手的問題,因為如果不手動檢查代碼的每個指令,我們就無法知道CPU到底處于什么狀態(tài)(以及我們可以破壞哪些寄存器/狀態(tài))。

但是,如果我們真的想要提高代碼覆蓋率,有一種方法可以做到:x86指令集包含一條指令,該指令同時接受一個立即數(shù)和一個立即數(shù)地址,并且不影響任何其他狀態(tài)(例如標志):movb$value,(addr)。我們唯一需要注意的是:確保addr是一個編譯時常量地址,它總是映射到某個物理內(nèi)存,并在頁表中標記為present,這樣我們在訪問它時就不會出現(xiàn)頁面錯誤。幸運的是,Linux已經(jīng)提供了一種機制:fixmaps,也就是“編譯時虛擬內(nèi)存分配”。這樣,我們就可以靜態(tài)地分配一個編譯時常量虛擬地址,該地址指向所有任務(wù)和上下文的相同底層物理頁面。由于它是在任務(wù)之間共享的,因此當在進程之間切換時,我們必須清除或以其他方式保存/恢復這些值。

通過組合使用C宏和匯編器宏,我們可以得到一個侵入性非常低的覆蓋原語,你可以在入口代碼中的任何地方加入這個原語,來記錄所采用的代碼路徑。我已經(jīng)編寫了一個補丁,但還有一些邊緣情況需要解決(例如,當SMAP被啟用時,它并不完全有效)。此外,我懷疑x86的維護者是否會喜歡在入口代碼中摻雜這些覆蓋率注釋。

在fuzzer方面,有一件事讓插樁技術(shù)反饋變得更加復雜,那就是你需要一個完整的系統(tǒng)來跟蹤測試用例、結(jié)果以及(可能的)你對每個測試用例應用了哪些突變。正因為如此,我選擇暫時忽略代碼覆蓋率;無論如何,這都是一個寬泛的fuzzing話題,與x86或特別是入口代碼沒有太大關(guān)系。

性能計數(shù)器/硬件反饋

收集代碼覆蓋率的一種完全不同的方法是使用性能計數(shù)器。我知道最近有兩個項目就是這樣做的:

· Resmack Fuzz Test

· kAFL

這里最大的好處顯然是不需要進行檢測(修改內(nèi)核)。最大的缺點在于性能計數(shù)器不是完全確定的(可能是由于硬件中斷等外部因素所致)。也許它對入口代碼也不起作用,因為在匯編代碼上只花費了很短的時間。

“如何測試Linux內(nèi)核入口代碼”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI