win-kexp is a Rust 2024 library for Windows kernel exploitation research. It collects shellcode helpers, process injection utilities, ROP-chain tooling, kernel pool helpers, Win32k wrappers, and Windows Debug Engine integration for controlled x86_64 and ARM64 lab environments.
This repository is intended for exploit research in isolated Windows test systems. Do not run the examples or payloads against systems you do not own or have explicit permission to test.
- Shellcode: token stealing, ACL editing, and
cmd.exespawning payloads. - Assembly fallback: MASM/ARMASM-built shellcode when assemblers are available, hardcoded byte arrays otherwise.
- Process utilities: process lookup, remote allocation, shellcode writes, and
CreateRemoteThreadlaunch. - ROP helpers: chain-writing macros and executable PE section scanning for gadget lookup.
- Kernel tooling: pool helpers, Win32k/device I/O wrappers, and driver base discovery.
- Debug engine:
dbgengwrappers for local/kernel attach, command execution, breakpoints, symbols, dumps, traces, and session cleanup.
- Windows x86_64 or Windows ARM64.
- Rust stable for normal builds and nightly for Miri.
- MSVC build tools.
- Optional assembler:
ml64for x86_64 orarmasm64for ARM64. - Optional local test runner:
cargo nextest.
The crate is Windows-only in practice because public modules call Windows APIs directly. CI installs assemblers with glslang/setup-masm. Local builds without an assembler silently enable the shellcode_fallback cfg path.
# Build for the active Windows target.
cargo build --verbose
# Build ARM64 shellcode for Windows 23H2. Default is 24H2.
WINDOWS_VERSION=23H2 cargo build --target aarch64-pc-windows-msvc
# Match the CI formatter gate.
cargo fmt --all -- --check
# Preferred test runner.
cargo nextest run --verbose
# Fallback test runner.
cargo test
# Nightly unsafe-code check used by CI.
cargo miri test --verboseSupported ARM64 shellcode versions are 23H2 and 24H2; unset WINDOWS_VERSION defaults to 24H2.
Building the library for the native x64 MSVC target produces both the normal Rust
library and target\debug\win_kexp.dll:
rustup target add x86_64-pc-windows-msvc
cargo build --libRun that command from an x64 MSVC Rust host. If an explicit
--target x86_64-pc-windows-msvc is supplied instead, the DLL is written to
target\x86_64-pc-windows-msvc\debug\win_kexp.dll and the .load path must use
that target-qualified directory.
The pool walker assumes Windows 10 19H1 or later allocator algorithms and requires
full private/public type information for nt. Configure a Microsoft symbol-server
path, break into the kernel target, and force-load kernel symbols before using it:
.symfix
.reload /f nt
.load C:\path\to\win-kexp\target\debug\win_kexp.dll
!win_kexp.help
The primary command is:
!win_kexp.poolmap -tag Pipe
!win_kexp.poolmap -tag ABC -paged
!win_kexp.poolmap -tag Test -nonpaged -refresh
!win_kexp.poolmap ffff800012345678
-tag accepts one through four ASCII bytes. Internally the tag remains the exact
four-byte raw little-endian value; printable display text is only a rendering. The
tag map retains nearby unrelated allocations and holes. -paged and -nonpaged
filter exact pool identities and cannot be combined. An address query prints detail
for the allocation or hole containing that address. -refresh discards a complete
cached snapshot and walks again.
“Snapshot” is literal: the extension examines current allocations, reusable frees, and cached/delay-free spans while the target is stopped. It does not install allocation breakpoints or reconstruct allocation history. The cache is invalidated when execution resumes or the debugger session changes, and incomplete or Ctrl+C-interrupted walks are never cached.
When WinDbg accepts DML, map cells have colors and clickable address-detail links. The same rows use meaningful ASCII glyphs and include a legend when DML is stripped or plain-text output is captured.
Pool walking is x64-only because the allocator encodings consulted here are specific to x64. The rest of the crate, including ARM64 shellcode support, remains buildable for ARM64. Per-session paged heaps are excluded from the initial pool-map scope and the command reports that limitation. Corrupt or unreadable allocator metadata is shown conservatively rather than treated as an exploitable condition.
| Module | Purpose |
|---|---|
shellcode |
Payload loaders and fallback byte arrays. |
process |
Target process discovery and remote shellcode execution. |
rop |
ROP macros plus PE executable-section and gadget search helpers. |
pool |
Anonymous pipe helpers for kernel pool shaping experiments. |
win32k |
Device handles, IOCTL helpers, allocation helpers, and kernel driver lookup. |
dbgeng |
Windows Debug Engine sessions, commands, breakpoints, symbols, dumps, and traces. |
util |
Pause, debug break, and byte-formatting helpers. |
Assembly sources live in src/asm/. On Windows, build.rs checks for the target assembler:
- If found,
.asmfiles are compiled into COFF.objfiles andgoblinextracts executable bytes. - If missing,
cargo:rustc-cfg=feature="shellcode_fallback"is emitted and the matching byte arrays insrc/shellcode.rsare used.
test_shellcodes_match_fallback verifies that assembled payloads and fallback bytes match. When changing src/asm/, update the corresponding fallback byte array in src/shellcode.rs.
Available payloads:
| Function | Architectures | Description |
|---|---|---|
token_stealing_shellcode() |
x86_64, ARM64 | Copies the SYSTEM token to the current process. |
acl_edit_shellcode() |
x86_64, ARM64 | Edits the current process ACL. |
spawn_cmd_shellcode() |
x86_64, ARM64 | Resolves CreateProcessA and spawns cmd.exe. |
token_stealing_shellcode_smep_no_kvashadow() |
x86_64 | Token stealing with SMEP bypass. |
token_stealing_shellcode_smep_no_kvashadow_pte() |
x86_64 | Token stealing with PTE-based SMEP bypass. |
use win_kexp::process::inject_shellcode_to_target_process;
use win_kexp::shellcode::token_stealing_shellcode;
let shellcode = token_stealing_shellcode();
let pid = inject_shellcode_to_target_process("target.exe", &shellcode);
println!("started remote thread in pid {pid}");use win_kexp::rop::{find_gadget_offset, get_executable_sections};
let sections = get_executable_sections(ntoskrnl_module)?;
let pop_rax_ret = find_gadget_offset(§ions, &[0x58, 0xC3], ntoskrnl_base);For a debugger smoke test, see examples/kdtest.rs:
cargo run --example kdtest -- "net:port=50000,key=w.x.y.z"The repository uses Windows-only GitHub Actions workflows:
| Workflow | What it does |
|---|---|
ci.yml |
Runs cargo fmt --all -- --check, cargo build --verbose, and cargo nextest run --verbose on windows-latest and windows-11-arm. |
coverage.yml |
Runs instrumented cargo test, generates LCOV with grcov, and uploads to Codecov. |
miri.yml |
Runs cargo miri test --verbose on nightly with -Zmiri-disable-isolation -Zmiri-ignore-leaks. |
Use rustfmt defaults and keep unsafe Windows FFI blocks small. Add focused unit tests next to the code under #[cfg(test)]; existing test names use test_*. Commit subjects use lowercase prefixes such as fix:, feat:, docs:, style:, refactor:, test:, perf:, and chore:.