From 7b634f252379dc2ed671fa0eaee96a57db1fd418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Michelland?= <sebastien.michelland@lcis.grenoble-inp.fr> Date: Fri, 29 Dec 2023 01:06:18 +0100 Subject: [PATCH] test setup and scripts (for campaigns only) --- Dockerfile | 32 ++-- Makefile | 109 +++++++++++ README.md | 61 +++++- fault.py | 501 +++++++++++++++++++++++++++++++++++++++++++++++++ riscv_cc_FSH | 13 ++ riscv_cc_REF | 12 ++ riscv_qemu_FSH | 5 + 7 files changed, 719 insertions(+), 14 deletions(-) create mode 100644 Makefile create mode 100755 fault.py create mode 100755 riscv_cc_FSH create mode 100755 riscv_cc_REF create mode 100755 riscv_qemu_FSH diff --git a/Dockerfile b/Dockerfile index 6cf706e..ae49b22 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:22.04 +FROM ubuntu:22.04 AS layered ENV DEBIAN_FRONTEND=noninteractive RUN apt -y update && apt -y upgrade && apt -y install \ @@ -24,9 +24,10 @@ WORKDIR /root # Copy source code of the tools we're going to build COPY llvm-property-preserving llvm-property-preserving/ COPY binutils-gdb binutils-gdb/ -# TODO: Remove this, use the tar instead -COPY qemu qemu/ COPY gem5 gem5/ +# For qemu, copy and extract the release tarball +COPY qemu.tar . +RUN mkdir qemu && cd qemu && tar -xf ../qemu.tar && rm ../qemu.tar # Build LLVM RUN mkdir llvm-property-preserving/build && \ @@ -62,15 +63,8 @@ RUN mkdir binutils-gdb/build && \ cd ../.. && \ rm -rf binutils-gdb -# More dependencies (TODO: merge with top apt) -# RUN apt -y update && apt -y upgrade && apt -y install \ - -COPY qemu.tar . -RUN rm -rf qemu -RUN mkdir qemu && cd qemu && tar -xf ../qemu.tar - - # Build QEMU +# TODO: Install and remove sources? RUN mkdir qemu/build && \ cd qemu/build && \ ../configure \ @@ -83,9 +77,21 @@ RUN cd gem5 && \ pip install -r requirements.txt && \ scons build/RISCV/gem5.opt -j$(nproc) -RUN rm qemu.tar - # Very crude cleaning because there's nothing better at first glance RUN cd gem5 && \ scons --clean && \ find build/RISCV -name '*.o' -delete + +# Copy test files +COPY mibench mibench/ +COPY riscv_cc_REF riscv_cc_FSH riscv_qemu_FSH \ + elf32lriscv_ref.x elf32lriscv_ccs.x fault.py Makefile . + +# TODO: Move up +RUN pip install pyelftools + +# Squash the final image so we don't ship source and build files as diffs +FROM scratch +COPY --from=layered / / +WORKDIR /root +CMD ["/bin/bash"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..64c586d --- /dev/null +++ b/Makefile @@ -0,0 +1,109 @@ +help: + @echo "all_NAT: Build native programs" + @echo "run_NAT: Run native programs" + @echo "all_REF: Build reference RISC-V programs (no FSH)" + @echo "run_REF: Run reference programs to get reference outputs" + @echo "all_FSH: Build programs with FSH" + @echo "run_FSH: Run FSH programs, giving hopefully the ref output" + @echo "clean: Clean up build products (not test results)" + @echo "distclean: Clean up everything (including test results)" + +all_NAT run_NAT all_REF run_REF all_FSH run_FSH clean distclean: %: + @ $(MAKE) -C mibench/automotive/basicmath $* + @ $(MAKE) -C mibench/automotive/bitcount $* + @ $(MAKE) -C mibench/automotive/qsort $* + @ $(MAKE) -C mibench/automotive/susan $* + @ $(MAKE) -C mibench/network/dijkstra $* + @ $(MAKE) -C mibench/network/patricia $* + @ $(MAKE) -C mibench/security/blowfish $* + @ $(MAKE) -C mibench/security/rijndael $* + @ $(MAKE) -C mibench/security/sha $* + +.PHONY: all_NAT run_NAT all_REF run_REF all_FSH run_FSH clean distclean + +OUT := out + +CAMPAIGNS_REF := ref-ex-s32-1 ref-ex-s32-2 ref-ex-sar32 +CAMPAIGNS_FSH := fsh-ex-s32-1 fsh-ex-s32-2 fsh-ex-sar32 fsh-multi-random + +PROGRAMS := \ + mibench/automotive/basicmath \ + mibench/automotive/bitcount \ + mibench/automotive/qsort \ + mibench/automotive/susan \ + mibench/network/dijkstra \ + mibench/network/patricia \ + mibench/security/blowfish \ + mibench/security/rijndael \ + mibench/security/sha + +PROG_CAMPAIGNS_FSH := \ + $(foreach P,$(PROGRAMS), \ + $(foreach C,$(CAMPAIGNS_FSH),$(OUT)/$(notdir $P)-campaign-$C.txt)) + +PROG_CAMPAIGNS_REF := \ + $(foreach P,$(PROGRAMS), \ + $(foreach C,$(CAMPAIGNS_REF),$(OUT)/$(notdir $P)-campaign-$C.txt)) + +# In this order, preferably +PROG_CAMPAIGNS_ALL := $(PROG_CAMPAIGNS_FSH) $(PROG_CAMPAIGNS_REF) + +# Remember the full path of each program x in variable $(PATH_x) +$(foreach P,$(PROGRAMS),$(eval PATH_$(notdir $P) := $P)) + +# Generate a campaign rule for a single program +# $1: Program name +# $2: Campaign name +# $3: Category-specific (REF/FSH) arguments +# TODO: Avoid the .PHONY +define campaign_rule +$(OUT)/$1-campaign-$2.txt: | $(OUT)/ + ./fault.py --campaign=$2 $3 -d "$(OUT)" -n $1 \ + --qemu="./riscv_qemu_FSH" "$(PATH_$1)" +.PHONY: $(OUT)/$1-campaign-$2.txt +endef + +# Generate all campaign rules for a given program +# $1: Program name (the notdir) +define prog_rules +$(foreach C,$(CAMPAIGNS_REF), + $(call campaign_rule,$1,$C,--block-wall)) +$(foreach C,$(CAMPAIGNS_FSH), + $(call campaign_rule,$1,$C,)) +endef + +$(foreach P,$(PROGRAMS),$(eval $(call prog_rules,$(notdir $P)))) + +# Generate a rule for running a single campaign on all programs +# $1: Campaign name +# $2: Campaign-specific arguments +define do_campaign +campaign-$1: $(foreach P,$(PROGRAMS),$(OUT)/$(notdir $P)-campaign-$1.txt) +endef + +# Intermediate targets campaigns-* (interesting because within each of these +# targets the campaigns for each program can run in parallel) +$(foreach C,$(CAMPAIGNS_FSH),$(eval $(call do_campaign,$C,))) +$(foreach C,$(CAMPAIGNS_REF),$(eval $(call do_campaign,$C,--block-wall))) + +# Single-core rules +campaigns-fsh: $(PROG_CAMPAIGNS_FSH) +campaigns-ref: $(PROG_CAMPAIGNS_REF) +campaigns: $(PROG_CAMPAIGNS_ALL) + +# Multi-core rules: avoid running multiple campaigns on the same program in +# parallel due to (1) file access races, (2) suboptimal use of notreached info +campaigns-multicore: + $(MAKE) campaign-fsh-ex-s32-1 + $(MAKE) campaign-fsh-ex-s32-2 + $(MAKE) campaign-fsh-ex-sar32 + $(MAKE) campaign-fsh-multi-random + $(MAKE) campaign-ref-ex-s32-1 + $(MAKE) campaign-ref-ex-s32-2 + $(MAKE) campaign-ref-ex-sar32 + +$(OUT)/: + @ mkdir -p $@ + +.PHONY: campaigns% +.PRECIOUS: $(OUT)/ diff --git a/README.md b/README.md index a0295f6..5b0276e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,15 @@ _This repository houses the artifact for a [CC'24](https://conf.researchr.org/home/CC-2024) paper titled “From low-level fault modeling (of a pipeline attack) to a proven hardening schemeâ€._ +- [Principle and what this is](#principle-and-what-this-is) +- [How to reproduce results from the paper](#how-to-reproduce-results-from-the-paper) +- [Detailed description](#detailed-description) +- [Technical notes](#technical-notes) +- [Manual build](#manual-build) +- [Generating the Docker image](#generating-the-docker-image) + +--- + ## Principle and what this is “Fetch skips†is fault model coined by Alshaer et al. [[2023](https://hal.science/hal-04273995v1)] which describes one common way microprocessors react to a glitch in their clock input. A typical model for this would be “instruction skipâ€, i.e. just skip an instruction in the execution of a program. Fetch skips are more precise and involve skipping or repeating 4 bytes of code, which can produce more complex effects for unaligned and variable-sized instructions. This is of course a major problem for security, as basically any incorrect execution can lead to abuse. @@ -33,12 +42,61 @@ A build of all the tools for x86\_64 is provided as a Docker image. To build man 4. Generate FSH outputs and compare with ref with `make run_FSH` (if there's a difference there will be an error). 5. Run the fault injection campaigns (single core: `make campaigns`, multiple cores: `make -j<CORES> campaigns-multicore`). There are 9 programs so up to 9 cores can be used effectively. +## Detailed description + +This repository contains the following tools as submodules: + +- [`llvm-property-preserving`](https://gricad-gitlab.univ-grenoble-alpes.fr/michelse/llvm-property-preserving): A Clang/LLVM mod by Son Tuan Vu [[2021](https://theses.hal.science/tel-03722753v1/)]. We ended up not using the mod here, so think of this as LLVM 12. We added the Xccs extension and a hardening pass to the RISC-V back-end and emitter. +- [`binutils-gdb`](https://gricad-gitlab.univ-grenoble-alpes.fr/michelse/binutils-gdb): The usual GNU toolchain. We added a new relocation type to precompute checksums of regions of code once they have been relocated. +- [`qemu`](https://gricad-gitlab.univ-grenoble-alpes.fr/michelse/qemu): We extended the emulator to support Xccs instructions/exceptions, and to simulate fetch errors by substituting bits during translation. We use it to validate security. +- [`gem5`](https://gricad-gitlab.univ-grenoble-alpes.fr/michelse/gem5): We extended the simulator to recognize Xccs instructions (in a non-faulty situation). We use it to validate performance. I also hacked it to replace 64-bit RISC-V instructions with their 32-bit counterparts. + +Other files used in the build process include: + +- `elf32lriscv_ccs.x`: A linker script for hardened programs. All it does is separate hardened code (all `.o` files except the runtime) from other code (the runtime and libraries) so that hardened code can be loaded at `0x40000` instead of the usual `0x10000`. +- `elf32lriscv_ref.x`: A linker script for reference (non-hardened) programs. It does even less, just separating user and library code within `.text` so that the campaign injection script is able to attack user code only. This makes campaigns much shorter and more comparable to the campaigns performed against hardened programs. +- `riscv_cc_REF`, `riscv_cc_FSH`: Wrappers around reference (non-hardened) and fetch-skips-hardened compilers. + +Both linker scripts can be diffed against the original, which can be found at `./riscv-custom/riscv32-unknown-elf/lib/ldscripts/elf32lriscv.x` where it is placed when the custom binutils in the `binutils-gdb` folder is installed. + +Other files used in the testing process include: + +- `mibench`: Programs from the [MiBench benchmark suite][https://vhosts.eecs.umich.edu/mibench/index.html). We target the Industrial, Network and Security applications. The source files are original but the Makefiles are basically new. +- `riscv_qemu_REF`, `riscv_qemu_FSH`: Wrappers around QEMU and QEMU-with-FSH-support. +- `fault.py`: Script for running fault injection campaigns (details inside). +- `fault_summary.py`: TODO. +- TODO: Generating figures. + +The Makefile just contains a few top-level commands for using the project. + +## Technical notes + +**False-positive QEMU “bugsâ€** + +The fault injection campaign script prints a result for each execution, such as `CCS_VIOLATION` or `NOT_REACHED`. When it doesn't recognize a result, it prints `OTHER` and logs the parameters along with the stdout/stderr of the QEMU invocation to the log file. On some machine there are many of these and they appear to be segfaults or assertion errors _within QEMU itself_, but this is mostly a red herring. The TL;DR is that QEMU is sometimes unable to catch exceptions from the emulated programs and crashes itself instead. + +QEMU's control flow during execution is rather complicated due to its use of long jumps and the sort-of-concurrent nature of signal handling. The main mechanism can be summarized like this: + +1. When QEMU start running a fragment (block) of emulated code it calls `cpu_exec()`, which calls `cpu_exec_setjmp()` to set up a long jump buffer. +2. If emulated code raises an exception or invokes a syscall, the long jump buffer is used to unwind back to `cpu_exec_setjmp()` and make the fragment return an appropriate result code. Note how this means that the SIGSEGV handler (like others) is instructed to go find the jump buffer and use it, and it would be a _shame_ if the associated stack frame was gone by then. +3. Once the fragment finishes, the result code (success, interrupted/killed by signal, syscall...) is checked and appropriate handling is performed; this includes running syscalls and handling exceptions. The handling exceptions part is why programs that segfault when emulated have a QEMU error report and not the kernel's default "Segmentation fault" message. +4. Go back to 1 to execute the next fragment. + +The problem is the following. Syscalls are emulated _after_ the block ends, so if a syscall invocation crashes, the signal handler goes to fetch the jump buffer from `cpu_exec_setjmp()` _which doesn't exist anymore because the fragment is done executing_. Usually this results in QEMU failing its `cpu == current_cpu` assertion. Sometimes this results in a crash of the QEMU process itself. + +At least 3 bugs I investigated led back to this: +- `brk()` failing to add memory because the heap starts after `.data` and I had placed `.text_css` (which is read-only) sometime after `.data`, leading to a privilege segfault. This caused a long jump to the expired jump buffer and then later failing the `cpu == current_cpu` assertion. +- `open()` failing to open files due to my glibc using different syscall numbers and different values for open flags than QEMU expected, with the same outcome. +- A faulted program trying to `brk((void *)3)` leading to a segfault in the syscall emulation code and then failing that same assertion. + ## Manual build **Property-preserving LLVM** The compiler transforms the program into a protected form and is the core of the countermeasure. Pull the [`llvm-property-preserving`](https://gricad-gitlab.univ-grenoble-alpes.fr/michelse/llvm-property-preserving) submodule and build it with CMake. We configure to install in the `prefix/` folder of this repo, but never do that - we run binaries from to build folder directly. +TODO: The Dockerfile does it. We should probably match that unless it saves space. + ```bash % git submodule update --init llvm-property-preserving % cd llvm-property-preserving @@ -98,9 +156,10 @@ Note: I was unsuccessful in getting a clean build on Arch; Ubuntu seems to be th ## Generating the Docker image -The Docker image for this projet is generated from the source files in this repository (including unstaged changes). Make sure all submodules are pulled. QEMU only builds out-of-git when using a release tarball, so we generate that first. +The Docker image for this projet is generated from the source files in this repository (including unstaged changes). Make sure all submodules are pulled. QEMU only builds out-of-git when using a release tarball, so we generate that first. We also clean any generated from the `mibench` folder, which will get copied. ```bash % (cd qemu && scripts/archive-source.sh ../qemu.tar) +% make distclean % podman build -t cc24-fetch-skips-hardening . ``` diff --git a/fault.py b/fault.py new file mode 100755 index 0000000..8a93678 --- /dev/null +++ b/fault.py @@ -0,0 +1,501 @@ +#! /usr/bin/env python3 + +usage = """ +fault.py -- Fault injection campaign main script. + +This script runs a single fault injection campaign on a reference or hardened +program. A campaign consists of injecting a kind of fault (eg. S32(1), S&R32, +random multi-fault...) at a set number of control points in the targeted +program (either exhaustively all PC values or random short intervals) and +collecting execution results. + +usage: fault.py --campaign=CAMPAIGN [--block-wall] + -d <FOLDER> -n <NAME> [--qemu=<PATH>] + <INPUT> + +Campaign selection with --campaign: + ref-ex-s32-1: Exhaustive S32(1) (reference binary) + ref-ex-s32-2: Exhaustive S32(2) (reference binary) + ref-ex-sar32: Exhaustive S&R32 (reference binary) + fsh-ex-s32-1: Exhaustive S32(1) (hardened binary) + fsh-ex-s32-2: Exhaustive S32(2) (hardened binary) + fsh-ex-sar32: Exhaustive S&R32 (hardened binary) + fsh-multi-random: Random-location injections of random multi-fault sequences. + +For reference campaigns (ref/...) a non-hardened binary is assumed. The +injection area is delimited by the __user_start and __user_end symbols in the +ELF. For hardened campaigns (fsh/...) a hardened binary is assumed. The +injection area is delimited by the __ccs_start and __ccs_end symbols. + +Simulation options: + --block-wall: Raise the CCS_BYPASSED exception upon reaching the end of a + block where a fault was injected. Default is false for + reference binaries. Always on for hardened binaries. + +File management options: + -d <FOLDER>: Folder in which statistics/logs should be output + -n <NAME>: Unique basename for this program within <FOLDER> + --qemu=<PATH>: Path to custom QEMU binary or wrapper script + +The command to execute is specified via an <INPUT> folder. The exact binary and +arguments to run are obtained through make rules faultcmd_REF and faultcmd_FSH +in that folder. In the command printed by these rules, the binary should come +first because this script opens it to find ELF sections and symbols. +""" + +import os +import sys +import enum +import shlex +import getopt +import random +import subprocess +import elftools.elf.elffile + +Result = enum.Enum("Result", [ + # Program exited normally (shouln't happen with block wall) + "EXITED", + # The countermeasure detected invalid checksums, jumps, etc. + "CCS_VIOLATION", + # The countermeasure was bypassed (block wall reached) + "CCS_BYPASSED", + # The faulted address wasn't reached (null result) + "NOT_REACHED", + # The fault was S&R32 and the repeated word is equal to the original one + "SILENT_REPLACE", + # Signals + "SIGSEGV", "SIGILL", "SIGTRAP", + # Anything else (logged in comment in the output file) + "OTHER"]) + +class TestSet: + def __init__(self, title, start, end): + self.title = title + self.total = {r: 0 for r in Result} + self.start = start + self.end = end + self.next = None + self.reported = [] + self.report_bypasses = True + self.loaded_str = "" + self.finished = False + + self.autosave = None + self.AUTOSAVE_FREQ = 20 + self.save_counter = 0 + + def set_report_bypasses(self, b): + self.report_bypasses = b + + def set_autosave(self, file): + self.autosave = file + + def register(self, r, fault_description): + if isinstance(r, tuple): + fault_description = str(fault_description) + ":\n" + str(r[1]) + r = r[0] + self.total[r] += 1 + if r == Result.EXITED or r == Result.OTHER: + self.reported.append((r, fault_description)) + if self.report_bypasses and r == Result.CCS_BYPASSED: + self.reported.append((r, fault_description)) + + if self.autosave is not None: + self.save_counter += 1 + if self.save_counter >= self.AUTOSAVE_FREQ: + with open(self.autosave, "w") as fp: + self.print_to_file(fp, False) + self.save_counter = 0 + + def load_from_file(self, fp): + l1 = fp.readline() + l2 = fp.readline().strip().split(",") + l3 = fp.readline().strip().split(",") + if l1[0] not in "@=" or len(l2) != 10 or len(l3) != 10 or \ + l2[0] != "setting" or l3[0] != self.title: + return + + self.finished = (l1[0] == '=') + + # Reload progress + self.next = int(l1[1:].strip()) + for (r_str, total) in zip(l2[1:], l3[1:]): + r = next(r for r in Result if r.name == r_str) + self.total[r] = int(total) + + # Reload other comments + self.loaded_str = fp.read() + + def print_to_file(self, fp, finished): + fp.write(f"{'=' if finished else '@'} {self.next}\n") + fp.write("setting") + for r in Result: + fp.write(f",{r.name}") + fp.write("\n") + fp.write(self.title) + for r in Result: + fp.write(f",{self.total[r]}") + fp.write("\n") + for (r, fault_description) in self.reported: + text = f"{r.name} for {fault_description}" + text = "\n".join(("# " + x).strip() for x in text.splitlines()) + fp.write(text + "\n") + fp.write("\n") + fp.write(self.loaded_str.strip() + "\n") + +# Run a QEMU command with faults by inserting arguments. Returns a Result enum. +def run_with_faults(command, faults, block_wall): + cmd = command[:] + if len(faults): + cmd.insert(1, "--riscv-faults") + cmd.insert(2, ";".join(faults)) + if block_wall: + cmd.insert(1 + 2 * (len(faults) > 0), "--riscv-faults-block-wall") + try: + p = subprocess.run(cmd, stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, timeout=5) + rc = p.returncode + except subprocess.TimeoutExpired: + return (Result.OTHER, "timeout") + + if b"qemu: unhandled CPU exception 0x18" in p.stderr: + assert rc == 1 + return Result.CCS_VIOLATION + elif b"qemu: unhandled CPU exception 0x19" in p.stderr: + assert rc == 1 + return Result.CCS_BYPASSED + elif rc == 132: + return Result.SIGILL + elif rc == 133: + return Result.SIGTRAP + elif rc == 139: + return Result.SIGSEGV + elif rc == 0: + return Result.EXITED + else: + return (Result.OTHER, p.stderr.decode("utf-8")) + +# Get a 4-byte little-endian value at virtual location PC of the given ELF. +def data_at(elf, pc): + for section in elf.iter_sections("SHT_PROGBITS"): + start = section["sh_addr"] + end = section["sh_addr"] + section["sh_size"] + + if start <= pc < end: + read = section.data()[pc-start : pc+4-start] + bytes(4) + return read[0] | (read[1] << 8) | (read[2] << 16) | (read[3] << 24) + +# Execute a single-fault injection on a binary. +# command: QEMU command that runs the binary +# fault: Fault description as a string ("s32,1", "s&r32", etc) +# elf: ELF used in the QEMU command +# pc: Location of the injection +# block_wall: Whether to enable the block wall after the fault +# +# If a program returns after an injection, this means either the targeted PC +# was not reached, or the fault was bypassed (and the block wall too if +# enabled). To check whether PC was reached a second execution replacing the +# instruction at PC with an illegal instruction is needed. Executions that exit +# normally are slow so doing two is a bit costly. Often there are multiple +# non-reached addresses in a row, so expect_not_reached can be set to do the +# second execution first, removing the need to run the first. +def do_fault_at(command, fault, elf, pc, block_wall, expect_not_reached=False): + faults = [hex(pc) + ":" + fault] + print(";".join(faults) + "... ", end="") + predicted = False + + # If we expect to miss the target, try the illegal instruction immediately. + # This saves on an unneeded double execution. + if expect_not_reached: + ill = [hex(pc) + ":ill"] + res = run_with_faults(command, ill, block_wall) + if res == Result.EXITED: + res = Result.NOT_REACHED + predicted = True + else: + res = run_with_faults(command, faults, block_wall) + else: + res = run_with_faults(command, faults, block_wall) + + # If the program exits, PC was probably never reached; try again by + # generating an illegal instruction on that spot. + if res == Result.EXITED: + ill = [hex(pc) + ":ill"] + res = run_with_faults(command, ill, block_wall) + if res == Result.EXITED: + res = Result.NOT_REACHED + elif res != Result.SIGILL: + print(f"\x1b[31;1m{res} in EXITED retry?! ", end="") + + # If S&R32 leads to a bypass, check if the replacement actually changed the + # value being decoded. + elif res == Result.CCS_BYPASSED and fault == "s&r32": + original = data_at(elf, pc) + replaced = data_at(elf, pc - 4) + assert original is not None + assert replaced is not None + if original == replaced: + res = Result.SILENT_REPLACE + + if res == Result.EXITED or res == Result.CCS_BYPASSED: + print(f"\x1b[31;1m{res.name}\x1b[0m", end="") + elif isinstance(res, tuple): + print(f"\x1b[32m{res[0].name} ({repr(res[1])})\x1b[0m", end="") + else: + print(f"\x1b[32m{res.name}\x1b[0m", end="") + + if predicted: + print(" (predicted)", end="") + print("") + + return res + +def generate_random_faults(offset): + faults = [] + pc = offset + maxfaults = random.randint(2, 6) + n = 0 + + while pc < offset + 64 and n < maxfaults: + x = random.randint(0, 2) + if x == 0: + faults.append((pc, "s32,1")) + pc += 8 + elif x == 1: + faults.append((pc, "s32,2")) + pc += 12 + elif x == 2: + faults.append((pc, "s&r32")) + pc += 4 + n += 1 + pc += random.randint(0, 6) * 4 + + return faults + +# Execute a multi-fault injection. +def do_faults(command, faults, elf, block_wall): + faultspec = [f"{pc:#x}:{f}" for pc, f in faults] + print(";".join(faultspec) + "... ", end="") + res = run_with_faults(command, faultspec, block_wall) + + # If the program exits, PC was probably never reached; try again by + # generating an illegal instruction on that spot. + if res == Result.EXITED: + faultspec = [f"{pc:#x}:ill" for pc, _ in faults] + res = run_with_faults(command, faultspec, block_wall) + if res == Result.EXITED: + res = Result.NOT_REACHED + elif res != Result.SIGILL: + print(f"\x1b[31;1m{res} in EXITED retry?! ", end="") + + if res == Result.EXITED or res == Result.CCS_BYPASSED: + print(f"\x1b[31;1m{res.name}\x1b[0m") + elif isinstance(res, tuple): + print(f"\x1b[32m{res[0].name} ({repr(res[1])})\x1b[0m") + else: + print(f"\x1b[32m{res.name}\x1b[0m") + + return res + +class Options: + pass + +def parse_args(argv): + i = 0 + opt = Options() + opt.campaign = None + opt.block_wall = None + opt.out = None + opt.name = None + opt.qemu = None + + try: + opts, args = getopt.getopt(argv[1:], "d:n:", + ["campaign=", "qemu=", "block-wall"]) + except getopt.GetoptError as e: + print("error:", e, file=sys.stderr) + sys.exit(1) + + for (name, value) in opts: + if name == "--campaign": + opt.campaign = value + if name == "--block-wall": + opt.block_wall = True + if name == "-d": + opt.out = os.path.abspath(value) + if name == "-n": + opt.name = value + if name == "--qemu": + opt.qemu = os.path.abspath(value) + + if "--help" in argv or len(argv) == 1 or len(args) != 1: + print(usage.strip()) + sys.exit("--help" not in argv) + + if opt.campaign is None: + print("error: please specify a campaign with --campaign") + sys.exit(1) + if opt.out is None or opt.name is None: + print("error: please specify an output folder/name with -d and -n") + sys.exit(1) + if opt.block_wall is None: + opt.block_wall = opt.campaign.startswith("ref/") + + return opt, args[0] + + +def file_notreached(out, name, campaign): + ckind, _ = split_campaign(campaign) + return os.path.join(out, name + "-" + ckind + "-notreached.txt") +def file_campaign(out, name, campaign): + return os.path.join(out, name + "-campaign-" + campaign + ".txt") + +def split_campaign(campaign): + i = campaign.index("-") + return campaign[:i], campaign[i+1:] + +def faultcmd(progdir, campaign): + ckind, _ = split_campaign(campaign) + cmd = ["make", "--no-print-directory", f"faultcmd_{ckind.upper()}"] + rc = subprocess.run(cmd, cwd=progdir, stdout=subprocess.PIPE, check=True) + out = rc.stdout.decode("utf-8").strip() + # Still need to silence more directory announcements... + out = "\n".join(filter(lambda l: not l.startswith("make["), + out.splitlines())) + return out + +def stat_or_none(path): + try: + return os.stat(path) + except FileNotFoundError: + return None + +def main(argv): + opt, progdir = parse_args(argv) + # TODO: Check opt.campaign is a valid campaign name + qemu = opt.qemu or "qemu-riscv32" + + out_campaign = os.path.abspath( + file_campaign(opt.out, opt.name, opt.campaign)) + out_notreached = os.path.abspath( + file_notreached(opt.out, opt.name, opt.campaign)) + + command = [qemu] + shlex.split(faultcmd(progdir, opt.campaign)) + print("-> COMMAND:", command) + print("-> BINARY:", command[1]) + + # Switch to the program's directory + os.chdir(progdir) + + fp = open(command[1], "rb") + elf = elftools.elf.elffile.ELFFile(fp) + + # Determine the address range to attack + # TODO: Use __user_start / __user_end ranges instead of whole .text + if opt.campaign.startswith("fsh-"): + symtable = elf.get_section_by_name(".symtab") + s = symtable.get_symbol_by_name("__ccs_start")[0].entry["st_value"] + e = symtable.get_symbol_by_name("__ccs_end")[0].entry["st_value"] + inj_start, inj_end = s, e + elif opt.campaign.startswith("ref-"): + symtable = elf.get_section_by_name(".symtab") + s = symtable.get_symbol_by_name("__user_start")[0].entry["st_value"] + e = symtable.get_symbol_by_name("__user_end")[0].entry["st_value"] + inj_start, inj_end = s, e + print(f"-> RANGE: {inj_start:#08x} -- {inj_end:#08x}") + + assert run_with_faults(command, [], True) == Result.EXITED + + # Delete the not-reached file if it's older than the program + stat_nr = stat_or_none(out_notreached) + stat_elf = os.stat(command[1]) + if stat_nr and stat_elf.st_mtime > stat_nr.st_mtime: + os.remove(out_notreached) + + # Load the not-reached file if it's there + not_reached = set() + try: + with open(out_notreached, "r") as fp: + not_reached = set(map(int, fp.read().split(", "))) + except: + pass + + ts = TestSet(opt.campaign, inj_start, inj_end) + ckind, cname = split_campaign(opt.campaign) + ts.set_report_bypasses(ckind != "ref") + ts.set_autosave(out_campaign) + + # Load the existing test set if it's not newer than the program + stat_ts = stat_or_none(out_campaign) + if stat_ts and stat_ts.st_mtime >= stat_elf.st_mtime: + with open(out_campaign, "r") as fp: + ts.load_from_file(fp) + + if ts.finished: + print("Nothing to do.") + return 0 + + # Save every SAVE_FREQ injections + SAVE_FREQ = 50 + save_counter = 0 + + if cname.startswith("ex-"): + fault = { + "ex-s32-1": "s32,1", + "ex-s32-2": "s32,2", + "ex-sar32": "s&r32", + }[cname] + + expect_not_reached = False + total = len(range(ts.start, ts.end+3, 4)) + + for i in range(total): + addr = ts.start + 4*i + if ts.next is not None and ts.next > addr: + continue + + if addr in not_reached: + r = Result.NOT_REACHED + expect_not_reached = False + else: + print("[{} {:4.1f}%] ".format(opt.name, 100 * i / total), + end="") + r = do_fault_at(command, fault, elf, addr, opt.block_wall, + expect_not_reached) + if r == Result.NOT_REACHED: + not_reached.add(addr) + # Expect a few consecutive not-reached cases + expect_not_reached = True + else: + expect_not_reached = False + + ts.next = addr + 4 + ts.register(r, (addr, fault)) + + else: + # Use a "hash" of the filename as a seed for consistency + seed = sum(map(ord, command[1])) + print("-> SEED:", seed) + random.seed(seed) + + for i in range(2000): + offset = random.randint(ts.start, ts.end) & -4 + faults = generate_random_faults(offset) + if ts.next is not None and ts.next > i: + continue + print(f"[{opt.name} {i:04d}] ", end="") + r = do_faults(command, faults, elf, opt.block_wall) + ts.next = i+1 + ts.register(r, faults) + + fp.close() + + with open(out_campaign, "w") as fp: + ts.print_to_file(fp, True) + with open(out_notreached, "w") as fp: + fp.write(", ".join(map(str, not_reached))) + + return 0 + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/riscv_cc_FSH b/riscv_cc_FSH new file mode 100755 index 0000000..d570d1a --- /dev/null +++ b/riscv_cc_FSH @@ -0,0 +1,13 @@ +#! /bin/bash + +ROOT="$(dirname $0)" + +PATH="${ROOT}/prefix/bin" + +# Similar to riscv_cc_REF, but this time with hardening +clang --gcc-toolchain="${ROOT}/riscv-custom" \ + --target=riscv32 -march=rv32gc -mabi=ilp32d \ + -mcpu=generic-rv32-fsh \ + -mllvm --riscv-enable-fetch-skips-hardening \ + -mllvm --riscv-fetch-skips-hardening-N=2 \ + -T "${ROOT}/elf32lriscv_ccs.x" "$@" diff --git a/riscv_cc_REF b/riscv_cc_REF new file mode 100755 index 0000000..79c3393 --- /dev/null +++ b/riscv_cc_REF @@ -0,0 +1,12 @@ +#! /bin/bash + +ROOT="$(dirname $0)" + +# Find clang in llvm-property-preserving +# Remove any other path to prevent clang from finding system ld +PATH="${ROOT}/prefix/bin" + +# Provide GCC toolchain so it finds proper ld to link +clang --gcc-toolchain="${ROOT}/riscv-custom" \ + --target=riscv32 -march=rv32gc -mabi=ilp32d \ + -T "${ROOT}/elf32lriscv_ref.x" "$@" diff --git a/riscv_qemu_FSH b/riscv_qemu_FSH new file mode 100755 index 0000000..5c3e1e5 --- /dev/null +++ b/riscv_qemu_FSH @@ -0,0 +1,5 @@ +#! /bin/bash + +ROOT="$(dirname $0)" + +"${ROOT}"/qemu/build/qemu-riscv32 -cpu rv32-fsh "$@" -- GitLab