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

成都創(chuàng)新互聯(lián)公司一直秉承“誠信做人,踏實做事”的原則,不欺瞞客戶,是我們最起碼的底線! 以服務(wù)為基礎(chǔ),以質(zhì)量求生存,以技術(shù)求發(fā)展,成交一個客戶多一個朋友!為您提供成都網(wǎng)站制作、成都做網(wǎng)站、外貿(mào)營銷網(wǎng)站建設(shè)、成都網(wǎng)頁設(shè)計、成都微信小程序、成都網(wǎng)站開發(fā)、成都網(wǎng)站制作、成都軟件開發(fā)、app軟件定制開發(fā)是成都本地專業(yè)的網(wǎng)站建設(shè)和網(wǎng)站設(shè)計公司,等你一起來見證!
更多標(biāo)志(%rflags)
方向標(biāo)志只是我們眾多感興趣的標(biāo)志之一。維基百科上關(guān)于%rflags的文章列出了我們感興趣的其他一些標(biāo)志:
· bit 8:陷阱標(biāo)志(用于單步調(diào)試)
· bit 18:對齊檢查
大多數(shù)與算術(shù)相關(guān)的標(biāo)志(進位標(biāo)志等)并不是我們感興趣的對象,因為它們在普通代碼的正常運行過程中變化較大,這意味著內(nèi)核對這些標(biāo)志的處理很可能已經(jīng)過了充分的測試。而另外一些標(biāo)志(如中斷啟用標(biāo)志)可能無法被用戶空間修改,所以即使嘗試也沒什么用。
我們需要重點關(guān)注陷阱標(biāo)志,因為設(shè)置該標(biāo)志后,CPU在每條指令后都會傳遞一個調(diào)試異常,自然也會干擾輸入代碼的正常運行。
對齊檢查標(biāo)志也應(yīng)當(dāng)重點關(guān)注,因為當(dāng)一個錯誤對齊的指針被解除引用時,它會使CPU傳遞一個對齊檢查異常。雖然CPU在0環(huán)中執(zhí)行時不應(yīng)該執(zhí)行對齊檢查,但是檢查是否存在因為對齊檢查異常而進入內(nèi)核的相關(guān)漏洞還是很有意思的(我們稍后再談)。
維基百科的文章給出了修改這些標(biāo)志的程序,但我們可以做得更好一點。
- 0: 9c pushfq
- 1: 48 81 34 24 00 01 00 00 xorq $0x100,(%rsp)
- 9: 48 81 34 24 00 04 00 00 xorq $0x400,(%rsp)
- 11: 48 81 34 24 00 00 04 00 xorq $0x40000,(%rsp)
- 19: 9d popfq
這段代碼將%rflags的內(nèi)容壓入堆棧上,然后直接修改堆棧上的標(biāo)志值,再將該值彈出到%rflags中。實際上,我們在這里可以選擇使用orq或者xorq指令;我選擇xorq,因為它可以切換寄存器中的任何值。這樣一來,如果我們連續(xù)進行多次系統(tǒng)調(diào)用(或內(nèi)核入口),我們可以隨機切換標(biāo)志,而不必關(guān)心現(xiàn)有的值是什么。
既然我們無論如何都要修改%rflags寄存器,那么我們不妨把方向標(biāo)志的修改納入進去,把三個標(biāo)志的修改合并到一條指令中。雖然這是一個很小的優(yōu)化,但沒有理由不這么做,最后的結(jié)果如下所示:
- // pushfq
- *out++ = 0x9c;
- uint32_t mask = 0;
- // trap flag
- mask |= std::uniform_int_distribution
- // direction flag
- mask |= std::uniform_int_distribution
- // alignment check
- mask |= std::uniform_int_distribution
- // xorq $mask, 0(%rsp)
- *out++ = 0x48;
- *out++ = 0x81;
- *out++ = 0x34;
- *out++ = 0x24;
- *out++ = mask;
- *out++ = mask >> 8;
- *out++ = mask >> 16;
- *out++ = mask >> 24;
- // popfq
- *out++ = 0x9d;
如果我們不希望進程在設(shè)置陷阱標(biāo)志時立即被SIGTRAP殺死,我們需要注冊一個信號處理程序來有效地忽略這個信號(顯然使用SIG_IGN是不夠的):
- static void handle_child_sigtrap(int signum, siginfo_t *siginfo, void *ucontext)
- {
- // this gets called when TF is set in %rflags; do nothing
- }
- ...
- struct sigaction sigtrap_act = {};
- sigtrap_act.sa_sigaction = &handle_child_sigtrap;
- sigtrap_act.sa_flags = SA_SIGINFO | SA_ONSTACK;
- if (sigaction(SIGTRAP, &sigtrap_act, NULL) == -1)
- error(EXIT_FAILURE, errno, "sigaction(SIGTRAP)");
關(guān)于上面的SA_ONSTACK標(biāo)志,我們將在下一節(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).
當(dāng)向用戶空間傳遞信號時,信號處理程序的堆棧幀由內(nèi)核創(chuàng)建,通常位于被中斷線程的當(dāng)前堆棧指針的上方。
如果由于某些錯誤,%rsp會被內(nèi)核直接訪問,那么在正常操作期間可能不會被注意到,因為堆棧指針通??偸侵赶蛞粋€有效地址。要捕捉這種漏洞,我們可以簡單地將其指向一個非映射地址(甚至是內(nèi)核地址?。?。
為了幫助我們測試堆棧指針的各種可能感興趣的值,我們可以定義一個helper:
- static void *page_not_present;
- static void *page_not_writable;
- static void *page_not_executable;
- static uint64_t get_random_address()
- {
- // very occasionally hand out a non-canonical address
- if (std::uniform_int_distribution
- return 1UL << 63;
- uint64_t value = 0;
- switch (std::uniform_int_distribution
- case 0:
- break;
- case 1:
- value = (uint64_t) page_not_present;
- break;
- case 2:
- value = (uint64_t) page_not_writable;
- break;
- case 3:
- value = (uint64_t) page_not_executable;
- break;
- case 4:
- static const uint64_t kernel_pointers[] = {
- 0xffffffff81000000UL,
- 0xffffffff82016000UL,
- 0xffffffffc0002000UL,
- 0xffffffffc2000000UL,
- };
- value = kernel_pointers[std::uniform_int_distribution
- // random ~2MiB offset
- value += PAGE_SIZE * std::uniform_int_distribution
- break;
- }
- // occasionally intentionally misalign it
- if (std::uniform_int_distribution
- value += std::uniform_int_distribution
- return value;
- }
- int main(...)
- {
- page_not_present = mmap(NULL, PAGE_SIZE, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT, -1, 0);
- page_not_writable = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT, -1, 0);
- page_not_executable = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT, -1, 0);
- ...
- }
在這里,我使用了自己機器上的/proc/kallsyms中找到的一些內(nèi)核指針。它們不一定是很好的選擇,只是用于演示。正如我前面所提到的,我們需要找到一個平衡點,既要選擇那些瘋狂到?jīng)]有人想過要處理它們的值(我們畢竟在這里試圖尋找的是邊緣案例),又要不迷失在巨大的非目標(biāo)值的海洋中;我們可以統(tǒng)一選擇隨機的64位值,但這很難帶來任何有效的指針(其中大部分可能是非規(guī)范的地址)。模糊測試的部分藝術(shù)是通過對哪些有可能和哪些不可能的關(guān)系進行有根據(jù)的猜測來抽出相關(guān)的邊緣案例。
現(xiàn)在只是設(shè)置值的問題,幸運的是,我們可以直接將64位的值加載到%rsp中:
- movq $0x12345678aabbccdd, %rsp
可以使用下列代碼:
- uint64_t rsp = get_random_address();
- // movq $imm, %rsp
- *out++ = 0x48;
- *out++ = 0xbc;
- for (int i = 0; i < 8; ++i)
- *out++ = rsp >> (8 * i);
但是,對于上面提到的%rflags來說,有一點需要引起我們的高度注意:一旦我們在%rflags中啟用了單步標(biāo)志,CPU就會在隨后執(zhí)行的每條指令中傳遞一個調(diào)試異常。內(nèi)核將通過向進程傳遞一個SIGTRAP信號來處理調(diào)試異常。默認情況下,這個信號是通過堆棧傳遞的,而堆棧上的值就是%rsp的值……如果%rsp無效,內(nèi)核會用一個不可觸發(fā)的SIGSEGV來殺死進程。
為了處理這樣的情況,內(nèi)核提供了一個函數(shù),以便在傳遞信號時將%rsp設(shè)置為一個已知的有效值:sigaltstack()。我們要做的就是像下面這樣來調(diào)用它:
- stack_t ss = {};
- ss.ss_sp = malloc(SIGSTKSZ);
- if (!ss.ss_sp)
- error(EXIT_FAILURE, errno, "malloc()");
- ss.ss_size = SIGSTKSZ;
- ss.ss_flags = 0;
- if (sigaltstack(&ss, NULL) == -1)
- error(EXIT_FAILURE, errno, "sigaltstack()");
然后,將SA_ONSTACK傳遞給處理SIGTRAP的sigaction()調(diào)用的sa_flags變量中。
段寄存器
說到段寄存器,你會經(jīng)??吹竭@樣的說法:其實在64位上已經(jīng)不太有用了。然而,這并不是全部的事實。的確,你不能改變基地址或段大小,但幾乎所有其他的東西都還是相關(guān)的。特別是一些與我們相關(guān)的東西,例如:
· %cs、%ds、%es和%ss必須含有有效的16位段選擇器,指向GDT(全局描述符表)或LDT(局部描述符表)中的有效條目。
· %cs不能使用mov指令加載,但我們可以使用ljmp(遠/長跳轉(zhuǎn))指令。
· %cs的CPL(當(dāng)前權(quán)限級別)字段是CPU正在執(zhí)行的權(quán)限級別。通常情況下,64位用戶空間進程運行的%cs為0x33,即GDT的索引6,特權(quán)級別為3,內(nèi)核運行的%cs為0x10,即GDT的索引2,特權(quán)級別為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指令設(shè)置%ss會使CPU在mov或pop指令之后的一條指令中屏蔽中斷、NMI、斷點和單步陷阱。如果下一條指令導(dǎo)致進入內(nèi)核,這些中斷、NMI、斷點或單步陷阱將在CPU開始在內(nèi)核空間執(zhí)行后生效。這就是CVE-2018-8897的來源,內(nèi)核沒有正確處理這種情況。
LDT
由于我們可能會從LDT中加載段寄存器,所以不妨從設(shè)置LDT開始入手。由于modify_ldt()沒有g(shù)libc封裝器,所以我們必須使用syscall()函數(shù)來調(diào)用它:
- #include
- #include
- #include
- #include
- for (unsigned int i = 0; i < 4; ++i) {
- struct user_desc desc = {};
- desc.entry_number = i;
- desc.base_addr = std::uniform_int_distribution
- desc.limit = std::uniform_int_distribution
- desc.seg_32bit = std::uniform_int_distribution
- desc.contents = std::uniform_int_distribution
- desc.read_exec_only = std::uniform_int_distribution
- desc.limit_in_pages = std::uniform_int_distribution
- desc.seg_not_present = std::uniform_int_distribution
- desc.useable = std::uniform_int_distribution
- syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));
- }
我們可能要檢查這里的返回值;我們不應(yīng)該生成無效的LDT條目,所以知道我們是否存在這種條目是很有用的。
- static uint16_t get_random_segment_selector()
- {
- unsigned int index;
- switch (std::uniform_int_distribution
- case 0:
- // The LDT is small, so favour smaller indices
- index = std::uniform_int_distribution
- break;
- case 1:
- // Linux defines 32 GDT entries by default
- index = std::uniform_int_distribution
- break;
- case 2:
- // Max table size
- index = std::uniform_int_distribution
- break;
- }
- unsigned int ti = std::uniform_int_distribution
- unsigned int rpl = std::uniform_int_distribution
- return (index << 3) | (ti << 2) | rpl;
- }
數(shù)據(jù)段(%ds)
下面展示如何使用數(shù)據(jù)段:
- if (std::uniform_int_distribution
- uint16_t sel = get_random_segment_selector();
- // movw $imm, %ax
- *out++ = 0x66;
- *out++ = 0xb8;
- *out++ = sel;
- *out++ = sel >> 8;
- // movw %ax, %ds
- *out++ = 0x8e;
- *out++ = 0xd8;
- }
%fs與 %gs
對于%fs和%gs,我們需要使用系統(tǒng)調(diào)用arch_prctl()。在普通(非JIT匯編)代碼中,可以這樣使用:
- #include
- #include
- ...
- syscall(SYS_arch_prctl, ARCH_SET_FS, get_random_address());
- syscall(SYS_arch_prctl, ARCH_SET_GS, get_random_address());
不幸的是,這樣做很有可能導(dǎo)致glibc/libstdc++在任何使用線程本地存儲的代碼上崩潰(甚至在第二次get_random_address()調(diào)用時就可能發(fā)生)。如果我們想生成系統(tǒng)調(diào)用來做這件事,我們可以通過支持代碼進行協(xié)助:
- enum machine_register {
- // 0
- RAX,
- RCX,
- RDX,
- RBX,
- RSP,
- RBP,
- RSI,
- RDI,
- // 8
- R8,
- R9,
- R10,
- R11,
- R12,
- R13,
- R14,
- R15,
- };
- const unsigned int REX = 0x40;
- const unsigned int REX_B = 0x01;
- const unsigned int REX_W = 0x08;
- static uint8_t *emit_mov_imm64_reg(uint8_t *out, uint64_t imm, machine_register reg)
- {
- *out++ = REX | REX_W | (REX_B * (reg >= 8));
- *out++ = 0xb8 | (reg & 7);
- for (int i = 0; i < 8; ++i)
- *out++ = imm >> (8 * i);
- return out;
- }
- static uint8_t *emit_call_arch_prctl(uint8_t *out, int code, unsigned long addr)
- {
- // int arch_prctl(int code, unsigned long addr);
- out = emit_mov_imm64_reg(out, SYS_arch_prctl, RAX);
- out = emit_mov_imm64_reg(out, code, RDI);
- out = emit_mov_imm64_reg(out, addr, RSI);
- // syscall
- *out++ = 0x0f;
- *out++ = 0x05;
- return out;
- }
需要注意的是,除了需要一些寄存器來執(zhí)行系統(tǒng)調(diào)用本身之外,syscall指令還用返回地址(即syscall指令后的指令地址)覆蓋%rcx,所以我們可能要在做其他事情之前進行這些調(diào)用。
小結(jié)
在本文中,我們?yōu)樽x者更進一步介紹了各種標(biāo)志寄存器、堆棧指針以及部分段寄存器,在下一篇文章中,我們將為讀者介紹調(diào)試寄存器以及進入內(nèi)核的不同方法。
本文翻譯自:https://blogs.oracle.com/linux/fuzzing-the-linux-kernel-x86-entry-code%2c-part-2-of-3如若轉(zhuǎn)載,請注明原文地址
分享名稱:Linux內(nèi)核(x86)入口代碼模糊測試指南Part2(上篇)
文章地址:http://www.dlmjj.cn/article/djdijej.html


咨詢
建站咨詢
