Linux內(nèi)核(x86)入口代碼模糊測試指南Part 2 (上篇)
- 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)容壓入堆棧上,然后直接修改堆棧上的標志值,再將該值彈出到%rflags中。實際上,我們在這里可以選擇使用orq或者xorq指令;我選擇xorq,因為它可以切換寄存器中的任何值。這樣一來,如果我們連續(xù)進行多次系統(tǒng)調(diào)用(或內(nèi)核入口),我們可以隨機切換標志,而不必關心現(xiàn)有的值是什么。
- // 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;
如果我們不希望進程在設置陷阱標志時立即被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)");
- 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);
- ...
- }
- 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中啟用了單步標志,CPU就會在隨后執(zhí)行的每條指令中傳遞一個調(diào)試異常。內(nèi)核將通過向進程傳遞一個SIGTRAP信號來處理調(diào)試異常。默認情況下,這個信號是通過堆棧傳遞的,而堆棧上的值就是%rsp的值……如果%rsp無效,內(nèi)核會用一個不可觸發(fā)的SIGSEGV來殺死進程。
- 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()");
- #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));
- }
我們可能要檢查這里的返回值;我們不應該生成無效的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;
- }
- 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;
- }
- #include
- #include
- ...
- syscall(SYS_arch_prctl, ARCH_SET_FS, get_random_address());
- syscall(SYS_arch_prctl, ARCH_SET_GS, get_random_address());
不幸的是,這樣做很有可能導致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)用。
本文翻譯自:https://blogs.oracle.com/linux/fuzzing-the-linux-kernel-x86-entry-code%2c-part-2-of-3如若轉(zhuǎn)載,請注明原文地址