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

成都創(chuàng)新互聯(lián)公司一直秉承“誠(chéng)信做人,踏實(shí)做事”的原則,不欺瞞客戶(hù),是我們最起碼的底線(xiàn)! 以服務(wù)為基礎(chǔ),以質(zhì)量求生存,以技術(shù)求發(fā)展,成交一個(gè)客戶(hù)多一個(gè)朋友!為您提供成都網(wǎng)站制作、成都做網(wǎng)站、外貿(mào)營(yíng)銷(xiāo)網(wǎng)站建設(shè)、成都網(wǎng)頁(yè)設(shè)計(jì)、成都微信小程序、成都網(wǎng)站開(kāi)發(fā)、成都網(wǎng)站制作、成都軟件開(kāi)發(fā)、app軟件定制開(kāi)發(fā)是成都本地專(zhuān)業(yè)的網(wǎng)站建設(shè)和網(wǎng)站設(shè)計(jì)公司,等你一起來(lái)見(jiàn)證!
更多標(biāo)志(%rflags)
方向標(biāo)志只是我們眾多感興趣的標(biāo)志之一。維基百科上關(guān)于%rflags的文章列出了我們感興趣的其他一些標(biāo)志:
· bit 8:陷阱標(biāo)志(用于單步調(diào)試)
· bit 18:對(duì)齊檢查
大多數(shù)與算術(shù)相關(guān)的標(biāo)志(進(jìn)位標(biāo)志等)并不是我們感興趣的對(duì)象,因?yàn)樗鼈冊(cè)谄胀ùa的正常運(yùn)行過(guò)程中變化較大,這意味著內(nèi)核對(duì)這些標(biāo)志的處理很可能已經(jīng)過(guò)了充分的測(cè)試。而另外一些標(biāo)志(如中斷啟用標(biāo)志)可能無(wú)法被用戶(hù)空間修改,所以即使嘗試也沒(méi)什么用。
我們需要重點(diǎn)關(guān)注陷阱標(biāo)志,因?yàn)樵O(shè)置該標(biāo)志后,CPU在每條指令后都會(huì)傳遞一個(gè)調(diào)試異常,自然也會(huì)干擾輸入代碼的正常運(yùn)行。
對(duì)齊檢查標(biāo)志也應(yīng)當(dāng)重點(diǎn)關(guān)注,因?yàn)楫?dāng)一個(gè)錯(cuò)誤對(duì)齊的指針被解除引用時(shí),它會(huì)使CPU傳遞一個(gè)對(duì)齊檢查異常。雖然CPU在0環(huán)中執(zhí)行時(shí)不應(yīng)該執(zhí)行對(duì)齊檢查,但是檢查是否存在因?yàn)閷?duì)齊檢查異常而進(jìn)入內(nèi)核的相關(guān)漏洞還是很有意思的(我們稍后再談)。
維基百科的文章給出了修改這些標(biāo)志的程序,但我們可以做得更好一點(diǎn)。
- 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中。實(shí)際上,我們?cè)谶@里可以選擇使用orq或者xorq指令;我選擇xorq,因?yàn)樗梢郧袚Q寄存器中的任何值。這樣一來(lái),如果我們連續(xù)進(jìn)行多次系統(tǒng)調(diào)用(或內(nèi)核入口),我們可以隨機(jī)切換標(biāo)志,而不必關(guān)心現(xiàn)有的值是什么。
既然我們無(wú)論如何都要修改%rflags寄存器,那么我們不妨把方向標(biāo)志的修改納入進(jìn)去,把三個(gè)標(biāo)志的修改合并到一條指令中。雖然這是一個(gè)很小的優(yōu)化,但沒(méi)有理由不這么做,最后的結(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;
如果我們不希望進(jìn)程在設(shè)置陷阱標(biāo)志時(shí)立即被SIGTRAP殺死,我們需要注冊(cè)一個(gè)信號(hào)處理程序來(lái)有效地忽略這個(gè)信號(hào)(顯然使用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之后,我們其實(shí)就不需使用堆棧了,這意味著我們可以在不影響程序執(zhí)行的情況下,自由地更改棧指針。不過(guò)我們?yōu)槭裁匆薷臈V羔樐兀績(jī)?nèi)核又不會(huì)用我們的用戶(hù)空間棧來(lái)做任何事情,對(duì)吧?事實(shí)上,它可能會(huì)。
像ftrace和perf這樣的調(diào)試工具偶爾會(huì)在系統(tǒng)調(diào)用跟蹤期間取消對(duì)用戶(hù)空間堆棧的引用。事實(shí)上,我在這方面至少發(fā)現(xiàn)了兩個(gè)不同的漏洞:
· report 1 (July 16, 2019),
· report 2 (May 10, 2020).
當(dāng)向用戶(hù)空間傳遞信號(hào)時(shí),信號(hào)處理程序的堆棧幀由內(nèi)核創(chuàng)建,通常位于被中斷線(xiàn)程的當(dāng)前堆棧指針的上方。
如果由于某些錯(cuò)誤,%rsp會(huì)被內(nèi)核直接訪(fǎng)問(wèn),那么在正常操作期間可能不會(huì)被注意到,因?yàn)槎褩V羔樛ǔ?偸侵赶蛞粋€(gè)有效地址。要捕捉這種漏洞,我們可以簡(jiǎn)單地將其指向一個(gè)非映射地址(甚至是內(nèi)核地址?。?/p>
為了幫助我們測(cè)試堆棧指針的各種可能感興趣的值,我們可以定義一個(gè)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);
- ...
- }
在這里,我使用了自己機(jī)器上的/proc/kallsyms中找到的一些內(nèi)核指針。它們不一定是很好的選擇,只是用于演示。正如我前面所提到的,我們需要找到一個(gè)平衡點(diǎn),既要選擇那些瘋狂到?jīng)]有人想過(guò)要處理它們的值(我們畢竟在這里試圖尋找的是邊緣案例),又要不迷失在巨大的非目標(biāo)值的海洋中;我們可以統(tǒng)一選擇隨機(jī)的64位值,但這很難帶來(lái)任何有效的指針(其中大部分可能是非規(guī)范的地址)。模糊測(cè)試的部分藝術(shù)是通過(guò)對(duì)哪些有可能和哪些不可能的關(guān)系進(jìn)行有根據(jù)的猜測(cè)來(lái)抽出相關(guān)的邊緣案例。
現(xiàn)在只是設(shè)置值的問(wèn)題,幸運(yùn)的是,我們可以直接將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);
但是,對(duì)于上面提到的%rflags來(lái)說(shuō),有一點(diǎn)需要引起我們的高度注意:一旦我們?cè)?rflags中啟用了單步標(biāo)志,CPU就會(huì)在隨后執(zhí)行的每條指令中傳遞一個(gè)調(diào)試異常。內(nèi)核將通過(guò)向進(jìn)程傳遞一個(gè)SIGTRAP信號(hào)來(lái)處理調(diào)試異常。默認(rèn)情況下,這個(gè)信號(hào)是通過(guò)堆棧傳遞的,而堆棧上的值就是%rsp的值……如果%rsp無(wú)效,內(nèi)核會(huì)用一個(gè)不可觸發(fā)的SIGSEGV來(lái)殺死進(jìn)程。
為了處理這樣的情況,內(nèi)核提供了一個(gè)函數(shù),以便在傳遞信號(hào)時(shí)將%rsp設(shè)置為一個(gè)已知的有效值:sigaltstack()。我們要做的就是像下面這樣來(lái)調(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變量中。
段寄存器
說(shuō)到段寄存器,你會(huì)經(jīng)??吹竭@樣的說(shuō)法:其實(shí)在64位上已經(jīng)不太有用了。然而,這并不是全部的事實(shí)。的確,你不能改變基地址或段大小,但幾乎所有其他的東西都還是相關(guān)的。特別是一些與我們相關(guān)的東西,例如:
· %cs、%ds、%es和%ss必須含有有效的16位段選擇器,指向GDT(全局描述符表)或LDT(局部描述符表)中的有效條目。
· %cs不能使用mov指令加載,但我們可以使用ljmp(遠(yuǎn)/長(zhǎng)跳轉(zhuǎn))指令。
· %cs的CPL(當(dāng)前權(quán)限級(jí)別)字段是CPU正在執(zhí)行的權(quán)限級(jí)別。通常情況下,64位用戶(hù)空間進(jìn)程運(yùn)行的%cs為0x33,即GDT的索引6,特權(quán)級(jí)別為3,內(nèi)核運(yùn)行的%cs為0x10,即GDT的索引2,特權(quán)級(jí)別為0(因此稱(chēng)為ring 0)。
· 實(shí)際上我們可以使用modify_ldt()系統(tǒng)調(diào)用在LDT中安裝條目,但要注意的是,內(nèi)核會(huì)對(duì)條目進(jìn)行消毒,所以我們不能創(chuàng)建一個(gè)指向DPL 0的段的調(diào)用門(mén)。
· %fs和%gs的基地址是由MSRs指定的。這些寄存器通常分別用于用戶(hù)空間進(jìn)程和內(nèi)核的TLS(線(xiàn)程本地存儲(chǔ))和per-CPU數(shù)據(jù)。我們可以使用arch_prctl()系統(tǒng)調(diào)用來(lái)改變這些寄存器的值。在某些CPU/內(nèi)核上,我們可以使用wrfsbase和wrgsbase指令。
· 使用mov或pop指令設(shè)置%ss會(huì)使CPU在mov或pop指令之后的一條指令中屏蔽中斷、NMI、斷點(diǎn)和單步陷阱。如果下一條指令導(dǎo)致進(jìn)入內(nèi)核,這些中斷、NMI、斷點(diǎn)或單步陷阱將在CPU開(kāi)始在內(nèi)核空間執(zhí)行后生效。這就是CVE-2018-8897的來(lái)源,內(nèi)核沒(méi)有正確處理這種情況。
LDT
由于我們可能會(huì)從LDT中加載段寄存器,所以不妨從設(shè)置LDT開(kāi)始入手。由于modify_ldt()沒(méi)有g(shù)libc封裝器,所以我們必須使用syscall()函數(shù)來(lái)調(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)該生成無(wú)效的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
對(duì)于%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++在任何使用線(xiàn)程本地存儲(chǔ)的代碼上崩潰(甚至在第二次get_random_address()調(diào)用時(shí)就可能發(fā)生)。如果我們想生成系統(tǒng)調(diào)用來(lái)做這件事,我們可以通過(guò)支持代碼進(jìn)行協(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;
- }
需要注意的是,除了需要一些寄存器來(lái)執(zhí)行系統(tǒng)調(diào)用本身之外,syscall指令還用返回地址(即syscall指令后的指令地址)覆蓋%rcx,所以我們可能要在做其他事情之前進(jìn)行這些調(diào)用。
小結(jié)
在本文中,我們?yōu)樽x者更進(jìn)一步介紹了各種標(biāo)志寄存器、堆棧指針以及部分段寄存器,在下一篇文章中,我們將為讀者介紹調(diào)試寄存器以及進(jìn)入內(nèi)核的不同方法。
本文翻譯自:https://blogs.oracle.com/linux/fuzzing-the-linux-kernel-x86-entry-code%2c-part-2-of-3如若轉(zhuǎn)載,請(qǐng)注明原文地址
新聞標(biāo)題:Linux內(nèi)核(x86)入口代碼模糊測(cè)試指南Part2(上篇)
URL鏈接:http://www.5511xx.com/article/djdijej.html


咨詢(xún)
建站咨詢(xún)
