diff --git a/.github/workflows/wtf.yml b/.github/workflows/wtf.yml index 6ffc756..33c7627 100644 --- a/.github/workflows/wtf.yml +++ b/.github/workflows/wtf.yml @@ -2,11 +2,6 @@ name: Builds on: [push, pull_request] -permissions: - actions: read - contents: read - security-events: write - jobs: Windows: runs-on: windows-2019 @@ -71,11 +66,15 @@ jobs: - name: Checkout uses: actions/checkout@v4 + # Revert to the below when https://github.com/llvm/llvm-project/issues/84271 is fixed: + # sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" - name: Installing dependencies run: | sudo apt-get -y update sudo apt install -y g++-10 ninja-build - sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" + curl -O https://apt.llvm.org/llvm.sh + chmod u+x ./llvm.sh + sudo ./llvm.sh 17 - name: Build with gcc if: matrix.compiler == 'gcc' @@ -87,11 +86,13 @@ jobs: chmod u+x ./build-release.sh ./build-release.sh + # Revert to the below when https://github.com/llvm/llvm-project/issues/84271 is fixed: + # clang-18 / clang++-18 - name: Build with clang if: matrix.compiler == 'clang' env: - CC: clang-18 - CXX: clang++-18 + CC: clang-17 + CXX: clang++-17 run: | cd src/build chmod u+x ./build-release.sh diff --git a/README.md b/README.md index d7fcc8f..561b80f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

what the fuzz

- A distributed, code-coverage guided, cross-platform snapshot-based fuzzer designed for attacking user and or kernel-mode targets running on Microsoft Windows. + A distributed, code-coverage guided, cross-platform snapshot-based fuzzer designed for attacking user and or kernel-mode targets running on Microsoft Windows and Linux user-mode (experimental!).

@@ -13,7 +13,7 @@ ## Overview -**what the fuzz** or **wtf** is a distributed, code-coverage guided, customizable, cross-platform snapshot-based fuzzer designed for attacking user and or kernel-mode targets running on Microsoft Windows. Execution of the target can be done inside an emulator with [bochscpu](https://github.com/yrp604/bochscpu) (slowest, most precise), inside a Windows VM with the [Windows Hypervisor Platform APIs](https://docs.microsoft.com/en-us/virtualization/api/hypervisor-platform/hypervisor-platform) or inside a Linux VM with the [KVM APIs](https://www.kernel.org/doc/html/latest/virt/kvm/api.html) (fastest). +**what the fuzz** or **wtf** is a distributed, code-coverage guided, customizable, cross-platform snapshot-based fuzzer designed for attacking user and or kernel-mode targets running on Microsoft Windows or Linux (**experimental**, see [linux_mode](linux_mode/)). Execution of the target can be done inside an emulator with [bochscpu](https://github.com/yrp604/bochscpu) (slowest, most precise), inside a Windows VM with the [Windows Hypervisor Platform APIs](https://docs.microsoft.com/en-us/virtualization/api/hypervisor-platform/hypervisor-platform) or inside a Linux VM with the [KVM APIs](https://www.kernel.org/doc/html/latest/virt/kvm/api.html) (fastest). It uncovered memory corruption vulnerabilities in a wide range of softwares: [IDA Pro](https://github.com/0vercl0k/fuzzing-ida75), a popular [AAA game](https://blog.ret2.io/2021/07/21/wtf-snapshot-fuzzing/), the [Windows kernel](https://microsoft.fandom.com/wiki/Architecture_of_Windows_NT), the [Microsoft RDP client](https://www.hexacon.fr/slides/Hexacon2022-Fuzzing_RDPEGFX_with_wtf.pdf), [NVIDIA GPU Display driver](https://nvidia.custhelp.com/app/answers/detail/a_id/5383), etc. @@ -25,8 +25,6 @@ If you would like to read more about its history or how to use it on a real targ - [Fuzzing RDPEGFX with "what the fuzz"](https://thalium.github.io/blog/posts/rdpegfx/) by [Colas Le Guernic](https://github.com/clslgrnc), Jérémy Rubert, and Anonymous - [A Journey to Network Protocol Fuzzing – Dissecting Microsoft IMAP Client Protocol](https://www.fortinet.com/blog/threat-research/analyzing-microsoft-imap-client-protocol) by [Wayne Chin Yick Low](https://www.fortinet.com/blog/search?author=Wayne+Chin+Yick+Low) -Special thanks to [@yrp604](https://github.com/yrp604) for providing valuable inputs throughout the project and [@masthoon](https://github.com/masthoon) for suggesting to write a demo targeting [HEVD](https://github.com/hacksysteam/HackSysExtremeVulnerableDriver) secure mode. - ## Usage The best way to try the features out is to work with the [fuzzer_hevd](src/wtf/fuzzer_hevd.cc) / [fuzzer_tlv_server](src/wtf/fuzzer_tlv_server.cc) modules. You can grab the [target-hevd.7z](https://github.com/0vercl0k/wtf/releases) / [target-tlv_server.7z](https://github.com/0vercl0k/wtf/releases) archives and extract them into the `targets/` directory. The archives contain the directory trees that are expected for every targets: @@ -316,3 +314,17 @@ To build it yourself you need to start a *Visual Studio Developper Command Promp ## Authors * Axel '[0vercl0k](https://twitter.com/0vercl0k)' Souchet + +## Contributors + +Special thanks to: +- [@yrp604](https://github.com/yrp604) for providing valuable inputs throughout the project, +- [@masthoon](https://github.com/masthoon) for suggesting to write a demo targeting [HEVD](https://github.com/hacksysteam/HackSysExtremeVulnerableDriver) secure mode, +- [Markus Gaasedelen](https://github.com/0vercl0k/wtf/pull/12/) for adding Tenet support, +- [@y0ny0ns0n](https://github.com/y0ny0ns0n) for contributing the [multi-input fuzzing example](https://github.com/0vercl0k/wtf/pull/67), +- [Colas Le Guernic](https://github.com/clslgrnc) / Jérémy Rubert / Anonymous for implementing [edge coverage for bochscpu](https://github.com/0vercl0k/wtf/pull/137), +- [@1ndahous3](https://github.com/1ndahous3) for contributing the [generic ioctl fuzzer module](https://github.com/0vercl0k/wtf/pull/155), +- Jason Crowder / [Kyle Ossinger](https://k0ss.net/) from Cisco ASIG for [the Linux mode](https://github.com/0vercl0k/wtf/pull/192), +- and all the other contributors 🙏 + +[ ![contributors-img](https://contrib.rocks/image?repo=0vercl0k/wtf) ](https://github.com/0vercl0k/wtf/graphs/contributors) diff --git a/linux_mode/.gitignore b/linux_mode/.gitignore new file mode 100644 index 0000000..9015cdb --- /dev/null +++ b/linux_mode/.gitignore @@ -0,0 +1,11 @@ +gdb.txt + +__pycache__/ + +# Snapshot files +mem.dmp +raw +symbol-store.json +vm.log +vm.pid +regs.json \ No newline at end of file diff --git a/linux_mode/README.md b/linux_mode/README.md new file mode 100644 index 0000000..7b59d55 --- /dev/null +++ b/linux_mode/README.md @@ -0,0 +1,212 @@ +

+

what the fuzz: Linux mode

+

+ Experimental user-mode Linux mode by Jason Crowder and Kyle Ossinger from Cisco ASIG +

+

+ +

+

+ +

+
+ +## Overview + +This provides experimental Linux ELF userland snapshotting support based on previous work by [Kasimir](https://github.com/0vercl0k/wtf/pull/102) and scripts from [Snapchange](https://github.com/awslabs/snapchange/tree/main/qemu_snapshot). + +

+ +

+ +## Setting up the environment + +Move into the `linux_mode/qemu_snapshot` directory and run `setup.sh`: + +```console +user@pc:/wtf/linux_mode/qemu_snapshot$ ./setup.sh +``` + +This script installs all pre-requisite tools, compiles qemu, and builds a target virtual machine consisting of a Linux kernel and disk image. + +## Taking a snapshot + +Create a subdirectory in `linux_mode` for your snapshot and create a `bkpt.py` file, like [linux_mode/crash_test/bkpt.py](crash_test/bkpt.py): + +```py +# imports +import sys, os + +# import fuzzing breakpoint +from gdb_fuzzbkpt import * + +target_dir = 'linux_crash_test' + +# address to break on, found using gdb +break_address = 'do_crash_test' + +# name of the file in which to break +file_name = 'a.out' + +# create the breakpoint for the executable specified +FuzzBkpt(target_dir, break_address, file_name, sym_path=file_name) +``` + +* `target_dir` - subdirectory in `targets` to save the snapshot data +* `break_address` - address to break on. This can be a hardcoded address or a symbol if `sym_path` is provided +* `file_name` - name of the file in the target VM associated with the breakpoint +* `sym_path` - optional argument if you'd like symbols to be loaded + +Start the virtual machine in one tab while in the snapshot subdirectory by running `../qemu_snapshot/gdb_server.sh`: + +```console +user@pc:/wtf/linux_mode/crash_test$ ../qemu_snapshot/gdb_server.sh +``` + +In a separate tab, scp the target file to the target VM. With `crash_test` this can be done by first compiling the target file: + +```console +user@pc:/wtf/linux_mode/crash_test$ gcc test.c +``` + +Then transfer the target file to the VM: + +```console +user@pc:/wtf/linux_mode/crash_test$ pushd ../qemu_snapshot/target_vm +user@pc:/wtf/linux_mode/qemu_snapshot/target_vm$ ./scp.sh ../../crash_test/a.out +a.out 100% 16KB 1.2MB/s 00:00 +``` + +Go back to the `crash_test` directory. + +```console +user@pc:/wtf/linux_mode/qemu_snapshot/target_vm$ popd +/wtf/linux_mode/crash_test +user@pc:/wtf/linux_mode/crash_test$ +``` + +Now, run `../qemu_snapshot/gdb_client.sh`: + +```console +user@pc:/wtf/linux_mode/crash_test$ ../qemu_snapshot/gdb_client.sh +``` + +In the first tab, log in to the Linux machine (user `root`) and run the target file: + +```console +linux login: root +Linux linux 6.7.0-rc3 #1 SMP PREEMPT_DYNAMIC Thu Nov 30 18:38:29 UTC 2023 x86_64 + +The programs included with the Debian GNU/Linux system are free software; +the exact distribution terms for each program are described in the +individual files in /usr/share/doc/*/copyright. + +Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent +permitted by applicable law. +A valid context for root could not be obtained. +Last login: Fri Dec 1 21:21:22 UTC 2023 on ttyS0 +root@linux:~# ./a.out + +Enter some input. +d +``` + +Once the breakpoint is hit, the second tab will start the snapshotting process: + +```console +Continuing. +In right process? True +Calling mlockall +Saving 67 bytes at 555555555146 +In right process? True +Restoring 67 bytes at 0x555555555146 +Restored +In the Qemu tab, press Ctrl+C, run the `cpu` command +``` + +Once the second tab indicates to run the `cpu` command, press Ctrl+C and run the `cpu` command from the first tab: + +```console +Thread 1 "qemu-system-x86" received signal SIGINT, Interrupt. +0x00007ffff77a4ebe in __ppoll (fds=0x5555568337d0, nfds=8, timeout=, timeout@entry=0x7fffffffdea0, sigmask=si +42 ../sysdeps/unix/sysv/linux/ppoll.c: No such file or directory. +(gdb) cpu +cpu_state: 0x55555681e240 +done...continuing debuggee +``` + +The second tab will detect once the first tab has finished executing the `cpu` command and continue creating a snapshot for the target VM. + +Once the second tab indicates that snapshotting is complete, the target VM can be terminated. + +```console +In the Qemu tab, press Ctrl+C, run the `cpu` command +Detected cpu registers dumped to regs.json +Connecting to Qemu monitor at localhost:55555 +Connected +Instructing Qemu to dump physical memory to file raw +Done +Converting raw file raw to dump file /wtf/targets/linux_crash_test/state/mem.dmp +Done +mv regs.json /wtf/targets/linux_crash_test/state/regs.json +mv symbol-store.json /wtf/targets/linux_crash_test/state/symbol-store.json +Snapshotting complete + +Breakpoint 1, 0x0000555555555189 in do_crash_test () +(gdb) +``` + +## Harnessing and Fuzzing + +Writing harnesses is the same process as writing harnesses for Windows executables. Example harnesses for crash_test and page_fault_test are present in [src/wtf/fuzzer_linux_crash_test.cc](../src/wtf/fuzzer_linux_crash_test.cc) and [src/wtf/fuzzer_linux_page_fault_test.cc](../src/wtf/fuzzer_linux_page_fault_test.cc). + +Now that we have everything set up we can start our server and fuzzer: + +Provide a seed input: + +```console +user@pc:/wtf/targets/linux_crash_test$ echo a>inputs/a +``` + +Run the master: + +```console +user@pc:/wtf/targets/linux_crash_test$ ../../src/build/wtf master --name linux_crash_test --max_len=10 +``` + +Run the fuzzee and note that crashes are found quickly. + +```console +user@pc:/wtf/targets/linux_crash_test$ ../../src/build/wtf fuzz --backend=bochscpu --name linux_crash_test +Setting @fptw to 0xff'ff. +The debugger instance is loaded with 16 items +Setting debug register status to zero. +Setting debug register status to zero. +Setting mxcsr_mask to 0xffbf. +Dialing to tcp://localhost:31337/.. +#113174 cov: 47 exec/s: 11.3k lastcov: 2.0s crash: 1782 timeout: 0 cr3: 0 uptime: 10.0s +``` + +To fuzz with KVM, create a coverage breakpoints file by loading the target file in IDA and running [scripts/gen_linux_coveragefile_ida.py](../scripts/gen_linux_coveragefile_ida.py). Transfer the coverage breakpoints file to the `coverage` subfolder in the target's directory. For example, for `linux_crash_test` transfer the coverage breakpoint file to `targets/linux_crash_test/coverage/a.cov`. Once transferred, KVM can be used for fuzzing: + +```console +user@pc:/wtf/targets/linux_crash_test$ sudo ../../src/build/wtf fuzz --backend=kvm --name linux_crash_test +Setting @fptw to 0xff'ff. +The debugger instance is loaded with 16 items +Parsing coverage/a.cov.. +Applied 44 code coverage breakpoints +Setting debug register status to zero. +Setting debug register status to zero. +Setting mxcsr_mask to 0xffbf. +Resolved breakpoint 0xffffffff82001240 at GPA 0x2001240 aka HVA 0x564428d2afe0 +Resolved breakpoint 0xffffffff82000ff0 at GPA 0x2000ff0 aka HVA 0x564428d2cda0 +Resolved breakpoint 0xffffffff81099dc0 at GPA 0x1099dc0 aka HVA 0x564428d2db80 +Resolved breakpoint 0xffffffff810708e0 at GPA 0x10708e0 aka HVA 0x564428d2e6b0 +Resolved breakpoint 0x5555555551e7 at GPA 0x972c1e7 aka HVA 0x564428d32117 +Dialing to tcp://localhost:31337/.. +#24348 cov: 8 exec/s: 2.4k lastcov: 3.0s crash: 871 timeout: 0 cr3: 0 uptime: 10.0s +``` + +## Symbolizing + +The only current way to symbolize and debug your testcases is to use the bochscpu backend and generate a Tenet traces as per [Generating Tenet traces](https://github.com/0vercl0k/wtf?tab=readme-ov-file#generating-tenet-traces). diff --git a/linux_mode/crash_test/README.md b/linux_mode/crash_test/README.md new file mode 100644 index 0000000..d6f3473 --- /dev/null +++ b/linux_mode/crash_test/README.md @@ -0,0 +1,2 @@ +Test program and breakpoint script to verify Linux snapshotting and fuzzing work +correctly. \ No newline at end of file diff --git a/linux_mode/crash_test/bkpt.py b/linux_mode/crash_test/bkpt.py new file mode 100644 index 0000000..3bd1fc8 --- /dev/null +++ b/linux_mode/crash_test/bkpt.py @@ -0,0 +1,17 @@ +# Jason Crowder - February 2024 +# imports +import sys, os + +# import fuzzing breakpoint +from gdb_fuzzbkpt import * + +target_dir = "linux_crash_test" + +# address to break on, found using gdb +break_address = "do_crash_test" + +# name of the file in which to break +file_name = "a.out" + +# create the breakpoint for the executable specified +FuzzBkpt(target_dir, break_address, file_name, sym_path=file_name) diff --git a/linux_mode/crash_test/test.c b/linux_mode/crash_test/test.c new file mode 100644 index 0000000..a0d8da1 --- /dev/null +++ b/linux_mode/crash_test/test.c @@ -0,0 +1,40 @@ +// Jason Crowder - February 2024 +#include +#include + +void do_crash_test(char* input) { + if (input[0] == 'C' && input[1] == 'R' && input[2] == 'A' && + input[3] == 'S' && input[4] == 'H') { + *(char*)NULL = '\0'; + } +} + +void end_crash_test() { printf("End crash test.\n"); } + +int main(int argc, char* argv[]) { + char* buf = NULL; + size_t cbBuf = 10; + ssize_t cbRead = 0; + + buf = (char*)calloc(1, cbBuf); + if (!buf) { + printf("calloc failed.\n"); + goto END; + } + + printf("Enter some input.\n"); + cbRead = getline(&buf, &cbBuf, stdin); + if (-1 == cbRead) { + perror("getline failure: "); + goto END; + } + + do_crash_test(buf); + + end_crash_test(); + +END: + if (buf) { + free(buf); + } +} \ No newline at end of file diff --git a/linux_mode/page_fault_test/README.md b/linux_mode/page_fault_test/README.md new file mode 100755 index 0000000..69987ff --- /dev/null +++ b/linux_mode/page_fault_test/README.md @@ -0,0 +1,2 @@ +Test page fault program for making sure memory locking instructions and code +work correctly. diff --git a/linux_mode/page_fault_test/bkpt.py b/linux_mode/page_fault_test/bkpt.py new file mode 100755 index 0000000..f4605b8 --- /dev/null +++ b/linux_mode/page_fault_test/bkpt.py @@ -0,0 +1,16 @@ +# Jason Crowder - February 2024 +# imports +import sys, os + +# import fuzzing breakpoint +from gdb_fuzzbkpt import * + +target_dir = "linux_page_fault_test" + +break_address = "page_fault_test" + +# name of the file in which to break +file_name = "a.out" + +# create the breakpoint for the executable specified +FuzzBkpt(target_dir, break_address, file_name, bp_hits_required=1, sym_path=file_name) diff --git a/linux_mode/page_fault_test/test.c b/linux_mode/page_fault_test/test.c new file mode 100644 index 0000000..6f086a7 --- /dev/null +++ b/linux_mode/page_fault_test/test.c @@ -0,0 +1,34 @@ +// Jason Crowder - February 2024 +#include +#include + +#define PAGE_SIZE 0x1000 +#define BUFF_SIZE (512 * 1024 * 1024) + +void page_fault_test(void* p) { + for (size_t i = 0; i < BUFF_SIZE; i += PAGE_SIZE) { + char* pc = (char*)p + i; + *pc = 'A'; + } +} + +void done_with_test() { printf("Done with test.\n"); } + +int main() { + void* p = malloc(BUFF_SIZE); + + if (!p) { + perror("malloc failed.\n"); + goto END; + } + + printf("Press enter to do page fault test.\n"); + getchar(); + page_fault_test(p); + done_with_test(); + +END: + if (p) { + free(p); + } +} \ No newline at end of file diff --git a/linux_mode/qemu_snapshot/.gitignore b/linux_mode/qemu_snapshot/.gitignore new file mode 100644 index 0000000..e08904f --- /dev/null +++ b/linux_mode/qemu_snapshot/.gitignore @@ -0,0 +1,3 @@ +linux/ +__pycache__/ +qemu/ diff --git a/linux_mode/qemu_snapshot/gdb_client.sh b/linux_mode/qemu_snapshot/gdb_client.sh new file mode 100755 index 0000000..2ad3919 --- /dev/null +++ b/linux_mode/qemu_snapshot/gdb_client.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# get our environmental variables +export LINUX_MODE_BASE=../ +export WTF=${LINUX_MODE_BASE}../ +export PYTHONPATH=${PYTHONPATH}:${LINUX_MODE_BASE}/qemu_snapshot +export KERNEL=${LINUX_MODE_BASE}qemu_snapshot/target_vm/linux/vmlinux +export LINUX_GDB=${LINUX_MODE_BASE}qemu_snapshot/target_vm/linux/scripts/gdb/vmlinux-gdb.py + +# initialize gdb +gdb \ + ${KERNEL} \ + -q \ + -ex "set pagination off" \ + -iex "add-auto-load-safe-path ${LINUX_GDB}" \ + -ex "set confirm off" \ + -ex "target remote localhost:1234" \ + -x ./bkpt.py \ + -ex continue diff --git a/linux_mode/qemu_snapshot/gdb_fuzzbkpt.py b/linux_mode/qemu_snapshot/gdb_fuzzbkpt.py new file mode 100755 index 0000000..a02c0ca --- /dev/null +++ b/linux_mode/qemu_snapshot/gdb_fuzzbkpt.py @@ -0,0 +1,512 @@ +# Jason Crowder - February 2024 +# imports +import gdb +import os +import re +import sys +import inspect +import time +import socket +import select +import struct +import subprocess +import pathlib + +currentdir = pathlib.Path(inspect.getfile(inspect.currentframe())) +currentdir = currentdir.absolute().parent +sys.path.insert(0, str(currentdir.parent / "qemu_snapshot")) + +import gdb_utils + +# set environmental variables +os.environ["PWNLIB_NOTERM"] = "1" +import pwn + +# set architecture +pwn.context.arch = "amd64" + +SYMSTORE_FILENAME = pathlib.Path("symbol-store.json") +RAW_FILENAME = pathlib.Path("raw") +DMP_FILENAME = pathlib.Path("mem.dmp") +REGS_JSON_FILENAME = pathlib.Path("regs.json") + + +class dump_file: + PAGE_SIZE = 0x1000 + + HEADER64_SIZE = 8248 + EXPECTED_SIGNATURE = b"PAGE" + EXPECTED_VALID_DUMP = b"DU64" + BMPDUMP = 5 + + BMP_HEADER64_SIZE = 56 + BMP_EXPECTED_SIGNATURE = b"SDMP" + BMP_EXPECTED_VALID_DUMP = b"DUMP" + + def convert_raw_to_dmp(out_filename: pathlib.Path): + dump_size = RAW_FILENAME.stat().st_size + pages_count = int(dump_size / dump_file.PAGE_SIZE) + bitmap_size = int(pages_count / 8) + + out_file = out_filename.open("wb") + + print(f"Converting raw file '{RAW_FILENAME}' to dump file '{out_filename}'") + + d = dump_file.EXPECTED_SIGNATURE + assert len(d) == 4 + + d += dump_file.EXPECTED_VALID_DUMP + assert len(d) == 8 + + d += b"\x00" * (3992 - len(d)) + assert len(d) == 3992 + + d += struct.pack("comm") + ) + + process_name = process_name.replace('"', "") + process_name = process_name[: process_name.find("\\0")] + + return process_name + + +# Fuzzing Breakpoint Class, wrapper for gdb breakpoint +class FuzzBkpt(gdb.Breakpoint): + # intializes the breakpoint + def __init__( + self, + target_dir, + addr, + program_name, + checkname=True, + bp_hits_required=1, + target_base=0x555555554000, + sym_path=None, + ): + target_syms_dict = {} + + if sym_path: + text_offset = self.get_text_offset(sym_path) + + gdb.execute(f"add-symbol-file {sym_path} {target_base + text_offset}") + + target_syms_list = self.get_target_syms(sym_path) + for name, rva in target_syms_list: + va = target_base + rva + target_syms_dict[name] = hex(va) + + target_syms_dict[program_name] = hex(target_base) + + gdb_utils.write_to_store(target_syms_dict) + + print(f"Removing '{REGS_JSON_FILENAME}' file if it exists...") + REGS_JSON_FILENAME.unlink(missing_ok=True) + + # convert address into format that gdb takes: break *0xFFFF + loc = f"""*{addr}""" + + # intialize the gdb breakpoint + gdb.Breakpoint.__init__(self, spec=loc, type=gdb.BP_HARDWARE_BREAKPOINT) + + target_dir = pathlib.Path(os.environ["WTF"]) / "targets" / target_dir + target_dir = target_dir.absolute().resolve() + print(f"Using '{target_dir}' as target directory") + self.target_dir = target_dir + + print(f"mkdir '{target_dir}'") + target_dir.mkdir(exist_ok=True) + + dirs = ("crashes", "inputs", "outputs", "state") + for d in dirs: + new_dir = self.target_dir / d + print(f"mkdir '{new_dir}'") + new_dir.mkdir(exist_ok=True) + + # set the program name and whether or not we should check the name + self.program_name = program_name + self.checkname = checkname + self.bp_hits = 0 + self.bp_hits_required = bp_hits_required + + self.got_base = False + + # save the addresses for the kernel exception handlers + self.save_kernel_exception_handlers() + + self.orig_bytes = None + self.start_orig_rip = None + + self.mlock = False + + self.did_snapshot = False + + # function that is called when the breakpoint is hit + def stop(self): + if self.did_snapshot: + return False + + print(f"In right process? {self.is_my_program()}") + if self.checkname and not self.is_my_program(): + return False + self.bp_hits += 1 + if self.bp_hits < self.bp_hits_required: + print( + f"Hit bp {self.bp_hits} time, but need to hit it {self.bp_hits_required} times" + ) + return False + + if not self.mlock: + self.call_mlockall() + self.mlock = True + return False + + self.restore_orig_bytes() + + def wait_for_cpu_regs_dump(): + print("In the QEMU tab, press Ctrl+C, run the `cpu` command") + while not REGS_JSON_FILENAME.exists(): + time.sleep(1) + + file_size = REGS_JSON_FILENAME.stat() + # Make sure entirety of regs file has been written + while True: + time.sleep(1) + new_file_size = REGS_JSON_FILENAME.stat() + if file_size == new_file_size: + break + file_size = new_file_size + print(f"Detected cpu registers dumped to '{REGS_JSON_FILENAME}'") + # sleep for a few seconds to allow Qemu to continue + time.sleep(3) + + wait_for_cpu_regs_dump() + + qemu_monitor.write_phys_mem_file_to_disk() + + out_filename = self.target_dir / "state" / DMP_FILENAME + dump_file.convert_raw_to_dmp(out_filename) + + files = (REGS_JSON_FILENAME, SYMSTORE_FILENAME) + for f in files: + dst = self.target_dir / "state" / f + print(f"mv '{f}' '{dst}'") + f.replace(dst) + print("Snapshotting complete") + self.did_snapshot = True + return True + + # checks if the current program is the program we wanted + # does this by checking if the program names match + def is_my_program(self): + curr_program_name = self.get_name() + return self.program_name in curr_program_name + + def get_target_syms(self, target_file): + syms = [] + output = subprocess.check_output(["nm", target_file]).decode().split("\n") + for line in output: + try: + (rva, t, name) = line.split(" ") + rva = int(rva, 16) + except ValueError: + continue + + if t not in ("t", "T"): + continue + + syms.append((name, rva)) + + return syms + + def get_text_offset(self, target_file): + output = ( + subprocess.check_output(["readelf", "-S", target_file]).decode().split("\n") + ) + + text_info_line = None + for line in filter(lambda x: ".text" in x, output): + text_info_line = line + break + + # Example: + # [16] .text PROGBITS 0000000000001100 00001100 + offset = int(text_info_line.split()[-2], 16) + + return offset + + # saves all of the kernel exception handlers + # used for context switch or crashes + def save_kernel_exception_handlers(self): + entry_syscall = kernel.parse_variable("entry_SYSCALL_64") + asm_exc_page_fault = kernel.parse_variable("asm_exc_page_fault") + asm_exc_divide_error = kernel.parse_variable("asm_exc_divide_error") + force_sigsegv = kernel.parse_variable("force_sigsegv") + page_fault_oops = kernel.parse_variable("page_fault_oops") + + gdb_utils.write_to_store( + { + "entry_syscall": hex(entry_syscall), + "asm_exc_page_fault": hex(asm_exc_page_fault), + "asm_exc_divide_error": hex(asm_exc_divide_error), + "force_sigsegv": hex(force_sigsegv), + "page_fault_oops": hex(page_fault_oops), + } + ) + + # gets the name of the current running process + def get_name(self): + return kernel.task.get_name() + + def save_orig_bytes(self, start_addr, num_bytes): + if self.orig_bytes is None: + print(f"Saving {num_bytes} bytes at 0x{start_addr:x}") + self.orig_bytes = [] + self.start_orig_rip = start_addr + addr_to_read = start_addr + for i in range(num_bytes): + self.orig_bytes.append(kernel.read_byte(addr_to_read)) + addr_to_read += 1 + elif start_addr < self.start_orig_rip: + num_bytes = self.start_orig_rip - start_addr + print( + f"Saving {num_bytes} bytes from 0x{start_addr:x} to 0x{self.start_orig_rip:x}" + ) + self.start_orig_rip = start_addr + addr_to_read = start_addr + prepend_bytes = [] + for i in range(num_bytes): + prepend_bytes.append(kernel.read_byte(addr_to_read)) + addr_to_read += 1 + self.orig_bytes = prepend_bytes + self.orig_bytes + + def restore_orig_bytes(self): + if self.orig_bytes is None: + return + addr_to_write = self.start_orig_rip + print(f"Restoring {len(self.orig_bytes)} bytes at 0x{addr_to_write:x}") + for b in self.orig_bytes: + kernel.write_byte(addr_to_write, b) + addr_to_write += 1 + print("Restored") + + def call_mlockall(self): + print("Calling mlockall") + + # assembly code to call mlock. Saves and restores all registers so they are + # unaffected by the call. + mlockall = """ + push rax + push rbx + push rcx + push rdx + push rbp + push rdi + push rsi + push r8 + push r9 + push r10 + push r11 + push r12 + push r13 + push r14 + push r15 + mov rax, 0x97 + mov rdi, 0x3 + syscall + """ + + # Used to detect when mlockall fails. You will see your target stop like this: + # $ ./a.out + # Press enter. + # + # Trace/breakpoint trap (core dumped) + mlockall += """ + test eax, eax + jz no_err + int3 + no_err: + """ + + mlockall += """ + pop r15 + pop r14 + pop r13 + pop r12 + pop r11 + pop r10 + pop r9 + pop r8 + pop rsi + pop rdi + pop rbp + pop rdx + pop rcx + pop rbx + pop rax + """ + + # assemble the shellcode + shellcode = pwn.asm(mlockall) + + # gets the current instruction pointer + rip = kernel.cpu.get_reg("rip") + + # get the address for the shellcode + shellcode_addr = rip - len(shellcode) + + self.save_orig_bytes(shellcode_addr, len(shellcode)) + + # write the shellcode + + # run the original code that was ahead of the ip so that we can restore it. + addr_to_write = shellcode_addr + for b in shellcode: + kernel.write_byte(addr_to_write, b) + addr_to_write += 1 + + # set the instruction pointer to the start of the shellcode + kernel.cpu.set_reg("rip", shellcode_addr) diff --git a/linux_mode/qemu_snapshot/gdb_qemu.py b/linux_mode/qemu_snapshot/gdb_qemu.py new file mode 100755 index 0000000..42d2180 --- /dev/null +++ b/linux_mode/qemu_snapshot/gdb_qemu.py @@ -0,0 +1,238 @@ +# Jason Crowder - February 2024 +# imports +import gdb, json, sys, pathlib + +sys.path.insert(1, str(pathlib.Path(__file__).parent)) + +import gdb_utils + +REGS_JSON_FILENAME = pathlib.Path("regs.json") + +cpu_state = 0 + + +class QemuBkpt(gdb.Breakpoint): + def __init__(self, function): + gdb.Breakpoint.__init__( + self, function=function, type=gdb.BP_HARDWARE_BREAKPOINT + ) + + def stop(self): + global cpu_state + cpu_state = gdb.parse_and_eval("cpu") + + +# creates the cpu command used to dump the cpu state +class DumpCPUStateCommand(gdb.Command): + # function init + def __init__(self): + super(DumpCPUStateCommand, self).__init__( + "cpu", gdb.COMMAND_USER, gdb.COMPLETE_FILENAME + ) + + # dump state to the file passed to the function + def dump(self, f): + # grabs a register + def get_reg(x): + global cpu_state + return gdb.parse_and_eval( + f"""((CPUX86State*)((CPUState*)({cpu_state}))->env_ptr)->{x}""" + ) + + data = {} + + data["rax"] = hex(get_reg("regs[0]")) + data["rcx"] = hex(get_reg("regs[1]")) + data["rdx"] = hex(get_reg("regs[2]")) + data["rbx"] = hex(get_reg("regs[3]")) + data["rsp"] = hex(get_reg("regs[4]")) + data["rbp"] = hex(get_reg("regs[5]")) + data["rsi"] = hex(get_reg("regs[6]")) + data["rdi"] = hex(get_reg("regs[7]")) + data["r8"] = hex(get_reg("regs[8]")) + data["r9"] = hex(get_reg("regs[9]")) + data["r10"] = hex(get_reg("regs[10]")) + data["r11"] = hex(get_reg("regs[11]")) + data["r12"] = hex(get_reg("regs[12]")) + data["r13"] = hex(get_reg("regs[13]")) + data["r14"] = hex(get_reg("regs[14]")) + data["r15"] = hex(get_reg("regs[15]")) + + data["rip"] = hex(get_reg("eip")) + data["rflags"] = hex(get_reg("eflags")) + data["dr0"] = hex(get_reg("dr[0]")) + data["dr1"] = hex(get_reg("dr[1]")) + data["dr2"] = hex(get_reg("dr[2]")) + data["dr3"] = hex(get_reg("dr[3]")) + data["dr6"] = hex(get_reg("dr[6]")) + data["dr7"] = hex(get_reg("dr[7]")) + + def update_attr(val, limit): + # Satisfy wtf sanity checks + # https://github.com/0vercl0k/wtf/blob/main/src/wtf/utils.cc#L237 + val = val >> 8 + return val | ((limit & 0xF0000) >> 8) + + limit = get_reg("segs[0].limit") + attr = update_attr(get_reg("segs[0].flags"), limit) + data["es"] = { + "present": True, + "selector": hex(get_reg("segs[0].selector")), + "base": hex(get_reg("segs[0].base")), + "limit": hex(limit), + "attr": hex(attr), + } + + limit = get_reg("segs[1].limit") + attr = update_attr(get_reg("segs[1].flags"), limit) + data["cs"] = { + "present": True, + "selector": hex(get_reg("segs[1].selector")), + "base": hex(get_reg("segs[1].base")), + "limit": hex(limit), + "attr": hex(attr), + } + + limit = get_reg("segs[2].limit") + attr = update_attr(get_reg("segs[2].flags"), limit) + data["ss"] = { + "present": True, + "selector": hex(get_reg("segs[2].selector")), + "base": hex(get_reg("segs[2].base")), + "limit": hex(limit), + "attr": hex(attr), + } + + limit = get_reg("segs[3].limit") + attr = update_attr(get_reg("segs[3].flags"), limit) + data["ds"] = { + "present": True, + "selector": hex(get_reg("segs[3].selector")), + "base": hex(get_reg("segs[3].base")), + "limit": hex(limit), + "attr": hex(attr), + } + + limit = get_reg("segs[4].limit") + attr = update_attr(get_reg("segs[4].flags"), limit) + data["fs"] = { + "present": True, + "selector": hex(get_reg("segs[4].selector")), + "base": hex(get_reg("segs[4].base")), + "limit": hex(limit), + "attr": hex(attr), + } + + limit = get_reg("segs[5].limit") + attr = update_attr(get_reg("segs[5].flags"), limit) + data["gs"] = { + "present": True, + "selector": hex(get_reg("segs[5].selector")), + "base": hex(get_reg("segs[5].base")), + "limit": hex(limit), + "attr": hex(attr), + } + + limit = get_reg("tr.limit") + attr = update_attr(get_reg("tr.flags"), limit) + # https://github.com/awslabs/snapchange/blob/a3db58d2545a34a18fcf3128d403deb0f78b3bba/src/cmdline.rs#L1047 + # Ensure TR.access rights has the 64-bit busy TSS enabled + attr |= 0xB + data["tr"] = { + "present": True, + "selector": hex(get_reg("tr.selector")), + "base": hex(get_reg("tr.base")), + "limit": hex(limit), + "attr": hex(attr), + } + + limit = get_reg("ldt.limit") + attr = update_attr(get_reg("ldt.flags"), limit) + data["ldtr"] = { + "present": True, + "selector": hex(get_reg("ldt.selector")), + "base": hex(get_reg("ldt.base")), + "limit": hex(limit), + "attr": hex(attr), + } + + data["tsc"] = hex(get_reg("tsc")) + + data["sysenter_cs"] = hex(get_reg("sysenter_cs")) + data["sysenter_esp"] = hex(get_reg("sysenter_esp")) + data["sysenter_eip"] = hex(get_reg("sysenter_eip")) + + data["pat"] = hex(get_reg("pat")) + + data["efer"] = hex(get_reg("efer")) + + data["star"] = hex(get_reg("star")) + data["lstar"] = hex(get_reg("lstar")) + + data["cstar"] = hex(get_reg("cstar")) + data["fmask"] = hex(get_reg("fmask")) + data["kernel_gs_base"] = hex(get_reg("kernelgsbase")) + data["tsc_aux"] = hex(get_reg("tsc_aux")) + + data["mxcsr"] = hex(get_reg("mxcsr")) + + data["cr0"] = hex(get_reg("cr[0]")) + data["cr2"] = hex(get_reg("cr[2]")) + data["cr3"] = hex(get_reg("cr[3]")) + data["cr4"] = hex(get_reg("cr[4]")) + data["cr8"] = "0x0" + + data["xcr0"] = hex(get_reg("xcr0")) + + data["gdtr"] = { + "base": hex(get_reg("gdt.base")), + "limit": hex(get_reg("gdt.limit")), + } + + data["idtr"] = { + "base": hex(get_reg("idt.base")), + "limit": hex(get_reg("idt.limit")), + } + + data["fpop"] = hex(get_reg("fpop")) + + data["apic_base"] = "0xfee00900" + data["sfmask"] = "0x4700" + data["fpcw"] = "0x27f" + data["fpsw"] = "0x0" + data["fptw"] = "0xffff" + data["mxcsr_mask"] = "0x0" + data["fpst"] = [{"fraction": "0x0", "exp": "0x0"}] * 8 + + # writes the data dictionary to the file + json.dump(data, f) + + # updates entry_syscall in symbol-store.json + gdb_utils.write_to_store({"entry_syscall": data["lstar"]}) + + # function that gets called when the cpu command has been called + def invoke(self, args, from_tty): + global cpu_state + + self.dont_repeat() + + print(f"cpu_state: 0x{cpu_state}") + print(f"Writing register information to '{REGS_JSON_FILENAME}'") + + # dump the cpu state to regs.json + with REGS_JSON_FILENAME.open("w") as f: + self.dump(f) + + print("Done...continuing debuggee") + gdb.execute("continue") + + +DumpCPUStateCommand() + +# When HW acceleration is enabled +QemuBkpt("kvm_cpu_exec") + +# When SW emulation is used +QemuBkpt("cpu_exec") + +gdb.execute("continue") diff --git a/linux_mode/qemu_snapshot/gdb_server.sh b/linux_mode/qemu_snapshot/gdb_server.sh new file mode 100755 index 0000000..523dfb0 --- /dev/null +++ b/linux_mode/qemu_snapshot/gdb_server.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# get our environmental variables +export LINUX_MODE_BASE=../ +export GDB_QEMU_PY_SCRIPT=${LINUX_MODE_BASE}qemu_snapshot/gdb_qemu.py +export QEMU=${LINUX_MODE_BASE}qemu_snapshot/target_vm/qemu/build/qemu-system-x86_64 +export KERNEL=${LINUX_MODE_BASE}qemu_snapshot/target_vm/linux/arch/x86_64/boot/bzImage +export IMAGE=${LINUX_MODE_BASE}qemu_snapshot/target_vm/image/bookworm.img + +gdb \ + -q \ + --ex "set pagination off" \ + --ex "set confirm off" \ + --ex "starti" \ + --ex "handle SIGUSR1 noprint nostop" \ + -x ${GDB_QEMU_PY_SCRIPT} \ + --args ${QEMU} \ + -m 2G \ + -smp 1 \ + -kernel ${KERNEL} \ + -append "console=ttyS0 root=/dev/sda earlyprintk=serial noapic ibpb=off ibrs=off kpti=0 l1tf=off mds=off mitigations=off no_stf_barrier noibpb noibrs pcil" \ + -machine type=pc,accel=kvm \ + -drive file=${IMAGE} \ + -net user,host=10.0.2.10,hostfwd=tcp:127.0.0.1:10021-:22 \ + -monitor tcp:127.0.0.1:55555,server,nowait \ + -s \ + -net nic,model=e1000 \ + -nographic \ + -pidfile vm.pid \ + 2>&1 | tee vm.log diff --git a/linux_mode/qemu_snapshot/gdb_utils.py b/linux_mode/qemu_snapshot/gdb_utils.py new file mode 100755 index 0000000..1eb8442 --- /dev/null +++ b/linux_mode/qemu_snapshot/gdb_utils.py @@ -0,0 +1,23 @@ +# Jason Crowder - February 2024 +# This file contains shared code between Python modules used for Qemu and the +# Linux kernel +import json, pathlib + +# symbol store filename +SYMBOL_STORE = pathlib.Path("symbol-store.json") + + +# write data to the symbol store file +def write_to_store(content): + # if the file doesn't exist then create it + if not SYMBOL_STORE.exists(): + SYMBOL_STORE.write_text("{}") + + # read the symbol store data into data variable + data = json.loads(SYMBOL_STORE.read_text("utf-8")) + + # update the dictionary + data.update(content) + + # write the data to the symbol store file + SYMBOL_STORE.write_text(json.dumps(data)) diff --git a/linux_mode/qemu_snapshot/setup.sh b/linux_mode/qemu_snapshot/setup.sh new file mode 100755 index 0000000..50c724e --- /dev/null +++ b/linux_mode/qemu_snapshot/setup.sh @@ -0,0 +1,3 @@ +pushd ./target_vm +./init.sh +popd diff --git a/linux_mode/qemu_snapshot/target_vm/LICENSE b/linux_mode/qemu_snapshot/target_vm/LICENSE new file mode 100644 index 0000000..19dc35b --- /dev/null +++ b/linux_mode/qemu_snapshot/target_vm/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. \ No newline at end of file diff --git a/linux_mode/qemu_snapshot/target_vm/NOTICE b/linux_mode/qemu_snapshot/target_vm/NOTICE new file mode 100644 index 0000000..f48b352 --- /dev/null +++ b/linux_mode/qemu_snapshot/target_vm/NOTICE @@ -0,0 +1 @@ +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/linux_mode/qemu_snapshot/target_vm/README.md b/linux_mode/qemu_snapshot/target_vm/README.md new file mode 100644 index 0000000..30e8a06 --- /dev/null +++ b/linux_mode/qemu_snapshot/target_vm/README.md @@ -0,0 +1,4 @@ +This code is based on code from Snapchange[^1], and is used to create a target +Linux VM. + +[^1]: https://github.com/awslabs/snapchange \ No newline at end of file diff --git a/linux_mode/qemu_snapshot/target_vm/connect.sh b/linux_mode/qemu_snapshot/target_vm/connect.sh new file mode 100755 index 0000000..e249b39 --- /dev/null +++ b/linux_mode/qemu_snapshot/target_vm/connect.sh @@ -0,0 +1,2 @@ +#!/bin/bash +ssh -i ./image/bookworm.id_rsa -p 10021 -o "StrictHostKeyChecking no" root@localhost diff --git a/linux_mode/qemu_snapshot/target_vm/image/.gitignore b/linux_mode/qemu_snapshot/target_vm/image/.gitignore new file mode 100644 index 0000000..b7ca8f4 --- /dev/null +++ b/linux_mode/qemu_snapshot/target_vm/image/.gitignore @@ -0,0 +1,4 @@ +bookworm.id_rsa +bookworm.id_rsa.pub +bookworm.img +chroot/ \ No newline at end of file diff --git a/linux_mode/qemu_snapshot/target_vm/image/create-image.sh b/linux_mode/qemu_snapshot/target_vm/image/create-image.sh new file mode 100755 index 0000000..eff58c9 --- /dev/null +++ b/linux_mode/qemu_snapshot/target_vm/image/create-image.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +# Copyright 2016 syzkaller project authors. All rights reserved. +# Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +# create-image.sh creates a minimal Debian Linux image suitable for syzkaller. + +set -eux + +# Create a minimal Debian distribution in a directory. +DIR=chroot +PREINSTALL_PKGS=openssh-server,curl,tar,gcc,libc6-dev,time,strace,sudo,less,psmisc,selinux-utils,policycoreutils,checkpolicy,selinux-policy-default,debian-ports-archive-keyring,vim,tmux,make,git,libc6-dbg,gcc-multilib + +# Variables affected by options +ARCH=$(uname -m) +RELEASE=bookworm +FEATURE=minimal +SEEK=3072 +PERF=false + +# Display help function +display_help() { + echo "Usage: $0 [option...] " >&2 + echo + echo " -a, --arch Set architecture" + echo " -d, --distribution Set on which debian distribution to create" + echo " -f, --feature Check what packages to install in the image, options are minimal, full" + echo " -s, --seek Image size (MB), default 2048 (2G)" + echo " -h, --help Display help message" + echo " -p, --add-perf Add perf support with this option enabled. Please set envrionment variable \$KERNEL at first" + echo +} + +while true; do + if [ $# -eq 0 ];then + echo $# + break + fi + case "$1" in + -h | --help) + display_help + exit 0 + ;; + -a | --arch) + ARCH=$2 + shift 2 + ;; + -d | --distribution) + RELEASE=$2 + shift 2 + ;; + -f | --feature) + FEATURE=$2 + shift 2 + ;; + -s | --seek) + SEEK=$(($2 - 1)) + shift 2 + ;; + -p | --add-perf) + PERF=true + shift 1 + ;; + -*) + echo "Error: Unknown option: $1" >&2 + exit 1 + ;; + *) # No more options + break + ;; + esac +done + +# Handle cases where qemu and Debian use different arch names +case "$ARCH" in + ppc64le) + DEBARCH=ppc64el + ;; + aarch64) + DEBARCH=arm64 + ;; + arm) + DEBARCH=armel + ;; + x86_64) + DEBARCH=amd64 + ;; + *) + DEBARCH=$ARCH + ;; +esac + +# Foreign architecture + +FOREIGN=false +if [ $ARCH != $(uname -m) ]; then + # i386 on an x86_64 host is exempted, as we can run i386 binaries natively + if [ $ARCH != "i386" -o $(uname -m) != "x86_64" ]; then + FOREIGN=true + fi +fi + +if [ $FOREIGN = "true" ]; then + # Check for according qemu static binary + if ! which qemu-$ARCH-static; then + echo "Please install qemu static binary for architecture $ARCH (package 'qemu-user-static' on Debian/Ubuntu/Fedora)" + exit 1 + fi + # Check for according binfmt entry + if [ ! -r /proc/sys/fs/binfmt_misc/qemu-$ARCH ]; then + echo "binfmt entry /proc/sys/fs/binfmt_misc/qemu-$ARCH does not exist" + exit 1 + fi +fi + +# Double check KERNEL when PERF is enabled +if [ $PERF = "true" ] && [ -z ${KERNEL+x} ]; then + echo "Please set KERNEL environment variable when PERF is enabled" + exit 1 +fi + +# If full feature is chosen, install more packages +if [ $FEATURE = "full" ]; then + PREINSTALL_PKGS=$PREINSTALL_PKGS","$ADD_PACKAGE +fi + +sudo rm -rf $DIR +sudo mkdir -p $DIR +sudo chmod 0755 $DIR + +# 1. debootstrap stage + +DEBOOTSTRAP_PARAMS="--arch=$DEBARCH --include=$PREINSTALL_PKGS --components=main,contrib,non-free $RELEASE $DIR" +if [ $FOREIGN = "true" ]; then + DEBOOTSTRAP_PARAMS="--foreign $DEBOOTSTRAP_PARAMS" +fi + +# riscv64 is hosted in the debian-ports repository +# debian-ports doesn't include non-free, so we exclude firmware-atheros +if [ $DEBARCH == "riscv64" ]; then + DEBOOTSTRAP_PARAMS="--keyring /usr/share/keyrings/debian-ports-archive-keyring.gpg --exclude firmware-atheros $DEBOOTSTRAP_PARAMS http://deb.debian.org/debian-ports" +fi +sudo debootstrap $DEBOOTSTRAP_PARAMS + +# 2. debootstrap stage: only necessary if target != host architecture + +if [ $FOREIGN = "true" ]; then + sudo cp $(which qemu-$ARCH-static) $DIR/$(which qemu-$ARCH-static) + sudo chroot $DIR /bin/bash -c "/debootstrap/debootstrap --second-stage" +fi + +# Set some defaults and enable promtless ssh to the machine for root. +sudo sed -i '/^root/ { s/:x:/::/ }' $DIR/etc/passwd +echo 'T0:23:respawn:/sbin/getty -L ttyS0 115200 vt100' | sudo tee -a $DIR/etc/inittab +printf '\nauto enp0s3\niface enp0s3 inet dhcp\n' | sudo tee -a $DIR/etc/network/interfaces +echo '/dev/root / ext4 defaults 0 0' | sudo tee -a $DIR/etc/fstab +echo 'debugfs /sys/kernel/debug debugfs defaults 0 0' | sudo tee -a $DIR/etc/fstab +echo 'securityfs /sys/kernel/security securityfs defaults 0 0' | sudo tee -a $DIR/etc/fstab +echo 'configfs /sys/kernel/config/ configfs defaults 0 0' | sudo tee -a $DIR/etc/fstab +echo 'binfmt_misc /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0' | sudo tee -a $DIR/etc/fstab +echo -en "127.0.0.1\tlocalhost\n" | sudo tee $DIR/etc/hosts +echo "nameserver 8.8.8.8" | sudo tee -a $DIR/etc/resolve.conf +echo 'kernel.randomize_va_space = 0' | sudo tee -a $DIR/etc/sysctl.d/01-disable-aslr.conf +echo "linux" | sudo tee $DIR/etc/hostname +echo "* hard memlock unlimited" | sudo tee -a $DIR/etc/security/limits.conf +echo "* soft memlock unlimited" | sudo tee -a $DIR/etc/security/limits.conf +echo "root hard memlock unlimited" | sudo tee -a $DIR/etc/security/limits.conf +echo "root soft memlock unlimited" | sudo tee -a $DIR/etc/security/limits.conf +# Example for setting afl-system-config.sh to run automatically on reboot +#sudo cp ./afl-system-config.sh $DIR/root +#sudo chroot $DIR /bin/bash -c "(crontab -l 2>/dev/null; echo \"@reboot /root/afl-system-config.sh\") | crontab -" +ssh-keygen -f $RELEASE.id_rsa -t rsa -N '' +sudo mkdir -p $DIR/root/.ssh/ +cat $RELEASE.id_rsa.pub | sudo tee $DIR/root/.ssh/authorized_keys + +# Add perf support +if [ $PERF = "true" ]; then + cp -r $KERNEL $DIR/tmp/ + BASENAME=$(basename $KERNEL) + sudo chroot $DIR /bin/bash -c "apt-get update; apt-get install -y flex bison python-dev libelf-dev libunwind8-dev libaudit-dev libslang2-dev libperl-dev binutils-dev liblzma-dev libnuma-dev" + sudo chroot $DIR /bin/bash -c "cd /tmp/$BASENAME/tools/perf/; make" + sudo chroot $DIR /bin/bash -c "cp /tmp/$BASENAME/tools/perf/perf /usr/bin/" + rm -r $DIR/tmp/$BASENAME +fi + +# Add GDB +sudo chroot $DIR /bin/bash -c "apt-get update; apt-get install -y gdb" + +# Add a new user +sudo chroot $DIR /bin/bash -c "/sbin/useradd -G sudo user" +sudo chroot $DIR /bin/bash -c "echo 'user ALL=(ALL:ALL) NOPASSWD: /usr/bin/apt-get' >> /etc/sudoers" +sudo chroot $DIR /bin/bash -c "su user -c 'sudo apt-get update'" +sudo chroot $DIR /bin/bash -c "su user -c 'sudo apt-get install -y gdb vim tmux make git libc6-dbg python3'" + +# Build a disk image +dd if=/dev/zero of=$RELEASE.img bs=1M seek=$SEEK count=1 +sudo mkfs.ext4 -F $RELEASE.img +sudo mkdir -p /mnt/$DIR +sudo mount -o loop $RELEASE.img /mnt/$DIR +sudo cp -a $DIR/. /mnt/$DIR/. +sudo umount /mnt/$DIR diff --git a/linux_mode/qemu_snapshot/target_vm/init.sh b/linux_mode/qemu_snapshot/target_vm/init.sh new file mode 100755 index 0000000..6570a86 --- /dev/null +++ b/linux_mode/qemu_snapshot/target_vm/init.sh @@ -0,0 +1,145 @@ +#!/bin/bash + +DEPTH="--depth 1" + +# Default to v5.15, latest version of Linux that's supported in fuzzbkpt.py +# Earliest Linux version tested is v5.15 +# Latest Linux version tested is 6.7.0-rc3 + +# Parse the command arguments +# --kernel-version v5.4 +# --with-kasan +# --full +# -h | --help +while [[ $# -gt 0 ]]; do + case $1 in + --kernel-version) + VERSION="$2" + # Shift past the argument + shift + # Shift past the value + shift + ;; + --with-kasan) + KASAN=1 + # Shift past the argument + shift + ;; + --full) + DEPTH= + # Shift past the argument + shift + ;; + -h|--help) + echo "Usage: " + echo "./init.sh [--kernel-version ] [--with-kasan]" + echo "Example:" + echo "./init.sh" + echo "./init.sh --kernel-version v5.4 --with-kasan" + exit 0 + ;; + *) + echo "Unknown argument: $1 | Options [--kernel-version|--with-kasan]" + exit 0 + ;; + esac +done + +# Immediately stop execution if an error occurs +set -e + +# Use GCC 9 for Ubuntu 22 and GCC 8 for everything else +if cat /etc/*rel* | grep "Ubuntu 22"; then + GCC=9 +else + GCC=8 +fi + + +download_prereqs() { + # Ensure prereqs are installed + sudo apt install -y gcc-$GCC g++-$GCC clang make ninja-build debootstrap libelf-dev \ + libssl-dev pkg-config flex bison gdb + + sudo apt-get install -y libglib2.0-dev libpixman-1-dev python3-pip cmake + + pip3 install pwntools + + # If there isn't a bookworm script for debootstrap (like in Ubuntu 18.04), copy + # over the bullseye script as it is the same + if [ ! -f /usr/share/debootstrap/scripts/bookworm ]; then + sudo cp /usr/share/debootstrap/scripts/bullseye /usr/share/debootstrap/scripts/bookworm + fi +} + +# Download and build an Linux image for use in QEMU snapshots +download_linux() { + # If the bzImage already exists, no need to rebuild + if [ -f ./linux/arch/x86/boot/bzImage ]; then + return + fi + + # If no specific linux kernel given, download the entire kernel + if [ -z "$VERSION" ]; then + echo "Downloading latest linux kernel" + git clone $DEPTH https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git + else + echo "Downloading kernel version: $VERSION" + git clone $DEPTH --branch "$VERSION" https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git + fi + + pushd linux + make defconfig + echo CONFIG_CONFIGFS_FS=y >> .config + echo CONFIG_SECURITYFS=y >> .config + echo CONFIG_DEBUG_INFO=y >> .config + echo CONFIG_DEBUG_INFO_DWARF4=y >> .config + echo CONFIG_RELOCATABLE=n >> .config + echo CONFIG_RANDOMIZE_BASE=n >> .config + echo CONFIG_GDB_SCRIPTS=y >> .config + echo CONFIG_DEBUG_INFO_REDUCED=n >> .config + + # Only enable KASAN if asked to + if [[ "$KASAN" ]]; then + echo CONFIG_KASAN=y >> .config + fi + + # If gcc is not in the path already, set gcc to the active gcc + if ! which gcc; then + sudo ln -s `which gcc-$GCC` /usr/bin/gcc + fi + + yes "" | make -j`nproc` bzImage + make scripts_gdb + popd +} + +download_qemu() { + if [ -f ./qemu/build/qemu-system-x86_64 ]; then + return + fi + + # Set up Qemu with debug build + git clone -b v7.1.0 https://github.com/qemu/qemu + pushd qemu + mkdir build + cd build + CXXFLAGS="-g" CFLAGS="-g" ../configure --cpu=x86_64 --target-list="x86_64-softmmu x86_64-linux-user" + make + popd +} + +init_debian_image() { + pushd image + ./create-image.sh + popd +} + +download_prereqs + +download_qemu + +# Pass the command line arguments to check for specific kernel version +download_linux $* + +init_debian_image diff --git a/linux_mode/qemu_snapshot/target_vm/scp.sh b/linux_mode/qemu_snapshot/target_vm/scp.sh new file mode 100755 index 0000000..a8cb275 --- /dev/null +++ b/linux_mode/qemu_snapshot/target_vm/scp.sh @@ -0,0 +1,2 @@ +#!/bin/bash +scp -i ./image/bookworm.id_rsa -P 10021 -o "StrictHostKeyChecking no" $1 root@localhost:/root diff --git a/linux_mode/qemu_snapshot/target_vm/start.sh b/linux_mode/qemu_snapshot/target_vm/start.sh new file mode 100755 index 0000000..666a84b --- /dev/null +++ b/linux_mode/qemu_snapshot/target_vm/start.sh @@ -0,0 +1,14 @@ +$PWD/QEMU/build/qemu-system-x86_64 \ + -m 4G \ + -smp 1 \ + -kernel $PWD/linux/arch/x86_64/boot/bzImage \ + -append "console=ttyS0 root=/dev/sda earlyprintk=serial noapic ibpb=off ibrs=off kpti=0 l1tf=off mds=off mitigations=off no_stf_barrier noibpb noibrs pcil" \ + -machine type=pc,accel=kvm \ + -drive file=$PWD/image/bookworm.img \ + -net user,host=10.0.2.10,hostfwd=tcp:127.0.0.1:10021-:22 \ + -net nic,model=e1000 \ + -nographic \ + -pidfile vm.pid \ + 2>&1 | tee vm.log + +# machine type=q35,accel=kvm,dump-guest-core=on diff --git a/pics/wtf-linux-snapshot.webp b/pics/wtf-linux-snapshot.webp new file mode 100644 index 0000000..4f0386a Binary files /dev/null and b/pics/wtf-linux-snapshot.webp differ diff --git a/pics/wtf-linux.gif b/pics/wtf-linux.gif new file mode 100644 index 0000000..1b703ce Binary files /dev/null and b/pics/wtf-linux.gif differ diff --git a/scripts/gen_coveragefile_ida.py b/scripts/gen_coveragefile_ida.py index 1848f24..db08bbc 100644 --- a/scripts/gen_coveragefile_ida.py +++ b/scripts/gen_coveragefile_ida.py @@ -6370,47 +6370,59 @@ def adjust_SectionAlignment( self, val, section_alignment, file_alignment ): return cache_adjust_SectionAlignment(val, section_alignment, file_alignment) # Axel '0vercl0k' Souchet - November 1 2020 +# Jason Crowder - Feburary 2024 import json import pathlib import idaapi import idautils import idc -def main(): - idaapi.auto_wait() - img_base = idaapi.get_imagebase() - filepath = pathlib.Path(idc.get_input_file_path()) - pe = PE(filepath) - addrs = set() - is_ntos = filepath.name == 'ntoskrnl.exe' +def get_bbl_rvas(): for fea in idautils.Functions(): for b in idaapi.FlowChart(idaapi.get_func(fea)): ea = b.start_ea is_code = idaapi.is_code(idaapi.get_full_flags(ea)) rva = ea - img_base - sect = pe.get_section_by_rva(rva) - discardable = (sect.Characteristics & SECTION_CHARACTERISTICS['IMAGE_SCN_MEM_DISCARDABLE']) != 0 - # After a bunch of experiment, it seems that ntos.INITKDBG is the section - # that patchguard uses to hide some of its tricks. It gets copied somewhere - # else at runtime and gets removed from memory. So special-casing it, and ignoring - # code residing in this section. - initkdbg = is_ntos and sect.Name == b'INITKDBG' - if is_code and not discardable and not initkdbg: - addrs.add(rva) + if is_code: + yield rva +def main_lin(filepath): + addrs = set(get_bbl_rvas()) + return addrs, filepath.with_suffix('').name + +def main_win(filepath): + pe = PE(filepath) + addrs = set() + is_ntos = filepath.name == 'ntoskrnl.exe' + for rva in get_bbl_rvas(): + sect = pe.get_section_by_rva(rva) + discardable = (sect.Characteristics & SECTION_CHARACTERISTICS['IMAGE_SCN_MEM_DISCARDABLE']) != 0 + # After a bunch of experiment, it seems that ntos.INITKDBG is the section + # that patchguard uses to hide some of its tricks. It gets copied somewhere + # else at runtime and gets removed from memory. So special-casing it, and ignoring + # code residing in this section. + initkdbg = is_ntos and sect.Name == b'INITKDBG' + if not discardable and not initkdbg: + addrs.add(rva) + + name = filepath.with_suffix('').name + if is_ntos: + name = 'nt' + + return addrs, name + +if __name__ == '__main__': + idaapi.auto_wait() + img_base = idaapi.get_imagebase() + filepath = pathlib.Path(idc.get_input_file_path()) + addrs, name = main_win(filepath) if filepath.suffix == '.exe' else main_lin(filepath) cov = { - 'name': filepath.with_suffix('').name, + 'name': name, 'addresses': sorted(addrs) } - if is_ntos: - cov['name'] = 'nt' - outfile = filepath.with_suffix('.cov') with open(outfile, 'w') as fd: json.dump(cov, fd) print(f'Done, {outfile}') - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/src/libs/bochscpu-bins/include/bochscpu.hpp b/src/libs/bochscpu-bins/include/bochscpu.hpp index b346a47..bc5b4d4 100644 --- a/src/libs/bochscpu-bins/include/bochscpu.hpp +++ b/src/libs/bochscpu-bins/include/bochscpu.hpp @@ -454,4 +454,4 @@ int32_t bochscpu_mem_virt_read(uint64_t cr3, uint64_t gva, uint8_t *hva, uintptr void bochscpu_log_set_level(uintptr_t level); -} // extern "C" +} // extern "C" \ No newline at end of file diff --git a/src/wtf/backend.cc b/src/wtf/backend.cc index 48d1f6b..7419d04 100644 --- a/src/wtf/backend.cc +++ b/src/wtf/backend.cc @@ -237,7 +237,7 @@ bool Backend_t::SaveCrash(const Gva_t ExceptionAddress, bool Backend_t::SetBreakpoint(const char *Symbol, const BreakpointHandler_t Handler) { - const Gva_t Gva = Gva_t(g_Dbg.GetSymbol(Symbol)); + const Gva_t Gva = Gva_t(g_Dbg->GetSymbol(Symbol)); if (Gva == Gva_t(0)) { fmt::print("Could not set a breakpoint at {}.\n", Symbol); return false; @@ -330,6 +330,11 @@ uint64_t Backend_t::R15() { return GetReg(Registers_t::R15); } void Backend_t::R15(const uint64_t Value) { SetReg(Registers_t::R15, Value); } void Backend_t::R15(const Gva_t Value) { R15(Value.U64()); } +uint64_t Backend_t::Cr2() { return GetReg(Registers_t::Cr2); } +void Backend_t::Cr2(const uint64_t Value) { SetReg(Registers_t::Cr2, Value); } +void Backend_t::Cr2(const Gva_t Value) { Cr2(Value.U64()); } + + void Backend_t::PrintRegisters() { const uint64_t Rax = GetReg(Registers_t::Rax), Rbx = GetReg(Registers_t::Rbx), Rcx = GetReg(Registers_t::Rcx); diff --git a/src/wtf/backend.h b/src/wtf/backend.h index c28b2f8..61cbd0a 100644 --- a/src/wtf/backend.h +++ b/src/wtf/backend.h @@ -581,6 +581,10 @@ class Backend_t { void R15(const uint64_t Value); void R15(const Gva_t Value); + [[nodiscard]] uint64_t Cr2(); + void Cr2(const uint64_t Value); + void Cr2(const Gva_t Value); + // // Gets the last new coverage generated by the last executed test-case. // diff --git a/src/wtf/bochscpu_backend.cc b/src/wtf/bochscpu_backend.cc index 5370c9d..c3197f1 100644 --- a/src/wtf/bochscpu_backend.cc +++ b/src/wtf/bochscpu_backend.cc @@ -1320,4 +1320,4 @@ void BochscpuBackend_t::DumpTenetDelta(const bool Force) { if (NeedNewLine) { fmt::print(TraceFile_, "\n"); } -} +} \ No newline at end of file diff --git a/src/wtf/crash_detection_umode.cc b/src/wtf/crash_detection_umode.cc index 59100db..b66fe09 100644 --- a/src/wtf/crash_detection_umode.cc +++ b/src/wtf/crash_detection_umode.cc @@ -151,7 +151,7 @@ bool SetupUsermodeCrashDetectionHooks() { return false; } - if (g_Dbg.GetModuleBase("verifier") > 0) { + if (g_Dbg->GetModuleBase("verifier") > 0) { if (!g_Backend->SetBreakpoint( "verifier!VerifierStopMessage", [](Backend_t *Backend) { const uint64_t Unique = Backend->Rsp(); diff --git a/src/wtf/debugger.cc b/src/wtf/debugger.cc index 719533d..4877395 100644 --- a/src/wtf/debugger.cc +++ b/src/wtf/debugger.cc @@ -1,4 +1,11 @@ // Axel '0vercl0k' Souchet - June 7 2020 #include "debugger.h" -Debugger_t g_Dbg; \ No newline at end of file +DebuggerLess_t g_NoDbg; + +#ifdef WINDOWS +WindowsDebugger_t WindowsDebugger; +Debugger_t *g_Dbg = &WindowsDebugger; +#else +Debugger_t *g_Dbg = &g_NoDbg; +#endif \ No newline at end of file diff --git a/src/wtf/debugger.h b/src/wtf/debugger.h index d194757..2d1d912 100644 --- a/src/wtf/debugger.h +++ b/src/wtf/debugger.h @@ -6,6 +6,7 @@ #include "tsl/robin_map.h" #include #include +#include #include #include #include @@ -14,6 +15,59 @@ namespace fs = std::filesystem; namespace json = nlohmann; +struct Debugger_t { + virtual bool Init(const fs::path &DumpPath, + const fs::path &SymbolFilePath) = 0; + + virtual uint64_t GetModuleBase(const char *Name) const = 0; + + virtual uint64_t GetSymbol(const char *Name) const = 0; + + virtual const std::string &GetName(const uint64_t SymbolAddress, + const bool Symbolized) = 0; +}; + +class DebuggerLess_t : public Debugger_t { + std::unordered_map Symbols_; + +public: + explicit DebuggerLess_t() = default; + bool Init(const fs::path &DumpPath, const fs::path &SymbolFilePath) { + json::json Json; + std::ifstream SymbolFile(SymbolFilePath); + SymbolFile >> Json; + for (const auto &[Key, Value] : Json.items()) { + const uint64_t Address = + std::strtoull(Value.get().c_str(), nullptr, 0); + Symbols_.emplace(Key, Address); + } + + fmt::print("The debugger instance is loaded with {} items\n", + Symbols_.size()); + return true; + } + + uint64_t GetModuleBase(const char *Name) const { return GetSymbol(Name); } + + uint64_t GetSymbol(const char *Name) const { + if (!Symbols_.contains(Name)) { + fmt::print("{} could not be found in the symbol store\n", Name); + exit(0); + return 0; + } + + return Symbols_.at(Name); + } + + const std::string &GetName(const uint64_t SymbolAddress, + const bool Symbolized) { + static const std::string foo("hello"); + fmt::print("GetName does not work on Linux\n"); + exit(0); + return foo; + } +}; + #ifdef WINDOWS #include "globals.h" #include @@ -54,7 +108,7 @@ class StdioOutputCallbacks : public IDebugOutputCallbacks { } }; -class Debugger_t { +class WindowsDebugger_t : public Debugger_t { IDebugClient *Client_ = nullptr; IDebugControl *Control_ = nullptr; IDebugRegisters *Registers_ = nullptr; @@ -65,9 +119,9 @@ class Debugger_t { tsl::robin_map SymbolCache_; public: - explicit Debugger_t() = default; + explicit WindowsDebugger_t() = default; - ~Debugger_t() { + ~WindowsDebugger_t() { if (Client_) { Client_->EndSession(DEBUG_END_ACTIVE_DETACH); Client_->Release(); @@ -86,8 +140,8 @@ class Debugger_t { } } - Debugger_t(const Debugger_t &) = delete; - Debugger_t &operator=(const Debugger_t &) = delete; + WindowsDebugger_t(const WindowsDebugger_t &) = delete; + WindowsDebugger_t &operator=(const WindowsDebugger_t &) = delete; [[nodiscard]] bool AddSymbol(const char *Name, const uint64_t Address) const { json::json Json; @@ -204,13 +258,13 @@ class Debugger_t { // Turn the below on to debug issues. // - //#define SYMOPT_DEBUG 0x80000000 - // Status = Symbols_->SetSymbolOptions(SYMOPT_DEBUG); - // if (FAILED(Status)) { - // fmt::print("IDebugSymbols::SetSymbolOptions failed with - // hr={:#x}\n", Status); return false; - // } - // Client_->SetOutputCallbacks(&StdioCallbacks_); + // #define SYMOPT_DEBUG 0x80000000 + // Status = Symbols_->SetSymbolOptions(SYMOPT_DEBUG); + // if (FAILED(Status)) { + // fmt::print("IDebugSymbols::SetSymbolOptions failed with + // hr={:#x}\n", Status); return false; + // } + // Client_->SetOutputCallbacks(&StdioCallbacks_); const std::string &DumpFileString = DumpPath.string(); const char *DumpFileA = DumpFileString.c_str(); @@ -341,48 +395,8 @@ class Debugger_t { } }; #else -#include - -class Debugger_t { - std::unordered_map Symbols_; - -public: - explicit Debugger_t() = default; - bool Init(const fs::path &DumpPath, const fs::path &SymbolFilePath) { - json::json Json; - std::ifstream SymbolFile(SymbolFilePath); - SymbolFile >> Json; - for (const auto &[Key, Value] : Json.items()) { - const uint64_t Address = - std::strtoull(Value.get().c_str(), nullptr, 0); - Symbols_.emplace(Key, Address); - } - - fmt::print("The debugger instance is loaded with {} items\n", - Symbols_.size()); - return true; - } - - uint64_t GetModuleBase(const char *Name) const { return GetSymbol(Name); } - uint64_t GetSymbol(const char *Name) const { - if (!Symbols_.contains(Name)) { - fmt::print("{} could not be found in the symbol store\n", Name); - exit(0); - return 0; - } - - return Symbols_.at(Name); - } - - const std::string &GetName(const uint64_t SymbolAddress, - const bool Symbolized) { - static const std::string foo("hello"); - fmt::print("GetName does not work on Linux\n"); - exit(0); - return foo; - } -}; #endif -extern Debugger_t g_Dbg; \ No newline at end of file +extern Debugger_t *g_Dbg; +extern DebuggerLess_t g_NoDbg; \ No newline at end of file diff --git a/src/wtf/fuzzer_hevd.cc b/src/wtf/fuzzer_hevd.cc index a7e35b0..a755350 100644 --- a/src/wtf/fuzzer_hevd.cc +++ b/src/wtf/fuzzer_hevd.cc @@ -93,7 +93,7 @@ bool Init(const Options_t &Opts, const CpuState_t &) { // kd> ub fffff805`3b8287c4 l1 // nt!ExGenRandom+0xe0: // fffff805`3b8287c0 480fc7f2 rdrand rdx - const Gva_t ExGenRandom = Gva_t(g_Dbg.GetSymbol("nt!ExGenRandom") + 0xe0 + 4); + const Gva_t ExGenRandom = Gva_t(g_Dbg->GetSymbol("nt!ExGenRandom") + 0xe0 + 4); if (g_Backend->VirtRead4(ExGenRandom - Gva_t(4)) != 0xf2c70f48) { fmt::print("It seems that nt!ExGenRandom's code has changed, update the " "offset!\n"); diff --git a/src/wtf/fuzzer_ioctl.cc b/src/wtf/fuzzer_ioctl.cc index 2285564..a58d941 100644 --- a/src/wtf/fuzzer_ioctl.cc +++ b/src/wtf/fuzzer_ioctl.cc @@ -192,7 +192,7 @@ bool Init(const Options_t &Opts, const CpuState_t &) { // kd> ub fffff805`3b8287c4 l1 // nt!ExGenRandom+0xe0: // fffff805`3b8287c0 480fc7f2 rdrand rdx - const Gva_t ExGenRandom = Gva_t(g_Dbg.GetSymbol("nt!ExGenRandom") + 0xe0 + 4); + const Gva_t ExGenRandom = Gva_t(g_Dbg->GetSymbol("nt!ExGenRandom") + 0xe0 + 4); if (g_Backend->VirtRead4(ExGenRandom - Gva_t(4)) != 0xf2c70f48) { fmt::print("It seems that nt!ExGenRandom's code has changed, update the " "offset!\n"); diff --git a/src/wtf/fuzzer_linux_crash_test.cc b/src/wtf/fuzzer_linux_crash_test.cc new file mode 100644 index 0000000..12fdf8e --- /dev/null +++ b/src/wtf/fuzzer_linux_crash_test.cc @@ -0,0 +1,67 @@ +// Jason Crowder - February 2024 +#include "backend.h" + +namespace linux_crash_test { +Crash_t GetCrashTestcaseName(const char *Prefix, Backend_t *Backend) { + return Crash_t(fmt::format("crash-{}-{:#x}", Prefix, Backend->Cr2())); +} + +bool InsertTestcase(const uint8_t *Buffer, const size_t BufferSize) { + if (BufferSize > 10) { + return true; + } + + if (!g_Backend->VirtWriteDirty(Gva_t(g_Backend->Rdi()), Buffer, BufferSize)) { + fmt::print("Failed to write payload.\n"); + return false; + } + + return true; +} + +bool Init(const Options_t &Opts, const CpuState_t &) { + + if (!g_Backend->SetBreakpoint("asm_exc_page_fault", [](Backend_t *Backend) { + Backend->Stop(GetCrashTestcaseName("asm_exc_page_fault", Backend)); + })) { + fmt::print("Failed to insert crash breakpoint.\n"); + return false; + } + + if (!g_Backend->SetBreakpoint("asm_exc_divide_error", [](Backend_t *Backend) { + Backend->Stop(GetCrashTestcaseName("asm_exc_divide_error", Backend)); + })) { + fmt::print("Failed to insert crash breakpoint.\n"); + return false; + } + + if (!g_Backend->SetBreakpoint("force_sigsegv", [](Backend_t *Backend) { + Backend->Stop(GetCrashTestcaseName("force_sigsegv", Backend)); + })) { + fmt::print("Failed to insert crash breakpoint.\n"); + return false; + } + + if (!g_Backend->SetBreakpoint("page_fault_oops", [](Backend_t *Backend) { + Backend->Stop(GetCrashTestcaseName("page_fault_oops", Backend)); + })) { + fmt::print("Failed to insert crash breakpoint.\n"); + return false; + } + + if (!g_Backend->SetBreakpoint("end_crash_test", [](Backend_t *Backend) { + Backend->Stop(Ok_t()); + })) { + return false; + } + + return true; +} + +// +// Register the target. +// + +Target_t linux_crash_test("linux_crash_test", Init, InsertTestcase); + +} // namespace linux_crash_test diff --git a/src/wtf/fuzzer_linux_page_fault_test.cc b/src/wtf/fuzzer_linux_page_fault_test.cc new file mode 100644 index 0000000..36485bc --- /dev/null +++ b/src/wtf/fuzzer_linux_page_fault_test.cc @@ -0,0 +1,54 @@ +// Jason Crowder - February 2024 +#include "backend.h" + +namespace linux_page_fault_test { +Crash_t GetCrashTestcaseName(const char *Prefix, Backend_t *Backend) { + return Crash_t(fmt::format("crash-{}-{:#x}", Prefix, Backend->Cr2())); +} + +bool InsertTestcase(const uint8_t *Buffer, const size_t BufferSize) { + return true; +} + +bool Init(const Options_t &Opts, const CpuState_t &) { + if (!g_Backend->SetBreakpoint("asm_exc_page_fault", [](Backend_t *Backend) { + Backend->Stop(GetCrashTestcaseName("asm_exc_page_fault", Backend)); + })) { + fmt::print("Failed to insert crash breakpoint.\n"); + return false; + } + + if (!g_Backend->SetBreakpoint("asm_exc_divide_error", [](Backend_t *Backend) { + Backend->Stop(GetCrashTestcaseName("asm_exc_divide_error", Backend)); + })) { + fmt::print("Failed to insert crash breakpoint.\n"); + return false; + } + + if (!g_Backend->SetBreakpoint("force_sigsegv", [](Backend_t *Backend) { + Backend->Stop(GetCrashTestcaseName("force_sigsegv", Backend)); + })) { + fmt::print("Failed to insert crash breakpoint.\n"); + return false; + } + + if (!g_Backend->SetBreakpoint("page_fault_oops", [](Backend_t *Backend) { + Backend->Stop(GetCrashTestcaseName("page_fault_oops", Backend)); + })) { + fmt::print("Failed to insert crash breakpoint.\n"); + return false; + } + + if (!g_Backend->SetBreakpoint("done_with_test", [](Backend_t *Backend) { + Backend->Stop(Ok_t()); + })) { + fmt::print("Failed to insert breakpoint.\n"); + return false; + } + + return true; +} + +Target_t linux_page_fault_test("linux_page_fault_test", Init, InsertTestcase); + +} // namespace linux_page_fault_test diff --git a/src/wtf/globals.h b/src/wtf/globals.h index bccf216..bed9188 100644 --- a/src/wtf/globals.h +++ b/src/wtf/globals.h @@ -1441,4 +1441,4 @@ struct Options_t { // MasterOptions_t Master; -}; +}; \ No newline at end of file diff --git a/src/wtf/kvm_backend.cc b/src/wtf/kvm_backend.cc index e0d5166..f5cdbfd 100644 --- a/src/wtf/kvm_backend.cc +++ b/src/wtf/kvm_backend.cc @@ -2361,4 +2361,4 @@ void KvmBackend_t::StaticSignalAlarm(int, siginfo_t *, void *) { KvmBackend->SignalAlarm(); } -#endif +#endif \ No newline at end of file diff --git a/src/wtf/utils.cc b/src/wtf/utils.cc index 6660501..e2c4c0b 100644 --- a/src/wtf/utils.cc +++ b/src/wtf/utils.cc @@ -348,7 +348,7 @@ ParseCovFiles(const Backend_t &Backend, const fs::path &CovFilesDir) { File >> Json; const std::string &ModuleName = Json["name"].get(); - const uint64_t Base = g_Dbg.GetModuleBase(ModuleName.c_str()); + const uint64_t Base = g_Dbg->GetModuleBase(ModuleName.c_str()); if (Base == 0) { fmt::print("Failed to find the base of {}\n", ModuleName); return std::nullopt; @@ -483,4 +483,4 @@ ExceptionCodeToStr(const uint32_t ExceptionCode) { return "EXCEPTION_ACCESS_VIOLATION_EXECUTE"; } return "UNKNOWN"; -} +} \ No newline at end of file diff --git a/src/wtf/whv_backend.cc b/src/wtf/whv_backend.cc index 67be7ff..2a977af 100644 --- a/src/wtf/whv_backend.cc +++ b/src/wtf/whv_backend.cc @@ -1365,4 +1365,4 @@ uint64_t WhvBackend_t::SetReg(const Registers_t Reg, const uint64_t Value) { return Value; } -#endif +#endif \ No newline at end of file diff --git a/src/wtf/wtf.cc b/src/wtf/wtf.cc index 2ac1bc4..67b9bcc 100644 --- a/src/wtf/wtf.cc +++ b/src/wtf/wtf.cc @@ -80,7 +80,7 @@ int main(int argc, const char *argv[]) { MasterCmd->add_option("--runs", Opts.Master.Runs, "Runs") ->description("Number of mutations done.") - ->required(); + ->default_val(std::numeric_limits::max()); MasterCmd ->add_option("--max_len", Opts.Master.TestcaseBufferMaxSize, @@ -400,25 +400,50 @@ int main(int argc, const char *argv[]) { return EXIT_FAILURE; } + switch (Opts.Backend) { #ifdef WINDOWS - if (Opts.Backend == BackendType_t::Whv) { + case BackendType_t::Whv: { g_Backend = new WhvBackend_t(); + break; } #endif + #ifdef LINUX - if (Opts.Backend == BackendType_t::Kvm) { + case BackendType_t::Kvm: { g_Backend = new KvmBackend_t(); + break; } #endif - if (Opts.Backend == BackendType_t::Bochscpu) { + + case BackendType_t::Bochscpu: { g_Backend = new BochscpuBackend_t(); + break; + } + + default: { + return EXIT_FAILURE; + } } + // + // If the target name starts with 'linux', then assume that we won't be able + // to have WinDbg operate on the dump file, so let's swap the debugger + // instance. + // + +#ifdef WINDOWS + if (Opts.TargetName.starts_with("linux_")) { + fmt::print("Target name starts with 'linux_' so turning off the Windows " + "debugger..\n"); + g_Dbg = &g_NoDbg; + } +#endif + // // Initialize the debugger instance. // - if (!g_Dbg.Init(Opts.DumpPath, Opts.SymbolFilePath)) { + if (!g_Dbg->Init(Opts.DumpPath, Opts.SymbolFilePath)) { return EXIT_FAILURE; }