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

成都創(chuàng)新互聯(lián)公司堅(jiān)持“要么做到,要么別承諾”的工作理念,服務(wù)領(lǐng)域包括:成都網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣等服務(wù),滿(mǎn)足客戶(hù)于互聯(lián)網(wǎng)時(shí)代的灌南網(wǎng)站設(shè)計(jì)、移動(dòng)媒體設(shè)計(jì)的需求,幫助企業(yè)找到有效的互聯(lián)網(wǎng)解決方案。努力成為您成熟可靠的網(wǎng)絡(luò)建設(shè)合作伙伴!
堆棧段(%ss)
寄存器%ss應(yīng)該是我們?cè)谶M(jìn)入內(nèi)核的指令之前設(shè)置的最后一個(gè)寄存器,這樣我們就可以確??吹饺魏窝舆t陷阱或異常的影響。我們可以使用與上面%ds相同的代碼;我們不使用popw %ss的原因是,我們可能已經(jīng)將%rsp設(shè)置為指向一個(gè)“奇怪”的位置,所以此時(shí)堆??赡軣o(wú)法使用。
32位兼容模式(%cs)
有趣的是:你實(shí)際上可以在執(zhí)行過(guò)程中把你的64位進(jìn)程改變成32位進(jìn)程,甚至不需要告訴內(nèi)核。CPU包含了一種機(jī)制,在ring 3模式下是允許的:遠(yuǎn)跳轉(zhuǎn)指令。
特別是,我們要使用的指令是“絕對(duì)間接遠(yuǎn)跳轉(zhuǎn)指令,地址由m16:32給出”。由于要弄清楚具體的語(yǔ)法和字節(jié)可能有點(diǎn)麻煩,所以下面將借助于一個(gè)完整的匯編例子進(jìn)行解釋。
- .global main
- main:
- ljmpl *target
- 1:
- .code32
- movl $1, %eax # __NR_exit == 1 from asm/unistd_32.h
- movl $2, %ebx # status == 0
- sysenter
- ret
- .data
- target:
- .long 1b # address (32 bits)
- .word 0x23 # segment selector (16 bits)
這里,ljmpl指令使用target標(biāo)簽處的內(nèi)存,該標(biāo)簽是一個(gè)32位指令指針,后跟一個(gè)16位段選擇器(這里指向用戶(hù)空間的32位代碼段0x23)。這里的目標(biāo)地址1b不是十六進(jìn)制值,它實(shí)際上是對(duì)標(biāo)簽1的引用;b代表“向后”。這個(gè)標(biāo)簽處的代碼是32位的,這就是為什么我們使用sysenter,而不是以前使用的syscall。調(diào)用約定也不同,實(shí)際上,我們需要使用32位ABI中的系統(tǒng)調(diào)用號(hào)(SYS_exit在64位系統(tǒng)上是60,但這里是1)。另一個(gè)有趣的事情是,如果你嘗試在strace下運(yùn)行這段代碼,將會(huì)看到如下所示的結(jié)果:
- [...]
- 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
- +++ exited with 0 +++
strace顯然認(rèn)為我們?nèi)匀皇且粋€(gè)64位進(jìn)程,并認(rèn)為我們調(diào)用了write(),而實(shí)際上我們是在調(diào)用exit()(最后一行就證明了這一點(diǎn),它清楚地告訴我們進(jìn)程退出了)。
由于ljmp的內(nèi)存操作數(shù)和目標(biāo)地址都是32位的,我們需要確保它們都位于高32位都為0的地址中,最好的方法是使用mmap()和MAP_32BIT標(biāo)志來(lái)分配內(nèi)存。
- struct ljmp_target {
- uint32_t rip;
- uint16_t cs;
- } __attribute__((packed));
- struct data {
- struct ljmp_target ljmp;
- };
- static struct data *data;
- int main(...)
- {
- ...
- void *addr = mmap(NULL, PAGE_SIZE,
- PROT_READ | PROT_WRITE,
- MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT,
- -1, 0);
- if (addr == MAP_FAILED)
- error(EXIT_FAILURE, errno, "mmap()");
- data = (struct data *) addr;
- ...
- }
- void emit_code()
- {
- ...
- // ljmp *target
- *out++ = 0xff;
- *out++ = 0x2c;
- *out++ = 0x25;
- for (unsigned int i = 0; i < 4; ++i)
- *out++ = ((uint64_t) &data->ljmp) >> (8 * i);
- // cs:rip (jump target; in our case, the next instruction)
- data->ljmp.cs = 0x23;
- data->ljmp.rip = (uint64_t) out;
- ...
- }
這里有幾件事需要注意:
這將改變CPU模式,這意味著后續(xù)指令必須在32位中有效(否則,您可能會(huì)得到一般保護(hù)故障或無(wú)效操作碼異常)。
上面我們用來(lái)加載段寄存器的指令序列(例如movw ..., %ax; movw %ax, %ss)在32位和64位上有完全相同的編碼,所以我們可以在切換到32位代碼段后毫不費(fèi)力地執(zhí)行它——這對(duì)于確保我們?cè)谶M(jìn)入內(nèi)核之前仍然可以加載%ss特別有用。
我們可以選擇是否始終更改為段4(段選擇器0x23),或者嘗試更改為隨機(jī)段選擇器(例如使用get_random_segment_selector())。如果我們選擇一個(gè)隨機(jī)的,我們甚至可能不知道我們是仍然在32位還是64位模式下執(zhí)行。
我們可能希望在從內(nèi)核返回后嘗試跳回我們的正常代碼段(段6,段選擇器0x33),如果我們沒(méi)有退出、崩潰或被殺死的話(huà)。對(duì)于不同的段選擇器,該過(guò)程完全相同。
調(diào)試寄存器(%dr0等)
x86上的調(diào)試寄存器用于設(shè)置代碼斷點(diǎn)和數(shù)據(jù)觀察點(diǎn)。寄存器%dr0到%dr3用于設(shè)置實(shí)際的斷點(diǎn)/觀察點(diǎn)地址,寄存器%dr7用于控制這四個(gè)地址的使用方式(是斷點(diǎn)還是觀察點(diǎn)等)。
設(shè)置調(diào)試寄存器比我們目前看到的要棘手一些,因?yàn)槟悴荒苤苯釉谟脩?hù)空間加載它們。就像修改LDT一樣,內(nèi)核要確保我們不會(huì)在內(nèi)核地址上設(shè)置斷點(diǎn)或觀察點(diǎn),但更重要的是,CPU本身不允許ring 3直接修改這些寄存器。我所知道的設(shè)置調(diào)試寄存器的唯一方法就是使用ptrace()。
ptrace()是一個(gè)非常難用的API。有很多隱含的狀態(tài)需要跟蹤器手動(dòng)跟蹤,還有很多圍繞信號(hào)處理的邊緣情況。幸運(yùn)的是,在這種情況下,我們只需要附加到子進(jìn)程,設(shè)置調(diào)試寄存器,然后脫離即可;即使在我們停止跟蹤之后,調(diào)試寄存器的變化也會(huì)持續(xù)存在。
- #include
- #include
- #include
- #include
- int main(...)
- {
- pid_t child = fork();
- if (child == -1)
- error(EXIT_FAILURE, errno, "fork()");
- if (child == 0) {
- // make us a tracee of the parent
- if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1)
- error(EXIT_FAILURE, errno, "ptrace(PTRACE_TRACEME)");
- // give the parent control
- raise(SIGTRAP);
- ...
- exit(EXIT_SUCCESS);
- }
- // parent; wait for child to stop
- while (1) {
- int status;
- if (waitpid(child, &status, 0) == -1) {
- if (errno == EINTR)
- continue;
- error(EXIT_FAILURE, errno, "waitpid()");
- }
- if (WIFEXITED(status))
- exit(WEXITSTATUS(status));
- if (WIFSIGNALED(status))
- exit(EXIT_FAILURE);
- if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP)
- break;
- continue;
- }
- // set debug registers and stop tracing
- if (ptrace(PTRACE_POKEUSER, child, offsetof(struct user, u_debugreg[0]), ...) == -1)
- error(EXIT_FAILURE, errno, "ptrace(PTRACE_POKEUSER)");
- if (ptrace(PTRACE_POKEUSER, child, offsetof(struct user, u_debugreg[7]), ...) == -1)
- error(EXIT_FAILURE, errno, "ptrace(PTRACE_POKEUSER)");
- if (ptrace(PTRACE_DETACH, child, 0, 0) == -1)
- error(EXIT_FAILURE, errno, "ptrace(PTRACE_DETACH)");
- ...
- }
即使在這個(gè)小例子中,等待子程序停止也是有點(diǎn)麻煩的。waitpid()總是有可能在子程序到達(dá)raise(SIGTRAP)之前返回,例如,如果它被某個(gè)外部進(jìn)程殺死了。我們對(duì)這些情況的處理方式也是簡(jiǎn)單的退出。
由于設(shè)置調(diào)試寄存器需要跟蹤,處理信號(hào)被進(jìn)行多次上下文切換(這些都很慢),我建議對(duì)每個(gè)子進(jìn)程只做一次,然后讓子進(jìn)程連續(xù)多次嘗試進(jìn)入內(nèi)核。
設(shè)置任何一個(gè)調(diào)試寄存器都可能失敗,所以在實(shí)際的fuzzer中,我們可能希望忽略所有錯(cuò)誤,每次將%dr7設(shè)置為一個(gè)斷點(diǎn),例如:
- // stddef.h offsetof() doesn't always allow non-const array indices,
- // so precompute them here.
- const unsigned int debugreg_offsets[] = {
- offsetof(struct user, u_debugreg[0]),
- offsetof(struct user, u_debugreg[1]),
- offsetof(struct user, u_debugreg[2]),
- offsetof(struct user, u_debugreg[3]),
- };
- for (unsigned int i = 0; i < 4; ++i) {
- // try random addresses until we succeed
- while (true) {
- unsigned long addr = get_random_address();
- if (ptrace(PTRACE_POKEUSER, child, debugreg_offsets[i], addr) != -1)
- break;
- }
- // Condition:
- // 0 - execution
- // 1 - write
- // 2 - (unused)
- // 3 - read or write
- unsigned int condition = std::uniform_int_distribution
- if (condition == 2)
- condition = 3;
- // Size
- // 0 - 1 byte
- // 1 - 2 bytes
- // 2 - 8 bytes
- // 3 - 4 bytes
- unsigned int size = std::uniform_int_distribution
- unsigned long dr7 = ptrace(PTRACE_PEEKUSER, child, offsetof(struct user, u_debugreg[7]), 0);
- dr7 &= ~((1 | (3 << 16) | (3 << 18)) << i);
- dr7 |= (1 | (condition << 16) | (size << 18)) << i;
- ptrace(PTRACE_POKEUSER, child, offsetof(struct user, u_debugreg[7]), dr7);
- }
進(jìn)入內(nèi)核
在本系列的第一篇文章中,我們已經(jīng)看到了如何進(jìn)行系統(tǒng)調(diào)用的代碼;在這里,我們使用相同的基本方法,但也考慮到所有其他進(jìn)入內(nèi)核的方式。正如我前面提到的,syscall指令不是進(jìn)入64位內(nèi)核的唯一方法,甚至不是進(jìn)行系統(tǒng)調(diào)用的唯一方法。對(duì)于系統(tǒng)調(diào)用,我們有以下選項(xiàng):
- int $0x80
- sysenter
- syscall
實(shí)際上,查看硬件生成的異常表也很有用。其中許多異常的處理方式與系統(tǒng)調(diào)用和常規(guī)中斷略有不同;例如,當(dāng)您試圖加載一個(gè)帶有無(wú)效段選擇器的段寄存器時(shí),CPU會(huì)將一個(gè)錯(cuò)誤代碼壓入(內(nèi)核)堆棧上。
我們可以觸發(fā)許多異常,但不是所有的異常。例如,通過(guò)簡(jiǎn)單地執(zhí)行除零來(lái)生成除零異常是非常簡(jiǎn)單的,但是我們不能輕松地按需生成NMI。(也就是說(shuō),我們可以做一些事情來(lái)使NMI更有可能發(fā)生,盡管是以一種不可控制的方式:如果我們?cè)赩M中測(cè)試內(nèi)核,我們可以從主機(jī)注入NMI,或者我們可以啟用內(nèi)核NMI watchdog功能。)
- enum entry_type {
- // system calls + software interrupts
- ENTRY_SYSCALL,
- ENTRY_SYSENTER,
- ENTRY_INT,
- ENTRY_INT_80,
- ENTRY_INT3,
- // exceptions
- ENTRY_DE, // Divide error
- ENTRY_OF, // Overflow
- ENTRY_BR, // Bound range exceeded
- ENTRY_UD, // Undefined opcode
- ENTRY_SS, // Stack segment fault
- ENTRY_GP, // General protection fault
- ENTRY_PF, // Page fault
- ENTRY_MF, // x87 floating-point exception
- ENTRY_AC, // Alignment check
- NR_ENTRY_TYPES,
- };
- enum entry_type type = (enum entry_type) std::uniform_int_distribution
- // Some entry types require a setup/preamble; do that here
- switch (type) {
- case ENTRY_DE:
- // xor %eax, %eax
- *out++ = 0x31;
- *out++ = 0xc0;
- break;
- case ENTRY_MF:
- // pxor %xmm0, %xmm0
- *out++ = 0x66;
- *out++ = 0x0f;
- *out++ = 0xef;
- *out++ = 0xc0;
- break;
- case ENTRY_BR:
- // xor %eax, %eax
- *out++ = 0x31;
- *out++ = 0xc0;
- break;
- case ENTRY_SS:
- {
- uint16_t sel = get_random_segment_selector();
- // movw $imm, %bx
- *out++ = 0x66;
- *out++ = 0xbb;
- *out++ = sel;
- *out++ = sel >> 8;
- }
- break;
- default:
- // do nothing
- break;
- }
- ...
- switch (type) {
- // system calls + software interrupts
- case ENTRY_SYSCALL:
- // syscall
- *out++ = 0x0f;
- *out++ = 0x05;
- break;
- case ENTRY_SYSENTER:
- // sysenter
- *out++ = 0x0f;
- *out++ = 0x34;
- break;
- case ENTRY_INT:
- // int $x
- *out++ = 0xcd;
- *out++ = std::uniform_int_distribution
- break;
- case ENTRY_INT_80:
- // int $0x80
- *out++ = 0xcd;
- *out++ = 0x80;
- break;
- case ENTRY_INT3:
- // int3
- *out++ = 0xcc;
- break;
- // exceptions
- case ENTRY_DE:
- // div %eax
- *out++ = 0xf7;
- *out++ = 0xf0;
- break;
- case ENTRY_OF:
- // into (32-bit only!)
- *out++ = 0xce;
- break;
- case ENTRY_BR:
- // bound %eax, data
- *out++ = 0x62;
- *out++ = 0x05;
- *out++ = 0x09;
- for (unsigned int i = 0; i < 4; ++i)
- *out++ = ((uint64_t) &data->bound) >> (8 * i);
- break;
- case ENTRY_UD:
- // ud2
- *out++ = 0x0f;
- *out++ = 0x0b;
- break;
- case ENTRY_SS:
- // Load %ss again, with a random segment selector (this is not
- // guaranteed to raise #SS, but most likely it will). The reason
- // we don't just rely on the load above to do it is that it could
- // be interesting to trigger #SS with a "weird" %ss too.
- // movw %bx, %ss
- *out++ = 0x8e;
- *out++ = 0xd3;
- break;
- case ENTRY_GP:
- // wrmsr
- *out++ = 0x0f;
- *out++ = 0x30;
- break;
- case ENTRY_PF:
- // testl %eax, (xxxxxxxx)
- *out++ = 0x85;
- *out++ = 0x04;
- *out++ = 0x25;
- for (int i = 0; i < 4; ++i)
- *out++ = ((uint64_t) page_not_present) >> (8 * i);
- break;
- case ENTRY_MF:
- // divss %xmm0, %xmm0
- *out++ = 0xf3;
- *out++ = 0x0f;
- *out++ = 0x5e;
- *out++ = 0xc0;
- break;
- case ENTRY_AC:
- // testl %eax, (page_not_writable + 1)
- *out++ = 0x85;
- *out++ = 0x04;
- *out++ = 0x25;
- for (int i = 0; i < 4; ++i)
- *out++ = ((uint64_t) page_not_writable + 1) >> (8 * i);
- break;
- }
小結(jié)
我們現(xiàn)在幾乎擁有了所有的東西,我們需要真正開(kāi)始進(jìn)行模糊測(cè)試了!不過(guò)還有幾件事要做……
如果你運(yùn)行目前的代碼,很快就會(huì)遇到一些問(wèn)題。首先,我們使用的許多指令可能會(huì)導(dǎo)致崩潰(而且是故意的),這使得fuzzer速度很慢。通過(guò)為一些常見(jiàn)的終止信號(hào)(SIGBUS、SIGSEGV等)安裝信號(hào)處理程序,我們可以跳過(guò)故障指令,(希望)在同一個(gè)子進(jìn)程內(nèi)繼續(xù)執(zhí)行。
其次,我們進(jìn)行的一些系統(tǒng)調(diào)用可能會(huì)產(chǎn)生意想不到的副作用。特別是,我們并不希望在I/O上進(jìn)行阻塞,因?yàn)檫@將使fuzzer停止運(yùn)行。一種解決方法是安裝一個(gè)間隔定時(shí)器報(bào)警,以檢測(cè)子進(jìn)程何時(shí)掛起。另一種方法可以是過(guò)濾掉某些已知會(huì)阻塞的系統(tǒng)調(diào)用(如read()、select()、sleep()等)。其他“不幸”的系統(tǒng)調(diào)用可能是fork()、exit()和kill()。fuzzer刪除文件或以其他方式擾亂系統(tǒng)的可能性較小,但我們?nèi)孕枰褂媚撤N形式的沙盒(如setuid(65534))。
如果你只是想看看最終的結(jié)果,這里有一個(gè)代碼的鏈接:
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)載,請(qǐng)注明原文地址。
文章標(biāo)題:Linux內(nèi)核(x86)入口代碼模糊測(cè)試指南Part2(下篇)
鏈接URL:http://m.fisionsoft.com.cn/article/dpsggjj.html


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