mirror of
https://github.com/nikaiw/VMkatz
synced 2026-09-25 07:51:06 +00:00
Add Kerberos AES/DES/RC4 key extraction from pKeyList
Read KIWI_KERBEROS_KEYS_LIST_6 from the session entry's pKeyList pointer, parse KERB_HASHPASSWORD_6[_1607] entries, decrypt and extract AES128, AES256, RC4 (NTLM), and DES key material. Offsets for Win10 1607+, Win10 1507, Win8, and Win7 variants. Keys are displayed in both text and Display formats. This closes the gap vs pypykatz which extracts these keys from minidumps. VMkatz now extracts passwords, keys, and tickets.
This commit is contained in:
Generated
+1
-1
@@ -735,7 +735,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vmkatz"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"anyhow",
|
||||
|
||||
@@ -1080,6 +1080,7 @@ fn scan_phys_for_kerberos_credentials<P: PhysicalMemory>(
|
||||
username,
|
||||
domain,
|
||||
password,
|
||||
keys: Vec::new(),
|
||||
tickets: Vec::new(),
|
||||
},
|
||||
));
|
||||
|
||||
+133
-2
@@ -1,7 +1,7 @@
|
||||
use crate::error::Result;
|
||||
use crate::lsass::crypto::CryptoKeys;
|
||||
use crate::lsass::patterns;
|
||||
use crate::lsass::types::{KerberosCredential, KerberosTicket, KerberosTicketType};
|
||||
use crate::lsass::types::{KerberosCredential, KerberosKey, KerberosTicket, KerberosTicketType};
|
||||
use crate::memory::VirtualMemory;
|
||||
use crate::pe::parser::PeHeaders;
|
||||
|
||||
@@ -15,6 +15,8 @@ struct KerbOffsets {
|
||||
credentials_ptr: u64,
|
||||
/// Password offset within KIWI_KERBEROS_PRIMARY_CREDENTIAL
|
||||
cred_password: u64,
|
||||
/// Pointer to KIWI_KERBEROS_KEYS_LIST_6 (AES/DES keys)
|
||||
key_list_ptr: u64,
|
||||
/// Offsets to ticket linked lists (LIST_ENTRY) within session entry.
|
||||
/// Each list: Flink/Blink (16 bytes). Flink points to the next
|
||||
/// KIWI_KERBEROS_INTERNAL_TICKET (at its Flink field).
|
||||
@@ -23,6 +25,26 @@ struct KerbOffsets {
|
||||
tickets_3: u64, // Client
|
||||
}
|
||||
|
||||
/// Kerberos key hash entry offsets per Windows version.
|
||||
struct KerbKeyEntryOffsets {
|
||||
/// Size of each KERB_HASHPASSWORD entry
|
||||
entry_size: u64,
|
||||
/// Offset to KERB_HASHPASSWORD_GENERIC within the entry
|
||||
generic_offset: u64,
|
||||
}
|
||||
|
||||
/// Win10 1607+: KERB_HASHPASSWORD_6_1607 (0x38 bytes, generic at 0x20)
|
||||
const KEY_ENTRY_1607: KerbKeyEntryOffsets = KerbKeyEntryOffsets {
|
||||
entry_size: 0x38,
|
||||
generic_offset: 0x20,
|
||||
};
|
||||
|
||||
/// Pre-1607 (Win7/8/Win10-1507): KERB_HASHPASSWORD_6 (0x30 bytes, generic at 0x18)
|
||||
const KEY_ENTRY_PRE1607: KerbKeyEntryOffsets = KerbKeyEntryOffsets {
|
||||
entry_size: 0x30,
|
||||
generic_offset: 0x18,
|
||||
};
|
||||
|
||||
/// Offsets within KIWI_KERBEROS_INTERNAL_TICKET (per version).
|
||||
#[allow(dead_code)]
|
||||
struct TicketOffsets {
|
||||
@@ -52,6 +74,7 @@ const KERB_OFFSET_VARIANTS: &[KerbOffsets] = &[
|
||||
luid: 0x48,
|
||||
credentials_ptr: 0x88,
|
||||
cred_password: 0x30,
|
||||
key_list_ptr: 0x118,
|
||||
tickets_1: 0x128,
|
||||
tickets_2: 0x140,
|
||||
tickets_3: 0x158,
|
||||
@@ -62,6 +85,7 @@ const KERB_OFFSET_VARIANTS: &[KerbOffsets] = &[
|
||||
luid: 0x48,
|
||||
credentials_ptr: 0x88,
|
||||
cred_password: 0x28,
|
||||
key_list_ptr: 0x108,
|
||||
tickets_1: 0x118,
|
||||
tickets_2: 0x130,
|
||||
tickets_3: 0x148,
|
||||
@@ -72,6 +96,7 @@ const KERB_OFFSET_VARIANTS: &[KerbOffsets] = &[
|
||||
luid: 0x40,
|
||||
credentials_ptr: 0x80,
|
||||
cred_password: 0x28,
|
||||
key_list_ptr: 0xD8,
|
||||
tickets_1: 0xE8,
|
||||
tickets_2: 0x100,
|
||||
tickets_3: 0x118,
|
||||
@@ -82,6 +107,7 @@ const KERB_OFFSET_VARIANTS: &[KerbOffsets] = &[
|
||||
luid: 0x18,
|
||||
credentials_ptr: 0x50,
|
||||
cred_password: 0x28,
|
||||
key_list_ptr: 0x90,
|
||||
tickets_1: 0xA0,
|
||||
tickets_2: 0xB8,
|
||||
tickets_3: 0xD0,
|
||||
@@ -256,6 +282,13 @@ pub fn extract_kerberos_credentials(
|
||||
let password =
|
||||
extract_kerb_password(vmem, cred_ptr, offsets.cred_password, keys).unwrap_or_default();
|
||||
|
||||
// Extract encryption keys (AES128, AES256, RC4, DES) from pKeyList
|
||||
let key_entry_offsets = match variant_idx {
|
||||
0 => &KEY_ENTRY_1607,
|
||||
_ => &KEY_ENTRY_PRE1607,
|
||||
};
|
||||
let kerb_keys = extract_kerb_keys(vmem, entry, offsets, key_entry_offsets, keys);
|
||||
|
||||
// Extract tickets from all 3 lists
|
||||
let mut tickets = Vec::new();
|
||||
let ticket_lists = [
|
||||
@@ -268,11 +301,12 @@ pub fn extract_kerberos_credentials(
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Kerberos: LUID=0x{:x} user={} domain={} password_len={} tickets={}",
|
||||
"Kerberos: LUID=0x{:x} user={} domain={} password_len={} keys={} tickets={}",
|
||||
luid,
|
||||
username,
|
||||
domain,
|
||||
password.len(),
|
||||
kerb_keys.len(),
|
||||
tickets.len()
|
||||
);
|
||||
|
||||
@@ -282,6 +316,7 @@ pub fn extract_kerberos_credentials(
|
||||
username: username.clone(),
|
||||
domain: domain.clone(),
|
||||
password,
|
||||
keys: kerb_keys,
|
||||
tickets,
|
||||
},
|
||||
));
|
||||
@@ -290,6 +325,102 @@ pub fn extract_kerberos_credentials(
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Extract Kerberos encryption keys (AES128, AES256, RC4, DES) from pKeyList.
|
||||
/// The pKeyList pointer in the session entry points to a KIWI_KERBEROS_KEYS_LIST_6:
|
||||
/// +0x00: unk0 (DWORD)
|
||||
/// +0x04: cbItem (DWORD) - number of key entries
|
||||
/// +0x08..+0x27: padding/unknown
|
||||
/// +0x28: array of KERB_HASHPASSWORD_6[_1607] entries
|
||||
fn extract_kerb_keys(
|
||||
vmem: &impl VirtualMemory,
|
||||
entry: u64,
|
||||
offsets: &KerbOffsets,
|
||||
key_entry_offsets: &KerbKeyEntryOffsets,
|
||||
keys: &CryptoKeys,
|
||||
) -> Vec<KerberosKey> {
|
||||
let key_list_ptr = match vmem.read_virt_u64(entry + offsets.key_list_ptr) {
|
||||
Ok(p) if p > 0x10000 && (p >> 48) == 0 => p,
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
|
||||
// Read key list header: cbItem at +0x04
|
||||
let cb_item = match vmem.read_virt_u32(key_list_ptr + 0x04) {
|
||||
Ok(n) if n > 0 && n <= 32 => n as usize,
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
|
||||
let mut result = Vec::new();
|
||||
let entries_base = key_list_ptr + 0x28; // past the KIWI_KERBEROS_KEYS_LIST_6 header
|
||||
|
||||
for i in 0..cb_item {
|
||||
let entry_base = entries_base + (i as u64) * key_entry_offsets.entry_size;
|
||||
let generic_base = entry_base + key_entry_offsets.generic_offset;
|
||||
|
||||
// KERB_HASHPASSWORD_GENERIC: Type (u32), pad, Size (u64), Checksump (u64)
|
||||
let etype = match vmem.read_virt_u32(generic_base) {
|
||||
Ok(t) => t,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let key_size = match vmem.read_virt_u64(generic_base + 0x08) {
|
||||
Ok(s) if s > 0 && s <= 256 => s as usize,
|
||||
_ => continue,
|
||||
};
|
||||
let checksum_ptr = match vmem.read_virt_u64(generic_base + 0x10) {
|
||||
Ok(p) if p > 0x10000 && (p >> 48) == 0 => p,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
// Read encrypted key bytes and decrypt
|
||||
let enc_key_data = match vmem.read_virt_bytes(checksum_ptr, key_size) {
|
||||
Ok(d) => d,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let decrypted = match crate::lsass::crypto::decrypt_credential(keys, &enc_key_data) {
|
||||
Ok(d) => d,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
// Validate key size matches expected for this etype
|
||||
let expected_len = match etype {
|
||||
17 => 16, // AES128
|
||||
18 => 32, // AES256
|
||||
23 => 16, // RC4/NTLM
|
||||
3 | 1 => 8, // DES
|
||||
_ => decrypted.len(),
|
||||
};
|
||||
if decrypted.len() < expected_len {
|
||||
continue;
|
||||
}
|
||||
let key_bytes = decrypted[..expected_len].to_vec();
|
||||
|
||||
// Skip all-zero keys
|
||||
if key_bytes.iter().all(|&b| b == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
" Kerberos key: etype={} ({}) size={} key={}",
|
||||
etype,
|
||||
match etype {
|
||||
17 => "AES128",
|
||||
18 => "AES256",
|
||||
23 => "RC4",
|
||||
3 => "DES",
|
||||
_ => "?",
|
||||
},
|
||||
key_bytes.len(),
|
||||
hex::encode(&key_bytes)
|
||||
);
|
||||
|
||||
result.push(KerberosKey {
|
||||
etype,
|
||||
key: key_bytes,
|
||||
});
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Walk a doubly-linked ticket list and extract each ticket.
|
||||
fn extract_tickets_from_list(
|
||||
vmem: &impl VirtualMemory,
|
||||
|
||||
+34
-1
@@ -115,12 +115,37 @@ pub struct WdigestCredential {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// Kerberos encryption key (AES128, AES256, RC4/NTLM, DES).
|
||||
#[derive(Debug)]
|
||||
pub struct KerberosKey {
|
||||
/// Kerberos encryption type (etype): 17=AES128, 18=AES256, 23=RC4, 3=DES
|
||||
pub etype: u32,
|
||||
/// Raw key bytes
|
||||
pub key: Vec<u8>,
|
||||
}
|
||||
|
||||
impl KerberosKey {
|
||||
/// Human-readable encryption type name.
|
||||
pub fn etype_name(&self) -> &'static str {
|
||||
match self.etype {
|
||||
1 => "DES_CBC_CRC",
|
||||
3 => "DES_CBC_MD5",
|
||||
17 => "AES128_HMAC",
|
||||
18 => "AES256_HMAC",
|
||||
23 => "RC4_HMAC",
|
||||
24 => "RC4_HMAC_EXP",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kerberos credential.
|
||||
#[derive(Debug)]
|
||||
pub struct KerberosCredential {
|
||||
pub username: String,
|
||||
pub domain: String,
|
||||
pub password: String,
|
||||
pub keys: Vec<KerberosKey>,
|
||||
pub tickets: Vec<KerberosTicket>,
|
||||
}
|
||||
|
||||
@@ -249,7 +274,7 @@ impl Credential {
|
||||
|| self
|
||||
.kerberos
|
||||
.as_ref()
|
||||
.is_some_and(|k| !k.password.is_empty() || !k.tickets.is_empty())
|
||||
.is_some_and(|k| !k.password.is_empty() || !k.keys.is_empty() || !k.tickets.is_empty())
|
||||
|| self.tspkg.as_ref().is_some_and(|t| !t.password.is_empty())
|
||||
|| !self.dpapi.is_empty()
|
||||
|| !self.credman.is_empty()
|
||||
@@ -313,6 +338,14 @@ impl std::fmt::Display for Credential {
|
||||
if !krb.password.is_empty() {
|
||||
writeln!(f, " Password: {}", krb.password)?;
|
||||
}
|
||||
for key in &krb.keys {
|
||||
writeln!(
|
||||
f,
|
||||
" {:11}: {}",
|
||||
key.etype_name(),
|
||||
hex::encode(&key.key)
|
||||
)?;
|
||||
}
|
||||
for ticket in &krb.tickets {
|
||||
writeln!(
|
||||
f,
|
||||
|
||||
@@ -1420,6 +1420,13 @@ fn print_text(credentials: &[Credential], c: &Colors) {
|
||||
if !krb.password.is_empty() {
|
||||
println!(" Password: {}{}{}", c.red, krb.password, c.reset);
|
||||
}
|
||||
for key in &krb.keys {
|
||||
println!(
|
||||
" {:11}: {}",
|
||||
key.etype_name(),
|
||||
hex::encode(&key.key)
|
||||
);
|
||||
}
|
||||
for ticket in &krb.tickets {
|
||||
println!(
|
||||
" [{}] {}",
|
||||
|
||||
Reference in New Issue
Block a user