<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  [](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

# Installation
## Install via shell script
```bash
## 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`.
| 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:
| `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)