use std::io::{Read, Seek, SeekFrom};
use crate::boot_code::{self, BootCodeId};
use crate::diag;
use crate::ebr::{walk_ebr_chain, EbrChain};
use crate::entropy;
use crate::findings::{Anomaly, AnomalyKind, MbrAnalysis, PartitionSummary};
use crate::gap::{compute_gaps, Gap, GapKind};
use crate::mbr::{parse_mbr_sector, MbrSector, SECTOR_SIZE};
use crate::signature::{self, DetectedFs};
use crate::Error;
const SECTOR_BYTES: u64 = SECTOR_SIZE as u64;
const PARTITION_TABLE_OFFSET: u64 = 446;
const ENTRY_SIZE: u64 = 16;
const RESERVED_OFFSET: u64 = 444;
const DISK_SERIAL_OFFSET: u64 = 440;
const EBR_SLACK_OFFSET: u64 = 478;
const EBR_INDEX_BASE: usize = 4;
#[inline]
fn lba_to_byte(lba: u64, sector_size: u64) -> u64 {
lba.saturating_mul(sector_size)
}
#[inline]
fn entry_offset(index: usize) -> u64 {
PARTITION_TABLE_OFFSET + index as u64 * ENTRY_SIZE
}
#[inline]
fn disk_last_lba(disk_size_bytes: u64, sector_size: u64) -> u64 {
if disk_size_bytes > 0 {
(disk_size_bytes / sector_size).saturating_sub(1)
} else {
u64::MAX
}
}
#[derive(Default)]
struct Findings {
anomalies: Vec<Anomaly>,
}
impl Findings {
fn record(&mut self, kind: AnomalyKind, offset: u64) {
let anomaly = Anomaly::new(kind, offset);
diag::anomaly_recorded(&anomaly);
self.anomalies.push(anomaly);
}
}
struct PrimaryScan {
extents: Vec<(u64, u64)>,
overlap_extents: Vec<(usize, u64, u64)>,
summaries: Vec<PartitionSummary>,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct AnalyseOptions {
pub sector_size: u64,
}
impl Default for AnalyseOptions {
fn default() -> Self {
Self {
sector_size: SECTOR_BYTES,
}
}
}
#[cfg_attr(feature = "trace", tracing::instrument(level = "debug", skip(reader)))]
pub fn analyse<R: Read + Seek>(reader: &mut R, disk_size_bytes: u64) -> Result<MbrAnalysis, Error> {
analyse_with_options(reader, disk_size_bytes, AnalyseOptions::default())
}
#[cfg_attr(feature = "trace", tracing::instrument(level = "debug", skip(reader)))]
pub fn analyse_with_options<R: Read + Seek>(
reader: &mut R,
disk_size_bytes: u64,
opts: AnalyseOptions,
) -> Result<MbrAnalysis, Error> {
let sector_size = opts.sector_size;
let mbr = read_mbr(reader)?;
let mut findings = Findings::default();
let boot_code_id = boot_code::identify(&mbr.boot_code);
let gpt_header = gpt_header_present(reader, sector_size);
let on_gpt_disk = gpt_header && is_pure_protective_mbr(&mbr);
check_boot_code(&mbr, boot_code_id, on_gpt_disk, &mut findings);
check_disk_signature(&mbr, boot_code_id, &mut findings);
check_reserved(&mbr, &mut findings);
check_bootable_flags(&mbr, &mut findings);
check_duplicate_entries(&mbr, &mut findings);
let last_lba = disk_last_lba(disk_size_bytes, sector_size);
check_gpt(&mbr, last_lba, gpt_header, sector_size, &mut findings);
let mut scan = scan_primary_entries(
reader,
&mbr,
disk_size_bytes,
last_lba,
sector_size,
&mut findings,
);
let ebr_chain = walk_extended(
reader,
&mbr,
&mut scan,
disk_size_bytes,
sector_size,
&mut findings,
);
check_overlaps(&scan.overlap_extents, &mut findings);
let gaps = check_gaps(
&scan.extents,
disk_size_bytes,
last_lba,
sector_size,
&mut findings,
);
check_gap_content(reader, &gaps, sector_size, &mut findings);
#[cfg(feature = "gpt")]
let gpt = if gpt_header {
gpt_forensic::analyse(reader, disk_size_bytes).ok()
} else {
None
};
let disk_serial = mbr.disk_serial;
let era = crate::provenance::infer_era(first_partition_lba(&mbr), boot_code_id);
diag::analysis_complete(
findings.anomalies.len(),
scan.summaries.len(),
gaps.len(),
boot_code_id,
);
Ok(MbrAnalysis {
mbr,
partitions: scan.summaries,
ebr_chain,
gaps,
boot_code_id,
disk_serial,
era,
#[cfg(feature = "gpt")]
gpt,
anomalies: findings.anomalies,
})
}
fn first_partition_lba(mbr: &MbrSector) -> Option<u64> {
mbr.entries
.iter()
.filter(|e| {
!e.is_empty() && !e.is_extended() && e.type_code.0 != crate::gpt::PROTECTIVE_TYPE_CODE
})
.map(|e| e.lba_start as u64)
.min()
}
fn gpt_header_present<R: Read + Seek>(reader: &mut R, sector_size: u64) -> bool {
match read_first_sector(reader, sector_size) {
Ok(lba1) => crate::gpt::has_gpt_header(&lba1),
Err(e) => {
diag::partition_read_failed(sector_size, &e);
false
}
}
}
fn is_pure_protective_mbr(mbr: &MbrSector) -> bool {
let mut nonempty = mbr.entries.iter().filter(|e| !e.is_empty());
matches!(
(nonempty.next(), nonempty.next()),
(Some(e), None) if e.type_code.0 == crate::gpt::PROTECTIVE_TYPE_CODE
)
}
fn read_mbr<R: Read + Seek>(reader: &mut R) -> Result<MbrSector, Error> {
reader.seek(SeekFrom::Start(0))?;
let mut raw = [0u8; SECTOR_SIZE];
reader.read_exact(&mut raw)?;
parse_mbr_sector(&raw)
}
fn check_boot_code(mbr: &MbrSector, id: BootCodeId, on_gpt_disk: bool, findings: &mut Findings) {
let kind = match id {
BootCodeId::AllZeros if on_gpt_disk => Some(AnomalyKind::EmptyProtectiveBootCode),
BootCodeId::AllZeros => Some(AnomalyKind::WipedBootCode),
BootCodeId::AllOnes => Some(AnomalyKind::ErasedBootCode),
BootCodeId::Unknown => Some(AnomalyKind::UnknownBootCode),
_ => None,
};
if let Some(kind) = kind {
findings.record(kind, 0);
}
if id == BootCodeId::Unknown {
let entropy = entropy::shannon(&mbr.boot_code);
if entropy > entropy::HIGH_ENTROPY_THRESHOLD {
findings.record(AnomalyKind::HighEntropySlack { offset: 0, entropy }, 0);
}
}
for name in crate::bootkit::scan(&mbr.boot_code) {
findings.record(AnomalyKind::KnownBootkit { name }, 0);
}
}
fn check_disk_signature(mbr: &MbrSector, id: BootCodeId, findings: &mut Findings) {
let is_windows = matches!(id, BootCodeId::WindowsVista | BootCodeId::Windows7Plus);
if is_windows && mbr.disk_serial == 0 {
findings.record(AnomalyKind::ZeroDiskSignature, DISK_SERIAL_OFFSET);
}
}
const PROTECTIVE_UNDERSIZE_TOLERANCE: u64 = 2048;
fn check_gpt(
mbr: &MbrSector,
last_lba: u64,
header_present: bool,
sector_size: u64,
findings: &mut Findings,
) {
let protective_idx = mbr
.entries
.iter()
.position(|e| !e.is_empty() && e.type_code.0 == crate::gpt::PROTECTIVE_TYPE_CODE);
let Some(idx) = protective_idx else {
if header_present {
findings.record(AnomalyKind::HiddenGpt, lba_to_byte(1, sector_size));
}
return;
};
let off = entry_offset(idx);
if !header_present {
findings.record(AnomalyKind::SpoofedProtectiveMbr, off);
return;
}
let extra = mbr
.entries
.iter()
.filter(|e| !e.is_empty() && e.type_code.0 != crate::gpt::PROTECTIVE_TYPE_CODE)
.count();
if extra > 0 {
findings.record(
AnomalyKind::HybridMbr {
extra_partition_count: extra,
},
off,
);
}
let ee = &mbr.entries[idx];
let covered_last_lba = ee.lba_end() as u64;
let spans_disk = ee.lba_count == u32::MAX; if last_lba != u64::MAX
&& !spans_disk
&& last_lba.saturating_sub(covered_last_lba) > PROTECTIVE_UNDERSIZE_TOLERANCE
{
findings.record(
AnomalyKind::ProtectiveMbrUndersized {
covered_last_lba,
disk_last_lba: last_lba,
},
off,
);
}
}
fn check_duplicate_entries(mbr: &MbrSector, findings: &mut Findings) {
let e = &mbr.entries;
for a in 0..e.len() {
if e[a].is_empty() {
continue;
}
for b in (a + 1)..e.len() {
if !e[b].is_empty()
&& e[a].lba_start == e[b].lba_start
&& e[a].lba_count == e[b].lba_count
{
findings.record(
AnomalyKind::DuplicatePartitionEntry { a, b },
entry_offset(a),
);
}
}
}
}
fn check_reserved(mbr: &MbrSector, findings: &mut Findings) {
if mbr.reserved != [0, 0] {
findings.record(
AnomalyKind::NonZeroReserved {
bytes: mbr.reserved,
},
RESERVED_OFFSET,
);
}
}
fn check_bootable_flags(mbr: &MbrSector, findings: &mut Findings) {
let bootable = mbr.entries.iter().filter(|e| e.is_bootable()).count();
let active = mbr.entries.iter().filter(|e| !e.is_empty()).count();
if bootable > 1 {
findings.record(
AnomalyKind::MultipleBootable { count: bootable },
PARTITION_TABLE_OFFSET,
);
}
if active > 0 && bootable == 0 {
findings.record(AnomalyKind::NoBootablePartition, PARTITION_TABLE_OFFSET);
}
}
fn scan_primary_entries<R: Read + Seek>(
reader: &mut R,
mbr: &MbrSector,
disk_size_bytes: u64,
last_lba: u64,
sector_size: u64,
findings: &mut Findings,
) -> PrimaryScan {
let mut extents = Vec::new();
let mut overlap_extents = Vec::new();
let mut summaries = Vec::new();
for (i, entry) in mbr.entries.iter().enumerate() {
let off = entry_offset(i);
if entry.type_code.is_empty() && (entry.lba_start != 0 || entry.lba_count != 0) {
findings.record(
AnomalyKind::ResidualEntry {
index: i,
lba_start: entry.lba_start,
lba_count: entry.lba_count,
},
off,
);
continue;
}
if entry.is_empty() {
continue;
}
if entry.status != 0x00 && entry.status != 0x80 {
findings.record(
AnomalyKind::InvalidPartitionStatus {
index: i,
status: entry.status,
},
off,
);
}
check_chs_lba(i, entry, findings);
let lba_start = entry.lba_start as u64;
let lba_end = entry.lba_end() as u64;
let byte_offset = lba_to_byte(lba_start, sector_size);
let byte_size = lba_to_byte(entry.lba_count as u64, sector_size);
if disk_size_bytes > 0 && lba_end > last_lba {
findings.record(
AnomalyKind::OutOfBounds {
index: i,
last_lba: lba_end,
disk_last_lba: last_lba,
},
off,
);
}
extents.push((lba_start, lba_end));
if !entry.is_extended() {
overlap_extents.push((i, lba_start, lba_end));
}
check_vbr(reader, i, lba_start, byte_offset, disk_size_bytes, findings);
let detected_fs = detect_and_check_fs(
reader,
i,
byte_offset,
entry.type_code,
disk_size_bytes,
findings,
);
summaries.push(PartitionSummary {
index: i,
lba_start,
lba_end,
byte_offset,
byte_size,
declared_type: entry.type_code,
detected_fs,
});
}
PrimaryScan {
extents,
overlap_extents,
summaries,
}
}
fn detect_and_check_fs<R: Read + Seek>(
reader: &mut R,
index: usize,
byte_offset: u64,
declared: crate::partition::TypeCode,
disk_size_bytes: u64,
findings: &mut Findings,
) -> Option<DetectedFs> {
let detected_fs = detect_partition_fs(reader, byte_offset, disk_size_bytes);
if let Some(detected) = detected_fs {
if signature::type_conflicts(declared.family(), detected) {
findings.record(
AnomalyKind::SignatureMismatch {
index,
declared,
detected,
},
byte_offset,
);
}
}
detected_fs
}
fn check_chs_lba(index: usize, entry: &crate::partition::PartitionEntry, findings: &mut Findings) {
use crate::partition::{
chs_consistency, ChsConsistency, STD_HEADS_PER_CYL, STD_SECTORS_PER_TRACK,
};
let first = chs_consistency(
entry.chs_first,
entry.lba_start,
STD_HEADS_PER_CYL,
STD_SECTORS_PER_TRACK,
);
let last = chs_consistency(
entry.chs_last,
entry.lba_end(),
STD_HEADS_PER_CYL,
STD_SECTORS_PER_TRACK,
);
if first == ChsConsistency::Inconsistent || last == ChsConsistency::Inconsistent {
findings.record(
AnomalyKind::ChsLbaInconsistency { index },
entry_offset(index),
);
}
}
fn check_vbr<R: Read + Seek>(
reader: &mut R,
index: usize,
lba_start: u64,
byte_offset: u64,
disk_size_bytes: u64,
findings: &mut Findings,
) {
if disk_size_bytes != 0 && byte_offset >= disk_size_bytes {
return;
}
let Ok(sector) = read_first_sector(reader, byte_offset) else {
return;
};
let Some(bpb) = crate::vbr::parse_bpb(§or) else {
return;
};
if bpb.hidden_sectors != 0 && u64::from(bpb.hidden_sectors) != lba_start {
findings.record(
AnomalyKind::VbrHiddenSectorsMismatch {
index,
bpb_hidden: bpb.hidden_sectors,
lba_start,
},
byte_offset,
);
}
}
fn check_overlaps(extents: &[(usize, u64, u64)], findings: &mut Findings) {
let mut sorted = extents.to_vec();
sorted.sort_by_key(|&(_, start, _)| start);
for pair in sorted.windows(2) {
let (a_id, _, a_end) = pair[0];
let (b_id, b_start, _) = pair[1];
if b_start <= a_end {
findings.record(
AnomalyKind::OverlappingPartitions {
a: a_id,
b: b_id,
a_end,
b_start,
},
entry_offset(a_id.min(3)),
);
}
}
}
fn walk_extended<R: Read + Seek>(
reader: &mut R,
mbr: &MbrSector,
scan: &mut PrimaryScan,
disk_size_bytes: u64,
sector_size: u64,
findings: &mut Findings,
) -> EbrChain {
let Some(ext) = mbr.entries.iter().find(|e| e.is_extended()) else {
return EbrChain::empty();
};
let ext_start = ext.lba_start as u64;
let chain = match walk_ebr_chain(reader, ext_start, sector_size) {
Ok(chain) => chain,
Err(e) => {
diag::ebr_walk_failed(ext_start, &e);
return EbrChain::empty();
}
};
let ext_offset = lba_to_byte(ext_start, sector_size);
if chain.had_cycle {
findings.record(AnomalyKind::EbrCycle, ext_offset);
}
if chain.depth_exceeded {
findings.record(
AnomalyKind::EbrExcessiveDepth {
depth: chain.entries.len(),
},
ext_offset,
);
}
for ebr in &chain.entries {
if ebr.has_slack {
let entropy = entropy::shannon(&ebr.slack);
findings.record(
AnomalyKind::EbrSlackData {
ebr_lba: ebr.ebr_lba,
entropy,
},
ebr.ebr_offset + EBR_SLACK_OFFSET,
);
}
let lba_start = ebr.logical_lba_start;
let lba_end = lba_start
.saturating_add(ebr.logical.lba_count as u64)
.saturating_sub(1);
let byte_offset = lba_to_byte(lba_start, sector_size);
let index = EBR_INDEX_BASE + scan.summaries.len();
scan.extents.push((lba_start, lba_end));
scan.overlap_extents.push((index, lba_start, lba_end));
check_vbr(
reader,
index,
lba_start,
byte_offset,
disk_size_bytes,
findings,
);
let detected_fs = detect_and_check_fs(
reader,
index,
byte_offset,
ebr.logical.type_code,
disk_size_bytes,
findings,
);
scan.summaries.push(PartitionSummary {
index,
lba_start,
lba_end,
byte_offset,
byte_size: lba_to_byte(ebr.logical.lba_count as u64, sector_size),
declared_type: ebr.logical.type_code,
detected_fs,
});
}
chain
}
fn check_gaps(
extents: &[(u64, u64)],
disk_size_bytes: u64,
last_lba: u64,
sector_size: u64,
findings: &mut Findings,
) -> Vec<Gap> {
if disk_size_bytes == 0 {
return vec![];
}
let mut sorted = extents.to_vec();
sorted.sort_by_key(|&(start, _)| start);
sorted.dedup();
let gaps = compute_gaps(&sorted, 1, last_lba, sector_size);
for gap in &gaps {
findings.record(
gap_anomaly_kind(gap),
lba_to_byte(gap.lba_start, sector_size),
);
}
gaps
}
const GAP_SAMPLE_BYTES: usize = 4096;
fn check_gap_content<R: Read + Seek>(
reader: &mut R,
gaps: &[Gap],
sector_size: u64,
findings: &mut Findings,
) {
for gap in gaps {
let byte_offset = lba_to_byte(gap.lba_start, sector_size);
let sample_len = gap.byte_size.min(GAP_SAMPLE_BYTES as u64) as usize;
if sample_len == 0 {
continue;
}
if reader.seek(SeekFrom::Start(byte_offset)).is_err() {
continue;
}
let mut buf = vec![0u8; sample_len];
if reader.read_exact(&mut buf).is_err() {
continue;
}
let pattern = crate::wipe::classify(&buf);
if pattern.is_deliberate_wipe() {
findings.record(
AnomalyKind::WipedRegion {
lba_start: gap.lba_start,
pattern,
},
byte_offset,
);
}
for artifact in crate::carve::carve(&buf, byte_offset) {
findings.record(
AnomalyKind::CarvedArtifact {
kind: artifact.kind,
},
artifact.offset,
);
}
}
}
fn gap_anomaly_kind(gap: &Gap) -> AnomalyKind {
match gap.kind {
GapKind::PrePartition => AnomalyKind::PrePartitionSpace {
lba_start: gap.lba_start,
lba_end: gap.lba_end,
byte_size: gap.byte_size,
},
GapKind::Between => AnomalyKind::InterPartitionGap {
lba_start: gap.lba_start,
lba_end: gap.lba_end,
byte_size: gap.byte_size,
},
GapKind::PostPartition => AnomalyKind::PostPartitionSpace {
lba_start: gap.lba_start,
lba_end: gap.lba_end,
byte_size: gap.byte_size,
},
}
}
const FS_FINGERPRINT_BYTES: usize = 65600 + 8;
fn detect_partition_fs<R: Read + Seek>(
reader: &mut R,
byte_offset: u64,
disk_size_bytes: u64,
) -> Option<DetectedFs> {
if disk_size_bytes != 0 && byte_offset >= disk_size_bytes {
return None;
}
match read_fingerprint(reader, byte_offset, FS_FINGERPRINT_BYTES) {
Ok(buf) => Some(signature::detect(&buf)),
Err(e) => {
diag::partition_read_failed(byte_offset, &e);
None
}
}
}
fn read_fingerprint<R: Read + Seek>(
reader: &mut R,
byte_offset: u64,
max: usize,
) -> Result<Vec<u8>, Error> {
reader.seek(SeekFrom::Start(byte_offset))?;
let mut buf = vec![0u8; max];
let mut filled = 0;
while filled < max {
match reader.read(&mut buf[filled..]) {
Ok(0) => break,
Ok(n) => filled += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => return Err(e.into()),
}
}
buf.truncate(filled);
Ok(buf)
}
fn read_first_sector<R: Read + Seek>(
reader: &mut R,
byte_offset: u64,
) -> Result<[u8; SECTOR_SIZE], Error> {
reader.seek(SeekFrom::Start(byte_offset))?;
let mut buf = [0u8; SECTOR_SIZE];
reader.read_exact(&mut buf)?;
Ok(buf)
}