在上一篇文章中,我們創(chuàng)建了一個(gè)雖然簡單卻能工作的fuzzer,接下來,我們將進(jìn)一步提高該fuzzer的可用性和實(shí)用性。
在上一篇文章中,我們創(chuàng)建了一個(gè)雖然簡單卻能工作的fuzzer,接下來,我們將進(jìn)一步提高該fuzzer的可用性和實(shí)用性。在本文中,我們將對(duì)該fuzzer的功能進(jìn)行必要的擴(kuò)展。

通用寄存器+delta狀態(tài)
到目前為止,我們尚未涉及的一件事是將其他通用寄存器也設(shè)置為隨機(jī)值。入口代碼在工作過程中確實(shí)會(huì)用到某些通用寄存器,如果我們真的在某個(gè)地方遇到了問題,那么它很可能因隨機(jī)值而崩潰。
我們可能還想找出更細(xì)微的漏洞——雖然這些漏洞不會(huì)使得內(nèi)核徹底崩潰,但可能會(huì)將內(nèi)核地址泄漏到用戶空間從未見過的某個(gè)寄存器中。一種檢查內(nèi)核是否正確,是否保存了我們的寄存器/標(biāo)志等的方法是在從內(nèi)核模式返回后寫出寄存器的狀態(tài)。這并不難實(shí)現(xiàn),因?yàn)槲覀兛梢詫⑺校ɑ蛘咧辽偈谴蟛糠郑┘拇嫫鞯闹荡娣诺焦潭ǖ牡刂分校ɡ?,在我們已?jīng)用于其他用途的數(shù)據(jù)頁中)。這里的難點(diǎn)在于如何將其與在一個(gè)子進(jìn)程中運(yùn)行多個(gè)進(jìn)入嘗試(entry attempts)/系統(tǒng)調(diào)用結(jié)合起來,因?yàn)樾枰獙⒔∪詸z查與進(jìn)入嘗試交織在一起,這可能會(huì)非常麻煩。
最大限度地降低崩潰的概率
我們?cè)诘诙恼轮幸呀?jīng)提到,令子進(jìn)程崩潰的代價(jià)相當(dāng)高,因?yàn)檫@意味著要啟動(dòng)一個(gè)全新的子進(jìn)程。因此,盡可能避免崩潰(并在同一個(gè)子進(jìn)程中運(yùn)行盡可能多的進(jìn)入嘗試)可能是提高fuzzer性能的可行策略。這包括兩個(gè)主要部分:
· 保存/恢復(fù)行內(nèi)所需的狀態(tài),例如,你要保存和恢復(fù)%rsp,以便后續(xù)的pushf/popf指令能夠繼續(xù)工作。
· 從信號(hào)處理程序中恢復(fù),例如通過安裝處理程序,可以將進(jìn)程恢復(fù)到已知的良好狀態(tài)。
檢查生成的匯編代碼
雖然代碼很容易在生成匯編代碼的時(shí)候出錯(cuò),但人們卻很難注意到,因?yàn)槌绦蚨急罎⒘?,你也看不出你得到的是一個(gè)意外的結(jié)果。我曾經(jīng)遇到過類似的問題,但是在2年的時(shí)間里一直沒有覺察到:我在編碼ljmp操作數(shù)的地址時(shí),不小心用錯(cuò)了字節(jié)順序,所以在32位兼容模式下,它實(shí)際上從來沒有運(yùn)行過任何東西!
一種檢查匯編代碼的簡便方法是使用像udis86這樣的反匯編庫,然后通過手動(dòng)方式驗(yàn)證生成的代碼。
- #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的交互
在一個(gè)案例中,我們看到了與KVM的交互,其中啟動(dòng)任何KVM實(shí)例都會(huì)破壞GDTR(GDT寄存器)的大小,并允許fuzzer通過使用超出GDT預(yù)期大小的段而導(dǎo)致崩潰。事實(shí)證明,這個(gè)漏洞是可利用的,并能獲得ring 0的執(zhí)行權(quán)限。在另一個(gè)案例中,我們看到了在硬件加速的嵌套式客戶機(jī)(客戶機(jī)中的客戶機(jī))中運(yùn)行時(shí)的交互。
通常,KVM需要模擬底層硬件的某些特性,這增加了相當(dāng)多的復(fù)雜性。fuzzer很有可能在KVM或Xen等管理程序中發(fā)現(xiàn)漏洞,因此在不同的裸機(jī)CPU和多種管理程序下運(yùn)行fuzzer是很有價(jià)值的。
要想以編程方式創(chuàng)建KVM實(shí)例,請(qǐng)參閱Serge Zaitsev撰寫的KVM host in a few lines of code一文。
一個(gè)相關(guān)的有趣實(shí)驗(yàn)可能是為運(yùn)行在x86上的Windows或其他操作系統(tǒng)編譯fuzzer,看看它們的效果如何。我在WSL(Windows Subsystem for Linux)上簡單地測(cè)試了Linux二進(jìn)制文件,沒有發(fā)生什么不良情況。
配置/啟動(dòng)選項(xiàng)
配置/啟動(dòng)選項(xiàng)會(huì)影響入口代碼的具體操作。下面是我在最新的內(nèi)核中發(fā)現(xiàn)的相關(guān)選項(xiàng):
- $ 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
其實(shí),還有更多的選項(xiàng),它們都隱藏在頭文件中。通過這些選項(xiàng)的不同組合來構(gòu)建多個(gè)內(nèi)核,可以幫助揭示那些被破壞的組合,也許只有在由fuzzer觸發(fā)的邊緣情況下才會(huì)出現(xiàn)。
通過查看Documentation/admin-guide/kernel-parameters.txt,你還可以找到一些可能影響入口代碼的選項(xiàng)。這里有一個(gè)Python腳本,它可以生成隨機(jī)的配置選項(xiàng)組合,這對(duì)于用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啟用時(shí),會(huì)在入口代碼中插入一些代碼,例如用于系統(tǒng)調(diào)用和irqflags跟蹤。這可能也非常值得進(jìn)行測(cè)試,所以我建議在運(yùn)行fuzzer之前,不妨調(diào)整一下這些文件(位于/sys/kernel/tracing路徑下):

PTRACE_SYSCALL
我們已經(jīng)看到,ptrace改變了處理系統(tǒng)調(diào)用進(jìn)入/退出的方式(因?yàn)樾枰V惯M(jìn)程并通知跟蹤器),所以最好在ptrace()下使用ptrace_syscall運(yùn)行一部分進(jìn)入嘗試。當(dāng)被ptrace停止時(shí),嘗試調(diào)整被跟蹤的進(jìn)程的一些/所有寄存器也可能很有趣。要完全正確地完成這個(gè)任務(wù)是非常困難的,所以這里就不多介紹了。
mkinitrd.sh
當(dāng)我在VM中進(jìn)行測(cè)試時(shí),我更喜歡將程序綁定在initrd中,并以init(pid1)的形式運(yùn)行,這樣就不需要將其復(fù)制到文件系統(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,它就會(huì)在開機(jī)后立即運(yùn)行fuzzer。
污點(diǎn)檢查
如果fuzzer真的遇到了某種內(nèi)核崩潰或漏洞,那么確保我們不會(huì)遺漏它們是很有用的。我個(gè)人喜歡在內(nèi)核命令行中使用參數(shù)ops=panic panic_on_warn panic=-1,并將-no-reboot傳遞給Qemu/KVM;這將確保任何警告都會(huì)立即導(dǎo)致Qemu退出(將任何診斷程序留在終端上)。如果你正在使用專門的裸機(jī)運(yùn)行fuzzer(例如,使用上面的initrd方法),可以令panic=0,這樣只會(huì)掛起機(jī)器。
如果你在普通的工作站上進(jìn)行測(cè)試,并且不想讓整臺(tái)機(jī)器掛掉,則可以檢查內(nèi)核是否被污染(每當(dāng)出現(xiàn)警告或漏洞時(shí)都會(huì)被污染),然后直接地退出:
- 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ò)中是非常有用的。我將給出一個(gè)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()");
-
- ...
- }
然后,在生成了每個(gè)入口/出口的代碼之后,您可以簡單地將其轉(zhuǎn)儲(chǔ)到這個(gè)套接字上:
- write(udp_socket, (char *) mem, out - (uint8_t *) mem);
我們希望日志服務(wù)器最后接收到的數(shù)據(jù)(這里是10.5.0.1:21000)會(huì)包含導(dǎo)致崩潰的匯編代碼。根據(jù)具體的用例,有時(shí)需要添加某種框架,以便可以輕松地判斷出測(cè)試用例的具體開始和結(jié)束位置。
檢查fuzzer是否能捕捉到已知的漏洞
多年來,人們已經(jīng)在入口代碼中找到了許多漏洞。因此,我們可以構(gòu)建一些舊的、有漏洞的內(nèi)核,并在它們上面運(yùn)行fuzzer,以確保它確實(shí)能捕捉到這些已知的漏洞。我們也可以用尋找漏洞所花費(fèi)的時(shí)間來衡量fuzzer的效率,但是,我們必須小心,不要過度優(yōu)化,以防止它們只找到這些漏洞。
代碼覆蓋率/插樁技術(shù)反饋
插樁技術(shù)
AFL和syzkaller這樣的fuzzer如此有效的原因之一是,它們使用代碼復(fù)蓋率來非常精確地衡量調(diào)整測(cè)試用例的各個(gè)二進(jìn)制位的效果。這通常是通過使用一個(gè)特殊的編譯器標(biāo)志編譯C代碼來實(shí)現(xiàn)的,該標(biāo)志發(fā)出額外的代碼來收集覆蓋率數(shù)據(jù)。對(duì)于匯編代碼,尤其是入口代碼,這是一個(gè)非常棘手的問題,因?yàn)槿绻皇謩?dòng)檢查代碼的每個(gè)指令,我們就無法知道CPU到底處于什么狀態(tài)(以及我們可以破壞哪些寄存器/狀態(tài))。
但是,如果我們真的想要提高代碼覆蓋率,有一種方法可以做到:x86指令集包含一條指令,該指令同時(shí)接受一個(gè)立即數(shù)和一個(gè)立即數(shù)地址,并且不影響任何其他狀態(tài)(例如標(biāo)志):movb$value,(addr)。我們唯一需要注意的是:確保addr是一個(gè)編譯時(shí)常量地址,它總是映射到某個(gè)物理內(nèi)存,并在頁表中標(biāo)記為present,這樣我們?cè)谠L問它時(shí)就不會(huì)出現(xiàn)頁面錯(cuò)誤。幸運(yùn)的是,Linux已經(jīng)提供了一種機(jī)制:fixmaps,也就是“編譯時(shí)虛擬內(nèi)存分配”。這樣,我們就可以靜態(tài)地分配一個(gè)編譯時(shí)常量虛擬地址,該地址指向所有任務(wù)和上下文的相同底層物理頁面。由于它是在任務(wù)之間共享的,因此當(dāng)在進(jìn)程之間切換時(shí),我們必須清除或以其他方式保存/恢復(fù)這些值。
通過組合使用C宏和匯編器宏,我們可以得到一個(gè)侵入性非常低的覆蓋原語,你可以在入口代碼中的任何地方加入這個(gè)原語,來記錄所采用的代碼路徑。我已經(jīng)編寫了一個(gè)補(bǔ)丁,但還有一些邊緣情況需要解決(例如,當(dāng)SMAP被啟用時(shí),它并不完全有效)。此外,我懷疑x86的維護(hù)者是否會(huì)喜歡在入口代碼中摻雜這些覆蓋率注釋。
在fuzzer方面,有一件事讓插樁技術(shù)反饋?zhàn)兊酶訌?fù)雜,那就是你需要一個(gè)完整的系統(tǒng)來跟蹤測(cè)試用例、結(jié)果以及(可能的)你對(duì)每個(gè)測(cè)試用例應(yīng)用了哪些突變。正因?yàn)槿绱?,我選擇暫時(shí)忽略代碼覆蓋率;無論如何,這都是一個(gè)寬泛的fuzzing話題,與x86或特別是入口代碼沒有太大關(guān)系。
性能計(jì)數(shù)器/硬件反饋
收集代碼覆蓋率的一種完全不同的方法是使用性能計(jì)數(shù)器。我知道最近有兩個(gè)項(xiàng)目就是這樣做的:
· Resmack Fuzz Test
· kAFL
這里最大的好處顯然是不需要進(jìn)行檢測(cè)(修改內(nèi)核)。最大的缺點(diǎn)在于性能計(jì)數(shù)器不是完全確定的(可能是由于硬件中斷等外部因素所致)。也許它對(duì)入口代碼也不起作用,因?yàn)樵趨R編代碼上只花費(fèi)了很短的時(shí)間。無論如何,這里有幾個(gè)鏈接可供進(jìn)一步參考:
· https://man7.org/linux/man-pages/man2/perf_event_open.2.html
· http://www.brendangregg.com/perf.html
本文翻譯自:https://blogs.oracle.com/linux/fuzzing-the-linux-kernel-x86-entry-code%2c-part-3-of-3如若轉(zhuǎn)載,請(qǐng)注明原文地址。