mirror of
https://github.com/nikaiw/VMkatz
synced 2026-09-25 07:51:06 +00:00
Add transparent BitLocker disk decryption using FVEK from memory
When BitLocker FVEK keys are extracted from a VM memory snapshot, vmkatz can now use them to decrypt the corresponding encrypted disk for SAM/NTDS extraction in the same run. New modules: - sam/aes_xts.rs: AES-XTS-128/256 sector-level decryption - sam/bitlocker_decrypt.rs: transparent Read+Seek wrapper that decrypts sectors on-the-fly, validates FVEK by checking for NTFS signature in decrypted sector 0 Flow: snapshot → extract FVEK → open disk → detect BitLocker → try each FVEK candidate → decrypt partition → extract credentials. Works in directory mode (auto-discovers snapshot + disk) and with explicit --disk flag.
This commit is contained in:
+157
-6
@@ -761,6 +761,38 @@ fn run_vmfs(device_path: &Path, vmdk_path: Option<&str>, args: &Args) -> anyhow:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Print DiskSecrets (SAM + LSA + cached creds) using the current output format.
|
||||
#[cfg(feature = "sam")]
|
||||
fn print_disk_secrets(secrets: &vmkatz::sam::DiskSecrets, args: &Args) {
|
||||
let c = get_colors(args);
|
||||
|
||||
match args.format.as_str() {
|
||||
"ntlm" => print_sam_ntlm(&secrets.sam_entries),
|
||||
"csv" => print_sam_csv(&secrets.sam_entries),
|
||||
"hashcat" => print_sam_hashcat(&secrets.sam_entries),
|
||||
"brief" => print_sam_brief(&secrets.sam_entries),
|
||||
_ => print_sam_text(&secrets.sam_entries, c),
|
||||
}
|
||||
|
||||
if !secrets.lsa_secrets.is_empty() {
|
||||
match args.format.as_str() {
|
||||
"csv" => print_lsa_csv(&secrets.lsa_secrets),
|
||||
"hashcat" => {} // LSA secrets not applicable for hashcat
|
||||
_ => print_lsa_secrets(&secrets.lsa_secrets, c),
|
||||
}
|
||||
}
|
||||
|
||||
export_dpapi_backup_keys(&secrets.lsa_secrets);
|
||||
|
||||
if !secrets.cached_credentials.is_empty() {
|
||||
match args.format.as_str() {
|
||||
"csv" => print_dcc2_csv(&secrets.cached_credentials),
|
||||
"hashcat" => print_dcc2_hashcat(&secrets.cached_credentials),
|
||||
_ => print_cached_credentials(&secrets.cached_credentials, c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sam")]
|
||||
fn run_sam(input_path: &Path, args: &Args) -> anyhow::Result<()> {
|
||||
#[cfg(feature = "ntds.dit")]
|
||||
@@ -1550,10 +1582,58 @@ fn run_directory(dir: &Path, args: &Args) -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract BitLocker FVEK keys from memory snapshots for transparent disk decryption.
|
||||
// This is done as a separate pass so we can pass keys to the SAM extraction phase.
|
||||
#[cfg(all(
|
||||
feature = "sam",
|
||||
any(
|
||||
feature = "vmware",
|
||||
feature = "vbox",
|
||||
feature = "qemu",
|
||||
feature = "hyperv"
|
||||
)
|
||||
))]
|
||||
let bitlocker_keys: Vec<vmkatz::lsass::bitlocker::BitLockerKey> = {
|
||||
let mut keys = Vec::new();
|
||||
for file in &discovery.lsass_files {
|
||||
let snapshot_keys = extract_bitlocker_keys_from_snapshot(file);
|
||||
if !snapshot_keys.is_empty() {
|
||||
log::info!(
|
||||
"BitLocker: {} FVEK candidate(s) from {}",
|
||||
snapshot_keys.len(),
|
||||
file.display()
|
||||
);
|
||||
keys.extend(snapshot_keys);
|
||||
}
|
||||
}
|
||||
// Deduplicate by FVEK content
|
||||
keys.sort_by(|a, b| a.fvek.cmp(&b.fvek));
|
||||
keys.dedup_by(|a, b| a.fvek == b.fvek && a.method == b.method);
|
||||
keys
|
||||
};
|
||||
|
||||
#[cfg(feature = "sam")]
|
||||
for file in &discovery.disk_files {
|
||||
let name = file.file_name().unwrap_or_default().to_string_lossy();
|
||||
eprintln!("\n[*] SAM: {}", name);
|
||||
// Try BitLocker-aware extraction if we have FVEK keys from memory
|
||||
#[cfg(any(
|
||||
feature = "vmware",
|
||||
feature = "vbox",
|
||||
feature = "qemu",
|
||||
feature = "hyperv"
|
||||
))]
|
||||
if !bitlocker_keys.is_empty() {
|
||||
match vmkatz::sam::extract_disk_secrets_with_bitlocker(file, &bitlocker_keys) {
|
||||
Ok(secrets) => {
|
||||
print_disk_secrets(&secrets, args);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
log::info!("BitLocker-aware extraction failed for {}: {}", name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(e) = run_sam(file, args) {
|
||||
eprintln!("[!] {}: {:#}", name, e);
|
||||
}
|
||||
@@ -2431,12 +2511,15 @@ fn export_kerberos_tickets(credentials: &[Credential], args: &Args) {
|
||||
feature = "qemu",
|
||||
feature = "hyperv"
|
||||
))]
|
||||
fn extract_and_output_bitlocker<L: PhysicalMemory>(layer: &L, args: &Args) {
|
||||
fn extract_and_output_bitlocker<L: PhysicalMemory>(
|
||||
layer: &L,
|
||||
args: &Args,
|
||||
) -> Vec<vmkatz::lsass::bitlocker::BitLockerKey> {
|
||||
let t_bl = std::time::Instant::now();
|
||||
let keys = vmkatz::lsass::bitlocker::extract_bitlocker_keys(layer);
|
||||
if keys.is_empty() {
|
||||
log::debug!("BitLocker: no FVEK candidates found ({:?})", t_bl.elapsed());
|
||||
return;
|
||||
return keys;
|
||||
}
|
||||
eprintln!(
|
||||
"[+] BitLocker: {} FVEK candidate(s) found ({:?})",
|
||||
@@ -2448,15 +2531,30 @@ fn extract_and_output_bitlocker<L: PhysicalMemory>(layer: &L, args: &Args) {
|
||||
|
||||
println!(
|
||||
"\n{}[+] BitLocker FVEK ({} candidate(s)):{}\n",
|
||||
c.green, keys.len(), c.reset
|
||||
c.green,
|
||||
keys.len(),
|
||||
c.reset
|
||||
);
|
||||
|
||||
for (i, key) in keys.iter().enumerate() {
|
||||
println!(" {}Candidate #{}{}", c.bold, i + 1, c.reset);
|
||||
println!(" Cipher : {}{}{}", c.yellow, key.cipher, c.reset);
|
||||
println!(" FVEK : {}{}{}", c.yellow, hex::encode(&key.fvek), c.reset);
|
||||
println!(
|
||||
" Cipher : {}{}{}",
|
||||
c.yellow, key.cipher, c.reset
|
||||
);
|
||||
println!(
|
||||
" FVEK : {}{}{}",
|
||||
c.yellow,
|
||||
hex::encode(&key.fvek),
|
||||
c.reset
|
||||
);
|
||||
if !key.tweak.is_empty() {
|
||||
println!(" Tweak : {}{}{}", c.yellow, hex::encode(&key.tweak), c.reset);
|
||||
println!(
|
||||
" Tweak : {}{}{}",
|
||||
c.yellow,
|
||||
hex::encode(&key.tweak),
|
||||
c.reset
|
||||
);
|
||||
}
|
||||
println!(" Pool tag: {} @ 0x{:x}", key.pool_tag, key.phys_addr);
|
||||
println!(
|
||||
@@ -2475,6 +2573,59 @@ fn extract_and_output_bitlocker<L: PhysicalMemory>(layer: &L, args: &Args) {
|
||||
if let Some(dir) = &args.bitlocker_fvek {
|
||||
export_bitlocker_fvek(&keys, dir);
|
||||
}
|
||||
|
||||
keys
|
||||
}
|
||||
|
||||
/// Extract BitLocker FVEK keys from a memory snapshot file (without full LSASS extraction).
|
||||
///
|
||||
/// Opens the snapshot in the appropriate format, scans physical memory for
|
||||
/// pool tags, and returns any FVEK candidates found.
|
||||
#[cfg(any(
|
||||
feature = "vmware",
|
||||
feature = "vbox",
|
||||
feature = "qemu",
|
||||
feature = "hyperv"
|
||||
))]
|
||||
fn extract_bitlocker_keys_from_snapshot(
|
||||
path: &Path,
|
||||
) -> Vec<vmkatz::lsass::bitlocker::BitLockerKey> {
|
||||
let ext = path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("");
|
||||
let format = detect_lsass_format(path, ext, false);
|
||||
|
||||
// Open the memory layer and extract BitLocker keys
|
||||
macro_rules! try_layer {
|
||||
($make_layer:expr) => {
|
||||
match $make_layer {
|
||||
Ok(layer) => {
|
||||
return vmkatz::lsass::bitlocker::extract_bitlocker_keys(&layer);
|
||||
}
|
||||
Err(e) => {
|
||||
log::info!("BitLocker key extraction: failed to open {}: {}", path.display(), e);
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
match format {
|
||||
#[cfg(feature = "vbox")]
|
||||
LsassFormat::VBox => try_layer!(VBoxLayer::open(path)),
|
||||
#[cfg(feature = "qemu")]
|
||||
LsassFormat::QemuElf => try_layer!(QemuElfLayer::open(path)),
|
||||
#[cfg(feature = "qemu")]
|
||||
LsassFormat::QemuSavevm => try_layer!(vmkatz::qemu::QemuSavevmLayer::open(path)),
|
||||
#[cfg(feature = "hyperv")]
|
||||
LsassFormat::HypervBin => try_layer!(HypervLayer::open(path)),
|
||||
#[cfg(feature = "hyperv")]
|
||||
LsassFormat::HypervVmrs => try_layer!(vmkatz::hyperv::VmrsLayer::open(path)),
|
||||
#[cfg(feature = "vmware")]
|
||||
LsassFormat::Vmware => try_layer!(VmwareLayer::open(path)),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write dislocker-compatible FVEK files to disk.
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
//! AES-XTS sector-level decryption for BitLocker volume decryption.
|
||||
//!
|
||||
//! Implements AES-XTS-128 and AES-XTS-256 as used by BitLocker (Win8+).
|
||||
//! Uses raw AES ECB operations from the `aes` crate — not CBC wrappers.
|
||||
|
||||
use aes::cipher::generic_array::GenericArray;
|
||||
use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit};
|
||||
use aes::{Aes128, Aes256};
|
||||
|
||||
use crate::error::{Result, VmkatzError};
|
||||
|
||||
/// Sector size for BitLocker XTS decryption.
|
||||
const SECTOR_SIZE: usize = 512;
|
||||
|
||||
/// AES block size.
|
||||
const BLOCK_SIZE: usize = 16;
|
||||
|
||||
/// Multiply tweak by x in GF(2^128) (little-endian representation).
|
||||
///
|
||||
/// This is the standard XTS tweak update: shift left by 1 bit,
|
||||
/// and if the carry bit was set, XOR with the reduction polynomial 0x87.
|
||||
pub(crate) fn gf128_mul_x(tweak: &mut [u8; 16]) {
|
||||
let mut carry = 0u8;
|
||||
for byte in tweak.iter_mut() {
|
||||
let new_carry = *byte >> 7;
|
||||
*byte = (*byte << 1) | carry;
|
||||
carry = new_carry;
|
||||
}
|
||||
if carry != 0 {
|
||||
tweak[0] ^= 0x87;
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypt a single 512-byte sector using AES-XTS.
|
||||
///
|
||||
/// `key` must be 32 bytes (AES-128-XTS: two 16-byte keys) or
|
||||
/// 64 bytes (AES-256-XTS: two 32-byte keys).
|
||||
///
|
||||
/// `sector` must be exactly 512 bytes and is decrypted in place.
|
||||
///
|
||||
/// `sector_number` is the sector index used to derive the XTS tweak value.
|
||||
pub fn aes_xts_decrypt_sector(key: &[u8], sector: &mut [u8], sector_number: u64) -> Result<()> {
|
||||
if sector.len() != SECTOR_SIZE {
|
||||
return Err(VmkatzError::DecryptionError(format!(
|
||||
"AES-XTS: sector size must be {} bytes, got {}",
|
||||
SECTOR_SIZE,
|
||||
sector.len()
|
||||
)));
|
||||
}
|
||||
|
||||
match key.len() {
|
||||
32 => xts_decrypt_128(key, sector, sector_number),
|
||||
64 => xts_decrypt_256(key, sector, sector_number),
|
||||
n => Err(VmkatzError::DecryptionError(format!(
|
||||
"AES-XTS: key must be 32 or 64 bytes, got {}",
|
||||
n
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// AES-128-XTS decryption: key1 and key2 are each 16 bytes.
|
||||
fn xts_decrypt_128(key: &[u8], sector: &mut [u8], sector_number: u64) -> Result<()> {
|
||||
let key1 = &key[..16];
|
||||
let key2 = &key[16..32];
|
||||
|
||||
let cipher1 = Aes128::new(GenericArray::from_slice(key1));
|
||||
let cipher2 = Aes128::new(GenericArray::from_slice(key2));
|
||||
|
||||
xts_decrypt_inner(&cipher1, &cipher2, sector, sector_number)
|
||||
}
|
||||
|
||||
/// AES-256-XTS decryption: key1 and key2 are each 32 bytes.
|
||||
fn xts_decrypt_256(key: &[u8], sector: &mut [u8], sector_number: u64) -> Result<()> {
|
||||
let key1 = &key[..32];
|
||||
let key2 = &key[32..64];
|
||||
|
||||
let cipher1 = Aes256::new(GenericArray::from_slice(key1));
|
||||
let cipher2 = Aes256::new(GenericArray::from_slice(key2));
|
||||
|
||||
xts_decrypt_inner(&cipher1, &cipher2, sector, sector_number)
|
||||
}
|
||||
|
||||
/// Generic XTS decryption core that works with any AES key size.
|
||||
fn xts_decrypt_inner<C1, C2>(
|
||||
cipher1: &C1,
|
||||
cipher2: &C2,
|
||||
sector: &mut [u8],
|
||||
sector_number: u64,
|
||||
) -> Result<()>
|
||||
where
|
||||
C1: BlockDecrypt,
|
||||
C2: BlockEncrypt,
|
||||
{
|
||||
// Step 1: Encrypt the sector number as the initial tweak
|
||||
let mut tweak = [0u8; 16];
|
||||
tweak[..8].copy_from_slice(§or_number.to_le_bytes());
|
||||
|
||||
let mut tweak_block = GenericArray::clone_from_slice(&tweak);
|
||||
cipher2.encrypt_block(&mut tweak_block);
|
||||
tweak.copy_from_slice(&tweak_block);
|
||||
|
||||
// Step 2: Decrypt each 16-byte block with XTS
|
||||
let num_blocks = SECTOR_SIZE / BLOCK_SIZE;
|
||||
for i in 0..num_blocks {
|
||||
let offset = i * BLOCK_SIZE;
|
||||
let block = &mut sector[offset..offset + BLOCK_SIZE];
|
||||
|
||||
// XOR with tweak (pre-decrypt)
|
||||
for (b, t) in block.iter_mut().zip(tweak.iter()) {
|
||||
*b ^= *t;
|
||||
}
|
||||
|
||||
// AES decrypt
|
||||
let mut aes_block = GenericArray::clone_from_slice(block);
|
||||
cipher1.decrypt_block(&mut aes_block);
|
||||
block.copy_from_slice(&aes_block);
|
||||
|
||||
// XOR with tweak (post-decrypt)
|
||||
for (b, t) in block.iter_mut().zip(tweak.iter()) {
|
||||
*b ^= *t;
|
||||
}
|
||||
|
||||
// Advance tweak for next block
|
||||
gf128_mul_x(&mut tweak);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decrypt a buffer of contiguous sectors using AES-XTS.
|
||||
///
|
||||
/// `data` length must be a multiple of 512. Sectors are numbered sequentially
|
||||
/// starting from `first_sector_number`.
|
||||
pub fn aes_xts_decrypt_sectors(
|
||||
key: &[u8],
|
||||
data: &mut [u8],
|
||||
first_sector_number: u64,
|
||||
) -> Result<()> {
|
||||
if !data.len().is_multiple_of(SECTOR_SIZE) {
|
||||
return Err(VmkatzError::DecryptionError(format!(
|
||||
"AES-XTS: data length {} is not a multiple of sector size {}",
|
||||
data.len(),
|
||||
SECTOR_SIZE
|
||||
)));
|
||||
}
|
||||
|
||||
let num_sectors = data.len() / SECTOR_SIZE;
|
||||
for i in 0..num_sectors {
|
||||
let offset = i * SECTOR_SIZE;
|
||||
let sector = &mut data[offset..offset + SECTOR_SIZE];
|
||||
aes_xts_decrypt_sector(key, sector, first_sector_number + i as u64)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_gf128_mul_x_no_carry() {
|
||||
let mut tweak = [0u8; 16];
|
||||
tweak[0] = 0x01; // x^0
|
||||
gf128_mul_x(&mut tweak);
|
||||
assert_eq!(tweak[0], 0x02); // x^1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gf128_mul_x_with_carry() {
|
||||
let mut tweak = [0u8; 16];
|
||||
tweak[15] = 0x80; // MSB set = carry
|
||||
gf128_mul_x(&mut tweak);
|
||||
// After shift: all zeros. Carry set, so XOR 0x87 into byte[0]
|
||||
assert_eq!(tweak[0], 0x87);
|
||||
assert_eq!(tweak[15], 0x00);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xts_decrypt_wrong_sector_size() {
|
||||
let key = [0u8; 32];
|
||||
let mut sector = [0u8; 256]; // wrong size
|
||||
let result = aes_xts_decrypt_sector(&key, &mut sector, 0);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xts_decrypt_wrong_key_size() {
|
||||
let key = [0u8; 48]; // invalid
|
||||
let mut sector = [0u8; 512];
|
||||
let result = aes_xts_decrypt_sector(&key, &mut sector, 0);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xts_roundtrip_128() {
|
||||
// Encrypt then decrypt should give back the original plaintext.
|
||||
// We'll encrypt manually and then verify decryption.
|
||||
let key = [0x42u8; 32];
|
||||
let plaintext = [0xABu8; 512];
|
||||
|
||||
// Encrypt: same as decrypt but use BlockEncrypt for cipher1
|
||||
let key1 = &key[..16];
|
||||
let key2 = &key[16..32];
|
||||
let cipher1 = Aes128::new(GenericArray::from_slice(key1));
|
||||
let cipher2 = Aes128::new(GenericArray::from_slice(key2));
|
||||
|
||||
let sector_number: u64 = 42;
|
||||
let mut tweak = [0u8; 16];
|
||||
tweak[..8].copy_from_slice(§or_number.to_le_bytes());
|
||||
let mut tweak_block = GenericArray::clone_from_slice(&tweak);
|
||||
cipher2.encrypt_block(&mut tweak_block);
|
||||
tweak.copy_from_slice(&tweak_block);
|
||||
|
||||
let mut ciphertext = plaintext;
|
||||
for i in 0..(512 / 16) {
|
||||
let offset = i * 16;
|
||||
let block = &mut ciphertext[offset..offset + 16];
|
||||
for (b, t) in block.iter_mut().zip(tweak.iter()) {
|
||||
*b ^= *t;
|
||||
}
|
||||
let mut aes_block = GenericArray::clone_from_slice(block);
|
||||
cipher1.encrypt_block(&mut aes_block);
|
||||
block.copy_from_slice(&aes_block);
|
||||
for (b, t) in block.iter_mut().zip(tweak.iter()) {
|
||||
*b ^= *t;
|
||||
}
|
||||
gf128_mul_x(&mut tweak);
|
||||
}
|
||||
|
||||
// Now decrypt and verify
|
||||
let mut decrypted = ciphertext;
|
||||
aes_xts_decrypt_sector(&key, &mut decrypted, sector_number).unwrap();
|
||||
assert_eq!(&decrypted[..], &plaintext[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xts_roundtrip_256() {
|
||||
let key = [0x55u8; 64];
|
||||
let plaintext = [0xCDu8; 512];
|
||||
|
||||
let key1 = &key[..32];
|
||||
let key2 = &key[32..64];
|
||||
let cipher1 = Aes256::new(GenericArray::from_slice(key1));
|
||||
let cipher2 = Aes256::new(GenericArray::from_slice(key2));
|
||||
|
||||
let sector_number: u64 = 100;
|
||||
let mut tweak = [0u8; 16];
|
||||
tweak[..8].copy_from_slice(§or_number.to_le_bytes());
|
||||
let mut tweak_block = GenericArray::clone_from_slice(&tweak);
|
||||
cipher2.encrypt_block(&mut tweak_block);
|
||||
tweak.copy_from_slice(&tweak_block);
|
||||
|
||||
let mut ciphertext = plaintext;
|
||||
for i in 0..(512 / 16) {
|
||||
let offset = i * 16;
|
||||
let block = &mut ciphertext[offset..offset + 16];
|
||||
for (b, t) in block.iter_mut().zip(tweak.iter()) {
|
||||
*b ^= *t;
|
||||
}
|
||||
let mut aes_block = GenericArray::clone_from_slice(block);
|
||||
cipher1.encrypt_block(&mut aes_block);
|
||||
block.copy_from_slice(&aes_block);
|
||||
for (b, t) in block.iter_mut().zip(tweak.iter()) {
|
||||
*b ^= *t;
|
||||
}
|
||||
gf128_mul_x(&mut tweak);
|
||||
}
|
||||
|
||||
let mut decrypted = ciphertext;
|
||||
aes_xts_decrypt_sector(&key, &mut decrypted, sector_number).unwrap();
|
||||
assert_eq!(&decrypted[..], &plaintext[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xts_multi_sector() {
|
||||
let key = [0x37u8; 32];
|
||||
let plaintext = [0x99u8; 1024]; // 2 sectors
|
||||
|
||||
// Encrypt both sectors
|
||||
let mut ciphertext = plaintext;
|
||||
for s in 0..2u64 {
|
||||
let key1 = &key[..16];
|
||||
let key2 = &key[16..32];
|
||||
let cipher1 = Aes128::new(GenericArray::from_slice(key1));
|
||||
let cipher2 = Aes128::new(GenericArray::from_slice(key2));
|
||||
|
||||
let mut tweak = [0u8; 16];
|
||||
tweak[..8].copy_from_slice(&s.to_le_bytes());
|
||||
let mut tweak_block = GenericArray::clone_from_slice(&tweak);
|
||||
cipher2.encrypt_block(&mut tweak_block);
|
||||
tweak.copy_from_slice(&tweak_block);
|
||||
|
||||
let sector = &mut ciphertext[(s as usize * 512)..((s as usize + 1) * 512)];
|
||||
for i in 0..(512 / 16) {
|
||||
let offset = i * 16;
|
||||
let block = &mut sector[offset..offset + 16];
|
||||
for (b, t) in block.iter_mut().zip(tweak.iter()) {
|
||||
*b ^= *t;
|
||||
}
|
||||
let mut aes_block = GenericArray::clone_from_slice(block);
|
||||
cipher1.encrypt_block(&mut aes_block);
|
||||
block.copy_from_slice(&aes_block);
|
||||
for (b, t) in block.iter_mut().zip(tweak.iter()) {
|
||||
*b ^= *t;
|
||||
}
|
||||
gf128_mul_x(&mut tweak);
|
||||
}
|
||||
}
|
||||
|
||||
let mut decrypted = ciphertext;
|
||||
aes_xts_decrypt_sectors(&key, &mut decrypted, 0).unwrap();
|
||||
assert_eq!(&decrypted[..], &plaintext[..]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
//! BitLocker transparent decryption wrapper.
|
||||
//!
|
||||
//! Wraps a `Read + Seek` disk reader and transparently decrypts BitLocker-encrypted
|
||||
//! sectors using an FVEK extracted from memory. The decrypted stream appears as a
|
||||
//! plain NTFS volume to downstream parsers (NTFS hive reader, etc.).
|
||||
//!
|
||||
//! Supports AES-XTS-128 (method 0x8004) and AES-XTS-256 (method 0x8005).
|
||||
|
||||
use std::io::{self, Read, Seek, SeekFrom};
|
||||
|
||||
use super::aes_xts;
|
||||
|
||||
/// Sector size for BitLocker decryption.
|
||||
const SECTOR_SIZE: u64 = 512;
|
||||
|
||||
/// A decrypting reader that transparently decrypts BitLocker-encrypted sectors.
|
||||
///
|
||||
/// Reads from the underlying reader at `partition_offset + position`, decrypts
|
||||
/// each 512-byte sector with AES-XTS, and presents a plain-text view.
|
||||
pub struct BitLockerReader<R: Read + Seek> {
|
||||
inner: R,
|
||||
/// Byte offset of the BitLocker partition on the disk image.
|
||||
partition_offset: u64,
|
||||
/// Full XTS key (32 bytes for AES-128-XTS, 64 bytes for AES-256-XTS).
|
||||
xts_key: Vec<u8>,
|
||||
/// Current read position relative to partition start.
|
||||
position: u64,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> BitLockerReader<R> {
|
||||
/// Create a new BitLocker decrypting reader.
|
||||
///
|
||||
/// `partition_offset` is the byte offset of the encrypted partition on disk.
|
||||
/// `xts_key` is the full AES-XTS key (key1 || key2): 32 bytes for XTS-128, 64 for XTS-256.
|
||||
pub fn new(inner: R, partition_offset: u64, xts_key: Vec<u8>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
partition_offset,
|
||||
xts_key,
|
||||
position: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to decrypt sector 0 and check for NTFS signature at offset 3.
|
||||
///
|
||||
/// Returns true if the decrypted first sector starts with "NTFS" at byte 3,
|
||||
/// indicating a valid NTFS boot sector and correct FVEK.
|
||||
pub fn validate_ntfs_signature(&mut self) -> bool {
|
||||
let mut sector = [0u8; SECTOR_SIZE as usize];
|
||||
|
||||
// Read encrypted sector 0
|
||||
if self.inner.seek(SeekFrom::Start(self.partition_offset)).is_err() {
|
||||
return false;
|
||||
}
|
||||
if self.inner.read_exact(&mut sector).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decrypt sector 0 (sector number = 0)
|
||||
if aes_xts::aes_xts_decrypt_sector(&self.xts_key, &mut sector, 0).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for "NTFS" OEM ID at offset 3 in the NTFS boot sector
|
||||
sector.get(3..7) == Some(b"NTFS")
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Read for BitLockerReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if buf.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut total_read = 0usize;
|
||||
|
||||
while total_read < buf.len() {
|
||||
let current_pos = self.position;
|
||||
let sector_number = current_pos / SECTOR_SIZE;
|
||||
let offset_in_sector = (current_pos % SECTOR_SIZE) as usize;
|
||||
|
||||
// Read the full encrypted sector from the underlying reader
|
||||
let disk_offset = self.partition_offset + sector_number * SECTOR_SIZE;
|
||||
self.inner.seek(SeekFrom::Start(disk_offset)).map_err(|e| {
|
||||
io::Error::other(
|
||||
format!("BitLocker seek to sector {}: {}", sector_number, e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut sector_buf = [0u8; SECTOR_SIZE as usize];
|
||||
match self.inner.read_exact(&mut sector_buf) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof && total_read > 0 => {
|
||||
// Partial read at end of volume
|
||||
break;
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
|
||||
return Ok(0); // EOF
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
// Decrypt the sector
|
||||
aes_xts::aes_xts_decrypt_sector(&self.xts_key, &mut sector_buf, sector_number)
|
||||
.map_err(|e| {
|
||||
io::Error::other(
|
||||
format!("BitLocker XTS decrypt sector {}: {}", sector_number, e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Copy the relevant portion to the output buffer
|
||||
let available = SECTOR_SIZE as usize - offset_in_sector;
|
||||
let remaining = buf.len() - total_read;
|
||||
let to_copy = available.min(remaining);
|
||||
|
||||
buf[total_read..total_read + to_copy]
|
||||
.copy_from_slice(§or_buf[offset_in_sector..offset_in_sector + to_copy]);
|
||||
|
||||
total_read += to_copy;
|
||||
self.position += to_copy as u64;
|
||||
}
|
||||
|
||||
Ok(total_read)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Seek for BitLockerReader<R> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
let new_position = match pos {
|
||||
SeekFrom::Start(offset) => offset,
|
||||
SeekFrom::Current(delta) => {
|
||||
if delta >= 0 {
|
||||
self.position.checked_add(delta as u64)
|
||||
} else {
|
||||
self.position.checked_sub((-delta) as u64)
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "Seek position overflow")
|
||||
})?
|
||||
}
|
||||
SeekFrom::End(_) => {
|
||||
// We don't know the partition size easily, so pass through to inner.
|
||||
// This is rarely used by NTFS parsers (they use SeekFrom::Start).
|
||||
let abs = self.inner.seek(pos)?;
|
||||
let relative = abs.saturating_sub(self.partition_offset);
|
||||
self.position = relative;
|
||||
return Ok(relative);
|
||||
}
|
||||
};
|
||||
|
||||
self.position = new_position;
|
||||
Ok(new_position)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the full AES-XTS key from a `BitLockerKey`.
|
||||
///
|
||||
/// For XTS modes (0x8004, 0x8005), the key is key1 || key2 where:
|
||||
/// - key1 = data encryption key
|
||||
/// - key2 = tweak encryption key
|
||||
///
|
||||
/// The Cngb extraction stores only one AES key in `fvek` (the two internal
|
||||
/// copies are validated to be equal). For XTS we need both halves, so this
|
||||
/// function tries multiple strategies:
|
||||
/// 1. If fvek is already the full size (32/64 bytes), use as-is
|
||||
/// 2. If fvek is half size (16/32 bytes), duplicate it (fvek || fvek)
|
||||
///
|
||||
/// Returns `None` for unsupported encryption methods (Diffuser/CBC).
|
||||
pub fn build_xts_key(key: &crate::lsass::bitlocker::BitLockerKey) -> Option<Vec<u8>> {
|
||||
let expected_full_len = match key.method {
|
||||
0x8004 => 32, // AES-128-XTS: 2 x 16 bytes
|
||||
0x8005 => 64, // AES-256-XTS: 2 x 32 bytes
|
||||
_ => return None, // CBC/Diffuser modes not yet supported
|
||||
};
|
||||
|
||||
let expected_half = expected_full_len / 2;
|
||||
|
||||
if key.fvek.len() == expected_full_len {
|
||||
// Already the full XTS key
|
||||
Some(key.fvek.clone())
|
||||
} else if key.fvek.len() == expected_half {
|
||||
// Half key — duplicate for both data and tweak
|
||||
// This is common with Cngb extraction where only one AES key is captured.
|
||||
// The real tweak key may differ; the caller should validate with NTFS signature.
|
||||
let mut full = Vec::with_capacity(expected_full_len);
|
||||
full.extend_from_slice(&key.fvek);
|
||||
full.extend_from_slice(&key.fvek);
|
||||
Some(full)
|
||||
} else {
|
||||
log::warn!(
|
||||
"BitLocker: unexpected FVEK length {} for method 0x{:04x} (expected {} or {})",
|
||||
key.fvek.len(),
|
||||
key.method,
|
||||
expected_half,
|
||||
expected_full_len,
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
/// Create a fake "encrypted" volume by XTS-encrypting known plaintext.
|
||||
fn make_encrypted_ntfs_volume(key: &[u8], partition_offset: u64) -> Vec<u8> {
|
||||
use aes::cipher::generic_array::GenericArray;
|
||||
use aes::cipher::{BlockEncrypt, KeyInit};
|
||||
use aes::Aes128;
|
||||
|
||||
// Create a 2-sector volume with NTFS signature
|
||||
let mut plaintext = vec![0u8; 1024];
|
||||
// NTFS boot sector: "NTFS" at offset 3
|
||||
plaintext[0] = 0xEB; // JMP short
|
||||
plaintext[1] = 0x52;
|
||||
plaintext[2] = 0x90;
|
||||
plaintext[3..7].copy_from_slice(b"NTFS");
|
||||
plaintext[7] = 0x20; // space
|
||||
// Fill rest with recognizable pattern
|
||||
for (i, byte) in plaintext[8..512].iter_mut().enumerate() {
|
||||
*byte = ((i + 8) & 0xFF) as u8;
|
||||
}
|
||||
for (i, byte) in plaintext[512..1024].iter_mut().enumerate() {
|
||||
*byte = ((i + 512 + 0x55) & 0xFF) as u8;
|
||||
}
|
||||
|
||||
// Encrypt each sector with AES-XTS
|
||||
let key1 = &key[..16];
|
||||
let key2 = &key[16..32];
|
||||
let cipher1 = Aes128::new(GenericArray::from_slice(key1));
|
||||
let cipher2 = Aes128::new(GenericArray::from_slice(key2));
|
||||
|
||||
let mut encrypted = plaintext.clone();
|
||||
for s in 0..2u64 {
|
||||
let sector = &mut encrypted[(s as usize * 512)..((s as usize + 1) * 512)];
|
||||
let mut tweak = [0u8; 16];
|
||||
tweak[..8].copy_from_slice(&s.to_le_bytes());
|
||||
let mut tweak_block = GenericArray::clone_from_slice(&tweak);
|
||||
cipher2.encrypt_block(&mut tweak_block);
|
||||
tweak.copy_from_slice(&tweak_block);
|
||||
|
||||
for i in 0..(512 / 16) {
|
||||
let offset = i * 16;
|
||||
let block = &mut sector[offset..offset + 16];
|
||||
for (b, t) in block.iter_mut().zip(tweak.iter()) {
|
||||
*b ^= *t;
|
||||
}
|
||||
let mut aes_block = GenericArray::clone_from_slice(block);
|
||||
cipher1.encrypt_block(&mut aes_block);
|
||||
block.copy_from_slice(&aes_block);
|
||||
for (b, t) in block.iter_mut().zip(tweak.iter()) {
|
||||
*b ^= *t;
|
||||
}
|
||||
super::super::aes_xts::gf128_mul_x(&mut tweak);
|
||||
}
|
||||
}
|
||||
|
||||
// Build disk image: padding + encrypted partition
|
||||
let mut disk = vec![0u8; partition_offset as usize];
|
||||
disk.extend_from_slice(&encrypted);
|
||||
disk
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bitlocker_reader_validates_ntfs() {
|
||||
let key = [0x42u8; 32]; // AES-128-XTS key
|
||||
let partition_offset = 1048576u64; // 1 MB
|
||||
|
||||
let disk = make_encrypted_ntfs_volume(&key, partition_offset);
|
||||
let cursor = Cursor::new(disk);
|
||||
|
||||
let mut reader = BitLockerReader::new(cursor, partition_offset, key.to_vec());
|
||||
assert!(reader.validate_ntfs_signature());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bitlocker_reader_rejects_wrong_key() {
|
||||
let key = [0x42u8; 32];
|
||||
let partition_offset = 1048576u64;
|
||||
|
||||
let disk = make_encrypted_ntfs_volume(&key, partition_offset);
|
||||
let cursor = Cursor::new(disk);
|
||||
|
||||
let wrong_key = [0x99u8; 32];
|
||||
let mut reader = BitLockerReader::new(cursor, partition_offset, wrong_key.to_vec());
|
||||
assert!(!reader.validate_ntfs_signature());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bitlocker_reader_read_and_seek() {
|
||||
use aes::cipher::generic_array::GenericArray;
|
||||
use aes::cipher::{BlockEncrypt, KeyInit};
|
||||
use aes::Aes128;
|
||||
|
||||
let key = [0x42u8; 32];
|
||||
let partition_offset = 512u64; // Small offset for simplicity
|
||||
|
||||
// Create plaintext
|
||||
let mut plaintext = vec![0u8; 1024];
|
||||
for (i, byte) in plaintext.iter_mut().enumerate() {
|
||||
*byte = (i & 0xFF) as u8;
|
||||
}
|
||||
plaintext[3..7].copy_from_slice(b"NTFS");
|
||||
|
||||
// Encrypt
|
||||
let key1 = &key[..16];
|
||||
let key2 = &key[16..32];
|
||||
let cipher1 = Aes128::new(GenericArray::from_slice(key1));
|
||||
let cipher2 = Aes128::new(GenericArray::from_slice(key2));
|
||||
|
||||
let mut encrypted = plaintext.clone();
|
||||
for s in 0..2u64 {
|
||||
let sector = &mut encrypted[(s as usize * 512)..((s as usize + 1) * 512)];
|
||||
let mut tweak = [0u8; 16];
|
||||
tweak[..8].copy_from_slice(&s.to_le_bytes());
|
||||
let mut tweak_block = GenericArray::clone_from_slice(&tweak);
|
||||
cipher2.encrypt_block(&mut tweak_block);
|
||||
tweak.copy_from_slice(&tweak_block);
|
||||
|
||||
for i in 0..(512 / 16) {
|
||||
let off = i * 16;
|
||||
let block = &mut sector[off..off + 16];
|
||||
for (b, t) in block.iter_mut().zip(tweak.iter()) {
|
||||
*b ^= *t;
|
||||
}
|
||||
let mut aes_block = GenericArray::clone_from_slice(block);
|
||||
cipher1.encrypt_block(&mut aes_block);
|
||||
block.copy_from_slice(&aes_block);
|
||||
for (b, t) in block.iter_mut().zip(tweak.iter()) {
|
||||
*b ^= *t;
|
||||
}
|
||||
super::super::aes_xts::gf128_mul_x(&mut tweak);
|
||||
}
|
||||
}
|
||||
|
||||
let mut disk = vec![0u8; partition_offset as usize];
|
||||
disk.extend_from_slice(&encrypted);
|
||||
let cursor = Cursor::new(disk);
|
||||
|
||||
let mut reader = BitLockerReader::new(cursor, partition_offset, key.to_vec());
|
||||
|
||||
// Read first 16 bytes
|
||||
let mut buf = [0u8; 16];
|
||||
reader.read_exact(&mut buf).unwrap();
|
||||
assert_eq!(&buf[3..7], b"NTFS");
|
||||
assert_eq!(&buf[..3], &plaintext[..3]);
|
||||
|
||||
// Seek to sector 1 start and read
|
||||
reader.seek(SeekFrom::Start(512)).unwrap();
|
||||
let mut buf2 = [0u8; 16];
|
||||
reader.read_exact(&mut buf2).unwrap();
|
||||
assert_eq!(&buf2, &plaintext[512..528]);
|
||||
|
||||
// Seek back to start
|
||||
reader.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut full = vec![0u8; 1024];
|
||||
reader.read_exact(&mut full).unwrap();
|
||||
assert_eq!(&full, &plaintext);
|
||||
}
|
||||
}
|
||||
+196
@@ -9,6 +9,8 @@ mod ntfs_reader;
|
||||
mod disk_fallbacks;
|
||||
mod vmdk_scan;
|
||||
|
||||
pub mod aes_xts;
|
||||
pub mod bitlocker_decrypt;
|
||||
pub mod dpapi_masterkey;
|
||||
|
||||
// Re-export pub(crate) items used by other modules (paging/pagefile, paging/filebacked)
|
||||
@@ -85,6 +87,200 @@ pub fn extract_ntds_artifacts(path: &Path) -> Result<NtdsArtifacts> {
|
||||
extract_ntds_artifacts_from_reader(&mut disk)
|
||||
}
|
||||
|
||||
/// Extract secrets from a disk, trying FVEK keys for BitLocker partitions.
|
||||
///
|
||||
/// When BitLocker-encrypted partitions are detected and FVEK candidates are
|
||||
/// available, each candidate is tried until one successfully decrypts a valid
|
||||
/// NTFS volume. Falls back to standard (non-BitLocker) extraction if no
|
||||
/// encrypted partition is found or no key works.
|
||||
pub fn extract_disk_secrets_with_bitlocker(
|
||||
path: &Path,
|
||||
fvek_keys: &[crate::lsass::bitlocker::BitLockerKey],
|
||||
) -> Result<DiskSecrets> {
|
||||
let mut disk = crate::disk::open_disk(path)?;
|
||||
extract_secrets_with_bitlocker(&mut disk, fvek_keys)
|
||||
}
|
||||
|
||||
/// Extract NTDS artifacts from a disk, trying FVEK keys for BitLocker partitions.
|
||||
pub fn extract_ntds_artifacts_with_bitlocker(
|
||||
path: &Path,
|
||||
fvek_keys: &[crate::lsass::bitlocker::BitLockerKey],
|
||||
) -> Result<NtdsArtifacts> {
|
||||
let mut disk = crate::disk::open_disk(path)?;
|
||||
extract_ntds_with_bitlocker(&mut disk, fvek_keys)
|
||||
}
|
||||
|
||||
/// Core BitLocker-aware extraction for SAM/LSA secrets.
|
||||
fn extract_secrets_with_bitlocker<R: Read + Seek>(
|
||||
reader: &mut R,
|
||||
fvek_keys: &[crate::lsass::bitlocker::BitLockerKey],
|
||||
) -> Result<DiskSecrets> {
|
||||
let partitions = find_ntfs_partitions(reader).unwrap_or_default();
|
||||
|
||||
for &partition_offset in &partitions {
|
||||
// Try unencrypted first
|
||||
if !is_bitlocker_partition(reader, partition_offset) {
|
||||
log::info!("Trying NTFS partition at offset 0x{:x}", partition_offset);
|
||||
match ntfs_reader::read_hive_files(reader, partition_offset) {
|
||||
Ok((sam_data, system_data, security_data)) => {
|
||||
return process_hive_data(sam_data, system_data, security_data);
|
||||
}
|
||||
Err(e) => {
|
||||
log::info!("Partition at 0x{:x}: {}", partition_offset, e);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// BitLocker-encrypted partition — try each FVEK candidate
|
||||
eprintln!(
|
||||
"[*] BitLocker partition at 0x{:x} — trying {} FVEK candidate(s)",
|
||||
partition_offset,
|
||||
fvek_keys.len()
|
||||
);
|
||||
|
||||
if let Some(secrets) = try_bitlocker_fvek_candidates(
|
||||
reader,
|
||||
partition_offset,
|
||||
fvek_keys,
|
||||
|bl_reader, _offset| {
|
||||
ntfs_reader::read_hive_files(bl_reader, 0)
|
||||
.and_then(|(sam, sys, sec)| process_hive_data(sam, sys, sec))
|
||||
},
|
||||
) {
|
||||
return Ok(secrets);
|
||||
}
|
||||
}
|
||||
|
||||
Err(crate::error::VmkatzError::DecryptionError(
|
||||
"No secrets found (BitLocker-aware scan exhausted all partitions and FVEK candidates)"
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Core BitLocker-aware extraction for NTDS artifacts.
|
||||
fn extract_ntds_with_bitlocker<R: Read + Seek>(
|
||||
reader: &mut R,
|
||||
fvek_keys: &[crate::lsass::bitlocker::BitLockerKey],
|
||||
) -> Result<NtdsArtifacts> {
|
||||
let partitions = find_ntfs_partitions(reader).unwrap_or_default();
|
||||
|
||||
for &partition_offset in &partitions {
|
||||
if !is_bitlocker_partition(reader, partition_offset) {
|
||||
log::info!(
|
||||
"Trying NTDS on NTFS partition at offset 0x{:x}",
|
||||
partition_offset
|
||||
);
|
||||
match ntfs_reader::read_ntds_artifacts(reader, partition_offset) {
|
||||
Ok((ntds_data, system_data)) => {
|
||||
return Ok(NtdsArtifacts {
|
||||
ntds_data,
|
||||
system_data,
|
||||
partition_offset,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
log::info!("Partition at 0x{:x}: {}", partition_offset, e);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"[*] BitLocker partition at 0x{:x} — trying {} FVEK candidate(s) for NTDS",
|
||||
partition_offset,
|
||||
fvek_keys.len()
|
||||
);
|
||||
|
||||
if let Some(artifacts) = try_bitlocker_fvek_candidates(
|
||||
reader,
|
||||
partition_offset,
|
||||
fvek_keys,
|
||||
|bl_reader, part_off| {
|
||||
ntfs_reader::read_ntds_artifacts(bl_reader, 0).map(|(ntds_data, system_data)| {
|
||||
NtdsArtifacts {
|
||||
ntds_data,
|
||||
system_data,
|
||||
partition_offset: part_off,
|
||||
}
|
||||
})
|
||||
},
|
||||
) {
|
||||
return Ok(artifacts);
|
||||
}
|
||||
}
|
||||
|
||||
Err(crate::error::VmkatzError::DecryptionError(
|
||||
"NTDS.dit not found (BitLocker-aware scan exhausted)".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Try each FVEK candidate on a BitLocker-encrypted partition.
|
||||
///
|
||||
/// For each candidate, validates the NTFS signature after decryption,
|
||||
/// then calls `extract_fn` to perform the actual extraction.
|
||||
/// Returns the first successful result, or `None` if no key works.
|
||||
fn try_bitlocker_fvek_candidates<R, T, F>(
|
||||
reader: &mut R,
|
||||
partition_offset: u64,
|
||||
fvek_keys: &[crate::lsass::bitlocker::BitLockerKey],
|
||||
extract_fn: F,
|
||||
) -> Option<T>
|
||||
where
|
||||
R: Read + Seek,
|
||||
F: Fn(&mut bitlocker_decrypt::BitLockerReader<&mut R>, u64) -> Result<T>,
|
||||
{
|
||||
for (i, key) in fvek_keys.iter().enumerate() {
|
||||
let xts_key = match bitlocker_decrypt::build_xts_key(key) {
|
||||
Some(k) => k,
|
||||
None => {
|
||||
log::info!(
|
||||
"BitLocker: skipping FVEK #{} — unsupported method 0x{:04x} ({})",
|
||||
i,
|
||||
key.method,
|
||||
key.cipher
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Validate by decrypting sector 0 and checking NTFS signature
|
||||
let mut bl_reader =
|
||||
bitlocker_decrypt::BitLockerReader::new(&mut *reader, partition_offset, xts_key.clone());
|
||||
|
||||
if !bl_reader.validate_ntfs_signature() {
|
||||
log::info!(
|
||||
"BitLocker: FVEK #{} ({}) — NTFS signature mismatch after decryption",
|
||||
i,
|
||||
key.cipher
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"[+] BitLocker: FVEK #{} ({}) unlocked partition at 0x{:x}",
|
||||
i, key.cipher, partition_offset
|
||||
);
|
||||
|
||||
// Re-create reader (validate consumed a seek) and extract
|
||||
let mut bl_reader =
|
||||
bitlocker_decrypt::BitLockerReader::new(&mut *reader, partition_offset, xts_key);
|
||||
|
||||
match extract_fn(&mut bl_reader, partition_offset) {
|
||||
Ok(result) => return Some(result),
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"BitLocker: FVEK #{} decrypted NTFS header but extraction failed: {}",
|
||||
i,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract both SAM hashes and LSA secrets from a disk image.
|
||||
pub fn extract_disk_secrets(path: &Path) -> Result<DiskSecrets> {
|
||||
let mut disk = crate::disk::open_disk(path)?;
|
||||
|
||||
Reference in New Issue
Block a user