|
@@ -0,0 +1,285 @@
|
|
|
|
+/* SPDX-License-Identifier: GPL-2.0 */
|
|
|
|
+/*
|
|
|
|
+ * mov_ss_trap.c: Exercise the bizarre side effects of a watchpoint on MOV SS
|
|
|
|
+ *
|
|
|
|
+ * This does MOV SS from a watchpointed address followed by various
|
|
|
|
+ * types of kernel entries. A MOV SS that hits a watchpoint will queue
|
|
|
|
+ * up a #DB trap but will not actually deliver that trap. The trap
|
|
|
|
+ * will be delivered after the next instruction instead. The CPU's logic
|
|
|
|
+ * seems to be:
|
|
|
|
+ *
|
|
|
|
+ * - Any fault: drop the pending #DB trap.
|
|
|
|
+ * - INT $N, INT3, INTO, SYSCALL, SYSENTER: enter the kernel and then
|
|
|
|
+ * deliver #DB.
|
|
|
|
+ * - ICEBP: enter the kernel but do not deliver the watchpoint trap
|
|
|
|
+ * - breakpoint: only one #DB is delivered (phew!)
|
|
|
|
+ *
|
|
|
|
+ * There are plenty of ways for a kernel to handle this incorrectly. This
|
|
|
|
+ * test tries to exercise all the cases.
|
|
|
|
+ *
|
|
|
|
+ * This should mostly cover CVE-2018-1087 and CVE-2018-8897.
|
|
|
|
+ */
|
|
|
|
+#define _GNU_SOURCE
|
|
|
|
+
|
|
|
|
+#include <stdlib.h>
|
|
|
|
+#include <sys/ptrace.h>
|
|
|
|
+#include <sys/types.h>
|
|
|
|
+#include <sys/wait.h>
|
|
|
|
+#include <sys/user.h>
|
|
|
|
+#include <sys/syscall.h>
|
|
|
|
+#include <unistd.h>
|
|
|
|
+#include <errno.h>
|
|
|
|
+#include <stddef.h>
|
|
|
|
+#include <stdio.h>
|
|
|
|
+#include <err.h>
|
|
|
|
+#include <string.h>
|
|
|
|
+#include <setjmp.h>
|
|
|
|
+#include <sys/prctl.h>
|
|
|
|
+
|
|
|
|
+#define X86_EFLAGS_RF (1UL << 16)
|
|
|
|
+
|
|
|
|
+#if __x86_64__
|
|
|
|
+# define REG_IP REG_RIP
|
|
|
|
+#else
|
|
|
|
+# define REG_IP REG_EIP
|
|
|
|
+#endif
|
|
|
|
+
|
|
|
|
+unsigned short ss;
|
|
|
|
+extern unsigned char breakpoint_insn[];
|
|
|
|
+sigjmp_buf jmpbuf;
|
|
|
|
+static unsigned char altstack_data[SIGSTKSZ];
|
|
|
|
+
|
|
|
|
+static void enable_watchpoint(void)
|
|
|
|
+{
|
|
|
|
+ pid_t parent = getpid();
|
|
|
|
+ int status;
|
|
|
|
+
|
|
|
|
+ pid_t child = fork();
|
|
|
|
+ if (child < 0)
|
|
|
|
+ err(1, "fork");
|
|
|
|
+
|
|
|
|
+ if (child) {
|
|
|
|
+ if (waitpid(child, &status, 0) != child)
|
|
|
|
+ err(1, "waitpid for child");
|
|
|
|
+ } else {
|
|
|
|
+ unsigned long dr0, dr1, dr7;
|
|
|
|
+
|
|
|
|
+ dr0 = (unsigned long)&ss;
|
|
|
|
+ dr1 = (unsigned long)breakpoint_insn;
|
|
|
|
+ dr7 = ((1UL << 1) | /* G0 */
|
|
|
|
+ (3UL << 16) | /* RW0 = read or write */
|
|
|
|
+ (1UL << 18) | /* LEN0 = 2 bytes */
|
|
|
|
+ (1UL << 3)); /* G1, RW1 = insn */
|
|
|
|
+
|
|
|
|
+ if (ptrace(PTRACE_ATTACH, parent, NULL, NULL) != 0)
|
|
|
|
+ err(1, "PTRACE_ATTACH");
|
|
|
|
+
|
|
|
|
+ if (waitpid(parent, &status, 0) != parent)
|
|
|
|
+ err(1, "waitpid for child");
|
|
|
|
+
|
|
|
|
+ if (ptrace(PTRACE_POKEUSER, parent, (void *)offsetof(struct user, u_debugreg[0]), dr0) != 0)
|
|
|
|
+ err(1, "PTRACE_POKEUSER DR0");
|
|
|
|
+
|
|
|
|
+ if (ptrace(PTRACE_POKEUSER, parent, (void *)offsetof(struct user, u_debugreg[1]), dr1) != 0)
|
|
|
|
+ err(1, "PTRACE_POKEUSER DR1");
|
|
|
|
+
|
|
|
|
+ if (ptrace(PTRACE_POKEUSER, parent, (void *)offsetof(struct user, u_debugreg[7]), dr7) != 0)
|
|
|
|
+ err(1, "PTRACE_POKEUSER DR7");
|
|
|
|
+
|
|
|
|
+ printf("\tDR0 = %lx, DR1 = %lx, DR7 = %lx\n", dr0, dr1, dr7);
|
|
|
|
+
|
|
|
|
+ if (ptrace(PTRACE_DETACH, parent, NULL, NULL) != 0)
|
|
|
|
+ err(1, "PTRACE_DETACH");
|
|
|
|
+
|
|
|
|
+ exit(0);
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void sethandler(int sig, void (*handler)(int, siginfo_t *, void *),
|
|
|
|
+ int flags)
|
|
|
|
+{
|
|
|
|
+ struct sigaction sa;
|
|
|
|
+ memset(&sa, 0, sizeof(sa));
|
|
|
|
+ sa.sa_sigaction = handler;
|
|
|
|
+ sa.sa_flags = SA_SIGINFO | flags;
|
|
|
|
+ sigemptyset(&sa.sa_mask);
|
|
|
|
+ if (sigaction(sig, &sa, 0))
|
|
|
|
+ err(1, "sigaction");
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static char const * const signames[] = {
|
|
|
|
+ [SIGSEGV] = "SIGSEGV",
|
|
|
|
+ [SIGBUS] = "SIBGUS",
|
|
|
|
+ [SIGTRAP] = "SIGTRAP",
|
|
|
|
+ [SIGILL] = "SIGILL",
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+static void sigtrap(int sig, siginfo_t *si, void *ctx_void)
|
|
|
|
+{
|
|
|
|
+ ucontext_t *ctx = ctx_void;
|
|
|
|
+
|
|
|
|
+ printf("\tGot SIGTRAP with RIP=%lx, EFLAGS.RF=%d\n",
|
|
|
|
+ (unsigned long)ctx->uc_mcontext.gregs[REG_IP],
|
|
|
|
+ !!(ctx->uc_mcontext.gregs[REG_EFL] & X86_EFLAGS_RF));
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void handle_and_return(int sig, siginfo_t *si, void *ctx_void)
|
|
|
|
+{
|
|
|
|
+ ucontext_t *ctx = ctx_void;
|
|
|
|
+
|
|
|
|
+ printf("\tGot %s with RIP=%lx\n", signames[sig],
|
|
|
|
+ (unsigned long)ctx->uc_mcontext.gregs[REG_IP]);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void handle_and_longjmp(int sig, siginfo_t *si, void *ctx_void)
|
|
|
|
+{
|
|
|
|
+ ucontext_t *ctx = ctx_void;
|
|
|
|
+
|
|
|
|
+ printf("\tGot %s with RIP=%lx\n", signames[sig],
|
|
|
|
+ (unsigned long)ctx->uc_mcontext.gregs[REG_IP]);
|
|
|
|
+
|
|
|
|
+ siglongjmp(jmpbuf, 1);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+int main()
|
|
|
|
+{
|
|
|
|
+ unsigned long nr;
|
|
|
|
+
|
|
|
|
+ asm volatile ("mov %%ss, %[ss]" : [ss] "=m" (ss));
|
|
|
|
+ printf("\tSS = 0x%hx, &SS = 0x%p\n", ss, &ss);
|
|
|
|
+
|
|
|
|
+ if (prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0) == 0)
|
|
|
|
+ printf("\tPR_SET_PTRACER_ANY succeeded\n");
|
|
|
|
+
|
|
|
|
+ printf("\tSet up a watchpoint\n");
|
|
|
|
+ sethandler(SIGTRAP, sigtrap, 0);
|
|
|
|
+ enable_watchpoint();
|
|
|
|
+
|
|
|
|
+ printf("[RUN]\tRead from watched memory (should get SIGTRAP)\n");
|
|
|
|
+ asm volatile ("mov %[ss], %[tmp]" : [tmp] "=r" (nr) : [ss] "m" (ss));
|
|
|
|
+
|
|
|
|
+ printf("[RUN]\tMOV SS; INT3\n");
|
|
|
|
+ asm volatile ("mov %[ss], %%ss; int3" :: [ss] "m" (ss));
|
|
|
|
+
|
|
|
|
+ printf("[RUN]\tMOV SS; INT 3\n");
|
|
|
|
+ asm volatile ("mov %[ss], %%ss; .byte 0xcd, 0x3" :: [ss] "m" (ss));
|
|
|
|
+
|
|
|
|
+ printf("[RUN]\tMOV SS; CS CS INT3\n");
|
|
|
|
+ asm volatile ("mov %[ss], %%ss; .byte 0x2e, 0x2e; int3" :: [ss] "m" (ss));
|
|
|
|
+
|
|
|
|
+ printf("[RUN]\tMOV SS; CSx14 INT3\n");
|
|
|
|
+ asm volatile ("mov %[ss], %%ss; .fill 14,1,0x2e; int3" :: [ss] "m" (ss));
|
|
|
|
+
|
|
|
|
+ printf("[RUN]\tMOV SS; INT 4\n");
|
|
|
|
+ sethandler(SIGSEGV, handle_and_return, SA_RESETHAND);
|
|
|
|
+ asm volatile ("mov %[ss], %%ss; int $4" :: [ss] "m" (ss));
|
|
|
|
+
|
|
|
|
+#ifdef __i386__
|
|
|
|
+ printf("[RUN]\tMOV SS; INTO\n");
|
|
|
|
+ sethandler(SIGSEGV, handle_and_return, SA_RESETHAND);
|
|
|
|
+ nr = -1;
|
|
|
|
+ asm volatile ("add $1, %[tmp]; mov %[ss], %%ss; into"
|
|
|
|
+ : [tmp] "+r" (nr) : [ss] "m" (ss));
|
|
|
|
+#endif
|
|
|
|
+
|
|
|
|
+ if (sigsetjmp(jmpbuf, 1) == 0) {
|
|
|
|
+ printf("[RUN]\tMOV SS; ICEBP\n");
|
|
|
|
+
|
|
|
|
+ /* Some emulators (e.g. QEMU TCG) don't emulate ICEBP. */
|
|
|
|
+ sethandler(SIGILL, handle_and_longjmp, SA_RESETHAND);
|
|
|
|
+
|
|
|
|
+ asm volatile ("mov %[ss], %%ss; .byte 0xf1" :: [ss] "m" (ss));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (sigsetjmp(jmpbuf, 1) == 0) {
|
|
|
|
+ printf("[RUN]\tMOV SS; CLI\n");
|
|
|
|
+ sethandler(SIGSEGV, handle_and_longjmp, SA_RESETHAND);
|
|
|
|
+ asm volatile ("mov %[ss], %%ss; cli" :: [ss] "m" (ss));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (sigsetjmp(jmpbuf, 1) == 0) {
|
|
|
|
+ printf("[RUN]\tMOV SS; #PF\n");
|
|
|
|
+ sethandler(SIGSEGV, handle_and_longjmp, SA_RESETHAND);
|
|
|
|
+ asm volatile ("mov %[ss], %%ss; mov (-1), %[tmp]"
|
|
|
|
+ : [tmp] "=r" (nr) : [ss] "m" (ss));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /*
|
|
|
|
+ * INT $1: if #DB has DPL=3 and there isn't special handling,
|
|
|
|
+ * then the kernel will die.
|
|
|
|
+ */
|
|
|
|
+ if (sigsetjmp(jmpbuf, 1) == 0) {
|
|
|
|
+ printf("[RUN]\tMOV SS; INT 1\n");
|
|
|
|
+ sethandler(SIGSEGV, handle_and_longjmp, SA_RESETHAND);
|
|
|
|
+ asm volatile ("mov %[ss], %%ss; int $1" :: [ss] "m" (ss));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+#ifdef __x86_64__
|
|
|
|
+ /*
|
|
|
|
+ * In principle, we should test 32-bit SYSCALL as well, but
|
|
|
|
+ * the calling convention is so unpredictable that it's
|
|
|
|
+ * not obviously worth the effort.
|
|
|
|
+ */
|
|
|
|
+ if (sigsetjmp(jmpbuf, 1) == 0) {
|
|
|
|
+ printf("[RUN]\tMOV SS; SYSCALL\n");
|
|
|
|
+ sethandler(SIGILL, handle_and_longjmp, SA_RESETHAND);
|
|
|
|
+ nr = SYS_getpid;
|
|
|
|
+ /*
|
|
|
|
+ * Toggle the high bit of RSP to make it noncanonical to
|
|
|
|
+ * strengthen this test on non-SMAP systems.
|
|
|
|
+ */
|
|
|
|
+ asm volatile ("btc $63, %%rsp\n\t"
|
|
|
|
+ "mov %[ss], %%ss; syscall\n\t"
|
|
|
|
+ "btc $63, %%rsp"
|
|
|
|
+ : "+a" (nr) : [ss] "m" (ss)
|
|
|
|
+ : "rcx"
|
|
|
|
+#ifdef __x86_64__
|
|
|
|
+ , "r11"
|
|
|
|
+#endif
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+#endif
|
|
|
|
+
|
|
|
|
+ printf("[RUN]\tMOV SS; breakpointed NOP\n");
|
|
|
|
+ asm volatile ("mov %[ss], %%ss; breakpoint_insn: nop" :: [ss] "m" (ss));
|
|
|
|
+
|
|
|
|
+ /*
|
|
|
|
+ * Invoking SYSENTER directly breaks all the rules. Just handle
|
|
|
|
+ * the SIGSEGV.
|
|
|
|
+ */
|
|
|
|
+ if (sigsetjmp(jmpbuf, 1) == 0) {
|
|
|
|
+ printf("[RUN]\tMOV SS; SYSENTER\n");
|
|
|
|
+ stack_t stack = {
|
|
|
|
+ .ss_sp = altstack_data,
|
|
|
|
+ .ss_size = SIGSTKSZ,
|
|
|
|
+ };
|
|
|
|
+ if (sigaltstack(&stack, NULL) != 0)
|
|
|
|
+ err(1, "sigaltstack");
|
|
|
|
+ sethandler(SIGSEGV, handle_and_longjmp, SA_RESETHAND | SA_ONSTACK);
|
|
|
|
+ nr = SYS_getpid;
|
|
|
|
+ asm volatile ("mov %[ss], %%ss; SYSENTER" : "+a" (nr)
|
|
|
|
+ : [ss] "m" (ss) : "flags", "rcx"
|
|
|
|
+#ifdef __x86_64__
|
|
|
|
+ , "r11"
|
|
|
|
+#endif
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ /* We're unreachable here. SYSENTER forgets RIP. */
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (sigsetjmp(jmpbuf, 1) == 0) {
|
|
|
|
+ printf("[RUN]\tMOV SS; INT $0x80\n");
|
|
|
|
+ sethandler(SIGSEGV, handle_and_longjmp, SA_RESETHAND);
|
|
|
|
+ nr = 20; /* compat getpid */
|
|
|
|
+ asm volatile ("mov %[ss], %%ss; int $0x80"
|
|
|
|
+ : "+a" (nr) : [ss] "m" (ss)
|
|
|
|
+ : "flags"
|
|
|
|
+#ifdef __x86_64__
|
|
|
|
+ , "r8", "r9", "r10", "r11"
|
|
|
|
+#endif
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ printf("[OK]\tI aten't dead\n");
|
|
|
|
+ return 0;
|
|
|
|
+}
|