ntoseye 0.12.0

Windows kernel debugger for Linux hosts running Windows under KVM/QEMU
<picture>
  <source media="(prefers-color-scheme: light)" srcset="media/logo_light.svg">
  <img align="right" width="24%" src="media/logo_dark.svg" alt="logo">
</picture>

# ntoseye ![license]https://img.shields.io/badge/license-MIT-blue [![crates.io]https://img.shields.io/crates/v/ntoseye.svg]https://crates.io/crates/ntoseye

Windows kernel debugger for Linux hosts running Windows under KVM/QEMU. Essentially, WinDbg for Linux.

## Features

- Command line interface
- WinDbg style commands
- Kernel debugging
- PDB fetching & parsing for offsets
- Breakpointing (kernel, usermode)
- Bugcheck analysis (decodes the bug check code, parameters, and faulting site on a guest crash)
- Three backends: Windows KD over a serial pipe (KDCOM, default), QEMU's `gdbstub`, and passive memory introspection (see [Choosing a backend]#choosing-a-backend)

### Supported Windows

`ntoseye` currently only supports Windows 10 and 11 guests.

### Disclaimer

`ntoseye` needs to download symbols and images to initialize required offsets, it will only download symbols from Microsoft's official symbol server. All files which will be read/written to will be located in `$XDG_CONFIG_HOME/ntoseye`, which includes the following subfolders:
- `commands/` for custom scripted commands
- `images/` for binaries downloaded from the VM
- `symbols/` for PDBs

### Preview

![ntos](media/preview.png)

# Installation

## Install via shell script

```bash
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/dmaivel/ntoseye/releases/latest/download/ntoseye-installer.sh | sh
```

## Install via cargo

```bash
cargo install ntoseye
```

## Building

```bash
git clone https://github.com/dmaivel/ntoseye.git
cd ntoseye
cargo build --release
```

# Usage

## Quickstart

The default and recommended backend is `kd` (KDCOM), which runs Windows KD over a QEMU serial socket. For a libvirt/virt-manager guest, the fastest path is:

1. Configure the VM transport with `ntoseye virsh`: pick the domain, choose *configure debug transports*, then `kd`. (Prefer editing the XML yourself? See [VM configuration]#vm-configuration.)
2. In the guest, enable kernel debugging and reboot (Administrator PowerShell):
   ```
   bcdedit /debug on
   bcdedit /dbgsettings serial debugport:1 baudrate:115200
   Restart-Computer
   ```
3. On the host, relax ptrace scope so `ntoseye` can attach to QEMU (resets on reboot):
   ```bash
   echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
   ```
4. Start the VM, then run `ntoseye`.

For guests that aren't configured for KD, see [Choosing a backend](#choosing-a-backend) for the `gdb` and `memory` alternatives.

The debugger is self-documented: run `ntoseye --help` for command-line arguments, and press tab in the REPL for completions and descriptions of commands, symbols, and types.

## Choosing a backend

`ntoseye` can talk to the guest three ways. Pick with `--backend kd` (default), `--backend gdb`, or `--backend memory`.

| | `kd` (default) | `gdb` | `memory` |
|---|---|---|---|
| Transport | Windows KD over a serial pipe (KDCOM) | QEMU's `gdbstub` | None; `/dev/kvm` memory introspection only |
| Requires in-guest configuration | Yes (`bcdedit /debug on`; anti-debug code, PatchGuard, and some Windows behaviour change once enabled) | No (guest is unaware it's being debugged) | No |
| Requires host VM configuration | Yes (serial socket) | Yes (`-s -S`) | No |
| Execution control | Yes | Yes | No |
| Kernel breakpoints | Yes | Yes | No |
| Usermode breakpoints | Yes | No | No |
| Kernel breakpoint mechanism | `DbgKdWriteBreakPointApi` | gdb `Z0` packets | No |

See [VM configuration](#vm-configuration) for the host-side setup of each backend.

## VM configuration

Manual host-side setup for each backend. libvirt/virt-manager users can do most of this automatically with `ntoseye virsh` (see [Quickstart](#quickstart)); `ntoseye virsh` can also remove ntoseye-managed debug transports later.

### GDBSTUB

Fallback backend for guests that are not configured for Windows KD. Expose QEMU's gdbstub on `127.0.0.1:1234` by passing `-s -S`, then run with `--backend gdb`.

> [!NOTE]
> Do not enable kernel debug mode (`bcdedit /debug on`) in the guest when using the `gdb` backend. That setting is only for the `kd` backend, and the `gdb` backend's whole advantage is that the guest is unaware it's being debugged. With debug mode on, the kernel changes behaviour (anti-debug code, PatchGuard) and expects a KD debugger to service breaks, while nothing on the `gdb` side answers the KD transport, so the guest can hang on `DbgBreakPoint`/exceptions. Leave debug mode off.

#### QEMU

Append `-s -S` to the qemu command.

#### virt-manager

Add the following to the XML configuration:
```xml
<domain xmlns:qemu="http://libvirt.org/schemas/domain/qemu/1.0" type="kvm">
  ...
  <qemu:commandline>
    <qemu:arg value="-s"/>
    <qemu:arg value="-S"/>
  </qemu:commandline>
</domain>
```

### KDCOM

Default backend. In the guest, enable kernel debugging (run as Administrator, then reboot):
```
bcdedit /debug on
bcdedit /dbgsettings serial debugport:1 baudrate:115200
```
Use `debugport:2` instead of `:1` if the KD chardev ends up as COM2 (see the virt-manager subsection below).

#### QEMU

Add a Unix-socket chardev and route a serial port to it:
```
-chardev socket,id=kd,path=/tmp/ntoseye-kd.sock,server=on,wait=off -serial chardev:kd
```
Then connect: `ntoseye`.

The initial KD handshake timeout is 8 seconds by default. For unusually slow guests, override it with `NTOSEYE_KD_TIMEOUT=<seconds>`.

#### virt-manager

> [!WARNING]
> virt-manager auto-adds a `<serial>` console device on every VM, which
> claims COM1. Either replace that device with one pointing at the KD socket
> (KD becomes COM1, use `debugport:1`), or leave it and add the KD chardev
> via `qemu:commandline` (KD becomes COM2, use `debugport:2`).

**Option A (recommended):** replace the auto-added serial. KD is COM1, `debugport:1` is correct.
```xml
<serial type="unix">
  <source mode="bind" path="/tmp/ntoseye-kd.sock"/>
  <target type="isa-serial" port="0"/>
</serial>
```

**Option B:** keep the auto-added serial and append the KD chardev via `qemu:commandline`. If KD is COM2, use `debugport:2`.
```xml
<domain xmlns:qemu="http://libvirt.org/schemas/domain/qemu/1.0" type="kvm">
  ...
  <qemu:commandline>
    <qemu:arg value="-chardev"/>
    <qemu:arg value="socket,id=kd,path=/tmp/ntoseye-kd.sock,server=on,wait=off"/>
    <qemu:arg value="-serial"/>
    <qemu:arg value="chardev:kd"/>
  </qemu:commandline>
</domain>
```

### Memory

Passive backend for guests where you only want `/dev/kvm` memory introspection. It requires no guest or VM debug transport configuration:

```bash
ntoseye --backend memory
```

Execution control, registers, execution-context selection, breakpoints, debug output, bugcheck stops, and reload detection are unavailable in this mode. Run `capabilities` in the REPL for the exact backend feature matrix.

### Recommended guest tweaks

Although not required, disabling memory paging and compression in the guest avoids memory-related issues. This only needs to be done once per Windows installation (Administrator PowerShell):
```
Get-CimInstance Win32_ComputerSystem | Set-CimInstance -Property @{ AutomaticManagedPagefile = $false }
Get-CimInstance Win32_PageFileSetting | Remove-CimInstance
Disable-MMAgent -MemoryCompression
Restart-Computer
```

## Scripting

`ntoseye` auto-loads any `*.lua` file in `$XDG_CONFIG_HOME/ntoseye/commands/` at REPL startup. Scripts can register new commands that appear in tab completion and dispatch alongside the builtins. Run `reload` in the REPL to pick up script edits without restarting.

Bundled scripts can be installed/updated with:

```bash
ntoseye scripts install --force
ntoseye scripts list
```

`ntoseye scripts install <source>` also accepts:
- a local `.lua` file 
- a local directory of `.lua` files
- single HTTPS URL ending in `.lua` 

Local and remote installs print a trust warning and prompt before copying; use `--yes` for non-interactive installs and `--force` to overwrite existing scripts. Remote installs are limited to one `.lua` file and print the downloaded content's SHA-256.

```lua
register_command("name", "help text", function(arg1, arg2) ... end)

-- with per-argument tab-completion hints:
register_command("name", "help text", {"process", "symbol"}, function(a, b) ... end)
```

Completion strategies: `"none"`, `"symbol"`, `"type"`, `"process"`, `"vcpu"`, `"breakpoint"`, `"driver"`. The strategies table is positional; arg 1 uses the first entry, arg 2 the second, etc. Missing positions fall back to `"none"`. Pass `{}` for an empty table if you want completion turned off explicitly.

Scripts run with a constrained Lua standard library: base globals plus `table`, `string`, `math`, and `utf8`. Host filesystem/process/module access through Lua's `io`, `os`, and `package` libraries is not available; debugger interaction should go through `ntos`.

Host API is exposed under a global `ntos` table:

| function | returns |
|---|---|
| `ntos.ps([filter])` | array of `{pid, name, eprocess}` |
| `ntos.process(target)` / `ntos.try_process(target)` | one `{pid, name, eprocess}`; strict form raises with a candidate list when ambiguous, try_ form collapses both no-match and ambiguous into nil (call `process()` if you need to surface the ambiguity to the user); numeric targets require exact PID |
| `ntos.command_usage()` | nil (prints the current command's registered help) |
| `ntos.eval(expr)` / `ntos.try_eval(expr)` | Address / Address or nil |
| `ntos.read_byte/word/dword/qword(addr)` | integer / integer / integer / Address |
| `ntos.try_read_byte/word/dword/qword(addr)` | value or nil |
| `ntos.read_bytes(addr, len)` / `ntos.try_read_bytes(addr, len)` | Lua string / Lua string or nil |
| `ntos.read_struct(type, addr)` / `ntos.try_read_struct(type, addr)` | table / table or nil (raises if `type` is unknown, that always indicates a script bug, not a runtime condition); only top-level pointer/primitive/bitfield/enum fields decode (nested struct/union/array fields come back as raw byte strings; read those explicitly with `try_read_field_*` using `offset_of`) |
| `ntos.try_read_unicode_string(addr)` | string or nil (`addr` points to a `_UNICODE_STRING`) |
| `ntos.write_byte/word/dword/qword(addr, v)` | nil |
| `ntos.write_bytes(addr, str)` | nil |
| `ntos.loaded_module_list()` | Address (value printed in the startup banner) |
| `ntos.driver_objects()` / `ntos.try_find_driver_object(name)` | array of driver tables / driver table or nil |
| `ntos.kernel_modules()` / `ntos.try_find_kernel_module(name)` | array of `{name, short_name, base, size, end}` / one module or nil (match is exact on `short_name`, substring on `name`, case-insensitive) |
| `ntos.search(addr, len, pattern)` / `ntos.search_first(addr, len, pattern)` | array of Addresses / Address or nil (`pattern` is a raw Lua byte string, e.g. `"\x48\x83..."`) |
| `ntos.offset_of(type, field)` / `ntos.try_offset_of(type, field)` | integer / integer or nil |
| `ntos.type_size(type)` / `ntos.try_type_size(type)` | integer / integer or nil |
| `ntos.fields_of(type)` / `ntos.try_fields_of(type)` | array of field tables / array or nil |
| `ntos.try_read_field_byte/word/dword/qword(type, field, addr)` | value or nil (raises if `type`/`field` is unknown, that always indicates a script bug, not a runtime condition) |
| `ntos.containing_record(entry, type, field)` | Address |
| `ntos.can_read(addr, len)` | boolean |
| `ntos.is_kernel_address(addr)` | boolean (true if `addr` is in the canonical kernel half) |
| `ntos.try_closest_symbol(addr)` / `ntos.try_closest_symbol_any(addr)` | symbol table or nil |
| `ntos.format_symbol(addr)` | `"module!name+0x<offset>"` (no offset suffix when zero); falls back to hex if no symbol resolves |
| `ntos.read_register(name)` | Address (read-only; returned as Address so high-half values render correctly and compose with pointer math, use `:to_int()` for bit-tests on small values) |
| `ntos.addr(n)` | Address (constructor for literals) |

Addresses are an opaque userdata supporting `+ - & | ^ << >> < ==` and `tostring` (renders as hex). 

# Credits

Functionality regarding initialization of guest information was written with the help of the following sources:

- [vmread]https://github.com/h33p/vmread
- [pcileech]https://github.com/ufrisk/pcileech
- [MemProcFS]https://github.com/ufrisk/MemProcFS
- [ReactOS]https://github.com/reactos/reactos