- 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的内容压入堆栈上,然后直接修改堆栈上的标志值,再将该值弹出到%rflags中。实际上,我们在这里可以选择使用orq或者xorq指令;我选择xorq,因为它可以切换寄存器中的任何值。这样一来,如果我们连续进行多次系统调用(或内核入口),我们可以随机切换标志,而不必关心现有的值是什么。
- // 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就会在随后执行的每条指令中传递一个调试异常。内核将通过向进程传递一个SIGTRAP信号来处理调试异常。默认情况下,这个信号是通过堆栈传递的,而堆栈上的值就是%rsp的值……如果%rsp无效,内核会用一个不可触发的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()调用时就可能发生)。如果我们想生成系统调用来做这件事,我们可以通过支持代码进行协助:
- 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;
- }
需要注意的是,除了需要一些寄存器来执行系统调用本身之外,syscall指令还用返回地址(即syscall指令后的指令地址)覆盖%rcx,所以我们可能要在做其他事情之前进行这些调用。
本文翻译自:https://blogs.oracle.com/linux/fuzzing-the-linux-kernel-x86-entry-code%2c-part-2-of-3如若转载,请注明原文地址